diff --git a/README.md b/README.md index b3ad4d03..846b3b91 100644 --- a/README.md +++ b/README.md @@ -280,26 +280,42 @@ 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: @@ -307,6 +323,9 @@ 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") } ``` diff --git a/pkg/parser/findings.go b/pkg/parser/findings.go index 5a89a4e6..95400407 100644 --- a/pkg/parser/findings.go +++ b/pkg/parser/findings.go @@ -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" @@ -52,6 +53,7 @@ func (p *Parser) generateFileFindings(file *os.File) (*result.FileResults, error reader := bufio.NewReader(file) + var ignoreNextLineText string line := 1 Loop: @@ -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 diff --git a/pkg/parser/findings_test.go b/pkg/parser/findings_test.go index 86421743..348f62d3 100644 --- a/pkg/parser/findings_test.go +++ b/pkg/parser/findings_test.go @@ -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) + }) + } +} diff --git a/pkg/parser/parser_test.go b/pkg/parser/parser_test.go index f5e20e11..352d71c2 100644 --- a/pkg/parser/parser_test.go +++ b/pkg/parser/parser_test.go @@ -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) { @@ -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() @@ -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) }) diff --git a/pkg/rule/rule.go b/pkg/rule/rule.go index 18482917..0e67138d 100644 --- a/pkg/rule/rule.go +++ b/pkg/rule/rule.go @@ -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) diff --git a/pkg/rule/rule_test.go b/pkg/rule/rule_test.go index a8765a5d..d2dfae5e 100644 --- a/pkg/rule/rule_test.go +++ b/pkg/rule/rule_test.go @@ -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", "", 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)) + }) + } +} diff --git a/pkg/util/string.go b/pkg/util/string.go index 1d5cd667..add134e5 100644 --- a/pkg/util/string.go +++ b/pkg/util/string.go @@ -1,6 +1,8 @@ package util -import "fmt" +import ( + "fmt" +) // MarkdownCodify returns a markdown code string // https://www.markdownguide.org/basic-syntax/#code @@ -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 +} diff --git a/pkg/util/string_test.go b/pkg/util/string_test.go index 6124b04e..e55294ee 100644 --- a/pkg/util/string_test.go +++ b/pkg/util/string_test.go @@ -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)) + }) + } +}