Skip to content

Commit

Permalink
Apply wokeignore rules on their own line to the following line (#119)
Browse files Browse the repository at this point in the history
Co-authored-by: Kevin Leung <[email protected]>
  • Loading branch information
jeremydelacruz and kevinleung23 authored Aug 2, 2021
1 parent 0b0466f commit 54b6fec
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 16 deletions.
29 changes: 24 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,33 +280,52 @@ This also follows the [gitignore](https://git-scm.com/docs/gitignore) convention

See [.wokeignore.example](.wokeignore.example) for a collection of common files and directories that may contain generated [SHA](https://en.wikipedia.org/wiki/Secure_Hash_Algorithms) and [GUID](https://en.wikipedia.org/wiki/Universally_unique_identifier)s. Dependency directories are also shown in the example as the linter will parse dependency source code and possibly find errors.

#### In-line ignoring
#### In-line and next-line ignoring

There may be times where you don't want to ignore an entire file.
You may ignore a specific line for one or more rules by creating an in-line comment.
You may ignore a specific line for one or more rules by creating an in-line or next-line comment.

This functionality is very rudimentary, it does a simple search for the phrase. Since
`woke` is just a text file analyzer, it has no concept of the comment syntax for every file
type it might encounter.

Simply add the following to the line you wish to ignore, using comment syntax that is supported for your file type.
For in-line ignoring, simply add the following to the line you wish to ignore, using comment syntax that is supported for your file type.
(`woke` is not responsible for broken code due to in-line ignoring. Make sure you comment correctly!)

Next-line ignoring works in a similar way. Instead of adding to the end of line you wish to ignore, you can create the ignore comment on its own line just before it. Any alphanumeric text to the left of the phrase will cause `woke` to treat it as an in-line ignore, but any text to the right of the phrase will not be considered.
(Note: next-line ignore comments takes precedence over in-line ignores, so try to only use one for any given line!)

```bash
This line has RULE_NAME but will be ignored # wokeignore:rule=RULE_NAME
# for example, to ignore the following line for the whitelist rule
# wokeignore:rule=RULENAME
Here is another line with RULE_NAME that will be ignored
# a couple of examples ignoring the following line for the whitelist rule
whitelist # wokeignore:rule=whitelist
# or for multiple rules
# wokeignore:rule=whitelist
whitelist
# a couple of examples doing the same for multiple rules
# rule names must be comma-separated with no spaces
whitelist and blacklist # wokeignore:rule=whitelist,blacklist
# wokeignore:rule=whitelist,blacklist
whitelist and blacklist
# wokeignore:rule=whitelist text here won't be considered by woke even if it contains whitelist
this line with whitelist will still be ignored
```

Here's an example in go:

```go
func main() {
fmt.Println("here is the whitelist") // wokeignore:rule=whitelist
// wokeignore:rule=blacklist
fmt.Println("and here is the blacklist")
}
```

Expand Down
34 changes: 27 additions & 7 deletions pkg/parser/findings.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"time"

"github.com/get-woke/woke/pkg/result"
"github.com/get-woke/woke/pkg/rule"
"github.com/get-woke/woke/pkg/util"

"github.com/rs/zerolog/log"
Expand Down Expand Up @@ -52,6 +53,7 @@ func (p *Parser) generateFileFindings(file *os.File) (*result.FileResults, error

reader := bufio.NewReader(file)

var ignoreNextLineText string
line := 1

Loop:
Expand All @@ -60,20 +62,38 @@ Loop:
case err == nil || (err == io.EOF && text != ""):
text = strings.TrimSuffix(text, "\n")

// Store current line's wokeignore text if ignoring next line
if rule.IsDirectiveOnlyLine(text) {
ignoreNextLineText = text
line++
continue
}

for _, r := range p.Rules {
if p.Ignorer != nil && r.CanIgnoreLine(text) {
log.Debug().
Str("rule", r.Name).
Str("file", filename).
Int("line", line).
Msg("ignoring via in-line")
continue
if p.Ignorer != nil {
if ignoreNextLineText == "" && r.CanIgnoreLine(text) {
log.Debug().
Str("rule", r.Name).
Str("file", filename).
Int("line", line).
Msg("ignoring via in-line")
continue
} else if r.CanIgnoreLine(ignoreNextLineText) {
// Check current rule against prev line's next-line wokeignore text (if applicable)
log.Debug().
Str("rule", r.Name).
Str("file", filename).
Int("line", line).
Msg("ignoring via next-line")
continue
}
}

lineResults := result.FindResults(r, results.Filename, text, line)
results.Results = append(results.Results, lineResults...)
}

ignoreNextLineText = ""
line++
case err == io.EOF:
break Loop
Expand Down
25 changes: 25 additions & 0 deletions pkg/parser/findings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,28 @@ func TestGenerateFileFindingsOverlappingRules(t *testing.T) {
})
}
}

// Tests for next-line wokeignore
func TestGenerateFileFindingsNewLineIgnores(t *testing.T) {
tests := []struct {
desc string
content string
matches int
}{
{"not matching newline ignore", "#wokeignore:rule=master-slave\n this has whitelist", 1},
{"matching newline ignore", "#wokeignore:rule=whitelist\n this has whitelist", 0},
{"matching newline ignore", "#wokeignore:rule=whitelist whitelist\n this has whitelist", 0},
{"newline ignore with potential match two lines down", "#wokeignore:rule=whitelist\n this line is fine\n this has whitelist", 1},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
f, err := newFile(t, tc.content)
assert.NoError(t, err)

p := testParser()
res, err := p.generateFileFindingsFromFilename(f.Name())
assert.NoError(t, err)
assert.Len(t, res.Results, tc.matches)
})
}
}
11 changes: 8 additions & 3 deletions pkg/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,12 @@ func parsePathTests(t *testing.T) {
t.Run("default path", func(t *testing.T) {
// Test default path (which would run tests against the parser package)
p := testParser()
p.Ignorer = ignore.NewIgnore([]string{"*_test.go"})
pr := new(testPrinter)
findings := p.ParsePaths(pr)

assert.Equal(t, len(pr.results), findings)
assert.Greater(t, len(pr.results), 0)
assert.Equal(t, len(pr.results), 0)
})

t.Run("stdin", func(t *testing.T) {
Expand Down Expand Up @@ -200,17 +201,21 @@ func parsePathTests(t *testing.T) {
})

t.Run("note is not included in output message", func(t *testing.T) {
f, err := newFile(t, "i have a whitelist")
assert.NoError(t, err)
const TestNote = "TEST NOTE"
p := testParser()
p.Rules[0].Note = TestNote
p.Rules[0].Options.IncludeNote = nil
pr := new(testPrinter)
p.ParsePaths(pr)
p.ParsePaths(pr, f.Name())

assert.NotContains(t, pr.results[0].Results[0].Reason(), TestNote)
})

t.Run("note is included in output message", func(t *testing.T) {
f, err := newFile(t, "i have a whitelist")
assert.NoError(t, err)
const TestNote = "TEST NOTE"
includeNote := true
p := testParser()
Expand All @@ -219,7 +224,7 @@ func parsePathTests(t *testing.T) {
// Test IncludeNote flag doesn't get overridden with SetIncludeNote method
p.Rules[0].SetIncludeNote(false)
pr := new(testPrinter)
p.ParsePaths(pr)
p.ParsePaths(pr, f.Name())

assert.Contains(t, pr.results[0].Results[0].Reason(), TestNote)
})
Expand Down
14 changes: 14 additions & 0 deletions pkg/rule/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,20 @@ func (r *Rule) CanIgnoreLine(line string) bool {
return false
}

// IsDirectiveOnlyLine returns a boolean value if the line contains only the wokeignore directive.
// For example, if a line is only a single-line comment containing wokeignore:rule=xyz with no other
// alphanumeric characters to the left of the directive, it will return true that it is a directive-only line.
// Any text to the right of the wokeignore directive will not be considered by woke for findings.
func IsDirectiveOnlyLine(line string) bool {
indices := ignoreRuleRegex.FindStringIndex(line)
if indices == nil {
return false
}
// in a one-line comment, left-text should be all that is considered to be "outside" of the ignore directive
leftText := line[0:indices[0]]
return !util.ContainsAlphanumeric(leftText)
}

func escape(ss []string) []string {
for i, s := range ss {
ss[i] = regexp.QuoteMeta(s)
Expand Down
29 changes: 29 additions & 0 deletions pkg/rule/rule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,32 @@ func TestRule_IncludeNote(t *testing.T) {
r.SetIncludeNote(false)
assert.Equal(t, true, r.includeNote())
}

func Test_IsDirectiveOnlyLine(t *testing.T) {
tests := []struct {
name string
line string
assertion assert.BoolAssertionFunc
}{
{"text and no wokeignore", "some text", assert.False},
{"text, then wokeignore", "some text #wokeignore:rule=rule1", assert.False},
{"text, then invalid wokeignore", "some text #wokeignore:rule", assert.False},
{"text, then multiple rules in wokeignore", "some text #wokeignore:rule=rule1,rule2", assert.False},
{"text, then text after ignore", "some text #wokeignore:rule=rule1 something else", assert.False},
{"text, then multiple ignores", "some text #wokeignore:rule=rule1 wokeignore:rule=rule2", assert.False},
{"empty line", "", assert.False},
{"only wokeignore", "#wokeignore:rule=rule1", assert.True},
// any text to the right of wokeignore when line starts with wokeignore will not be considered by woke for findings
{"wokeignore, then text", "#wokeignore:rule=rule1 something else", assert.True},
{"non-alphanumeric text before and after wokeignore", "<!-- wokeignore:rule=rule1 -->", assert.True},
{"spaces before wokeignore", " #wokeignore:rule=rule1", assert.True},
{"tabs before wokeignore", "\t\t\t#wokeignore:rule=rule1", assert.True},
{"tabs and spaces before wokeignore", " \t \t \t #wokeignore:rule=rule1", assert.True},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.assertion(t, IsDirectiveOnlyLine(tt.line))
})
}
}
20 changes: 19 additions & 1 deletion pkg/util/string.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package util

import "fmt"
import (
"fmt"
)

// MarkdownCodify returns a markdown code string
// https://www.markdownguide.org/basic-syntax/#code
Expand All @@ -20,3 +22,19 @@ func InSlice(s string, slice []string) bool {
}
return false
}

// ContainsAlphanumeric returns true if alphanumeric chars are found in the string
func ContainsAlphanumeric(s string) bool {
if len(s) == 0 {
return false
}
for i := 0; i < len(s); i++ {
char := s[i]
if ('a' <= char && char <= 'z') ||
('A' <= char && char <= 'Z') ||
('0' <= char && char <= '9') {
return true
}
}
return false
}
21 changes: 21 additions & 0 deletions pkg/util/string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,24 @@ func TestInSlice(t *testing.T) {
})
}
}

func TestContainsAlphanumeric(t *testing.T) {
tests := []struct {
s string
assertion assert.BoolAssertionFunc
}{
{"foo", assert.True},
{"bar123", assert.True},
{"", assert.False},
{" ", assert.False},
{"123", assert.True},
{"<-- -->", assert.False},
{"#", assert.False},
{" //", assert.False},
}
for i, tt := range tests {
t.Run(fmt.Sprintf("%s-%d", tt.s, i), func(t *testing.T) {
tt.assertion(t, ContainsAlphanumeric(tt.s))
})
}
}

0 comments on commit 54b6fec

Please sign in to comment.