From c3e0c1d03637ef26dad0bc086793b38878852b48 Mon Sep 17 00:00:00 2001 From: omissis Date: Wed, 10 Aug 2022 14:28:02 +0200 Subject: [PATCH] feat: add file content regex matching expression, refactor shoulds code and tests --- README.md | 4 +- internal/arch/file/except/except.go | 10 ++ internal/arch/file/except/this.go | 6 +- internal/arch/file/rule_all_test.go | 8 +- internal/arch/file/rule_set_test.go | 32 +++---- internal/arch/file/should/end_with.go | 13 +-- internal/arch/file/should/exist.go | 13 +-- .../arch/file/should/have_content_matching.go | 67 ++++++++----- .../should/have_content_matching_regex.go | 74 +++++++++++++++ .../have_content_matching_regex_test.go | 94 +++++++++++++++++++ .../file/should/have_content_matching_test.go | 36 +++++-- internal/arch/file/should/match_glob.go | 13 +-- internal/arch/file/should/match_glob_test.go | 20 ++-- internal/arch/file/should/match_regex.go | 13 +-- internal/arch/file/should/should.go | 70 ++++++++++++-- internal/arch/file/should/start_with.go | 13 +-- internal/arch/file/should/test/baz.txt | 3 + internal/arch/file/test/project3/baz.go | 5 - internal/arch/file/test/project3/baz.txt | 1 + internal/arch/file/test/project3/quux.go | 5 - internal/arch/file/test/project3/quux.txt | 1 + 21 files changed, 386 insertions(+), 115 deletions(-) create mode 100644 internal/arch/file/should/have_content_matching_regex.go create mode 100644 internal/arch/file/should/have_content_matching_regex_test.go create mode 100644 internal/arch/file/should/test/baz.txt delete mode 100644 internal/arch/file/test/project3/baz.go create mode 100644 internal/arch/file/test/project3/baz.txt delete mode 100644 internal/arch/file/test/project3/quux.go create mode 100644 internal/arch/file/test/project3/quux.txt diff --git a/README.md b/README.md index 09e1a20..68a02fc 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ if a file: - [x] does not exist - [x] name matches a regex - [x] name does not match a regex -- [ ] content matches value -- [ ] content matches regex +- [x] content matches value +- [x] content matches regex - [ ] content matches template - [ ] is gitignored - [ ] is gitcrypted diff --git a/internal/arch/file/except/except.go b/internal/arch/file/except/except.go index 1bf9254..7c5e392 100644 --- a/internal/arch/file/except/except.go +++ b/internal/arch/file/except/except.go @@ -5,6 +5,16 @@ import ( "goarkitect/internal/arch/rule" ) +type evaluateFunc func(filePath string) bool + +func NewExpression( + evaluate evaluateFunc, +) *Expression { + return &Expression{ + evaluate: evaluate, + } +} + type Expression struct { evaluate func(filePath string) bool } diff --git a/internal/arch/file/except/this.go b/internal/arch/file/except/this.go index 583be55..d4532da 100644 --- a/internal/arch/file/except/this.go +++ b/internal/arch/file/except/this.go @@ -3,9 +3,9 @@ package except import "path/filepath" func This(value string) *Expression { - return &Expression{ - evaluate: func(filePath string) bool { + return NewExpression( + func(filePath string) bool { return filepath.Base(filePath) != value }, - } + ) } diff --git a/internal/arch/file/rule_all_test.go b/internal/arch/file/rule_all_test.go index 6f957e1..80a05b4 100644 --- a/internal/arch/file/rule_all_test.go +++ b/internal/arch/file/rule_all_test.go @@ -161,16 +161,16 @@ func Test_It_Checks_All_Files_Paths_In_A_Folder_Match_A_Glob_Pattern(t *testing. { desc: "check that all files' names in a folder match a glob pattern", folder: filepath.Join(basePath, "test/project3"), - glob: "*/*/*.go", + glob: "*/*/*.txt", wantViolations: nil, }, { desc: "check that all files' names in a folder do not match a glob pattern", folder: filepath.Join(basePath, "test/project3"), - glob: "*/*/*.ts", + glob: "*/*/*.doc", wantViolations: []rule.Violation{ - rule.NewViolation("file's path 'baz.go' does not match glob pattern '*/*/*.ts'"), - rule.NewViolation("file's path 'quux.go' does not match glob pattern '*/*/*.ts'"), + rule.NewViolation("file's path 'baz.txt' does not match glob pattern '*/*/*.doc'"), + rule.NewViolation("file's path 'quux.txt' does not match glob pattern '*/*/*.doc'"), }, }, } diff --git a/internal/arch/file/rule_set_test.go b/internal/arch/file/rule_set_test.go index ec986d2..03b41ed 100644 --- a/internal/arch/file/rule_set_test.go +++ b/internal/arch/file/rule_set_test.go @@ -198,43 +198,43 @@ func Test_It_Checks_A_Set_Of_Files_Names_Matches_A_Glob_Pattern(t *testing.T) { { desc: "check that a set of files' names match a glob pattern when it's actually there", filenames: []string{ - filepath.Join(basePath, "test/project3/baz.go"), - filepath.Join(basePath, "test/project3/quux.go"), + filepath.Join(basePath, "test/project3/baz.txt"), + filepath.Join(basePath, "test/project3/quux.txt"), }, - glob: "*/*/*.go", + glob: "*/*/*.txt", wantViolations: nil, }, { desc: "check that a set of files' names match a glob pattern when it's not actually there", filenames: []string{ - filepath.Join(basePath, "test/project3/baz.ts"), - filepath.Join(basePath, "test/project3/quux.ts"), + filepath.Join(basePath, "test/project3/baz.doc"), + filepath.Join(basePath, "test/project3/quux.doc"), }, - glob: "*/*/*.ts", + glob: "*/*/*.doc", wantViolations: nil, }, { desc: "check that a set of files' names do not match a glob pattern when it's actually there", filenames: []string{ - filepath.Join(basePath, "test/project3/baz.go"), - filepath.Join(basePath, "test/project3/quux.go"), + filepath.Join(basePath, "test/project3/baz.txt"), + filepath.Join(basePath, "test/project3/quux.txt"), }, - glob: "*/*/*.ts", + glob: "*/*/*.doc", wantViolations: []rule.Violation{ - rule.NewViolation("file's path 'baz.go' does not match glob pattern '*/*/*.ts'"), - rule.NewViolation("file's path 'quux.go' does not match glob pattern '*/*/*.ts'"), + rule.NewViolation("file's path 'baz.txt' does not match glob pattern '*/*/*.doc'"), + rule.NewViolation("file's path 'quux.txt' does not match glob pattern '*/*/*.doc'"), }, }, { desc: "check that a set of files' names do not match a glob pattern when it's not actually there", filenames: []string{ - filepath.Join(basePath, "test/project3/baz.ts"), - filepath.Join(basePath, "test/project3/quux.ts"), + filepath.Join(basePath, "test/project3/baz.doc"), + filepath.Join(basePath, "test/project3/quux.doc"), }, - glob: "*/*/*.go", + glob: "*/*/*.txt", wantViolations: []rule.Violation{ - rule.NewViolation("file's path 'baz.ts' does not match glob pattern '*/*/*.go'"), - rule.NewViolation("file's path 'quux.ts' does not match glob pattern '*/*/*.go'"), + rule.NewViolation("file's path 'baz.doc' does not match glob pattern '*/*/*.txt'"), + rule.NewViolation("file's path 'quux.doc' does not match glob pattern '*/*/*.txt'"), }, }, } diff --git a/internal/arch/file/should/end_with.go b/internal/arch/file/should/end_with.go index cbeeb46..0e809d0 100644 --- a/internal/arch/file/should/end_with.go +++ b/internal/arch/file/should/end_with.go @@ -6,20 +6,20 @@ import ( "path/filepath" ) -func EndWith(suffix string) *Expression { +func EndWith(suffix string, opts ...Option) *Expression { ls := len(suffix) - return &Expression{ - evaluate: func(_ rule.Builder, filePath string) bool { + return NewExpression( + func(_ rule.Builder, filePath string) bool { fileName := filepath.Base(filePath) lf := len(fileName) return ls <= lf && fileName[lf-ls:] != suffix }, - getViolation: func(filePath string, negated bool) rule.Violation { + func(filePath string, options options) rule.Violation { format := "file's name '%s' does not end with '%s'" - if negated { + if options.negated { format = "file's name '%s' does end with '%s'" } @@ -27,5 +27,6 @@ func EndWith(suffix string) *Expression { fmt.Sprintf(format, filepath.Base(filePath), suffix), ) }, - } + opts..., + ) } diff --git a/internal/arch/file/should/exist.go b/internal/arch/file/should/exist.go index 06ac58f..b2e19ab 100644 --- a/internal/arch/file/should/exist.go +++ b/internal/arch/file/should/exist.go @@ -7,9 +7,9 @@ import ( "path/filepath" ) -func Exist() *Expression { - return &Expression{ - evaluate: func(rb rule.Builder, filePath string) bool { +func Exist(opts ...Option) *Expression { + return NewExpression( + func(rb rule.Builder, filePath string) bool { if _, err := os.Stat(filePath); err != nil { if !os.IsNotExist(err) { rb.AddError(err) @@ -20,9 +20,9 @@ func Exist() *Expression { return false }, - getViolation: func(filePath string, negated bool) rule.Violation { + func(filePath string, options options) rule.Violation { format := "file '%s' does not exist" - if negated { + if options.negated { format = "file '%s' does exist" } @@ -30,5 +30,6 @@ func Exist() *Expression { fmt.Sprintf(format, filepath.Base(filePath)), ) }, - } + opts..., + ) } diff --git a/internal/arch/file/should/have_content_matching.go b/internal/arch/file/should/have_content_matching.go index a553636..c5017b9 100644 --- a/internal/arch/file/should/have_content_matching.go +++ b/internal/arch/file/should/have_content_matching.go @@ -10,50 +10,65 @@ import ( "golang.org/x/exp/slices" ) -type ContentMatchOption interface { - apply(data []byte) []byte -} - -type IgnoreNewLinesAtTheEndOfFile struct{} - -func (opt IgnoreNewLinesAtTheEndOfFile) apply(data []byte) []byte { - return bytes.TrimRight(data, "\n") -} - -type IgnoreCase struct{} - -func (opt IgnoreCase) apply(data []byte) []byte { - return bytes.ToLower(data) -} - -func HaveContentMatching(want []byte, opts ...ContentMatchOption) *Expression { - return &Expression{ - evaluate: func(rb rule.Builder, filePath string) bool { +func HaveContentMatching(want []byte, opts ...Option) *Expression { + return NewExpression( + func(rb rule.Builder, filePath string) bool { data, err := os.ReadFile(filePath) if err != nil { rb.AddError(err) return true } + match := "SINGLE" + separator := []byte("\n") for _, opt := range opts { - data = opt.apply(data) + switch opt.(type) { + case IgnoreNewLinesAtTheEndOfFile: + data = bytes.TrimRight(data, "\n") + want = bytes.TrimRight(want, "\n") + case IgnoreCase: + data = bytes.ToLower(data) + want = bytes.ToLower(want) + case MatchSingleLines: + match = "MULTIPLE" + if sep := opt.(MatchSingleLines).Separator; sep != "" { + separator = []byte(sep) + } + } } - for _, opt := range opts { - want = opt.apply(want) + if match == "SINGLE" { + return slices.Compare(data, want) != 0 + } + + linesData := bytes.Split(data, separator) + for _, ld := range linesData { + if slices.Compare(ld, want) != 0 { + return true + } } - return slices.Compare(data, want) != 0 + return false }, - getViolation: func(filePath string, negated bool) rule.Violation { + func(filePath string, options options) rule.Violation { format := "file '%s' does not have content matching '%s'" - if negated { + + if options.matchSingleLines { + format = "file '%s' does not have all lines matching '%s'" + } + + if options.negated { format = "file '%s' does have content matching '%s'" } + if options.negated && options.matchSingleLines { + format = "file '%s' does have all lines matching '%s'" + } + return rule.NewViolation( fmt.Sprintf(format, filepath.Base(filePath), want), ) }, - } + opts..., + ) } diff --git a/internal/arch/file/should/have_content_matching_regex.go b/internal/arch/file/should/have_content_matching_regex.go new file mode 100644 index 0000000..3953c95 --- /dev/null +++ b/internal/arch/file/should/have_content_matching_regex.go @@ -0,0 +1,74 @@ +package should + +import ( + "bytes" + "fmt" + "goarkitect/internal/arch/rule" + "os" + "path/filepath" + "regexp" +) + +func HaveContentMatchingRegex(res string, opts ...Option) *Expression { + rx := regexp.MustCompile(res) + + return NewExpression( + func(rb rule.Builder, filePath string) bool { + data, err := os.ReadFile(filePath) + if err != nil { + rb.AddError(err) + + return true + } + + match := "SINGLE" + separator := []byte("\n") + for _, opt := range opts { + switch opt.(type) { + case IgnoreNewLinesAtTheEndOfFile: + data = bytes.TrimRight(data, "\n") + case IgnoreCase: + data = bytes.ToLower(data) + case MatchSingleLines: + match = "MULTIPLE" + if sep := opt.(MatchSingleLines).Separator; sep != "" { + separator = []byte(sep) + } + } + } + + if match == "SINGLE" { + return !rx.Match(data) + } + + linesData := bytes.Split(data, separator) + for _, ld := range linesData { + if !rx.Match(ld) { + return true + } + } + + return false + }, + func(filePath string, options options) rule.Violation { + format := "file '%s' does not have content matching regex '%s'" + + if options.matchSingleLines { + format = "file '%s' does not have all lines matching regex '%s'" + } + + if options.negated { + format = "file '%s' does have content matching regex '%s'" + } + + if options.negated && options.matchSingleLines { + format = "file '%s' does have all lines matching regex '%s'" + } + + return rule.NewViolation( + fmt.Sprintf(format, filepath.Base(filePath), res), + ) + }, + opts..., + ) +} diff --git a/internal/arch/file/should/have_content_matching_regex_test.go b/internal/arch/file/should/have_content_matching_regex_test.go new file mode 100644 index 0000000..708ae22 --- /dev/null +++ b/internal/arch/file/should/have_content_matching_regex_test.go @@ -0,0 +1,94 @@ +package should_test + +import ( + "goarkitect/internal/arch/file" + "goarkitect/internal/arch/file/should" + "goarkitect/internal/arch/rule" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func Test_HaveContentMatchingRegex(t *testing.T) { + basePath, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + testCases := []struct { + desc string + ruleBuilder *file.RuleBuilder + regexp string + options []should.Option + want []rule.Violation + }{ + { + desc: "content of file 'foobar.txt' matches regex", + ruleBuilder: file.One(filepath.Join(basePath, "test/foobar.txt")), + regexp: "^foo.+", + want: nil, + }, + { + desc: "content of file 'foobar.txt' matches regex, ignoring newlines at the end of file", + ruleBuilder: file.One(filepath.Join(basePath, "test/foobar.txt")), + regexp: "^foo.+", + options: []should.Option{ + should.IgnoreNewLinesAtTheEndOfFile{}, + }, + want: nil, + }, + { + desc: "content of file 'foobar.txt' matches expected content, ignoring case", + ruleBuilder: file.One(filepath.Join(basePath, "test/foobar.txt")), + regexp: "^foo.+", + options: []should.Option{ + should.IgnoreCase{}, + }, + want: nil, + }, + { + desc: "content of file 'foobar.txt' does not match expected content", + ruleBuilder: file.One(filepath.Join(basePath, "test/foobar.txt")), + regexp: "^something\\ else.+", + want: []rule.Violation{ + rule.NewViolation("file 'foobar.txt' does not have content matching regex '^something\\ else.+'"), + }, + }, + { + desc: "every line of file 'baz.txt' matches regex", + ruleBuilder: file.One(filepath.Join(basePath, "test/baz.txt")), + regexp: "^foo.+", + options: []should.Option{ + should.IgnoreNewLinesAtTheEndOfFile{}, + should.MatchSingleLines{}, + }, + want: nil, + }, + { + desc: "not every line of file 'baz.txt' matches regex", + ruleBuilder: file.One(filepath.Join(basePath, "test/baz.txt")), + regexp: "^bar.+", + options: []should.Option{ + should.IgnoreNewLinesAtTheEndOfFile{}, + should.MatchSingleLines{}, + }, + want: []rule.Violation{ + rule.NewViolation("file 'baz.txt' does not have all lines matching regex '^bar.+'"), + }, + }, + } + + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + hcm := should.HaveContentMatchingRegex(tC.regexp, tC.options...) + got := hcm.Evaluate(tC.ruleBuilder) + + if !cmp.Equal(got, tC.want, cmp.AllowUnexported(rule.Violation{}), cmpopts.EquateEmpty()) { + t.Errorf("want = %+v, got = %+v", tC.want, got) + } + }) + } +} diff --git a/internal/arch/file/should/have_content_matching_test.go b/internal/arch/file/should/have_content_matching_test.go index 552e42b..1ba5cd2 100644 --- a/internal/arch/file/should/have_content_matching_test.go +++ b/internal/arch/file/should/have_content_matching_test.go @@ -22,41 +22,63 @@ func Test_HaveContentMatching(t *testing.T) { desc string ruleBuilder *file.RuleBuilder content string - options []should.ContentMatchOption + options []should.Option want []rule.Violation }{ { - desc: "content of file 'foobar' matches expected content", + desc: "content of file 'foobar.txt' matches expected content", ruleBuilder: file.One(filepath.Join(basePath, "test/foobar.txt")), content: "foo bar baz quux\n", want: nil, }, { - desc: "content of file 'foobar' matches expected content, ignoring newlines at the end of file", + desc: "content of file 'foobar.txt' matches expected content, ignoring newlines at the end of file", ruleBuilder: file.One(filepath.Join(basePath, "test/foobar.txt")), content: "foo bar baz quux", - options: []should.ContentMatchOption{ + options: []should.Option{ should.IgnoreNewLinesAtTheEndOfFile{}, }, want: nil, }, { - desc: "content of file 'foobar' matches expected content, ignoring case", + desc: "content of file 'foobar.txt' matches expected content, ignoring case", ruleBuilder: file.One(filepath.Join(basePath, "test/foobar.txt")), content: "FOO BAR BAZ QUUX\n", - options: []should.ContentMatchOption{ + options: []should.Option{ should.IgnoreCase{}, }, want: nil, }, { - desc: "content of file 'foobar' does not match expected content", + desc: "content of file 'foobar.txt' does not match expected content", ruleBuilder: file.One(filepath.Join(basePath, "test/foobar.txt")), content: "something else", want: []rule.Violation{ rule.NewViolation("file 'foobar.txt' does not have content matching 'something else'"), }, }, + { + desc: "every line of file 'baz.txt' matches expected content", + ruleBuilder: file.One(filepath.Join(basePath, "test/foobar.txt")), + content: "foo bar baz quux", + options: []should.Option{ + should.IgnoreNewLinesAtTheEndOfFile{}, + should.MatchSingleLines{}, + }, + want: nil, + }, + { + desc: "not every line of file 'baz.txt' matches regex", + ruleBuilder: file.One(filepath.Join(basePath, "test/baz.txt")), + content: "something else", + options: []should.Option{ + should.IgnoreNewLinesAtTheEndOfFile{}, + should.MatchSingleLines{}, + }, + want: []rule.Violation{ + rule.NewViolation("file 'baz.txt' does not have all lines matching 'something else'"), + }, + }, } for _, tC := range testCases { diff --git a/internal/arch/file/should/match_glob.go b/internal/arch/file/should/match_glob.go index 27658ed..534d3e7 100644 --- a/internal/arch/file/should/match_glob.go +++ b/internal/arch/file/should/match_glob.go @@ -6,9 +6,9 @@ import ( "path/filepath" ) -func MatchGlob(glob string, basePath string) *Expression { - return &Expression{ - evaluate: func(rb rule.Builder, filePath string) bool { +func MatchGlob(glob string, basePath string, opts ...Option) *Expression { + return NewExpression( + func(rb rule.Builder, filePath string) bool { match, err := filepath.Match(filepath.Join(basePath, glob), filePath) if err != nil { rb.AddError(err) @@ -16,9 +16,9 @@ func MatchGlob(glob string, basePath string) *Expression { return !match }, - getViolation: func(filePath string, negated bool) rule.Violation { + func(filePath string, options options) rule.Violation { format := "file's path '%s' does not match glob pattern '%s'" - if negated { + if options.negated { format = "file's path '%s' does match glob pattern '%s'" } @@ -30,5 +30,6 @@ func MatchGlob(glob string, basePath string) *Expression { ), ) }, - } + opts..., + ) } diff --git a/internal/arch/file/should/match_glob_test.go b/internal/arch/file/should/match_glob_test.go index ce632a7..7f5c9a7 100644 --- a/internal/arch/file/should/match_glob_test.go +++ b/internal/arch/file/should/match_glob_test.go @@ -21,8 +21,8 @@ func Test_MatchGlob(t *testing.T) { newRuleBuilder := func() *file.RuleBuilder { rb := file.All() rb.SetFiles([]string{ - filepath.Join(basePath, "test/project3/baz.go"), - filepath.Join(basePath, "test/project3/quux.go"), + filepath.Join(basePath, "test/project3/baz.txt"), + filepath.Join(basePath, "test/project3/quux.txt"), }) return rb } @@ -34,24 +34,24 @@ func Test_MatchGlob(t *testing.T) { want []rule.Violation }{ { - desc: "project3 matches '*.go'", + desc: "project3 matches '*.txt'", ruleBuilder: newRuleBuilder(), - glob: "*/*/*.go", + glob: "*/*/*.txt", want: nil, }, { - desc: "project3 matches 'foo/*/*.go'", + desc: "project3 matches 'foo/*/*.txt'", ruleBuilder: newRuleBuilder(), - glob: "test/*/*.go", + glob: "test/*/*.txt", want: nil, }, { - desc: "project3 does not match '**/*.ts'", + desc: "project3 does not match '**/*.doc'", ruleBuilder: newRuleBuilder(), - glob: "**/*.ts", + glob: "**/*.doc", want: []rule.Violation{ - rule.NewViolation("file's path 'baz.go' does not match glob pattern '**/*.ts'"), - rule.NewViolation("file's path 'quux.go' does not match glob pattern '**/*.ts'"), + rule.NewViolation("file's path 'baz.txt' does not match glob pattern '**/*.doc'"), + rule.NewViolation("file's path 'quux.txt' does not match glob pattern '**/*.doc'"), }, }, } diff --git a/internal/arch/file/should/match_regex.go b/internal/arch/file/should/match_regex.go index 74f020d..d1bd5bb 100644 --- a/internal/arch/file/should/match_regex.go +++ b/internal/arch/file/should/match_regex.go @@ -7,18 +7,18 @@ import ( "regexp" ) -func MatchRegex(res string) *Expression { +func MatchRegex(res string, opts ...Option) *Expression { rx := regexp.MustCompile(res) - return &Expression{ - evaluate: func(_ rule.Builder, filePath string) bool { + return NewExpression( + func(_ rule.Builder, filePath string) bool { return !rx.MatchString( filepath.Base(filePath), ) }, - getViolation: func(filePath string, negated bool) rule.Violation { + func(filePath string, options options) rule.Violation { format := "file's name '%s' does not match regex '%s'" - if negated { + if options.negated { format = "file's name '%s' does match regex '%s'" } @@ -30,5 +30,6 @@ func MatchRegex(res string) *Expression { ), ) }, - } + opts..., + ) } diff --git a/internal/arch/file/should/should.go b/internal/arch/file/should/should.go index c62cc06..6c3093f 100644 --- a/internal/arch/file/should/should.go +++ b/internal/arch/file/should/should.go @@ -5,18 +5,44 @@ import ( "goarkitect/internal/arch/rule" ) +type evaluateFunc func(rb rule.Builder, filePath string) bool +type getViolationFunc func(filePath string, options options) rule.Violation +type options struct { + negated bool + ignoreCase bool + ignoreNewLinesAtTheEndOfFile bool + matchSingleLines bool +} + +func NewExpression( + evaluate evaluateFunc, + getViolation getViolationFunc, + opts ...Option, +) *Expression { + expr := &Expression{ + evaluate: evaluate, + getViolation: getViolation, + } + + for _, opt := range opts { + opt.apply(expr) + } + + return expr +} + type Expression struct { - negated bool - evaluate func(rb rule.Builder, filePath string) bool - getViolation func(filePath string, negated bool) rule.Violation + options options + evaluate evaluateFunc + getViolation getViolationFunc } -func (e Expression) Evaluate(rb rule.Builder) []rule.Violation { +func (e *Expression) Evaluate(rb rule.Builder) []rule.Violation { violations := make([]rule.Violation, 0) for _, fp := range rb.(*file.RuleBuilder).GetFiles() { result := e.evaluate(rb, fp) - if (!e.negated && result) || (e.negated && !result) { - violations = append(violations, e.getViolation(fp, e.negated)) + if (!e.options.negated && result) || (e.options.negated && !result) { + violations = append(violations, e.getViolation(fp, e.options)) } } @@ -24,7 +50,37 @@ func (e Expression) Evaluate(rb rule.Builder) []rule.Violation { } func Not(expr *Expression) *Expression { - expr.negated = !expr.negated + expr.options.negated = !expr.options.negated return expr } + +type Option interface { + apply(expr *Expression) +} + +type Negated struct{} + +func (opt Negated) apply(expr *Expression) { + expr.options.negated = !expr.options.negated +} + +type IgnoreCase struct{} + +func (opt IgnoreCase) apply(expr *Expression) { + expr.options.ignoreCase = true +} + +type IgnoreNewLinesAtTheEndOfFile struct{} + +func (opt IgnoreNewLinesAtTheEndOfFile) apply(expr *Expression) { + expr.options.ignoreNewLinesAtTheEndOfFile = true +} + +type MatchSingleLines struct { + Separator string +} + +func (opt MatchSingleLines) apply(expr *Expression) { + expr.options.matchSingleLines = true +} diff --git a/internal/arch/file/should/start_with.go b/internal/arch/file/should/start_with.go index 828d991..5170ce4 100644 --- a/internal/arch/file/should/start_with.go +++ b/internal/arch/file/should/start_with.go @@ -6,19 +6,19 @@ import ( "path/filepath" ) -func StartWith(prefix string) *Expression { +func StartWith(prefix string, opts ...Option) *Expression { le := len(prefix) - return &Expression{ - evaluate: func(_ rule.Builder, filePath string) bool { + return NewExpression( + func(_ rule.Builder, filePath string) bool { fileName := filepath.Base(filePath) lf := len(fileName) return le <= lf && fileName[:le] != prefix }, - getViolation: func(filePath string, negated bool) rule.Violation { + func(filePath string, options options) rule.Violation { format := "file's name '%s' does not start with '%s'" - if negated { + if options.negated { format = "file's name '%s' does start with '%s'" } @@ -26,5 +26,6 @@ func StartWith(prefix string) *Expression { fmt.Sprintf(format, filepath.Base(filePath), prefix), ) }, - } + opts..., + ) } diff --git a/internal/arch/file/should/test/baz.txt b/internal/arch/file/should/test/baz.txt new file mode 100644 index 0000000..aa9570a --- /dev/null +++ b/internal/arch/file/should/test/baz.txt @@ -0,0 +1,3 @@ +foo bar +foo baz +foo quux diff --git a/internal/arch/file/test/project3/baz.go b/internal/arch/file/test/project3/baz.go deleted file mode 100644 index fe6548e..0000000 --- a/internal/arch/file/test/project3/baz.go +++ /dev/null @@ -1,5 +0,0 @@ -package bar - -func baz() string { - return "baz" -} diff --git a/internal/arch/file/test/project3/baz.txt b/internal/arch/file/test/project3/baz.txt new file mode 100644 index 0000000..2668907 --- /dev/null +++ b/internal/arch/file/test/project3/baz.txt @@ -0,0 +1 @@ +This is some baz content diff --git a/internal/arch/file/test/project3/quux.go b/internal/arch/file/test/project3/quux.go deleted file mode 100644 index 9e25330..0000000 --- a/internal/arch/file/test/project3/quux.go +++ /dev/null @@ -1,5 +0,0 @@ -package bar - -func quux() string { - return "quux" -} diff --git a/internal/arch/file/test/project3/quux.txt b/internal/arch/file/test/project3/quux.txt new file mode 100644 index 0000000..b3e336e --- /dev/null +++ b/internal/arch/file/test/project3/quux.txt @@ -0,0 +1 @@ +This is some quux content