diff --git a/cmd/go-mutesting/main.go b/cmd/go-mutesting/main.go index 5749905..4df55d9 100644 --- a/cmd/go-mutesting/main.go +++ b/cmd/go-mutesting/main.go @@ -20,12 +20,12 @@ import ( "gopkg.in/yaml.v3" - "github.com/avito-tech/go-mutesting/internal/annotation" "github.com/avito-tech/go-mutesting/internal/console" "github.com/avito-tech/go-mutesting/internal/filter" "github.com/avito-tech/go-mutesting/internal/importing" "github.com/avito-tech/go-mutesting/internal/models" "github.com/avito-tech/go-mutesting/internal/parser" + "github.com/avito-tech/go-mutesting/internal/processor/annotation" "github.com/avito-tech/go-mutesting/internal/reportmaker" "github.com/jessevdk/go-flags" "github.com/zimmski/osutil" @@ -205,7 +205,7 @@ MUTATOR: for _, file := range files { console.Verbose(opts, "Mutate %q", file) - annotationProcessor := annotation.NewProcessor() + annotationProcessor := annotation.NewProcessor(annotation.WithGlobalRegexpFilter(opts.Config.ExcludeRegexp...)) skipFilterProcessor := filter.NewSkipMakeArgsFilter() collectors := []filter.NodeCollector{ diff --git a/config.yml.dist b/config.yml.dist index ad57957..20f3cc5 100644 --- a/config.yml.dist +++ b/config.yml.dist @@ -3,4 +3,4 @@ skip_with_build_tags: true json_output: false silent_mode: false exclude_dirs: - - example + - example \ No newline at end of file diff --git a/internal/models/options.go b/internal/models/options.go index 3c7d3a3..5a9565f 100644 --- a/internal/models/options.go +++ b/internal/models/options.go @@ -47,5 +47,6 @@ type Options struct { HTMLOutput bool `yaml:"html_output"` SilentMode bool `yaml:"silent_mode"` ExcludeDirs []string `yaml:"exclude_dirs"` + ExcludeRegexp []string `yaml:"exclude_regexp"` } } diff --git a/internal/parser/parse_test.go b/internal/parser/parse_test.go index 40f2e2c..e3e4cba 100644 --- a/internal/parser/parse_test.go +++ b/internal/parser/parse_test.go @@ -1,11 +1,11 @@ package parser import ( + "github.com/avito-tech/go-mutesting/internal/processor/annotation" "testing" "github.com/stretchr/testify/assert" - "github.com/avito-tech/go-mutesting/internal/annotation" "github.com/avito-tech/go-mutesting/internal/filter" ) diff --git a/internal/annotation/annotation.go b/internal/processor/annotation/annotation.go similarity index 89% rename from internal/annotation/annotation.go rename to internal/processor/annotation/annotation.go index ebd603e..1835498 100644 --- a/internal/annotation/annotation.go +++ b/internal/processor/annotation/annotation.go @@ -19,26 +19,38 @@ const ( // Processor handles mutation exclusion logic based on source code annotations. type Processor struct { + options + FunctionAnnotation FunctionAnnotation RegexAnnotation RegexAnnotation LineAnnotation LineAnnotation } // NewProcessor creates and returns a new initialized Processor. -func NewProcessor() *Processor { - return &Processor{ +func NewProcessor(optionFunc ...OptionFunc) *Processor { + opts := options{} + + for _, f := range optionFunc { + f(&opts) + } + + processor := &Processor{ + options: opts, FunctionAnnotation: FunctionAnnotation{ Exclusions: make(map[token.Pos]struct{}), // *ast.FuncDecl node + all its children Name: FuncAnnotation}, RegexAnnotation: RegexAnnotation{ - Exclusions: make(map[int]map[token.Pos]mutatorInfo), // source code line -> node -> excluded mutators - Name: RegexpAnnotation, + GlobalRegexCollector: NewRegexCollector(opts.global.filteredRegexps), + Exclusions: make(map[int]map[token.Pos]mutatorInfo), // source code line -> node -> excluded mutators + Name: RegexpAnnotation, }, LineAnnotation: LineAnnotation{ Exclusions: make(map[int]map[token.Pos]mutatorInfo), // source code line -> node -> excluded mutators Name: NextLineAnnotation, }, } + + return processor } type mutatorInfo struct { @@ -46,7 +58,12 @@ type mutatorInfo struct { } // Collect processes an AST file to gather all mutation exclusions based on annotations. -func (p *Processor) Collect(file *ast.File, fset *token.FileSet, fileAbs string) { +func (p *Processor) Collect( + file *ast.File, + fset *token.FileSet, + fileAbs string, +) { + // comment based collectors for _, decl := range file.Decls { if f, ok := decl.(*ast.FuncDecl); ok { if p.existsFuncAnnotation(f) { @@ -64,6 +81,8 @@ func (p *Processor) Collect(file *ast.File, fset *token.FileSet, fileAbs string) } } + p.RegexAnnotation.GlobalRegexCollector.Collect(fset, file, fileAbs) + p.collectNodesForBlockStmt() } diff --git a/internal/annotation/annotation_test.go b/internal/processor/annotation/annotation_test.go similarity index 94% rename from internal/annotation/annotation_test.go rename to internal/processor/annotation/annotation_test.go index 1e2d9e8..224fa8f 100644 --- a/internal/annotation/annotation_test.go +++ b/internal/processor/annotation/annotation_test.go @@ -467,3 +467,29 @@ func TestCollect(t *testing.T) { }) } + +func TestCollectGlobal(t *testing.T) { + filePath := "../../testdata/annotation/global/collect.go" + + fs := token.NewFileSet() + file, err := parser.ParseFile( + fs, + filePath, + nil, + parser.AllErrors|parser.ParseComments, + ) + assert.NoError(t, err) + + processor := NewProcessor(WithGlobalRegexpFilter("\\.Log *")) + + processor.Collect(file, fs, filePath) + + assert.NotEmpty(t, processor.RegexAnnotation.GlobalRegexCollector.Exclusions) + assert.Equal(t, processor.RegexAnnotation.GlobalRegexCollector.Exclusions, map[int]map[token.Pos]mutatorInfo{ + 17: { + 256: {Names: []string{"*"}}, + 263: {Names: []string{"*"}}, + 267: {Names: []string{"*"}}, + }, + }) +} diff --git a/internal/annotation/block.go b/internal/processor/annotation/block.go similarity index 100% rename from internal/annotation/block.go rename to internal/processor/annotation/block.go diff --git a/internal/annotation/chain.go b/internal/processor/annotation/chain.go similarity index 92% rename from internal/annotation/chain.go rename to internal/processor/annotation/chain.go index 25cf258..21e3201 100644 --- a/internal/annotation/chain.go +++ b/internal/processor/annotation/chain.go @@ -45,9 +45,15 @@ type NextLineAnnotationCollector struct { } // Handle processes regex pattern annotations, delegating other types to the next handler. -func (r *RegexAnnotationCollector) Handle(name string, comment *ast.Comment, fset *token.FileSet, file *ast.File, fileAbs string) { +func (r *RegexAnnotationCollector) Handle( + name string, + comment *ast.Comment, + fset *token.FileSet, + file *ast.File, + fileAbs string, +) { if name == RegexpAnnotation { - r.Processor.collectMatchNodes(comment, fset, file, fileAbs) + r.Processor.collectMatchNodes(comment.Text, fset, file, fileAbs) } else { r.BaseCollector.Handle(name, comment, fset, file, fileAbs) } diff --git a/internal/annotation/function.go b/internal/processor/annotation/function.go similarity index 100% rename from internal/annotation/function.go rename to internal/processor/annotation/function.go diff --git a/internal/annotation/line.go b/internal/processor/annotation/line.go similarity index 100% rename from internal/annotation/line.go rename to internal/processor/annotation/line.go diff --git a/internal/processor/annotation/options.go b/internal/processor/annotation/options.go new file mode 100644 index 0000000..742d845 --- /dev/null +++ b/internal/processor/annotation/options.go @@ -0,0 +1,19 @@ +package annotation + +type filters struct { + filteredRegexps []string +} + +type options struct { + global filters +} + +// OptionFunc function that allows you to change options +type OptionFunc func(*options) + +// WithGlobalRegexpFilter returns OptionFunc which enables global regexp exclusion for mutators +func WithGlobalRegexpFilter(filteredRegexps ...string) OptionFunc { + return func(o *options) { + o.global.filteredRegexps = filteredRegexps + } +} diff --git a/internal/annotation/regex.go b/internal/processor/annotation/regex.go similarity index 89% rename from internal/annotation/regex.go rename to internal/processor/annotation/regex.go index 285e5b4..e0e2bda 100644 --- a/internal/annotation/regex.go +++ b/internal/processor/annotation/regex.go @@ -12,8 +12,9 @@ import ( // RegexAnnotation represents a collection of exclusions based on regex pattern matches. type RegexAnnotation struct { - Exclusions map[int]map[token.Pos]mutatorInfo - Name string + GlobalRegexCollector RegexCollector + Exclusions map[int]map[token.Pos]mutatorInfo + Name string } // parseRegexAnnotation parses a comment line containing a regex annotation. @@ -46,8 +47,13 @@ func (r *RegexAnnotation) parseRegexAnnotation(comment string) (*regexp.Regexp, // 1. Parsing the regex pattern and mutators from the comment // 2. Finding all lines in the file that match the regex // 3. Recording nodes from matching lines to be excluded -func (r *RegexAnnotation) collectMatchNodes(comment *ast.Comment, fset *token.FileSet, file *ast.File, fileAbs string) { - regex, mutators := r.parseRegexAnnotation(comment.Text) +func (r *RegexAnnotation) collectMatchNodes( + comment string, + fset *token.FileSet, + file *ast.File, + fileAbs string, +) { + regex, mutators := r.parseRegexAnnotation(comment) lines, err := r.findLinesMatchingRegex(fileAbs, regex) if err != nil { diff --git a/internal/processor/annotation/regexcollector.go b/internal/processor/annotation/regexcollector.go new file mode 100644 index 0000000..3ed9308 --- /dev/null +++ b/internal/processor/annotation/regexcollector.go @@ -0,0 +1,135 @@ +package annotation + +import ( + "bufio" + "go/ast" + "go/token" + "log" + "os" + "regexp" + "strings" +) + +// Collector defines the interface for handlers. +// Implementations should handle specific annotation types. +type Collector interface { + // Handle processes an annotation if it matches the handler's type, + // otherwise delegates to the next handler in the chain. + Handle(name string, comment *ast.Comment, fset *token.FileSet, file *ast.File, fileAbs string) +} + +// RegexExclusion structure that contains info required for ast.Node exclusion from mutations +type RegexExclusion struct { + regex *regexp.Regexp + mutators mutatorInfo +} + +// RegexCollector Collector based on regular expressions parse all file +type RegexCollector struct { + Exclusions map[int]map[token.Pos]mutatorInfo + GlobalExclusionsRegex []RegexExclusion +} + +// NewRegexCollector constructor for RegexCollector +func NewRegexCollector( + exclusionsConfig []string, +) RegexCollector { + exclusionsRegex := make([]RegexExclusion, 0, len(exclusionsConfig)) + for _, exclusion := range exclusionsConfig { + re, inf := parseConfig(exclusion) + if re != nil { + exclusionsRegex = append(exclusionsRegex, RegexExclusion{ + regex: re, + mutators: inf, + }) + } + } + + return RegexCollector{ + Exclusions: make(map[int]map[token.Pos]mutatorInfo), + GlobalExclusionsRegex: exclusionsRegex, + } +} + +// Collect processes regex pattern +func (r *RegexCollector) Collect( + fset *token.FileSet, + file *ast.File, + fileAbs string, +) { + for _, exclusions := range r.GlobalExclusionsRegex { + lines, err := r.findLinesMatchingRegex(fileAbs, exclusions.regex) + if err != nil { + log.Printf("Error scaning a source file: %v", err) + } + + if len(lines) > 0 { + collectExcludedNodes(fset, file, lines, r.Exclusions, exclusions.mutators) + } + } +} + +func parseConfig(configLine string) (*regexp.Regexp, mutatorInfo) { + // splitted[0] - contains regexp splitted[1] contains mutators + splitted := strings.SplitN(configLine, " ", 2) + + if len(splitted) < 1 { + return nil, mutatorInfo{} + } + + pattern := splitted[0] + re, err := regexp.Compile(pattern) + if err != nil { + log.Printf("Warning: invalid regex in annotation: %q, error: %v\n", pattern, err) + return nil, mutatorInfo{} + } + + var mutators []string + if len(splitted) > 1 { + mutators = parseMutators(splitted[1]) + } else { + mutators = []string{"*"} + } + + return re, mutatorInfo{ + Names: mutators, + } +} + +// findLinesMatchingRegex scans a source file and returns line numbers that match the given regex. +func (r *RegexCollector) findLinesMatchingRegex(filePath string, regex *regexp.Regexp) ([]int, error) { + var matchedLineNumbers []int + + if regex == nil { + return matchedLineNumbers, nil + } + + f, err := os.Open(filePath) + if err != nil { + log.Printf("Error opening file: %v", err) + } + + reader := bufio.NewReader(f) + + lineNumber := 0 + for { + line, err := reader.ReadString('\n') + if err != nil { + break + } + + if regex.MatchString(line) { + matchedLineNumbers = append(matchedLineNumbers, lineNumber+1) + } + lineNumber++ + } + + defer func() { + err = f.Close() + if err != nil { + log.Printf("Error while file closing duting processing regex annotation: %v", err.Error()) + } + }() + + return matchedLineNumbers, nil +} diff --git a/mutator/statement/remove.go b/mutator/statement/remove.go index 73fd2bd..68b8514 100644 --- a/mutator/statement/remove.go +++ b/mutator/statement/remove.go @@ -1,7 +1,7 @@ package statement import ( - "github.com/avito-tech/go-mutesting/internal/annotation" + "github.com/avito-tech/go-mutesting/internal/processor/annotation" "go/ast" "go/token" "go/types" diff --git a/test/mutator.go b/test/mutator.go index 736c382..93b13ae 100644 --- a/test/mutator.go +++ b/test/mutator.go @@ -3,9 +3,9 @@ package test import ( "bytes" "fmt" - "github.com/avito-tech/go-mutesting/internal/annotation" "github.com/avito-tech/go-mutesting/internal/filter" "github.com/avito-tech/go-mutesting/internal/parser" + "github.com/avito-tech/go-mutesting/internal/processor/annotation" "go/printer" "os" "testing" diff --git a/testdata/annotation/global/collect.go b/testdata/annotation/global/collect.go new file mode 100644 index 0000000..9e97bc7 --- /dev/null +++ b/testdata/annotation/global/collect.go @@ -0,0 +1,21 @@ +//go:build examplemain +// +build examplemain + +package main + +type Logger struct{} + +func (l *Logger) Log(items ...any) {} + +func (l *Logger) Debug(items ...any) {} + +func main() { + logger := &Logger{} + _, err := fmt.Println("hello world") + + if err != nil { + logger.Log(err) + } else { + logger.Debug("debug log") + } +}