Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/go-mutesting/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
2 changes: 1 addition & 1 deletion config.yml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ skip_with_build_tags: true
json_output: false
silent_mode: false
exclude_dirs:
- example
- example
29 changes: 24 additions & 5 deletions internal/annotation/annotation.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,34 +19,51 @@ 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 {
Names []string
}

// 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) {
Expand All @@ -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()
}

Expand Down
26 changes: 26 additions & 0 deletions internal/annotation/annotation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{"*"}},
},
})
}
10 changes: 8 additions & 2 deletions internal/annotation/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
19 changes: 19 additions & 0 deletions internal/annotation/options.go
Original file line number Diff line number Diff line change
@@ -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
}
}
14 changes: 10 additions & 4 deletions internal/annotation/regex.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
135 changes: 135 additions & 0 deletions internal/annotation/regexcollector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package annotation
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

глобальное исключение по конфигу - это уже не аннотирование. стоит перенести в другой пакет


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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

не совсем понимаю, зачем нам тут нужен интерфейс

// 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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

не очень красиво, когда функция полностью скопипащена из другого файла. давай сделаем ее общей и будем переиспользовать

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
}
1 change: 1 addition & 0 deletions internal/models/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
}
21 changes: 21 additions & 0 deletions testdata/annotation/global/collect.go
Original file line number Diff line number Diff line change
@@ -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")
}
}