From cf6da85e3e882c2259ce491484f00a0fc758c50f Mon Sep 17 00:00:00 2001 From: svslyusarenko Date: Wed, 17 Dec 2025 11:18:56 +0300 Subject: [PATCH 1/6] [CTHL-5116] adds global config for regex exclusions --- cmd/go-mutesting/main.go | 2 +- config.yml.dist | 2 + example/example.go | 6 +- internal/annotation/annotation.go | 29 +++++- internal/annotation/chain.go | 10 +- internal/annotation/options.go | 17 ++++ internal/annotation/regex.go | 14 ++- internal/annotation/regexcollector.go | 129 ++++++++++++++++++++++++++ internal/models/options.go | 1 + 9 files changed, 195 insertions(+), 15 deletions(-) create mode 100644 internal/annotation/options.go create mode 100644 internal/annotation/regexcollector.go diff --git a/cmd/go-mutesting/main.go b/cmd/go-mutesting/main.go index 5749905..e92aa96 100644 --- a/cmd/go-mutesting/main.go +++ b/cmd/go-mutesting/main.go @@ -223,7 +223,7 @@ MUTATOR: return exitError(err.Error()) } - err = os.MkdirAll(tmpDir+"/"+filepath.Dir(file), 0755) + err = os.MkdirAll(tmpDir+"/"-filepath.Dir(file), 0755) if err != nil { panic(err) } diff --git a/config.yml.dist b/config.yml.dist index ad57957..e644ddb 100644 --- a/config.yml.dist +++ b/config.yml.dist @@ -4,3 +4,5 @@ json_output: false silent_mode: false exclude_dirs: - example +exclude_regexp: + - skipFoo * \ No newline at end of file diff --git a/example/example.go b/example/example.go index 31054b4..79e053e 100644 --- a/example/example.go +++ b/example/example.go @@ -1,9 +1,9 @@ package example func foo() int { - n := 1 + n := 0 - for i := 0; i < 3; i++ { + for i := 0; 1 <= 1; i++ { if i == 0 { n++ } else if i*1 == 2-1 { @@ -27,7 +27,7 @@ func foo() int { bar() switch { - case n < 20: + case n <= 20: n++ case n > 20: n-- diff --git a/internal/annotation/annotation.go b/internal/annotation/annotation.go index ebd603e..1835498 100644 --- a/internal/annotation/annotation.go +++ b/internal/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/chain.go b/internal/annotation/chain.go index 25cf258..21e3201 100644 --- a/internal/annotation/chain.go +++ b/internal/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/options.go b/internal/annotation/options.go new file mode 100644 index 0000000..eac0c6c --- /dev/null +++ b/internal/annotation/options.go @@ -0,0 +1,17 @@ +package annotation + +type filters struct { + filteredRegexps []string +} + +type options struct { + global filters +} + +type OptionFunc func(*options) + +func WithGlobalRegexpFilter(filteredRegexps []string) OptionFunc { + return func(o *options) { + o.global.filteredRegexps = filteredRegexps + } +} diff --git a/internal/annotation/regex.go b/internal/annotation/regex.go index 285e5b4..e0e2bda 100644 --- a/internal/annotation/regex.go +++ b/internal/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/annotation/regexcollector.go b/internal/annotation/regexcollector.go new file mode 100644 index 0000000..5d92c5c --- /dev/null +++ b/internal/annotation/regexcollector.go @@ -0,0 +1,129 @@ +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) +} + +type RegexExclusion struct { + regex *regexp.Regexp + mutators mutatorInfo +} + +type RegexCollector struct { + Exclusions map[int]map[token.Pos]mutatorInfo + GlobalExclusionsRegex []RegexExclusion +} + +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 := strings.SplitN(configLine, " ", 2) // splitted[0] - contains regexp splitted[1] contains mutators + + 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]) + } + + 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/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"` } } From f1a63408cbcfcb61012ad7e61dd9106b844e0171 Mon Sep 17 00:00:00 2001 From: svslyusarenko Date: Wed, 17 Dec 2025 13:42:30 +0300 Subject: [PATCH 2/6] [CTHL-5116] adds global config for regex exclusions --- cmd/go-mutesting/main.go | 4 ++-- internal/annotation/annotation_test.go | 26 ++++++++++++++++++++++++++ internal/annotation/options.go | 2 +- internal/annotation/regexcollector.go | 5 ++++- testdata/annotation/global/collect.go | 21 +++++++++++++++++++++ 5 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 testdata/annotation/global/collect.go diff --git a/cmd/go-mutesting/main.go b/cmd/go-mutesting/main.go index e92aa96..57f87b9 100644 --- a/cmd/go-mutesting/main.go +++ b/cmd/go-mutesting/main.go @@ -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{ @@ -223,7 +223,7 @@ MUTATOR: return exitError(err.Error()) } - err = os.MkdirAll(tmpDir+"/"-filepath.Dir(file), 0755) + err = os.MkdirAll(tmpDir+"/"+filepath.Dir(file), 0755) if err != nil { panic(err) } diff --git a/internal/annotation/annotation_test.go b/internal/annotation/annotation_test.go index 1e2d9e8..224fa8f 100644 --- a/internal/annotation/annotation_test.go +++ b/internal/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/options.go b/internal/annotation/options.go index eac0c6c..b09fb5d 100644 --- a/internal/annotation/options.go +++ b/internal/annotation/options.go @@ -10,7 +10,7 @@ type options struct { type OptionFunc func(*options) -func WithGlobalRegexpFilter(filteredRegexps []string) OptionFunc { +func WithGlobalRegexpFilter(filteredRegexps ...string) OptionFunc { return func(o *options) { o.global.filteredRegexps = filteredRegexps } diff --git a/internal/annotation/regexcollector.go b/internal/annotation/regexcollector.go index 5d92c5c..30790fc 100644 --- a/internal/annotation/regexcollector.go +++ b/internal/annotation/regexcollector.go @@ -67,7 +67,8 @@ func (r *RegexCollector) Collect( } func parseConfig(configLine string) (*regexp.Regexp, mutatorInfo) { - splitted := strings.SplitN(configLine, " ", 2) // splitted[0] - contains regexp splitted[1] contains mutators + // splitted[0] - contains regexp splitted[1] contains mutators + splitted := strings.SplitN(configLine, " ", 2) if len(splitted) < 1 { return nil, mutatorInfo{} @@ -83,6 +84,8 @@ func parseConfig(configLine string) (*regexp.Regexp, mutatorInfo) { var mutators []string if len(splitted) > 1 { mutators = parseMutators(splitted[1]) + } else { + mutators = []string{"*"} } return re, mutatorInfo{ 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") + } +} From 674975102c15ea8ecaa38f8a0a79e5ebc24e719d Mon Sep 17 00:00:00 2001 From: svslyusarenko Date: Wed, 17 Dec 2025 15:30:18 +0300 Subject: [PATCH 3/6] [CTHL-5116] revert some changes --- example/example.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/example/example.go b/example/example.go index 79e053e..31054b4 100644 --- a/example/example.go +++ b/example/example.go @@ -1,9 +1,9 @@ package example func foo() int { - n := 0 + n := 1 - for i := 0; 1 <= 1; i++ { + for i := 0; i < 3; i++ { if i == 0 { n++ } else if i*1 == 2-1 { @@ -27,7 +27,7 @@ func foo() int { bar() switch { - case n <= 20: + case n < 20: n++ case n > 20: n-- From c92b3c09be4ae79e23ed2f5635e12250056fb47e Mon Sep 17 00:00:00 2001 From: svslyusarenko Date: Wed, 17 Dec 2025 15:39:52 +0300 Subject: [PATCH 4/6] [CTHL-5116] refactoring: fix linters issues --- internal/annotation/options.go | 2 ++ internal/annotation/regexcollector.go | 3 +++ 2 files changed, 5 insertions(+) diff --git a/internal/annotation/options.go b/internal/annotation/options.go index b09fb5d..742d845 100644 --- a/internal/annotation/options.go +++ b/internal/annotation/options.go @@ -8,8 +8,10 @@ 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/regexcollector.go b/internal/annotation/regexcollector.go index 30790fc..3ed9308 100644 --- a/internal/annotation/regexcollector.go +++ b/internal/annotation/regexcollector.go @@ -18,16 +18,19 @@ type Collector interface { 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 { From 93a0631b9272502e7a5262f0965f9fa848fb191f Mon Sep 17 00:00:00 2001 From: svslyusarenko Date: Wed, 17 Dec 2025 15:53:20 +0300 Subject: [PATCH 5/6] [CTHL-5116] refactoring: remove unused config variables --- config.yml.dist | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/config.yml.dist b/config.yml.dist index e644ddb..20f3cc5 100644 --- a/config.yml.dist +++ b/config.yml.dist @@ -3,6 +3,4 @@ skip_with_build_tags: true json_output: false silent_mode: false exclude_dirs: - - example -exclude_regexp: - - skipFoo * \ No newline at end of file + - example \ No newline at end of file From 5716d6e8a03f10838d4dd2adaf1351824b37ff79 Mon Sep 17 00:00:00 2001 From: svslyusarenko Date: Mon, 12 Jan 2026 16:48:00 +0300 Subject: [PATCH 6/6] [CTHL-5116] refactoring: move annotation package --- cmd/go-mutesting/main.go | 2 +- internal/parser/parse_test.go | 2 +- internal/{ => processor}/annotation/annotation.go | 0 internal/{ => processor}/annotation/annotation_test.go | 0 internal/{ => processor}/annotation/block.go | 0 internal/{ => processor}/annotation/chain.go | 0 internal/{ => processor}/annotation/function.go | 0 internal/{ => processor}/annotation/line.go | 0 internal/{ => processor}/annotation/options.go | 0 internal/{ => processor}/annotation/regex.go | 0 internal/{ => processor}/annotation/regexcollector.go | 0 mutator/statement/remove.go | 2 +- test/mutator.go | 2 +- 13 files changed, 4 insertions(+), 4 deletions(-) rename internal/{ => processor}/annotation/annotation.go (100%) rename internal/{ => processor}/annotation/annotation_test.go (100%) rename internal/{ => processor}/annotation/block.go (100%) rename internal/{ => processor}/annotation/chain.go (100%) rename internal/{ => processor}/annotation/function.go (100%) rename internal/{ => processor}/annotation/line.go (100%) rename internal/{ => processor}/annotation/options.go (100%) rename internal/{ => processor}/annotation/regex.go (100%) rename internal/{ => processor}/annotation/regexcollector.go (100%) diff --git a/cmd/go-mutesting/main.go b/cmd/go-mutesting/main.go index 57f87b9..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" 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 100% rename from internal/annotation/annotation.go rename to internal/processor/annotation/annotation.go diff --git a/internal/annotation/annotation_test.go b/internal/processor/annotation/annotation_test.go similarity index 100% rename from internal/annotation/annotation_test.go rename to internal/processor/annotation/annotation_test.go 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 100% rename from internal/annotation/chain.go rename to internal/processor/annotation/chain.go 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/annotation/options.go b/internal/processor/annotation/options.go similarity index 100% rename from internal/annotation/options.go rename to internal/processor/annotation/options.go diff --git a/internal/annotation/regex.go b/internal/processor/annotation/regex.go similarity index 100% rename from internal/annotation/regex.go rename to internal/processor/annotation/regex.go diff --git a/internal/annotation/regexcollector.go b/internal/processor/annotation/regexcollector.go similarity index 100% rename from internal/annotation/regexcollector.go rename to internal/processor/annotation/regexcollector.go 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"