├── .github └── workflows │ └── makefile.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── Makefile ├── README.md ├── astutil ├── create.go └── query.go ├── cmd └── go-mutesting │ ├── go-mutesting-bash_completion.sh │ ├── main.go │ └── main_test.go ├── config.yml.dist ├── example ├── a.go ├── b.go ├── example.go ├── example_test.go └── sub │ ├── sub.go │ └── sub_test.go ├── go.mod ├── go.sum ├── internal ├── annotation │ ├── annotation.go │ ├── annotation_test.go │ ├── block.go │ ├── chain.go │ ├── function.go │ ├── line.go │ └── regex.go ├── console │ └── printer.go ├── filter │ ├── filter.go │ ├── skip_mutation.go │ └── skip_mutation_test.go ├── importing │ ├── filepath.go │ ├── filepath_test.go │ ├── filepathfixtures │ │ ├── first.go │ │ ├── second.go │ │ ├── second_test.go │ │ ├── secondfixturespackage │ │ │ └── fourth.go │ │ ├── third.go │ │ └── third_test.go │ └── import.go ├── models │ ├── .coverignore │ ├── options.go │ └── report.go └── parser │ ├── diff.go │ ├── diff_test.go │ ├── parse.go │ └── parse_test.go ├── mutator ├── arithmetic │ ├── assign_invert.go │ ├── assign_invert_test.go │ ├── assignment.go │ ├── assignment_test.go │ ├── base.go │ ├── base_test.go │ ├── bitwise.go │ └── bitwise_test.go ├── branch │ ├── mutatecase.go │ ├── mutatecase_test.go │ ├── mutateelse.go │ ├── mutateelse_test.go │ ├── mutateif.go │ └── mutateif_test.go ├── conditional │ ├── negated.go │ └── negated_test.go ├── expression │ ├── comparison.go │ ├── comparison_test.go │ ├── remove.go │ └── remove_test.go ├── loop │ ├── break.go │ ├── break_test.go │ ├── condition.go │ ├── condition_test.go │ ├── range_break.go │ └── range_break_test.go ├── mutation.go ├── mutator.go ├── mutator_test.go ├── numbers │ ├── decrementer.go │ ├── decrementer_test.go │ ├── incrementer.go │ └── incrementer_test.go └── statement │ ├── remove.go │ └── remove_test.go ├── scripts ├── ci │ ├── errcheck.sh │ ├── gofmt.sh │ ├── govet.sh │ └── lint.sh └── exec │ ├── test-current-directory.sh │ └── test-mutated-package.sh ├── test └── mutator.go ├── testdata ├── annotation │ ├── collect.go │ ├── empty.go │ └── regex.go ├── arithmetic │ ├── assign_invert.go │ ├── assign_invert.go.0.go │ ├── assign_invert.go.1.go │ ├── assign_invert.go.2.go │ ├── assign_invert.go.3.go │ ├── assign_invert.go.4.go │ ├── assignment.go │ ├── assignment.go.0.go │ ├── assignment.go.1.go │ ├── assignment.go.10.go │ ├── assignment.go.2.go │ ├── assignment.go.3.go │ ├── assignment.go.4.go │ ├── assignment.go.5.go │ ├── assignment.go.6.go │ ├── assignment.go.7.go │ ├── assignment.go.8.go │ ├── assignment.go.9.go │ ├── base.go │ ├── base.go.0.go │ ├── base.go.1.go │ ├── base.go.2.go │ ├── base.go.3.go │ ├── base.go.4.go │ ├── bitwise.go │ ├── bitwise.go.0.go │ ├── bitwise.go.1.go │ ├── bitwise.go.2.go │ ├── bitwise.go.3.go │ ├── bitwise.go.4.go │ └── bitwise.go.5.go ├── branch │ ├── mutatecase.go │ ├── mutatecase.go.0.go │ ├── mutatecase.go.1.go │ ├── mutatecase.go.2.go │ ├── mutateelse.go │ ├── mutateelse.go.0.go │ ├── mutateif.go │ ├── mutateif.go.0.go │ └── mutateif.go.1.go ├── conditional │ ├── negated.go │ ├── negated.go.0.go │ ├── negated.go.1.go │ ├── negated.go.2.go │ ├── negated.go.3.go │ ├── negated.go.4.go │ └── negated.go.5.go ├── configs │ ├── configForJson.yml.test │ └── configSkipWithoutTest.yml.test ├── expression │ ├── comparison.go │ ├── comparison.go.0.go │ ├── comparison.go.1.go │ ├── comparison.go.2.go │ ├── comparison.go.3.go │ ├── remove.go │ ├── remove.go.0.go │ ├── remove.go.1.go │ ├── remove.go.2.go │ ├── remove.go.3.go │ ├── remove.go.4.go │ └── remove.go.5.go ├── loop │ ├── break.go │ ├── break.go.0.go │ ├── break.go.1.go │ ├── condition.go │ ├── condition.go.0.go │ ├── condition.go.1.go │ ├── range_break.go │ ├── range_break.go.0.go │ └── range_break.go.1.go ├── numbers │ ├── decrementer.go │ ├── decrementer.go.0.go │ ├── decrementer.go.1.go │ ├── incrementer.go │ ├── incrementer.go.0.go │ └── incrementer.go.1.go └── statement │ ├── remove.go │ ├── remove.go.0.go │ ├── remove.go.1.go │ ├── remove.go.10.go │ ├── remove.go.11.go │ ├── remove.go.12.go │ ├── remove.go.13.go │ ├── remove.go.14.go │ ├── remove.go.15.go │ ├── remove.go.16.go │ ├── remove.go.2.go │ ├── remove.go.3.go │ ├── remove.go.4.go │ ├── remove.go.5.go │ ├── remove.go.6.go │ ├── remove.go.7.go │ ├── remove.go.8.go │ └── remove.go.9.go └── walk.go /.github/workflows/makefile.yml: -------------------------------------------------------------------------------- 1 | name: Makefile for git actions 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | env: 11 | GOPATH: ${{ github.workspace }} 12 | defaults: 13 | run: 14 | working-directory: ${{ env.GOPATH }}/src/github.com/${{ github.repository }} 15 | 16 | steps: 17 | - name: Set up Go 18 | uses: actions/setup-go@v2 19 | with: 20 | go-version: 1.23.6 21 | 22 | - uses: actions/checkout@v2 23 | with: 24 | path: ${{ env.GOPATH }}/src/github.com/${{ github.repository }} 25 | 26 | - name: Install dependencies 27 | run: make install-dependencies 28 | 29 | - name: Install tools 30 | run: make install-tools 31 | 32 | - name: Install 33 | run: make install 34 | 35 | - name: Run go errcheck 36 | run: make ci-errcheck 37 | 38 | - name: Run go fmt 39 | run: make ci-gofmt 40 | 41 | - name: Run go vet 42 | run: make ci-govet 43 | 44 | - name: Run lint 45 | run: make ci-lint 46 | 47 | - name: Run test 48 | run: make test-verbose-with-coverage 49 | 50 | - name: Run gover 51 | run: gover 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.formatFlags": [ 3 | "-s" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Markus Zimmermann 4 | Copyright (c) 2021 Avito 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean clean-coverage generate install install-dependencies install-tools lint test test-verbose test-verbose-with-coverage 2 | 3 | export ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) 4 | export PKG := github.com/avito-tech/go-mutesting 5 | export ROOT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) 6 | 7 | export TEST_TIMEOUT_IN_SECONDS := 240 8 | 9 | $(eval $(ARGS):;@:) # turn arguments into do-nothing targets 10 | export ARGS 11 | 12 | ifdef ARGS 13 | PKG_TEST := $(ARGS) 14 | else 15 | PKG_TEST := $(PKG)/... 16 | endif 17 | 18 | all: install-dependencies install-tools install lint test 19 | .PHONY: all 20 | 21 | clean: 22 | go clean -i $(PKG)/... 23 | go clean -i -race $(PKG)/... 24 | .PHONY: clean 25 | 26 | clean-coverage: 27 | find $(ROOT_DIR) | grep .coverprofile | xargs rm 28 | .PHONY: clean-coverage 29 | 30 | generate: clean 31 | go generate $(PKG)/... 32 | .PHONY: generate 33 | 34 | install: 35 | go install -v $(PKG)/... 36 | .PHONY: install 37 | 38 | install-dependencies: 39 | go get -t -v $(PKG)/... 40 | go test -v $(PKG)/... 41 | .PHONY: install-dependencies 42 | 43 | install-tools: 44 | # generation 45 | go install golang.org/x/tools/cmd/stringer 46 | 47 | # linting 48 | go install golang.org/x/lint/golint@latest 49 | go install github.com/kisielk/errcheck@latest 50 | 51 | # code coverage 52 | go install github.com/onsi/ginkgo/ginkgo@latest 53 | go install github.com/modocache/gover@latest 54 | go install github.com/mattn/goveralls@latest 55 | .PHONY: install-tools 56 | 57 | lint: ci-errcheck ci-gofmt ci-govet ci-lint 58 | .PHONY: lint 59 | 60 | test: 61 | go test -race -test.timeout "$(TEST_TIMEOUT_IN_SECONDS)s" $(PKG_TEST) 62 | .PHONY: test 63 | 64 | test-verbose: 65 | go test -race -test.timeout "$(TEST_TIMEOUT_IN_SECONDS)s" -v $(PKG_TEST) 66 | .PHONY: test-verbose 67 | 68 | test-verbose-with-coverage: 69 | ginkgo -r -v -cover -race -skipPackage="testdata" 70 | .PHONY: test-verbose-with-coverage 71 | 72 | ci-errcheck: 73 | $(ROOT_DIR)/scripts/ci/errcheck.sh 74 | .PHONY: ci-errcheck 75 | 76 | ci-gofmt: 77 | $(ROOT_DIR)/scripts/ci/gofmt.sh 78 | .PHONY: ci-gofmt 79 | 80 | ci-govet: 81 | $(ROOT_DIR)/scripts/ci/govet.sh 82 | .PHONY: ci-govet 83 | 84 | ci-lint: 85 | $(ROOT_DIR)/scripts/ci/lint.sh 86 | .PHONY: ci-lint -------------------------------------------------------------------------------- /astutil/create.go: -------------------------------------------------------------------------------- 1 | package astutil 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | ) 8 | 9 | // CreateNoopOfStatement creates a syntactically safe noop statement out of a given statement. 10 | func CreateNoopOfStatement(pkg *types.Package, info *types.Info, stmt ast.Stmt) ast.Stmt { 11 | return CreateNoopOfStatements(pkg, info, []ast.Stmt{stmt}) 12 | } 13 | 14 | // CreateNoopOfStatements creates a syntactically safe noop statement out of a given statement. 15 | func CreateNoopOfStatements(pkg *types.Package, info *types.Info, stmts []ast.Stmt) ast.Stmt { 16 | var ids []ast.Expr 17 | for _, stmt := range stmts { 18 | ids = append(ids, IdentifiersInStatement(pkg, info, stmt)...) 19 | } 20 | 21 | if len(ids) == 0 { 22 | return &ast.EmptyStmt{ 23 | Semicolon: token.NoPos, 24 | } 25 | } 26 | 27 | lhs := make([]ast.Expr, len(ids)) 28 | for i := range ids { 29 | lhs[i] = ast.NewIdent("_") 30 | } 31 | 32 | return &ast.AssignStmt{ 33 | Lhs: lhs, 34 | Rhs: ids, 35 | Tok: token.ASSIGN, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /astutil/query.go: -------------------------------------------------------------------------------- 1 | package astutil 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | ) 8 | 9 | // IdentifiersInStatement returns all identifiers with their found in a statement. 10 | func IdentifiersInStatement(pkg *types.Package, info *types.Info, stmt ast.Stmt) []ast.Expr { 11 | w := &identifierWalker{ 12 | pkg: pkg, 13 | info: info, 14 | } 15 | 16 | ast.Walk(w, stmt) 17 | 18 | return w.identifiers 19 | } 20 | 21 | type identifierWalker struct { 22 | identifiers []ast.Expr 23 | pkg *types.Package 24 | info *types.Info 25 | } 26 | 27 | func checkForSelectorExpr(node ast.Expr) bool { 28 | switch n := node.(type) { 29 | case *ast.Ident: 30 | return true 31 | case *ast.SelectorExpr: 32 | return checkForSelectorExpr(n.X) 33 | } 34 | 35 | return false 36 | } 37 | 38 | func (w *identifierWalker) Visit(node ast.Node) ast.Visitor { 39 | switch n := node.(type) { 40 | case *ast.Ident: 41 | // Ignore the blank identifier 42 | if n.Name == "_" { 43 | return nil 44 | } 45 | 46 | // Ignore keywords 47 | if token.Lookup(n.Name) != token.IDENT { 48 | return nil 49 | } 50 | 51 | // We are only interested in variables 52 | if obj, ok := w.info.Uses[n]; ok { 53 | if _, ok := obj.(*types.Var); !ok { 54 | return nil 55 | } 56 | } 57 | 58 | // FIXME instead of manually creating a new node, clone it and trim the node from its comments and position https://github.com/zimmski/go-mutesting/issues/49 59 | w.identifiers = append(w.identifiers, &ast.Ident{ 60 | Name: n.Name, 61 | }) 62 | 63 | return nil 64 | case *ast.SelectorExpr: 65 | if !checkForSelectorExpr(n) { 66 | return nil 67 | } 68 | 69 | // Check if we need to instantiate the expression 70 | initialize := false 71 | if n.Sel != nil { 72 | if obj, ok := w.info.Uses[n.Sel]; ok { 73 | t := obj.Type() 74 | 75 | switch t.Underlying().(type) { 76 | case *types.Array, *types.Map, *types.Slice, *types.Struct: 77 | initialize = true 78 | } 79 | } 80 | } 81 | 82 | if initialize { 83 | // FIXME we need to clone the node and trim comments and position recursively https://github.com/zimmski/go-mutesting/issues/49 84 | w.identifiers = append(w.identifiers, &ast.CompositeLit{ 85 | Type: n, 86 | }) 87 | } else { 88 | // FIXME we need to clone the node and trim comments and position recursively https://github.com/zimmski/go-mutesting/issues/49 89 | w.identifiers = append(w.identifiers, n) 90 | } 91 | 92 | return nil 93 | } 94 | 95 | return w 96 | } 97 | 98 | // Functions returns all found functions. 99 | func Functions(n ast.Node) []*ast.FuncDecl { 100 | w := &functionWalker{} 101 | 102 | ast.Walk(w, n) 103 | 104 | return w.functions 105 | } 106 | 107 | type functionWalker struct { 108 | functions []*ast.FuncDecl 109 | } 110 | 111 | func (w *functionWalker) Visit(node ast.Node) ast.Visitor { 112 | switch n := node.(type) { 113 | case *ast.FuncDecl: 114 | w.functions = append(w.functions, n) 115 | 116 | return nil 117 | } 118 | 119 | return w 120 | } 121 | -------------------------------------------------------------------------------- /cmd/go-mutesting/go-mutesting-bash_completion.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | _go_mutesting() { 4 | args=("${COMP_WORDS[@]:1:$COMP_CWORD}") 5 | 6 | local IFS=$'\n' 7 | COMPREPLY=($(GO_FLAGS_COMPLETION=1 ${COMP_WORDS[0]} "${args[@]}")) 8 | return 1 9 | } 10 | 11 | complete -F _go_mutesting go-mutesting 12 | -------------------------------------------------------------------------------- /cmd/go-mutesting/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "encoding/json" 7 | "fmt" 8 | "go/ast" 9 | "go/format" 10 | "go/printer" 11 | "go/token" 12 | "go/types" 13 | "io" 14 | "log" 15 | "os" 16 | "os/exec" 17 | "path/filepath" 18 | "regexp" 19 | "strings" 20 | "syscall" 21 | 22 | "gopkg.in/yaml.v3" 23 | 24 | "github.com/avito-tech/go-mutesting/internal/annotation" 25 | "github.com/avito-tech/go-mutesting/internal/console" 26 | "github.com/avito-tech/go-mutesting/internal/filter" 27 | "github.com/avito-tech/go-mutesting/internal/importing" 28 | "github.com/avito-tech/go-mutesting/internal/models" 29 | "github.com/avito-tech/go-mutesting/internal/parser" 30 | "github.com/jessevdk/go-flags" 31 | "github.com/zimmski/osutil" 32 | 33 | "github.com/avito-tech/go-mutesting" 34 | "github.com/avito-tech/go-mutesting/astutil" 35 | "github.com/avito-tech/go-mutesting/mutator" 36 | _ "github.com/avito-tech/go-mutesting/mutator/arithmetic" 37 | _ "github.com/avito-tech/go-mutesting/mutator/branch" 38 | _ "github.com/avito-tech/go-mutesting/mutator/expression" 39 | _ "github.com/avito-tech/go-mutesting/mutator/loop" 40 | _ "github.com/avito-tech/go-mutesting/mutator/numbers" 41 | _ "github.com/avito-tech/go-mutesting/mutator/statement" 42 | ) 43 | 44 | const ( 45 | returnOk = iota 46 | returnHelp 47 | returnBashCompletion 48 | returnError 49 | ) 50 | 51 | func checkArguments(args []string, opts *models.Options) (bool, int) { 52 | p := flags.NewNamedParser("go-mutesting", flags.None) 53 | 54 | p.ShortDescription = "Mutation testing for Go source code" 55 | 56 | if _, err := p.AddGroup("go-mutesting", "go-mutesting arguments", opts); err != nil { 57 | return true, exitError(err.Error()) 58 | } 59 | 60 | completion := len(os.Getenv("GO_FLAGS_COMPLETION")) > 0 61 | 62 | _, err := p.ParseArgs(args) 63 | if (opts.General.Help || len(args) == 0) && !completion { 64 | p.WriteHelp(os.Stdout) 65 | 66 | return true, returnHelp 67 | } else if opts.Mutator.ListMutators { 68 | for _, name := range mutator.List() { 69 | fmt.Println(name) 70 | } 71 | 72 | return true, returnOk 73 | } 74 | 75 | if err != nil { 76 | return true, exitError(err.Error()) 77 | } 78 | 79 | if completion { 80 | return true, returnBashCompletion 81 | } 82 | 83 | if opts.General.Debug { 84 | opts.General.Verbose = true 85 | } 86 | 87 | if opts.General.Config != "" { 88 | yamlFile, err := os.ReadFile(opts.General.Config) 89 | if err != nil { 90 | return true, exitError("Could not read config file: %q", opts.General.Config) 91 | } 92 | err = yaml.Unmarshal(yamlFile, &opts.Config) 93 | if err != nil { 94 | return true, exitError("Could not unmarshall config file: %q, %v", opts.General.Config, err) 95 | } 96 | } 97 | 98 | return false, 0 99 | } 100 | 101 | func exitError(format string, args ...interface{}) int { 102 | _, _ = fmt.Fprintf(os.Stderr, format+"\n", args...) 103 | 104 | return returnError 105 | } 106 | 107 | type mutatorItem struct { 108 | Name string 109 | Mutator mutator.Mutator 110 | } 111 | 112 | func mainCmd(args []string) int { 113 | var opts = &models.Options{} 114 | var mutationBlackList = map[string]struct{}{} 115 | 116 | if exit, exitCode := checkArguments(args, opts); exit { 117 | return exitCode 118 | } 119 | 120 | files := importing.FilesOfArgs(opts.Remaining.Targets, opts) 121 | if len(files) == 0 { 122 | return exitError("Could not find any suitable Go source files") 123 | } 124 | 125 | if opts.Files.ListFiles { 126 | for _, file := range files { 127 | fmt.Println(file) 128 | } 129 | 130 | return returnOk 131 | } else if opts.Files.PrintAST { 132 | for _, file := range files { 133 | fmt.Println(file) 134 | 135 | src, _, err := parser.ParseFile(file) 136 | if err != nil { 137 | return exitError("Could not open file %q: %v", file, err) 138 | } 139 | 140 | mutesting.PrintWalk(src) 141 | 142 | fmt.Println() 143 | } 144 | 145 | return returnOk 146 | } 147 | 148 | if len(opts.Files.Blacklist) > 0 { 149 | for _, f := range opts.Files.Blacklist { 150 | c, err := os.ReadFile(f) 151 | if err != nil { 152 | return exitError("Cannot read blacklist file %q: %v", f, err) 153 | } 154 | 155 | for _, line := range strings.Split(string(c), "\n") { 156 | if line == "" { 157 | continue 158 | } 159 | 160 | if len(line) != 32 { 161 | return exitError("%q is not a MD5 checksum", line) 162 | } 163 | 164 | mutationBlackList[line] = struct{}{} 165 | } 166 | } 167 | } 168 | 169 | var mutators []mutatorItem 170 | 171 | MUTATOR: 172 | for _, name := range mutator.List() { 173 | if len(opts.Mutator.DisableMutators) > 0 { 174 | for _, d := range opts.Mutator.DisableMutators { 175 | pattern := strings.HasSuffix(d, "*") 176 | 177 | if (pattern && strings.HasPrefix(name, d[:len(d)-2])) || (!pattern && name == d) { 178 | continue MUTATOR 179 | } 180 | } 181 | } 182 | 183 | console.Verbose(opts, "Enable mutator %q", name) 184 | 185 | m, _ := mutator.New(name) 186 | mutators = append(mutators, mutatorItem{ 187 | Name: name, 188 | Mutator: m, 189 | }) 190 | } 191 | 192 | tmpDir, err := os.MkdirTemp("", "go-mutesting-") 193 | if err != nil { 194 | panic(err) 195 | } 196 | console.Verbose(opts, "Save mutations into %q", tmpDir) 197 | 198 | var execs []string 199 | if opts.Exec.Exec != "" { 200 | execs = strings.Split(opts.Exec.Exec, " ") 201 | } 202 | 203 | report := &models.Report{} 204 | 205 | for _, file := range files { 206 | console.Verbose(opts, "Mutate %q", file) 207 | 208 | annotationProcessor := annotation.NewProcessor() 209 | skipFilterProcessor := filter.NewSkipMakeArgsFilter() 210 | 211 | collectors := []filter.NodeCollector{ 212 | annotationProcessor, 213 | skipFilterProcessor, 214 | } 215 | 216 | filters := []filter.NodeFilter{ 217 | annotationProcessor, 218 | skipFilterProcessor, 219 | } 220 | 221 | src, fset, pkg, info, err := parser.ParseAndTypeCheckFile(file, collectors) 222 | if err != nil { 223 | return exitError(err.Error()) 224 | } 225 | 226 | err = os.MkdirAll(tmpDir+"/"+filepath.Dir(file), 0755) 227 | if err != nil { 228 | panic(err) 229 | } 230 | 231 | tmpFile := tmpDir + "/" + file 232 | 233 | originalFile := fmt.Sprintf("%s.original", tmpFile) 234 | err = osutil.CopyFile(file, originalFile) 235 | if err != nil { 236 | panic(err) 237 | } 238 | console.Debug(opts, "Save original into %q", originalFile) 239 | 240 | mutationID := 0 241 | 242 | if opts.Filter.Match != "" { 243 | m, err := regexp.Compile(opts.Filter.Match) 244 | if err != nil { 245 | return exitError("Match regex is not valid: %v", err) 246 | } 247 | 248 | for _, f := range astutil.Functions(src) { 249 | if m.MatchString(f.Name.Name) { 250 | mutationID = mutate(opts, mutators, mutationBlackList, mutationID, pkg, info, file, fset, src, f, tmpFile, execs, report, filters) 251 | } 252 | } 253 | } else { 254 | _ = mutate(opts, mutators, mutationBlackList, mutationID, pkg, info, file, fset, src, src, tmpFile, execs, report, filters) 255 | } 256 | } 257 | 258 | if !opts.General.DoNotRemoveTmpFolder { 259 | err = os.RemoveAll(tmpDir) 260 | if err != nil { 261 | panic(err) 262 | } 263 | console.Debug(opts, "Remove %q", tmpDir) 264 | } 265 | 266 | report.Calculate() 267 | 268 | if !opts.Exec.NoExec { 269 | if !opts.Config.SilentMode { 270 | fmt.Printf("The mutation score is %f (%d passed, %d failed, %d duplicated, %d skipped, total is %d)\n", 271 | report.Stats.Msi, 272 | report.Stats.KilledCount, 273 | report.Stats.EscapedCount, 274 | report.Stats.DuplicatedCount, 275 | report.Stats.SkippedCount, 276 | report.Stats.TotalMutantsCount, 277 | ) 278 | } 279 | } else { 280 | fmt.Println("Cannot do a mutation testing summary since no exec command was executed.") 281 | } 282 | 283 | jsonContent, err := json.Marshal(report) 284 | if err != nil { 285 | return exitError(err.Error()) 286 | } 287 | 288 | file, err := os.OpenFile(models.ReportFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) 289 | if err != nil { 290 | return exitError(err.Error()) 291 | } 292 | 293 | if file == nil { 294 | return exitError("Cannot create file for report") 295 | } 296 | 297 | defer func() { 298 | err = file.Close() 299 | if err != nil { 300 | fmt.Printf("Error while report file closing: %v", err.Error()) 301 | } 302 | }() 303 | 304 | _, err = file.WriteString(string(jsonContent)) 305 | if err != nil { 306 | return exitError(err.Error()) 307 | } 308 | 309 | console.Verbose(opts, "Save report into %q", models.ReportFileName) 310 | 311 | return returnOk 312 | } 313 | 314 | func mutate( 315 | opts *models.Options, 316 | mutators []mutatorItem, 317 | mutationBlackList map[string]struct{}, 318 | mutationID int, 319 | pkg *types.Package, 320 | info *types.Info, 321 | originalFile string, 322 | fset *token.FileSet, 323 | src ast.Node, 324 | node ast.Node, 325 | mutatedFile string, 326 | execs []string, 327 | stats *models.Report, 328 | filters []filter.NodeFilter, 329 | ) int { 330 | for _, m := range mutators { 331 | console.Debug(opts, "Mutator %s", m.Name) 332 | 333 | mutatorAnnotated := annotation.DecoratorFilter(m.Mutator, m.Name, filters...) 334 | 335 | changed := mutesting.MutateWalk(pkg, info, node, mutatorAnnotated) 336 | 337 | for { 338 | _, ok := <-changed 339 | 340 | if !ok { 341 | break 342 | } 343 | 344 | originalSourceCode, err := os.ReadFile(originalFile) 345 | if err != nil { 346 | log.Fatal(err) 347 | } 348 | 349 | mutant := models.Mutant{} 350 | mutant.Mutator.MutatorName = m.Name 351 | mutant.Mutator.OriginalFilePath = originalFile 352 | mutant.Mutator.OriginalSourceCode = string(originalSourceCode) 353 | 354 | mutationFile := fmt.Sprintf("%s.%d", mutatedFile, mutationID) 355 | checksum, duplicate, err := saveAST(mutationBlackList, mutationFile, fset, src) 356 | if err != nil { 357 | fmt.Printf("INTERNAL ERROR %s\n", err.Error()) 358 | } else if duplicate { 359 | console.Debug(opts, "%q is a duplicate, we ignore it", mutationFile) 360 | 361 | stats.Stats.DuplicatedCount++ 362 | } else { 363 | console.Debug(opts, "Save mutation into %q with checksum %s", mutationFile, checksum) 364 | 365 | if !opts.Exec.NoExec { 366 | execExitCode := mutateExec(opts, pkg, originalFile, src, mutationFile, execs, &mutant) 367 | 368 | console.Debug(opts, "Exited with %d", execExitCode) 369 | 370 | mutatedSourceCode, err := os.ReadFile(mutationFile) 371 | if err != nil { 372 | log.Fatal(err) 373 | } 374 | mutant.Mutator.MutatedSourceCode = string(mutatedSourceCode) 375 | 376 | msg := fmt.Sprintf("%q with checksum %s", mutationFile, checksum) 377 | 378 | switch execExitCode { 379 | case 0: // Tests failed - all ok 380 | out := fmt.Sprintf("PASS %s\n", msg) 381 | if !opts.Config.SilentMode { 382 | console.PrintPass(out) 383 | } 384 | 385 | mutant.ProcessOutput = out 386 | stats.Killed = append(stats.Killed, mutant) 387 | stats.Stats.KilledCount++ 388 | case 1: // Tests passed 389 | out := fmt.Sprintf("FAIL %s\n", msg) 390 | if !opts.Config.SilentMode { 391 | console.PrintFail(out) 392 | } 393 | 394 | mutant.ProcessOutput = out 395 | stats.Escaped = append(stats.Escaped, mutant) 396 | stats.Stats.EscapedCount++ 397 | case 2: // Did not compile 398 | out := fmt.Sprintf("SKIP %s\n", msg) 399 | if !opts.Config.SilentMode { 400 | console.PrintSkip(out) 401 | } 402 | 403 | mutant.ProcessOutput = out 404 | stats.Stats.SkippedCount++ 405 | default: 406 | out := fmt.Sprintf("UNKOWN exit code for %s\n", msg) 407 | if !opts.Config.SilentMode { 408 | console.PrintUnknown(out) 409 | } 410 | 411 | mutant.ProcessOutput = out 412 | stats.Errored = append(stats.Errored, mutant) 413 | stats.Stats.ErrorCount++ 414 | } 415 | } 416 | } 417 | 418 | changed <- true 419 | 420 | // Ignore original state 421 | <-changed 422 | changed <- true 423 | 424 | mutationID++ 425 | } 426 | } 427 | 428 | return mutationID 429 | } 430 | 431 | func mutateExec( 432 | opts *models.Options, 433 | pkg *types.Package, 434 | file string, 435 | src ast.Node, 436 | mutationFile string, 437 | execs []string, 438 | mutant *models.Mutant, 439 | ) (execExitCode int) { 440 | if len(execs) == 0 { 441 | console.Debug(opts, "Execute built-in exec command for mutation") 442 | 443 | diff, err := exec.Command("diff", "--label=Original", "--label=New", "-u", file, mutationFile).CombinedOutput() 444 | 445 | startLine := parser.FindOriginalStartLine(diff) 446 | mutant.Mutator.OriginalStartLine = startLine 447 | 448 | if err == nil { 449 | execExitCode = 0 450 | } else if e, ok := err.(*exec.ExitError); ok { 451 | execExitCode = e.Sys().(syscall.WaitStatus).ExitStatus() 452 | } else { 453 | panic(err) 454 | } 455 | if execExitCode != 0 && execExitCode != 1 { 456 | fmt.Printf("%s\n", diff) 457 | 458 | panic("Could not execute diff on mutation file") 459 | } 460 | 461 | defer func() { 462 | _ = os.Rename(file+".tmp", file) 463 | }() 464 | 465 | err = os.Rename(file, file+".tmp") 466 | if err != nil { 467 | panic(err) 468 | } 469 | err = osutil.CopyFile(mutationFile, file) 470 | if err != nil { 471 | panic(err) 472 | } 473 | 474 | pkgName := pkg.Path() 475 | if opts.Test.Recursive { 476 | pkgName += "/..." 477 | } 478 | 479 | goTestCmd := exec.Command("go", "test", "-timeout", fmt.Sprintf("%ds", opts.Exec.Timeout), pkgName) 480 | goTestCmd.Env = os.Environ() 481 | 482 | test, err := goTestCmd.CombinedOutput() 483 | if err == nil { 484 | execExitCode = 0 485 | } else if e, ok := err.(*exec.ExitError); ok { 486 | execExitCode = e.Sys().(syscall.WaitStatus).ExitStatus() 487 | } else { 488 | panic(err) 489 | } 490 | 491 | if opts.General.Debug { 492 | fmt.Printf("%s\n", test) 493 | } 494 | 495 | mutant.Diff = string(diff) 496 | 497 | switch execExitCode { 498 | case 0: // Tests passed -> FAIL 499 | if !opts.Config.SilentMode { 500 | console.PrintDiff(diff) 501 | } 502 | 503 | execExitCode = 1 504 | case 1: // Tests failed -> PASS 505 | if opts.General.Debug { 506 | console.PrintDiff(diff) 507 | } 508 | 509 | execExitCode = 0 510 | case 2: // Did not compile -> SKIP 511 | if opts.General.Verbose { 512 | fmt.Println("Mutation did not compile") 513 | } 514 | 515 | if opts.General.Debug { 516 | console.PrintDiff(diff) 517 | } 518 | default: // Unknown exit code -> SKIP 519 | if !opts.Config.SilentMode { 520 | fmt.Println("Unknown exit code") 521 | console.PrintDiff(diff) 522 | } 523 | } 524 | 525 | return execExitCode 526 | } 527 | 528 | console.Debug(opts, "Execute %q for mutation", opts.Exec.Exec) 529 | 530 | execCommand := exec.Command(execs[0], execs[1:]...) 531 | 532 | execCommand.Stderr = os.Stderr 533 | execCommand.Stdout = os.Stdout 534 | 535 | execCommand.Env = append(os.Environ(), []string{ 536 | "MUTATE_CHANGED=" + mutationFile, 537 | fmt.Sprintf("MUTATE_DEBUG=%t", opts.General.Debug), 538 | "MUTATE_ORIGINAL=" + file, 539 | "MUTATE_PACKAGE=" + pkg.Path(), 540 | fmt.Sprintf("MUTATE_TIMEOUT=%d", opts.Exec.Timeout), 541 | fmt.Sprintf("MUTATE_VERBOSE=%t", opts.General.Verbose), 542 | }...) 543 | if opts.Test.Recursive { 544 | execCommand.Env = append(execCommand.Env, "TEST_RECURSIVE=true") 545 | } 546 | 547 | err := execCommand.Start() 548 | if err != nil { 549 | panic(err) 550 | } 551 | 552 | // TODO timeout here 553 | 554 | err = execCommand.Wait() 555 | 556 | if err == nil { 557 | execExitCode = 0 558 | } else if e, ok := err.(*exec.ExitError); ok { 559 | execExitCode = e.Sys().(syscall.WaitStatus).ExitStatus() 560 | } else { 561 | panic(err) 562 | } 563 | 564 | return execExitCode 565 | } 566 | 567 | func main() { 568 | os.Exit(mainCmd(os.Args[1:])) 569 | } 570 | 571 | func saveAST(mutationBlackList map[string]struct{}, file string, fset *token.FileSet, node ast.Node) (string, bool, error) { 572 | var buf bytes.Buffer 573 | 574 | h := md5.New() 575 | 576 | err := printer.Fprint(io.MultiWriter(h, &buf), fset, node) 577 | if err != nil { 578 | return "", false, err 579 | } 580 | 581 | checksum := fmt.Sprintf("%x", h.Sum(nil)) 582 | 583 | if _, ok := mutationBlackList[checksum]; ok { 584 | return checksum, true, nil 585 | } 586 | 587 | mutationBlackList[checksum] = struct{}{} 588 | 589 | src, err := format.Source(buf.Bytes()) 590 | if err != nil { 591 | return "", false, err 592 | } 593 | 594 | err = os.WriteFile(file, src, 0666) 595 | if err != nil { 596 | return "", false, err 597 | } 598 | 599 | return checksum, false, nil 600 | } 601 | -------------------------------------------------------------------------------- /cmd/go-mutesting/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "os" 9 | "testing" 10 | 11 | "github.com/avito-tech/go-mutesting/internal/models" 12 | 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestMainSimple(t *testing.T) { 17 | testMain( 18 | t, 19 | "../../example", 20 | []string{"--debug", "--exec-timeout", "1"}, 21 | returnOk, 22 | "The mutation score is 0.564516 (35 passed, 27 failed, 8 duplicated, 0 skipped, total is 62)", 23 | ) 24 | } 25 | 26 | func TestMainRecursive(t *testing.T) { 27 | testMain( 28 | t, 29 | "../../example", 30 | []string{"--debug", "--exec-timeout", "1", "./..."}, 31 | returnOk, 32 | "The mutation score is 0.590909 (39 passed, 27 failed, 8 duplicated, 0 skipped, total is 66)", 33 | ) 34 | } 35 | 36 | func TestMainFromOtherDirectory(t *testing.T) { 37 | testMain( 38 | t, 39 | "../..", 40 | []string{"--debug", "--exec-timeout", "1", "github.com/avito-tech/go-mutesting/example"}, 41 | returnOk, 42 | "The mutation score is 0.564516 (35 passed, 27 failed, 8 duplicated, 0 skipped, total is 62)", 43 | ) 44 | } 45 | 46 | func TestMainMatch(t *testing.T) { 47 | testMain( 48 | t, 49 | "../../example", 50 | []string{"--debug", "--exec", "../scripts/exec/test-mutated-package.sh", "--exec-timeout", "1", "--match", "baz", "./..."}, 51 | returnOk, 52 | "The mutation score is 0.500000 (4 passed, 4 failed, 0 duplicated, 0 skipped, total is 8)", 53 | ) 54 | } 55 | 56 | func TestMainSkipWithoutTest(t *testing.T) { 57 | testMain( 58 | t, 59 | "../../example", 60 | []string{"--debug", "--exec-timeout", "1", "--config", "../testdata/configs/configSkipWithoutTest.yml.test"}, 61 | returnOk, 62 | "The mutation score is 0.583333 (35 passed, 25 failed, 8 duplicated, 0 skipped, total is 60)", 63 | ) 64 | } 65 | 66 | func TestMainJSONReport(t *testing.T) { 67 | tmpDir, err := os.MkdirTemp("", "go-mutesting-main-test-") 68 | assert.NoError(t, err) 69 | 70 | reportFileName := "reportTestMainJSONReport.json" 71 | jsonFile := tmpDir + "/" + reportFileName 72 | if _, err := os.Stat(jsonFile); err == nil { 73 | err = os.Remove(jsonFile) 74 | assert.NoError(t, err) 75 | } 76 | 77 | models.ReportFileName = jsonFile 78 | 79 | testMain( 80 | t, 81 | "../../example", 82 | []string{"--debug", "--exec-timeout", "1", "--config", "../testdata/configs/configForJson.yml.test"}, 83 | returnOk, 84 | "The mutation score is 0.583333 (35 passed, 25 failed, 8 duplicated, 0 skipped, total is 60)", 85 | ) 86 | 87 | info, err := os.Stat(jsonFile) 88 | assert.NoError(t, err) 89 | assert.NotNil(t, info) 90 | 91 | defer func() { 92 | err = os.Remove(jsonFile) 93 | if err != nil { 94 | fmt.Println("Error while deleting temp file") 95 | } 96 | }() 97 | 98 | jsonData, err := os.ReadFile(jsonFile) 99 | assert.NoError(t, err) 100 | 101 | var mutationReport models.Report 102 | err = json.Unmarshal(jsonData, &mutationReport) 103 | assert.NoError(t, err) 104 | 105 | expectedStats := models.Stats{ 106 | TotalMutantsCount: 60, 107 | KilledCount: 35, 108 | NotCoveredCount: 0, 109 | EscapedCount: 25, 110 | ErrorCount: 0, 111 | SkippedCount: 0, 112 | TimeOutCount: 0, 113 | Msi: 0.5833333333333334, 114 | MutationCodeCoverage: 0, 115 | CoveredCodeMsi: 0, 116 | DuplicatedCount: 0, 117 | } 118 | 119 | assert.Equal(t, expectedStats, mutationReport.Stats) 120 | assert.Equal(t, 25, len(mutationReport.Escaped)) 121 | assert.Nil(t, mutationReport.Timeouted) 122 | assert.Equal(t, 35, len(mutationReport.Killed)) 123 | assert.Nil(t, mutationReport.Errored) 124 | 125 | for i := 0; i < len(mutationReport.Escaped); i++ { 126 | assert.Contains(t, mutationReport.Escaped[i].ProcessOutput, "FAIL") 127 | } 128 | for i := 0; i < len(mutationReport.Killed); i++ { 129 | assert.Contains(t, mutationReport.Killed[i].ProcessOutput, "PASS") 130 | } 131 | } 132 | 133 | func testMain(t *testing.T, root string, exec []string, expectedExitCode int, contains string) { 134 | saveStderr := os.Stderr 135 | saveStdout := os.Stdout 136 | saveCwd, err := os.Getwd() 137 | assert.Nil(t, err) 138 | 139 | r, w, err := os.Pipe() 140 | assert.Nil(t, err) 141 | 142 | os.Stderr = w 143 | os.Stdout = w 144 | assert.Nil(t, os.Chdir(root)) 145 | 146 | bufChannel := make(chan string) 147 | 148 | go func() { 149 | buf := new(bytes.Buffer) 150 | _, err = io.Copy(buf, r) 151 | assert.Nil(t, err) 152 | assert.Nil(t, r.Close()) 153 | 154 | bufChannel <- buf.String() 155 | }() 156 | 157 | exitCode := mainCmd(exec) 158 | 159 | assert.Nil(t, w.Close()) 160 | 161 | os.Stderr = saveStderr 162 | os.Stdout = saveStdout 163 | assert.Nil(t, os.Chdir(saveCwd)) 164 | 165 | out := <-bufChannel 166 | 167 | assert.Equal(t, expectedExitCode, exitCode) 168 | assert.Contains(t, out, contains) 169 | } 170 | -------------------------------------------------------------------------------- /config.yml.dist: -------------------------------------------------------------------------------- 1 | skip_without_test: true 2 | skip_with_build_tags: true 3 | json_output: false 4 | silent_mode: false 5 | exclude_dirs: 6 | - example 7 | -------------------------------------------------------------------------------- /example/a.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type A struct{} 8 | 9 | func fooA() (a A, b http.Header) { //nolint:unused 10 | _, _, _ = a, b, http.Header{} 11 | 12 | return a, b 13 | } 14 | -------------------------------------------------------------------------------- /example/b.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | func fooB() (a A, b http.Header) { //nolint:unused 8 | a, b = A{}, http.Header{} 9 | 10 | return a, b 11 | } 12 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | func foo() int { 4 | n := 1 5 | 6 | for i := 0; i < 3; i++ { 7 | if i == 0 { 8 | n++ 9 | } else if i*1 == 2-1 { 10 | n += 2 11 | } else { 12 | n += 3 13 | } 14 | 15 | n++ 16 | } 17 | 18 | if n < 0 { 19 | n = 0 20 | } 21 | 22 | n++ 23 | 24 | n += bar() 25 | 26 | bar() 27 | bar() 28 | 29 | switch { 30 | case n < 20: 31 | n++ 32 | case n > 20: 33 | n-- 34 | default: 35 | n = 0 36 | } 37 | 38 | skip := true 39 | if true { 40 | _ = skip 41 | } 42 | 43 | return n 44 | } 45 | 46 | func bar() int { 47 | return 4 48 | } 49 | 50 | func baz() int { //nolint:unused 51 | i := 1 52 | i = i + i 53 | 54 | return i 55 | } 56 | -------------------------------------------------------------------------------- /example/example_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestFoo(t *testing.T) { 10 | Equal(t, foo(), 16) 11 | } 12 | -------------------------------------------------------------------------------- /example/sub/sub.go: -------------------------------------------------------------------------------- 1 | package sub 2 | 3 | func baz() int { 4 | i := 1 5 | i = i + i 6 | 7 | return i 8 | } 9 | -------------------------------------------------------------------------------- /example/sub/sub_test.go: -------------------------------------------------------------------------------- 1 | package sub 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestBaz(t *testing.T) { 10 | Equal(t, baz(), 2) 11 | } 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/avito-tech/go-mutesting 2 | 3 | go 1.23.6 4 | 5 | require ( 6 | github.com/fatih/color v1.18.0 7 | github.com/jessevdk/go-flags v1.5.0 8 | github.com/stretchr/testify v1.9.0 9 | github.com/zimmski/osutil v1.6.1 10 | golang.org/x/tools v0.31.0 11 | gopkg.in/yaml.v3 v3.0.1 12 | ) 13 | 14 | require ( 15 | github.com/avast/retry-go v3.0.0+incompatible // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/mattn/go-colorable v0.1.13 // indirect 18 | github.com/mattn/go-isatty v0.0.20 // indirect 19 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect 20 | github.com/pkg/errors v0.9.1 // indirect 21 | github.com/pmezard/go-difflib v1.0.0 // indirect 22 | github.com/rivo/uniseg v0.4.7 // indirect 23 | github.com/rogpeppe/go-internal v1.9.0 // indirect 24 | github.com/schollz/progressbar/v3 v3.14.2 // indirect 25 | github.com/termie/go-shutil v0.0.0-20140729215957-bcacb06fecae // indirect 26 | github.com/ulikunitz/xz v0.5.11 // indirect 27 | golang.org/x/sys v0.31.0 // indirect 28 | golang.org/x/term v0.17.0 // indirect 29 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= 2 | github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 7 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 8 | github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= 9 | github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 10 | github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= 11 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 12 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 13 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 14 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 15 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 16 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 17 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 18 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 19 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 20 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 21 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 22 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 23 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= 24 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= 25 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 26 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 27 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 28 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 29 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 30 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 31 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 32 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 33 | github.com/schollz/progressbar/v3 v3.14.2 h1:EducH6uNLIWsr560zSV1KrTeUb/wZGAHqyMFIEa99ks= 34 | github.com/schollz/progressbar/v3 v3.14.2/go.mod h1:aQAZQnhF4JGFtRJiw/eobaXpsqpVQAftEQ+hLGXaRc4= 35 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 36 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 37 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 38 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 39 | github.com/termie/go-shutil v0.0.0-20140729215957-bcacb06fecae h1:vgGSvdW5Lqg+I1aZOlG32uyE6xHpLdKhZzcTEktz5wM= 40 | github.com/termie/go-shutil v0.0.0-20140729215957-bcacb06fecae/go.mod h1:quDq6Se6jlGwiIKia/itDZxqC5rj6/8OdFyMMAwTxCs= 41 | github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= 42 | github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 43 | github.com/zimmski/osutil v1.6.1 h1:YkOE1KOIWH/BjFwYdjNH3VEsNXmL4FxCzYPW5GkhTkE= 44 | github.com/zimmski/osutil v1.6.1/go.mod h1:ZKmKdiH2j5/sitEaLd4UOU3BA1o1dEXg8smRBxgT1W4= 45 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 46 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 47 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 48 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 49 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 50 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 51 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 52 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 53 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 54 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 55 | golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= 56 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 57 | golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= 58 | golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= 59 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 60 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 61 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 62 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 63 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 64 | -------------------------------------------------------------------------------- /internal/annotation/annotation.go: -------------------------------------------------------------------------------- 1 | package annotation 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | "strings" 8 | 9 | "github.com/avito-tech/go-mutesting/internal/filter" 10 | "github.com/avito-tech/go-mutesting/mutator" 11 | ) 12 | 13 | // Annotation constants define the comment patterns used to disable mutations 14 | const ( 15 | FuncAnnotation = "// mutator-disable-func" 16 | RegexpAnnotation = "// mutator-disable-regexp" 17 | NextLineAnnotation = "// mutator-disable-next-line" 18 | ) 19 | 20 | // Processor handles mutation exclusion logic based on source code annotations. 21 | type Processor struct { 22 | FunctionAnnotation FunctionAnnotation 23 | RegexAnnotation RegexAnnotation 24 | LineAnnotation LineAnnotation 25 | } 26 | 27 | // NewProcessor creates and returns a new initialized Processor. 28 | func NewProcessor() *Processor { 29 | return &Processor{ 30 | FunctionAnnotation: FunctionAnnotation{ 31 | Exclusions: make(map[token.Pos]struct{}), // *ast.FuncDecl node + all its children 32 | Name: FuncAnnotation}, 33 | RegexAnnotation: RegexAnnotation{ 34 | Exclusions: make(map[int]map[token.Pos]mutatorInfo), // source code line -> node -> excluded mutators 35 | Name: RegexpAnnotation, 36 | }, 37 | LineAnnotation: LineAnnotation{ 38 | Exclusions: make(map[int]map[token.Pos]mutatorInfo), // source code line -> node -> excluded mutators 39 | Name: NextLineAnnotation, 40 | }, 41 | } 42 | } 43 | 44 | type mutatorInfo struct { 45 | Names []string 46 | } 47 | 48 | // Collect processes an AST file to gather all mutation exclusions based on annotations. 49 | func (p *Processor) Collect(file *ast.File, fset *token.FileSet, fileAbs string) { 50 | for _, decl := range file.Decls { 51 | if f, ok := decl.(*ast.FuncDecl); ok { 52 | if p.existsFuncAnnotation(f) { 53 | p.FunctionAnnotation.collectFunctions(f) 54 | } 55 | } 56 | } 57 | 58 | handler := p.buildChain() 59 | 60 | for _, commentGroup := range file.Comments { 61 | for _, comm := range commentGroup.List { 62 | name := getAnnotationName(comm) 63 | handler.Handle(name, comm, fset, file, fileAbs) 64 | } 65 | } 66 | 67 | p.collectNodesForBlockStmt() 68 | } 69 | 70 | // ShouldSkip determines if a given node should be excluded from mutation. 71 | func (p *Processor) ShouldSkip(node ast.Node, mutatorName string) bool { 72 | return p.FunctionAnnotation.filterFunctions(node) || 73 | p.RegexAnnotation.filterRegexNodes(node, mutatorName) || 74 | p.LineAnnotation.filterNodesOnNextLine(node, mutatorName) 75 | } 76 | 77 | // DecoratorFilter creates a mutator that applies one or more filters before executing the provided mutator. 78 | func DecoratorFilter(m mutator.Mutator, name string, filters ...filter.NodeFilter) mutator.Mutator { 79 | return func(pkg *types.Package, info *types.Info, node ast.Node) []mutator.Mutation { 80 | for _, f := range filters { 81 | if f.ShouldSkip(node, name) { 82 | return nil 83 | } 84 | } 85 | 86 | return m(pkg, info, node) 87 | } 88 | } 89 | 90 | // getAnnotationName identifies the type of annotation 91 | func getAnnotationName(comment *ast.Comment) string { 92 | content := strings.TrimSpace(comment.Text) 93 | if strings.HasPrefix(content, RegexpAnnotation) { 94 | return RegexpAnnotation 95 | } 96 | if strings.HasPrefix(content, NextLineAnnotation) { 97 | return NextLineAnnotation 98 | } 99 | if strings.HasPrefix(content, FuncAnnotation) { 100 | return FuncAnnotation 101 | } 102 | 103 | return "" 104 | } 105 | 106 | // collectExcludedNodes populates a map of nodes to exclude from mutation based on the line numbers. 107 | func collectExcludedNodes( 108 | fileSet *token.FileSet, 109 | file *ast.File, 110 | lines []int, 111 | excludedNodes map[int]map[token.Pos]mutatorInfo, 112 | mutators mutatorInfo) { 113 | ast.Inspect(file, func(n ast.Node) bool { 114 | if n == nil { 115 | return true 116 | } 117 | 118 | startLine, endLine := getNodeLineRange(fileSet, n) 119 | 120 | for _, line := range lines { 121 | if startLine == line || endLine == line { 122 | if _, exists := excludedNodes[line]; !exists { 123 | excludedNodes[line] = make(map[token.Pos]mutatorInfo) 124 | } 125 | excludedNodes[line][n.Pos()] = mutators 126 | } 127 | } 128 | 129 | return true 130 | }) 131 | } 132 | 133 | // collectNodesForBlockStmt is a temporary workaround specifically for handling BlockStmt nodes in AST. 134 | // It performs cleanup and transfers collected annotation data to statement nodes within blocks. 135 | // This is a tactical solution to handle edge cases where mutators only look at nodes inside block statements. 136 | // A more robust architectural solution should be implemented in future versions. 137 | func (p *Processor) collectNodesForBlockStmt() { 138 | cleanupGlobalStatBlock() 139 | p.RegexAnnotation.copyToStatNodesInBlock() 140 | p.LineAnnotation.copyToStatNodesInBlock() 141 | } 142 | 143 | // parseMutators parses a comma-separated string of mutator names into a clean slice of strings. 144 | func parseMutators(mutatorList string) []string { 145 | mutators := make([]string, 0) 146 | 147 | rawTargets := strings.Split(mutatorList, ",") 148 | for _, t := range rawTargets { 149 | name := strings.TrimSpace(t) 150 | if name != "" { 151 | mutators = append(mutators, name) 152 | } 153 | } 154 | 155 | return mutators 156 | } 157 | 158 | // shouldSkipMutator determines whether a specific mutator should be skipped 159 | func shouldSkipMutator(mutatorInfo mutatorInfo, mutatorName string) bool { 160 | for _, name := range mutatorInfo.Names { 161 | if name == mutatorName || name == "*" { 162 | return true 163 | } 164 | } 165 | 166 | return false 167 | } 168 | 169 | // getNodeLineRange calculates the line number range (start to end) that a given AST node occupies in the source file. 170 | func getNodeLineRange(fileSet *token.FileSet, node ast.Node) (startLine, endLine int) { 171 | startPos := fileSet.Position(node.Pos()) 172 | endPos := fileSet.Position(node.End()) 173 | 174 | return startPos.Line, endPos.Line 175 | } 176 | 177 | // findLine determines the line number range of a comment node. 178 | func findLine(fileSet *token.FileSet, comment *ast.Comment) (int, int) { 179 | startLine, endLine := getNodeLineRange(fileSet, comment) 180 | 181 | return startLine, endLine 182 | } 183 | -------------------------------------------------------------------------------- /internal/annotation/annotation_test.go: -------------------------------------------------------------------------------- 1 | package annotation 2 | 3 | import ( 4 | "go/ast" 5 | "go/parser" 6 | "go/token" 7 | "regexp" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestShouldSkipMutator(t *testing.T) { 14 | tests := []struct { 15 | name string 16 | mutatorInfo mutatorInfo 17 | mutatorName string 18 | expected bool 19 | }{ 20 | { 21 | name: "Mutator matches exactly", 22 | mutatorInfo: mutatorInfo{Names: []string{"MutatorA", "MutatorB"}}, 23 | mutatorName: "MutatorA", 24 | expected: true, 25 | }, 26 | { 27 | name: "Mutator matches wildcard", 28 | mutatorInfo: mutatorInfo{Names: []string{"*"}}, 29 | mutatorName: "AnyMutator", 30 | expected: true, 31 | }, 32 | { 33 | name: "Mutator does not match", 34 | mutatorInfo: mutatorInfo{Names: []string{"MutatorA", "MutatorB"}}, 35 | mutatorName: "MutatorC", 36 | expected: false, 37 | }, 38 | } 39 | 40 | for _, tt := range tests { 41 | t.Run(tt.name, func(t *testing.T) { 42 | result := shouldSkipMutator(tt.mutatorInfo, tt.mutatorName) 43 | if result != tt.expected { 44 | t.Errorf("Expected %v, but got %v", tt.expected, result) 45 | } 46 | }) 47 | } 48 | } 49 | 50 | func TestParseMutators(t *testing.T) { 51 | tests := []struct { 52 | name string 53 | mutatorList string 54 | expected []string 55 | }{ 56 | { 57 | name: "Valid list of mutators", 58 | mutatorList: "MutatorA, MutatorB, MutatorC", 59 | expected: []string{"MutatorA", "MutatorB", "MutatorC"}, 60 | }, 61 | { 62 | name: "List with leading and trailing spaces", 63 | mutatorList: " MutatorA, MutatorB , MutatorC ", 64 | expected: []string{"MutatorA", "MutatorB", "MutatorC"}, 65 | }, 66 | { 67 | name: "Empty string", 68 | mutatorList: "", 69 | expected: []string{}, 70 | }, 71 | { 72 | name: "String with only commas", 73 | mutatorList: ",,,", 74 | expected: []string{}, 75 | }, 76 | { 77 | name: "Single mutator", 78 | mutatorList: "MutatorA", 79 | expected: []string{"MutatorA"}, 80 | }, 81 | { 82 | name: "Multiple empty elements", 83 | mutatorList: "MutatorA,,,MutatorB,,MutatorC,,", 84 | expected: []string{"MutatorA", "MutatorB", "MutatorC"}, 85 | }, 86 | } 87 | 88 | for _, tt := range tests { 89 | t.Run(tt.name, func(t *testing.T) { 90 | result := parseMutators(tt.mutatorList) 91 | if !assert.Equal(t, result, tt.expected) { 92 | t.Errorf("Expected %v, but got %v", tt.expected, result) 93 | } 94 | }) 95 | } 96 | } 97 | 98 | func TestParseRegexAnnotation(t *testing.T) { 99 | tests := []struct { 100 | name string 101 | commentText string 102 | expectedRegex *regexp.Regexp 103 | expectedInfo mutatorInfo 104 | }{ 105 | { 106 | name: "Valid regex and mutators", 107 | commentText: "RegexName ^[a-z]+$ MutatorA, MutatorB", 108 | expectedRegex: func() *regexp.Regexp { 109 | re, _ := regexp.Compile("^[a-z]+$") 110 | return re 111 | }(), 112 | expectedInfo: mutatorInfo{ 113 | Names: []string{"MutatorA", "MutatorB"}, 114 | }, 115 | }, 116 | { 117 | name: "Valid regex without mutators", 118 | commentText: "RegexName ^[0-9]{4}$", 119 | expectedRegex: func() *regexp.Regexp { 120 | re, _ := regexp.Compile("^[0-9]{4}$") 121 | return re 122 | }(), 123 | expectedInfo: mutatorInfo{ 124 | Names: []string{}, 125 | }, 126 | }, 127 | { 128 | name: "Invalid regex", 129 | commentText: "RegexName [a-z", 130 | expectedRegex: nil, 131 | expectedInfo: mutatorInfo{}, 132 | }, 133 | { 134 | name: "Empty comment", 135 | commentText: "RegexName ", 136 | expectedRegex: nil, 137 | expectedInfo: mutatorInfo{}, 138 | }, 139 | } 140 | 141 | r := &RegexAnnotation{Name: "RegexName"} 142 | 143 | for _, tt := range tests { 144 | t.Run(tt.name, func(t *testing.T) { 145 | resultRegex, resultInfo := r.parseRegexAnnotation(tt.commentText) 146 | 147 | if resultRegex == nil && tt.expectedRegex != nil || resultRegex != nil && resultRegex.String() != tt.expectedRegex.String() { 148 | t.Errorf("Expected regex %v, but got %v", tt.expectedRegex, resultRegex) 149 | } 150 | 151 | if len(resultInfo.Names) != len(tt.expectedInfo.Names) { 152 | t.Errorf("Expected mutators %v, but got %v", tt.expectedInfo.Names, resultInfo.Names) 153 | } else { 154 | for i, mutator := range resultInfo.Names { 155 | if mutator != tt.expectedInfo.Names[i] { 156 | t.Errorf("Expected mutator %v, but got %v", tt.expectedInfo.Names[i], mutator) 157 | } 158 | } 159 | } 160 | }) 161 | } 162 | } 163 | 164 | func TestParseLineAnnotation(t *testing.T) { 165 | tests := []struct { 166 | name string 167 | commentText string 168 | expectedInfo mutatorInfo 169 | }{ 170 | { 171 | name: "Valid mutators", 172 | commentText: "LineName MutatorA, MutatorB, MutatorC", 173 | expectedInfo: mutatorInfo{ 174 | Names: []string{"MutatorA", "MutatorB", "MutatorC"}, 175 | }, 176 | }, 177 | { 178 | name: "Single mutator", 179 | commentText: "LineName MutatorA", 180 | expectedInfo: mutatorInfo{ 181 | Names: []string{"MutatorA"}, 182 | }, 183 | }, 184 | { 185 | name: "Multiple mutators with spaces", 186 | commentText: "LineName MutatorA, MutatorB , MutatorC ", 187 | expectedInfo: mutatorInfo{ 188 | Names: []string{"MutatorA", "MutatorB", "MutatorC"}, 189 | }, 190 | }, 191 | { 192 | name: "Empty comment", 193 | commentText: "LineName ", 194 | expectedInfo: mutatorInfo{ 195 | Names: []string{}, 196 | }, 197 | }, 198 | { 199 | name: "Only spaces in mutators", 200 | commentText: "LineName ,,,", 201 | expectedInfo: mutatorInfo{ 202 | Names: []string{}, 203 | }, 204 | }, 205 | { 206 | name: "Empty mutators", 207 | commentText: "LineName", 208 | expectedInfo: mutatorInfo{ 209 | Names: []string{}, 210 | }, 211 | }, 212 | } 213 | 214 | l := &LineAnnotation{Name: "LineName"} 215 | 216 | for _, tt := range tests { 217 | t.Run(tt.name, func(t *testing.T) { 218 | result := l.parseLineAnnotation(tt.commentText) 219 | 220 | if len(result.Names) != len(tt.expectedInfo.Names) { 221 | t.Errorf("Expected mutators %v, but got %v", tt.expectedInfo.Names, result.Names) 222 | } else { 223 | for i, mutator := range result.Names { 224 | if mutator != tt.expectedInfo.Names[i] { 225 | t.Errorf("Expected mutator %v, but got %v", tt.expectedInfo.Names[i], mutator) 226 | } 227 | } 228 | } 229 | }) 230 | } 231 | } 232 | 233 | func TestExistsFuncAnnotation(t *testing.T) { 234 | tests := []struct { 235 | name string 236 | funcDecl *ast.FuncDecl 237 | expectedExist bool 238 | }{ 239 | { 240 | name: "No annotations", 241 | funcDecl: &ast.FuncDecl{ 242 | Doc: &ast.CommentGroup{ 243 | List: []*ast.Comment{ 244 | {Text: "// Some other comment"}, 245 | }, 246 | }, 247 | }, 248 | expectedExist: false, 249 | }, 250 | { 251 | name: "Valid annotation exists", 252 | funcDecl: &ast.FuncDecl{ 253 | Doc: &ast.CommentGroup{ 254 | List: []*ast.Comment{ 255 | {Text: "// mutator-disable-func something here"}, 256 | }, 257 | }, 258 | }, 259 | expectedExist: true, 260 | }, 261 | { 262 | name: "Multiple comments, valid annotation exists", 263 | funcDecl: &ast.FuncDecl{ 264 | Doc: &ast.CommentGroup{ 265 | List: []*ast.Comment{ 266 | {Text: "// Another comment"}, 267 | {Text: "// mutator-disable-func something here"}, 268 | }, 269 | }, 270 | }, 271 | expectedExist: true, 272 | }, 273 | { 274 | name: "Doc is nil", 275 | funcDecl: &ast.FuncDecl{ 276 | Doc: nil, 277 | }, 278 | expectedExist: false, 279 | }, 280 | } 281 | 282 | p := NewProcessor() 283 | 284 | for _, tt := range tests { 285 | t.Run(tt.name, func(t *testing.T) { 286 | result := p.existsFuncAnnotation(tt.funcDecl) 287 | if result != tt.expectedExist { 288 | t.Errorf("Expected %v, but got %v", tt.expectedExist, result) 289 | } 290 | }) 291 | } 292 | } 293 | 294 | func TestCollectFunctionsAndFilterFunctions(t *testing.T) { 295 | tests := []struct { 296 | name string 297 | code string 298 | expected bool 299 | filterPos token.Pos 300 | }{ 301 | { 302 | name: "Function with one statement", 303 | code: `package main; func test() { var a = 10 }`, 304 | expected: true, 305 | filterPos: token.Pos(1), 306 | }, 307 | { 308 | name: "Function with nested statements", 309 | code: `package main; func test() { if true { var a = 10 } }`, 310 | expected: true, 311 | filterPos: token.Pos(2), 312 | }, 313 | } 314 | 315 | f := &FunctionAnnotation{Exclusions: make(map[token.Pos]struct{})} 316 | 317 | for _, tt := range tests { 318 | t.Run(tt.name, func(t *testing.T) { 319 | fs := token.NewFileSet() 320 | node, err := parser.ParseFile(fs, "func_annotation_test.go", tt.code, parser.Mode(0)) 321 | if err != nil { 322 | t.Fatalf("Failed to parse code: %v", err) 323 | } 324 | 325 | // Находим первую функцию в коде 326 | var funcDecl *ast.FuncDecl 327 | ast.Inspect(node, func(n ast.Node) bool { 328 | if fDecl, ok := n.(*ast.FuncDecl); ok { 329 | funcDecl = fDecl 330 | return false 331 | } 332 | return true 333 | }) 334 | 335 | f.collectFunctions(funcDecl) 336 | 337 | filtered := f.filterFunctions(funcDecl) 338 | assert.Equal(t, tt.expected, filtered) 339 | 340 | }) 341 | } 342 | } 343 | 344 | func TestFindLinesMatchedRegex(t *testing.T) { 345 | tests := []struct { 346 | name string 347 | filePath string 348 | re *regexp.Regexp 349 | expectedLines []int 350 | }{ 351 | { 352 | name: "No regex match", 353 | filePath: "../../testdata/annotation/regex.go", 354 | re: regexp.MustCompile("notmatching"), 355 | expectedLines: []int{}, 356 | }, 357 | { 358 | name: "Match variable declaration", 359 | filePath: "../../testdata/annotation/regex.go", 360 | re: regexp.MustCompile(`test`), 361 | expectedLines: []int{37, 38}, 362 | }, 363 | { 364 | name: "Match Println", 365 | filePath: "../../testdata/annotation/regex.go", 366 | re: regexp.MustCompile(`Println`), 367 | expectedLines: []int{16, 17, 23, 33, 38}, 368 | }, 369 | { 370 | name: "Multiple matches on multiple lines", 371 | filePath: "../../testdata/annotation/regex.go", 372 | re: regexp.MustCompile(`xx+`), 373 | expectedLines: []int{12, 13, 16, 17}, 374 | }, 375 | { 376 | name: "Match MyStruct", 377 | filePath: "../../testdata/annotation/regex.go", 378 | re: regexp.MustCompile(`MyStruct`), 379 | expectedLines: []int{19, 27, 31}, 380 | }, 381 | { 382 | name: "Match interface declaration", 383 | filePath: "../../testdata/annotation/regex.go", 384 | re: regexp.MustCompile(`interface`), 385 | expectedLines: []int{42}, 386 | }, 387 | { 388 | name: "Match slog.Info", 389 | filePath: "../../testdata/annotation/regex.go", 390 | re: regexp.MustCompile(`slog\.Info`), 391 | expectedLines: []int{49}, 392 | }, 393 | { 394 | name: "Match s.Method()", 395 | filePath: "../../testdata/annotation/regex.go", 396 | re: regexp.MustCompile(`s\.Method\(\)`), 397 | expectedLines: []int{20}, 398 | }, 399 | { 400 | name: "Regex is nil", 401 | filePath: "../../testdata/annotation/regex.go", 402 | re: nil, 403 | expectedLines: []int{}, 404 | }, 405 | { 406 | name: "Empty file", 407 | filePath: "../../testdata/annotation/empty.go", 408 | re: regexp.MustCompile(`test`), 409 | expectedLines: []int{}, 410 | }, 411 | } 412 | 413 | for _, tt := range tests { 414 | t.Run(tt.name, func(t *testing.T) { 415 | 416 | r := &RegexAnnotation{Name: "TestRegexAnnotation"} 417 | 418 | actual, _ := r.findLinesMatchingRegex(tt.filePath, tt.re) 419 | 420 | assert.ElementsMatch(t, tt.expectedLines, actual) 421 | }) 422 | } 423 | } 424 | 425 | func TestCollect(t *testing.T) { 426 | fs := token.NewFileSet() 427 | file, err := parser.ParseFile(fs, "../../testdata/annotation/collect.go", nil, parser.AllErrors|parser.ParseComments) 428 | if err != nil { 429 | t.Fatalf("failed to parse file: %v", err) 430 | } 431 | 432 | processor := NewProcessor() 433 | 434 | processor.Collect(file, fs, "../../testdata/annotation/collect.go") 435 | 436 | assert.NotEmpty(t, processor.FunctionAnnotation.Exclusions) 437 | assert.Equal(t, processor.FunctionAnnotation.Exclusions, map[token.Pos]struct{}{ 438 | 75: {}, 99: {}, 104: {}, 114: {}, 115: {}, 117: {}, 122: {}, 126: {}, 129: {}, 136: {}, 140: {}, 439 | }) 440 | 441 | assert.NotEmpty(t, processor.RegexAnnotation.Exclusions) 442 | assert.Equal(t, processor.RegexAnnotation.Exclusions, map[int]map[token.Pos]mutatorInfo{ 443 | 14: { 444 | 169: {Names: []string{"*"}}, 445 | 173: {Names: []string{"*"}}, 446 | 181: {Names: []string{"*"}}, 447 | }, 448 | 22: { 449 | 304: {Names: []string{"*"}}, 450 | 308: {Names: []string{"*"}}, 451 | 316: {Names: []string{"*"}}, 452 | }, 453 | 21: { 454 | 288: {Names: []string{"*"}}, 455 | 292: {Names: []string{"*"}}, 456 | 300: {Names: []string{"*"}}, 457 | }, 458 | }) 459 | 460 | assert.NotEmpty(t, processor.LineAnnotation.Exclusions) 461 | assert.Equal(t, processor.LineAnnotation.Exclusions, map[int]map[token.Pos]mutatorInfo{ 462 | 19: { 463 | 275: {Names: []string{"numbers/incrementer"}}, 464 | 279: {Names: []string{"numbers/incrementer"}}, 465 | 283: {Names: []string{"numbers/incrementer"}}, 466 | }, 467 | }) 468 | 469 | } 470 | -------------------------------------------------------------------------------- /internal/annotation/block.go: -------------------------------------------------------------------------------- 1 | package annotation 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | ) 7 | 8 | var statNodesInBlockForRegex = make(map[int]map[token.Pos]mutatorInfo) 9 | var statNodesInBlockForLine = make(map[int]map[token.Pos]mutatorInfo) 10 | 11 | // HandleBlockStmt is a temporary workaround specifically for handling BlockStmt nodes in AST. 12 | // It performs cleanup and transfers collected annotation data to statement nodes within blocks. 13 | // This is a tactical solution to handle edge cases where mutators only look at nodes inside block statements. 14 | // A more robust architectural solution should be implemented in future versions. 15 | func HandleBlockStmt(node ast.Stmt) bool { 16 | for _, n := range statNodesInBlockForRegex { 17 | if mutatorName, exists := n[node.Pos()]; exists { 18 | if shouldSkipMutator(mutatorName, "statement/remove") { 19 | return true 20 | } 21 | } 22 | } 23 | 24 | for _, n := range statNodesInBlockForLine { 25 | if mutatorName, exists := n[node.Pos()]; exists { 26 | if shouldSkipMutator(mutatorName, "statement/remove") { 27 | return true 28 | } 29 | } 30 | } 31 | 32 | return false 33 | } 34 | 35 | func cleanupGlobalStatBlock() { 36 | statNodesInBlockForRegex = make(map[int]map[token.Pos]mutatorInfo) 37 | statNodesInBlockForLine = make(map[int]map[token.Pos]mutatorInfo) 38 | } 39 | 40 | func (r *RegexAnnotation) copyToStatNodesInBlock() { 41 | for line, nodes := range r.Exclusions { 42 | if _, exists := statNodesInBlockForRegex[line]; !exists { 43 | statNodesInBlockForRegex[line] = make(map[token.Pos]mutatorInfo) 44 | } 45 | 46 | for pos, mutatorInfo := range nodes { 47 | statNodesInBlockForRegex[line][pos] = mutatorInfo 48 | } 49 | } 50 | } 51 | 52 | func (l *LineAnnotation) copyToStatNodesInBlock() { 53 | for line, nodes := range l.Exclusions { 54 | if _, exists := statNodesInBlockForLine[line]; !exists { 55 | statNodesInBlockForLine[line] = make(map[token.Pos]mutatorInfo) 56 | } 57 | 58 | for pos, mutatorInfo := range nodes { 59 | statNodesInBlockForLine[line][pos] = mutatorInfo 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /internal/annotation/chain.go: -------------------------------------------------------------------------------- 1 | package annotation 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | ) 7 | 8 | // ChainCollector defines the interface for handlers in the annotation processing chain. 9 | // Implementations should handle specific annotation types and pass unhandled cases to the next handler in the chain. 10 | type ChainCollector interface { 11 | // Handle processes an annotation if it matches the handler's type, 12 | // otherwise delegates to the next handler in the chain. 13 | Handle(name string, comment *ast.Comment, fset *token.FileSet, file *ast.File, fileAbs string) 14 | // SetNext establishes the next handler in the chain of responsibility. 15 | SetNext(next ChainCollector) 16 | } 17 | 18 | // BaseCollector provides default chain handling behavior 19 | type BaseCollector struct { 20 | next ChainCollector 21 | } 22 | 23 | // SetNext sets the next handler in the chain of responsibility. 24 | func (h *BaseCollector) SetNext(next ChainCollector) { 25 | h.next = next 26 | } 27 | 28 | // Handle implements the default chain behavior by delegating to the next handler. 29 | func (h *BaseCollector) Handle(name string, comment *ast.Comment, fset *token.FileSet, file *ast.File, fileAbs string) { 30 | if h.next != nil { 31 | h.next.Handle(name, comment, fset, file, fileAbs) 32 | } 33 | } 34 | 35 | // RegexAnnotationCollector implements the ChainCollector interface for "mutator-disable-regexp" annotations. 36 | type RegexAnnotationCollector struct { 37 | BaseCollector 38 | Processor RegexAnnotation 39 | } 40 | 41 | // NextLineAnnotationCollector implements the ChainCollector interface for "mutator-disable-next-line" annotations. 42 | type NextLineAnnotationCollector struct { 43 | BaseCollector 44 | Processor LineAnnotation 45 | } 46 | 47 | // Handle processes regex pattern annotations, delegating other types to the next handler. 48 | func (r *RegexAnnotationCollector) Handle(name string, comment *ast.Comment, fset *token.FileSet, file *ast.File, fileAbs string) { 49 | if name == RegexpAnnotation { 50 | r.Processor.collectMatchNodes(comment, fset, file, fileAbs) 51 | } else { 52 | r.BaseCollector.Handle(name, comment, fset, file, fileAbs) 53 | } 54 | } 55 | 56 | // Handle processes regex pattern annotations, delegating other types to the next handler. 57 | func (n *NextLineAnnotationCollector) Handle(name string, comment *ast.Comment, fset *token.FileSet, file *ast.File, fileAbs string) { 58 | if name == NextLineAnnotation { 59 | n.Processor.collectNodesOnNextLine(comment, fset, file) 60 | } else { 61 | n.BaseCollector.Handle(name, comment, fset, file, fileAbs) 62 | } 63 | } 64 | 65 | func (p *Processor) buildChain() ChainCollector { 66 | regexHandler := &RegexAnnotationCollector{Processor: p.RegexAnnotation} 67 | nextLineHandler := &NextLineAnnotationCollector{Processor: p.LineAnnotation} 68 | regexHandler.SetNext(nextLineHandler) 69 | 70 | return regexHandler 71 | } 72 | -------------------------------------------------------------------------------- /internal/annotation/function.go: -------------------------------------------------------------------------------- 1 | package annotation 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "strings" 7 | ) 8 | 9 | // FunctionAnnotation represents a collection of exclusions of function declarations. 10 | type FunctionAnnotation struct { 11 | Exclusions map[token.Pos]struct{} 12 | Name string 13 | } 14 | 15 | // collectFunctions records all nodes within a function declaration to be excluded from mutation. 16 | // It collects both the function declaration itself and all its child nodes. 17 | func (f *FunctionAnnotation) collectFunctions(fun *ast.FuncDecl) { 18 | f.Exclusions[fun.Pos()] = struct{}{} 19 | 20 | ast.Inspect(fun, func(n ast.Node) bool { 21 | if n != nil { 22 | f.Exclusions[n.Pos()] = struct{}{} 23 | } 24 | 25 | return true 26 | }) 27 | } 28 | 29 | // filterFunctions checks whether a given node should be excluded from mutation 30 | func (f *FunctionAnnotation) filterFunctions(node ast.Node) bool { 31 | _, exists := f.Exclusions[node.Pos()] 32 | return exists 33 | } 34 | 35 | // existsFuncAnnotation checks if a function declaration has the annotation 36 | func (p *Processor) existsFuncAnnotation(f *ast.FuncDecl) bool { 37 | if f.Doc == nil { 38 | return false 39 | } 40 | 41 | for _, comment := range f.Doc.List { 42 | if strings.HasPrefix(comment.Text, p.FunctionAnnotation.Name) { 43 | return true 44 | } 45 | } 46 | 47 | return false 48 | } 49 | -------------------------------------------------------------------------------- /internal/annotation/line.go: -------------------------------------------------------------------------------- 1 | package annotation 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "strings" 7 | ) 8 | 9 | // LineAnnotation represents a collection of exclusions based on lines in the file. 10 | type LineAnnotation struct { 11 | Exclusions map[int]map[token.Pos]mutatorInfo 12 | Name string 13 | } 14 | 15 | // parseLineAnnotation parses a comment line containing a next-line annotation. 16 | func (l *LineAnnotation) parseLineAnnotation(comment string) mutatorInfo { 17 | content := strings.TrimSpace(strings.TrimPrefix(comment, l.Name)) 18 | if content == "" { 19 | return mutatorInfo{} 20 | } 21 | 22 | mutators := parseMutators(content) 23 | 24 | return mutatorInfo{ 25 | Names: mutators, 26 | } 27 | } 28 | 29 | // collectNodesOnNextLine processes a "mutator-disable-next-line" annotation. 30 | // It: 31 | // 1. Parses the mutator names from the annotation comment 32 | // 2. Determines the line number immediately following the comment 33 | // 3. Collects all AST nodes that appear on that line 34 | // 4. Records the exclusion information for those nodes 35 | func (l *LineAnnotation) collectNodesOnNextLine(comment *ast.Comment, fset *token.FileSet, file *ast.File) { 36 | mutators := l.parseLineAnnotation(comment.Text) 37 | 38 | start, end := findLine(fset, comment) 39 | var nextLine int 40 | if start == end { 41 | nextLine = start + 1 42 | } 43 | 44 | lines := []int{nextLine} 45 | 46 | collectExcludedNodes(fset, file, lines, l.Exclusions, mutators) 47 | } 48 | 49 | // filterNodesOnNextLine checks if a given node should be excluded from mutation based on: 50 | // 1. Whether the node appears in the Exclusions map 51 | // 2. Whether the current mutator is in the node's exclusion list 52 | func (l *LineAnnotation) filterNodesOnNextLine(node ast.Node, mutatorName string) bool { 53 | for _, n := range l.Exclusions { 54 | if mutators, exists := n[node.Pos()]; exists { 55 | if shouldSkipMutator(mutators, mutatorName) { 56 | return true 57 | } 58 | } 59 | } 60 | 61 | return false 62 | } 63 | -------------------------------------------------------------------------------- /internal/annotation/regex.go: -------------------------------------------------------------------------------- 1 | package annotation 2 | 3 | import ( 4 | "bufio" 5 | "go/ast" 6 | "go/token" 7 | "log" 8 | "os" 9 | "regexp" 10 | "strings" 11 | ) 12 | 13 | // RegexAnnotation represents a collection of exclusions based on regex pattern matches. 14 | type RegexAnnotation struct { 15 | Exclusions map[int]map[token.Pos]mutatorInfo 16 | Name string 17 | } 18 | 19 | // parseRegexAnnotation parses a comment line containing a regex annotation. 20 | func (r *RegexAnnotation) parseRegexAnnotation(comment string) (*regexp.Regexp, mutatorInfo) { 21 | content := strings.TrimSpace(strings.TrimPrefix(comment, r.Name)) 22 | if content == "" { 23 | return nil, mutatorInfo{} 24 | } 25 | 26 | parts := strings.SplitN(content, " ", 2) 27 | 28 | pattern := strings.TrimSpace(parts[0]) 29 | re, err := regexp.Compile(pattern) 30 | if err != nil { 31 | log.Printf("Warning: invalid regex in annotation: %q, error: %v\n", pattern, err) 32 | return nil, mutatorInfo{} 33 | } 34 | 35 | var mutators []string 36 | if len(parts) > 1 { 37 | mutators = parseMutators(parts[1]) 38 | } 39 | 40 | return re, mutatorInfo{ 41 | Names: mutators, 42 | } 43 | } 44 | 45 | // collectMatchNodes processes a "mutator-disable-regexp" annotation comment by: 46 | // 1. Parsing the regex pattern and mutators from the comment 47 | // 2. Finding all lines in the file that match the regex 48 | // 3. Recording nodes from matching lines to be excluded 49 | func (r *RegexAnnotation) collectMatchNodes(comment *ast.Comment, fset *token.FileSet, file *ast.File, fileAbs string) { 50 | regex, mutators := r.parseRegexAnnotation(comment.Text) 51 | 52 | lines, err := r.findLinesMatchingRegex(fileAbs, regex) 53 | if err != nil { 54 | log.Printf("Error scaning a source file: %v", err) 55 | } 56 | 57 | collectExcludedNodes(fset, file, lines, r.Exclusions, mutators) 58 | } 59 | 60 | // findLinesMatchingRegex scans a source file and returns line numbers that match the given regex. 61 | func (r *RegexAnnotation) findLinesMatchingRegex(filePath string, regex *regexp.Regexp) ([]int, error) { 62 | var matchedLineNumbers []int 63 | 64 | if regex == nil { 65 | return matchedLineNumbers, nil 66 | } 67 | 68 | f, err := os.Open(filePath) 69 | if err != nil { 70 | log.Printf("Error opening file: %v", err) 71 | } 72 | 73 | reader := bufio.NewReader(f) 74 | 75 | lineNumber := 0 76 | for { 77 | line, err := reader.ReadString('\n') 78 | if err != nil { 79 | break 80 | } 81 | 82 | if regex.MatchString(line) { 83 | matchedLineNumbers = append(matchedLineNumbers, lineNumber+1) 84 | } 85 | lineNumber++ 86 | } 87 | 88 | defer func() { 89 | err = f.Close() 90 | if err != nil { 91 | log.Printf("Error while file closing duting processing regex annotation: %v", err.Error()) 92 | } 93 | }() 94 | 95 | return matchedLineNumbers, nil 96 | } 97 | 98 | // filterRegexNodes checks if a given node should be excluded from mutation based on: 99 | // 1. Whether the node appears in the Exclusions map 100 | // 2. Whether the current mutator is in the node's exclusion list 101 | func (r *RegexAnnotation) filterRegexNodes(node ast.Node, mutatorName string) bool { 102 | for _, nodes := range r.Exclusions { 103 | if mutatorInfo, exists := nodes[node.Pos()]; exists { 104 | if shouldSkipMutator(mutatorInfo, mutatorName) { 105 | return true 106 | } 107 | } 108 | } 109 | 110 | return false 111 | } 112 | -------------------------------------------------------------------------------- /internal/console/printer.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "fmt" 5 | "github.com/avito-tech/go-mutesting/internal/models" 6 | "log" 7 | "strings" 8 | 9 | "github.com/fatih/color" 10 | ) 11 | 12 | // for colouring 13 | const ( 14 | PASS = "PASS" 15 | FAIL = "FAIL" 16 | SKIP = "SKIP" 17 | UNKNOWN = "UNKNOWN" 18 | ) 19 | 20 | var ( 21 | length = 150 22 | frameLine = strings.Repeat("-", length) 23 | ) 24 | 25 | // PrintPass prints in green 26 | func PrintPass(out string) { 27 | pass := color.New(color.FgHiWhite, color.BgGreen).SprintfFunc() 28 | out = strings.Replace(out, PASS, pass(PASS), 1) 29 | fmt.Print(out) 30 | color.Blue(frameLine) 31 | } 32 | 33 | // PrintFail prints in red 34 | func PrintFail(out string) { 35 | fail := color.New(color.FgHiWhite, color.BgRed).SprintfFunc() 36 | out = strings.Replace(out, FAIL, fail(FAIL), 1) 37 | fmt.Print(out) 38 | color.Blue(frameLine) 39 | } 40 | 41 | // PrintSkip prints in yellow 42 | func PrintSkip(out string) { 43 | skip := color.New(color.FgHiWhite, color.BgYellow).SprintfFunc() 44 | out = strings.Replace(out, SKIP, skip(SKIP), 1) 45 | fmt.Print(out) 46 | color.Blue(frameLine) 47 | } 48 | 49 | // PrintUnknown prints in magenta 50 | func PrintUnknown(out string) { 51 | unknown := color.New(color.FgHiWhite, color.BgMagenta).SprintfFunc() 52 | out = strings.Replace(out, UNKNOWN, unknown(UNKNOWN), 1) 53 | fmt.Print(out) 54 | color.Blue(frameLine) 55 | } 56 | 57 | // PrintDiff prints colorful diff 58 | func PrintDiff(diff []byte) { 59 | green := color.New(color.FgHiWhite).Add(color.BgGreen) 60 | red := color.New(color.FgHiWhite).Add(color.BgRed) 61 | 62 | lines := string(diff) 63 | for _, line := range strings.Split(lines, "\n") { 64 | switch { 65 | case strings.HasPrefix(line, "+++"): 66 | _, err := green.Println(line) 67 | if err != nil { 68 | log.Printf("Error printing output: %s", err) 69 | } 70 | case strings.HasPrefix(line, "---"): 71 | _, err := red.Println(line) 72 | if err != nil { 73 | log.Printf("Error printing output: %s", err) 74 | } 75 | case strings.HasPrefix(line, "+"): 76 | _, err := green.Println(line) 77 | if err != nil { 78 | log.Printf("Error printing output: %s", err) 79 | } 80 | case strings.HasPrefix(line, "-"): 81 | _, err := red.Println(line) 82 | if err != nil { 83 | log.Printf("Error printing output: %s", err) 84 | } 85 | default: 86 | fmt.Println(line) 87 | } 88 | } 89 | } 90 | 91 | // Debug prints formatted debug messages when debug mode is enabled in options. 92 | func Debug(opts *models.Options, format string, args ...interface{}) { 93 | if opts.General.Debug { 94 | fmt.Printf(format+"\n", args...) 95 | } 96 | } 97 | 98 | // Verbose prints formatted messages when either verbose or debug mode is enabled. 99 | func Verbose(opts *models.Options, format string, args ...interface{}) { 100 | if opts.General.Verbose || opts.General.Debug { 101 | fmt.Printf(format+"\n", args...) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /internal/filter/filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | ) 7 | 8 | // NodeCollector defines the interface for types that can collect and process AST nodes from a source file. 9 | type NodeCollector interface { 10 | Collect(file *ast.File, fset *token.FileSet, fileAbs string) 11 | } 12 | 13 | // NodeFilter defines the interface for types that can determine if an AST node should be excluded from mutation. 14 | type NodeFilter interface { 15 | ShouldSkip(node ast.Node, mutatorName string) bool 16 | } 17 | -------------------------------------------------------------------------------- /internal/filter/skip_mutation.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | ) 7 | 8 | // SkipMakeArgsFilter is a filter that tracks numeric arguments in 'make' calls 9 | // for slices and maps to be ignored during mutation. 10 | type SkipMakeArgsFilter struct { 11 | // IgnoredNodes maps positions of numeric literals to their parent 'make' call expressions 12 | IgnoredNodes map[token.Pos]*ast.CallExpr 13 | } 14 | 15 | // NewSkipMakeArgsFilter creates and returns a new initialized Processor. 16 | func NewSkipMakeArgsFilter() *SkipMakeArgsFilter { 17 | return &SkipMakeArgsFilter{IgnoredNodes: make(map[token.Pos]*ast.CallExpr)} 18 | } 19 | 20 | // Collect collects numeric arguments (children) from 'make' calls (parents) for slices/maps to be ignored during mutation 21 | func (s *SkipMakeArgsFilter) Collect(file *ast.File, _ *token.FileSet, _ string) { 22 | ast.Inspect(file, func(n ast.Node) bool { 23 | if callExpr, ok := n.(*ast.CallExpr); ok { 24 | if ident, ok := callExpr.Fun.(*ast.Ident); ok && ident.Name == "make" && len(callExpr.Args) > 1 { 25 | arg0 := callExpr.Args[0] 26 | _, isArray := arg0.(*ast.ArrayType) 27 | _, isMap := arg0.(*ast.MapType) 28 | if isArray || isMap { 29 | if lit, ok := callExpr.Args[1].(*ast.BasicLit); ok && lit.Kind == token.INT { 30 | s.IgnoredNodes[lit.Pos()] = callExpr 31 | } 32 | if len(callExpr.Args) > 2 { 33 | if lit, ok := callExpr.Args[2].(*ast.BasicLit); ok && lit.Kind == token.INT { 34 | s.IgnoredNodes[lit.Pos()] = callExpr 35 | } 36 | } 37 | return false 38 | } 39 | } 40 | } 41 | return true 42 | }) 43 | } 44 | 45 | // ShouldSkip determines whether a given AST node should be skipped during mutation. 46 | func (s *SkipMakeArgsFilter) ShouldSkip(node ast.Node, _ string) bool { 47 | _, exists := s.IgnoredNodes[node.Pos()] 48 | return exists 49 | } 50 | -------------------------------------------------------------------------------- /internal/filter/skip_mutation_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "go/ast" 5 | "go/parser" 6 | "go/token" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestSkipMutationForInitSlicesAndMaps(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | code string 16 | expected bool 17 | }{ 18 | { 19 | name: "skip mutation for slice init with len", 20 | code: `package main; var a = make([]int, 10)`, 21 | expected: true, 22 | }, 23 | { 24 | name: "skip mutation for slice init with len and cap", 25 | code: `package main; var a = make([]int, 10, 20)`, 26 | expected: true, 27 | }, 28 | { 29 | name: "skip mutation for slice init in assigning inside a struct", 30 | code: `package ccc; 31 | type TestCase struct { 32 | Devices []DeviceStatus 33 | }; 34 | type DeviceStatus struct { 35 | DeviceName string 36 | Status string 37 | ReportViewerID string 38 | }; 39 | func fff() { 40 | testCase := &TestCase{ Devices: make([]DeviceStatus, 0) } 41 | }`, 42 | expected: true, 43 | }, 44 | { 45 | name: "skip mutation for map init with cap", 46 | code: `package main; var a = make(map[int]bool, 0)`, 47 | expected: true, 48 | }, 49 | { 50 | name: "do not skip mutation for slice init with variable", 51 | code: `package main; var x = 10; var a = make([]int, x)`, 52 | expected: false, 53 | }, 54 | { 55 | name: "do not skip mutation for other literals", 56 | code: `package main; var a = 42`, 57 | expected: false, 58 | }, 59 | } 60 | 61 | for _, tt := range tests { 62 | t.Run(tt.name, func(t *testing.T) { 63 | fs := token.NewFileSet() 64 | node, err := parser.ParseFile(fs, "skip_mutation_test.go", tt.code, parser.Mode(0)) 65 | if err != nil { 66 | t.Fatalf("Failed to parse code: %v", err) 67 | } 68 | 69 | s := NewSkipMakeArgsFilter() 70 | s.Collect(node, nil, "") 71 | s.ShouldSkip(node, "") 72 | 73 | var result bool 74 | ast.Inspect(node, func(n ast.Node) bool { 75 | if lit, ok := n.(*ast.BasicLit); ok && lit.Kind == token.INT { 76 | _, found := s.IgnoredNodes[lit.Pos()] 77 | result = found 78 | return false 79 | } 80 | return true 81 | }) 82 | 83 | assert.Equal(t, tt.expected, result) 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /internal/importing/filepath.go: -------------------------------------------------------------------------------- 1 | package importing 2 | 3 | /* 4 | 5 | This file holds lots of code of the golint project https://github.com/golang/lint and some code of a pull request of mine https://github.com/golang/lint/pull/76 6 | This is just temporary until I have time to clean up this code and make a more general solution for go-commands as I stated here https://github.com/kisielk/errcheck/issues/45#issuecomment-57732642 7 | 8 | so TODO and FIXME. Heck I also give you a WORKAROUND. 9 | 10 | */ 11 | 12 | import ( 13 | "fmt" 14 | "go/build" 15 | "log" 16 | "os" 17 | "path" 18 | "path/filepath" 19 | "regexp" 20 | "sort" 21 | "strings" 22 | 23 | "github.com/avito-tech/go-mutesting/internal/models" 24 | ) 25 | 26 | func packagesWithFilesOfArgs(args []string, opts *models.Options) map[string]map[string]struct{} { 27 | var filenames []string 28 | 29 | if len(args) == 0 { 30 | filenames = append(filenames, checkDir(".")...) 31 | } else { 32 | for _, arg := range args { 33 | if strings.HasSuffix(arg, "/...") && isDir(arg[:len(arg)-4]) { 34 | for _, dirname := range allPackagesInFS(arg) { 35 | filenames = append(filenames, checkDir(dirname)...) 36 | } 37 | } else if isDir(arg) { 38 | filenames = append(filenames, checkDir(arg)...) 39 | } else if exists(arg) { 40 | filenames = append(filenames, arg) 41 | } else { 42 | for _, pkgname := range importPaths([]string{arg}) { 43 | filenames = append(filenames, checkPackage(pkgname)...) 44 | } 45 | } 46 | } 47 | } 48 | 49 | fileLookup := make(map[string]struct{}) 50 | pkgs := make(map[string]map[string]struct{}) 51 | var re *regexp.Regexp 52 | if opts.Config.SkipFileWithBuildTag { 53 | re = regexp.MustCompile("\\+build (.*)(\\s+)package") //nolint:gosimple 54 | } 55 | 56 | for _, filename := range filenames { 57 | if _, ok := fileLookup[filename]; ok { 58 | continue 59 | } 60 | 61 | if len(opts.Config.ExcludeDirs) > 0 { // ignore files in excluded dirs 62 | dirIsExcluded := false 63 | for _, exDir := range opts.Config.ExcludeDirs { 64 | if strings.HasPrefix(filename, exDir) { 65 | dirIsExcluded = true 66 | break 67 | } 68 | } 69 | 70 | if dirIsExcluded { 71 | continue 72 | } 73 | } 74 | 75 | if strings.HasSuffix(filename, "_test.go") { // ignore test files 76 | continue 77 | } 78 | 79 | if opts.Config.SkipFileWithoutTest || opts.Config.SkipFileWithBuildTag { // ignore files without tests 80 | nameSize := len(filename) 81 | if nameSize <= 3 { 82 | continue 83 | } 84 | 85 | testName := filename[:nameSize-3] + "_test.go" 86 | if !exists(testName) { 87 | continue 88 | } 89 | 90 | if opts.Config.SkipFileWithBuildTag { // ignore files with test with build tags 91 | isBuildTag := regexpSearchInFile(testName, re) 92 | if isBuildTag { 93 | continue 94 | } 95 | } 96 | } 97 | 98 | if !exists(filename) { 99 | fmt.Printf("%q does not exist", filename) 100 | 101 | continue 102 | } 103 | fileLookup[filename] = struct{}{} 104 | 105 | pkgName := path.Dir(filename) 106 | 107 | pkg, ok := pkgs[pkgName] 108 | if !ok { 109 | pkg = make(map[string]struct{}) 110 | 111 | pkgs[pkgName] = pkg 112 | } 113 | 114 | pkg[filename] = struct{}{} 115 | } 116 | 117 | return pkgs 118 | } 119 | 120 | func regexpSearchInFile(file string, re *regexp.Regexp) bool { 121 | contents, err := os.ReadFile(file) 122 | if err != nil { 123 | log.Fatal(err) 124 | } 125 | 126 | return re.MatchString(string(contents)) 127 | } 128 | 129 | // FilesOfArgs returns all available Go files given a list of packages, directories and files which can embed patterns. 130 | func FilesOfArgs(args []string, opts *models.Options) []string { 131 | pkgs := packagesWithFilesOfArgs(args, opts) 132 | 133 | pkgsNames := make([]string, 0, len(pkgs)) 134 | for name := range pkgs { 135 | pkgsNames = append(pkgsNames, name) 136 | } 137 | sort.Strings(pkgsNames) 138 | 139 | var files []string 140 | 141 | for _, name := range pkgsNames { 142 | var filenames []string 143 | for name := range pkgs[name] { 144 | filenames = append(filenames, name) 145 | } 146 | sort.Strings(filenames) 147 | 148 | files = append(files, filenames...) 149 | } 150 | 151 | return files 152 | } 153 | 154 | // Package holds file information of a package. 155 | type Package struct { 156 | Name string 157 | Files []string 158 | } 159 | 160 | // Packages defines a list of packages. 161 | type Packages []Package 162 | 163 | // Len is the number of elements in the collection. 164 | func (p Packages) Len() int { return len(p) } 165 | 166 | // Swap swaps the elements with indexes i and j. 167 | func (p Packages) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 168 | 169 | // PackagesByName sorts a list of packages by their name. 170 | type PackagesByName struct{ Packages } 171 | 172 | // Less reports whether the element with index i should sort before the element with index j. 173 | func (p PackagesByName) Less(i, j int) bool { return p.Packages[i].Name < p.Packages[j].Name } 174 | 175 | // PackagesWithFilesOfArgs returns all available Go files sorted by their packages given a list of packages, directories and files which can embed patterns. 176 | func PackagesWithFilesOfArgs(args []string, opts *models.Options) []Package { 177 | pkgs := packagesWithFilesOfArgs(args, opts) 178 | 179 | r := make([]Package, 0, len(pkgs)) 180 | for name := range pkgs { 181 | r = append(r, Package{ 182 | Name: name, 183 | }) 184 | } 185 | sort.Sort(PackagesByName{r}) 186 | 187 | for i := range r { 188 | var filenames []string 189 | for name := range pkgs[r[i].Name] { 190 | filenames = append(filenames, name) 191 | } 192 | sort.Strings(filenames) 193 | 194 | r[i].Files = filenames 195 | } 196 | 197 | return r 198 | } 199 | 200 | func isDir(filename string) bool { 201 | fi, err := os.Stat(filename) 202 | return err == nil && fi.IsDir() 203 | } 204 | 205 | func exists(filename string) bool { 206 | _, err := os.Stat(filename) 207 | return err == nil 208 | } 209 | 210 | func checkDir(dirname string) []string { 211 | pkg, err := build.ImportDir(dirname, 0) 212 | 213 | return checkImportedPackage(pkg, err) 214 | } 215 | 216 | func checkPackage(pkgname string) []string { 217 | pkg, err := build.Import(pkgname, ".", 0) 218 | 219 | return checkImportedPackage(pkg, err) 220 | } 221 | 222 | func checkImportedPackage(pkg *build.Package, err error) []string { 223 | if err != nil { 224 | if _, nogo := err.(*build.NoGoError); nogo { 225 | // Don't complain if the failure is due to no Go source files. 226 | return []string{} 227 | } 228 | _, err := fmt.Fprintln(os.Stderr, err) 229 | if err != nil { 230 | fmt.Println(err) 231 | } 232 | 233 | return []string{} 234 | } 235 | 236 | var files []string 237 | 238 | files = append(files, pkg.GoFiles...) 239 | 240 | joinDirWithFilenames(pkg.Dir, files) 241 | 242 | return files 243 | } 244 | 245 | func joinDirWithFilenames(dir string, files []string) { 246 | if dir != "." { 247 | for i, f := range files { 248 | files[i] = filepath.Join(dir, f) 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /internal/importing/filepath_test.go: -------------------------------------------------------------------------------- 1 | package importing 2 | 3 | import ( 4 | "fmt" 5 | "github.com/avito-tech/go-mutesting/internal/models" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestFilesOfArgs(t *testing.T) { 13 | p := os.Getenv("GOPATH") + "/src/" 14 | 15 | for _, test := range []struct { 16 | args []string 17 | expect []string 18 | }{ 19 | // empty 20 | { 21 | []string{}, 22 | []string{"filepath.go", "import.go"}, 23 | }, 24 | // files 25 | { 26 | []string{"./filepathfixtures/first.go"}, 27 | []string{"./filepathfixtures/first.go"}, 28 | }, 29 | // directories 30 | { 31 | []string{"./filepathfixtures"}, 32 | []string{"filepathfixtures/first.go", "filepathfixtures/second.go", "filepathfixtures/third.go"}, 33 | }, 34 | { 35 | []string{"../importing/filepathfixtures"}, 36 | []string{ 37 | "../importing/filepathfixtures/first.go", 38 | "../importing/filepathfixtures/second.go", 39 | "../importing/filepathfixtures/third.go", 40 | }, 41 | }, 42 | // packages 43 | { 44 | []string{"github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures"}, 45 | []string{ 46 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/first.go", 47 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/second.go", 48 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/third.go", 49 | }, 50 | }, 51 | { 52 | []string{"github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/..."}, 53 | []string{ 54 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/first.go", 55 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/second.go", 56 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/third.go", 57 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/secondfixturespackage/fourth.go", 58 | }, 59 | }, 60 | } { 61 | var opts = &models.Options{} 62 | got := FilesOfArgs(test.args, opts) 63 | 64 | assert.Equal(t, test.expect, got, fmt.Sprintf("With args: %#v", test.args)) 65 | } 66 | } 67 | 68 | func TestPackagesWithFilesOfArgs(t *testing.T) { 69 | p := os.Getenv("GOPATH") + "/src/" 70 | 71 | for _, test := range []struct { 72 | args []string 73 | expect []Package 74 | }{ 75 | // empty 76 | { 77 | []string{}, 78 | []Package{{Name: ".", Files: []string{"filepath.go", "import.go"}}}, 79 | }, 80 | // files 81 | { 82 | []string{"./filepathfixtures/first.go"}, 83 | []Package{{Name: "filepathfixtures", Files: []string{"./filepathfixtures/first.go"}}}, 84 | }, 85 | // directories 86 | { 87 | []string{"./filepathfixtures"}, 88 | []Package{{Name: "filepathfixtures", Files: []string{ 89 | "filepathfixtures/first.go", 90 | "filepathfixtures/second.go", 91 | "filepathfixtures/third.go", 92 | }}}, 93 | }, 94 | { 95 | []string{"../importing/filepathfixtures"}, 96 | []Package{{Name: "../importing/filepathfixtures", Files: []string{ 97 | "../importing/filepathfixtures/first.go", 98 | "../importing/filepathfixtures/second.go", 99 | "../importing/filepathfixtures/third.go", 100 | }}}, 101 | }, 102 | // packages 103 | { 104 | []string{"github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures"}, 105 | []Package{{ 106 | Name: p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures", 107 | Files: []string{ 108 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/first.go", 109 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/second.go", 110 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/third.go", 111 | }, 112 | }}, 113 | }, 114 | { 115 | []string{"github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/..."}, 116 | []Package{ 117 | { 118 | Name: p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures", 119 | Files: []string{ 120 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/first.go", 121 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/second.go", 122 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/third.go", 123 | }, 124 | }, 125 | { 126 | Name: p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/secondfixturespackage", 127 | Files: []string{ 128 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/secondfixturespackage/fourth.go", 129 | }, 130 | }, 131 | }, 132 | }, 133 | } { 134 | var opts = &models.Options{} 135 | got := PackagesWithFilesOfArgs(test.args, opts) 136 | 137 | assert.Equal(t, test.expect, got, fmt.Sprintf("With args: %#v", test.args)) 138 | } 139 | } 140 | 141 | func TestFilesWithSkipWithoutTests(t *testing.T) { 142 | p := os.Getenv("GOPATH") + "/src/" 143 | 144 | for _, test := range []struct { 145 | args []string 146 | expect []string 147 | }{ 148 | // files 149 | { 150 | []string{"./filepathfixtures/first.go"}, 151 | []string(nil), 152 | }, 153 | { 154 | []string{"./filepathfixtures/second.go"}, 155 | []string{"./filepathfixtures/second.go"}, 156 | }, 157 | // directories 158 | { 159 | []string{"./filepathfixtures"}, 160 | []string{"filepathfixtures/second.go", "filepathfixtures/third.go"}, 161 | }, 162 | // packages 163 | { 164 | []string{"github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/..."}, 165 | []string{ 166 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/second.go", 167 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/third.go", 168 | }, 169 | }, 170 | } { 171 | var opts = &models.Options{} 172 | opts.Config.SkipFileWithoutTest = true 173 | got := FilesOfArgs(test.args, opts) 174 | 175 | assert.Equal(t, test.expect, got, fmt.Sprintf("With args: %#v", test.args)) 176 | } 177 | } 178 | 179 | func TestFilesWithSkipWithBuildTagsTests(t *testing.T) { 180 | p := os.Getenv("GOPATH") + "/src/" 181 | 182 | for _, test := range []struct { 183 | args []string 184 | expect []string 185 | }{ 186 | // files 187 | { 188 | []string{"./filepathfixtures/first.go"}, 189 | []string(nil), 190 | }, 191 | { 192 | []string{"./filepathfixtures/third.go"}, 193 | []string(nil), 194 | }, 195 | { 196 | []string{"./filepathfixtures/second.go"}, 197 | []string{"./filepathfixtures/second.go"}, 198 | }, 199 | // directories 200 | { 201 | []string{"./filepathfixtures"}, 202 | []string{"filepathfixtures/second.go"}, 203 | }, 204 | // packages 205 | { 206 | []string{"github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/..."}, 207 | []string{ 208 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/second.go", 209 | }, 210 | }, 211 | } { 212 | var opts = &models.Options{} 213 | opts.Config.SkipFileWithBuildTag = true 214 | got := FilesOfArgs(test.args, opts) 215 | 216 | assert.Equal(t, test.expect, got, fmt.Sprintf("With args: %#v", test.args)) 217 | } 218 | } 219 | 220 | func TestFilesWithExcludedDirs(t *testing.T) { 221 | p := os.Getenv("GOPATH") + "/src/" 222 | 223 | for _, test := range []struct { 224 | args []string 225 | expect []string 226 | config []string 227 | }{ 228 | // files 229 | { 230 | []string{"./filepathfixtures/first.go"}, 231 | []string{"./filepathfixtures/first.go"}, 232 | []string(nil), 233 | }, 234 | { 235 | []string{"./filepathfixtures/second.go"}, 236 | []string{"./filepathfixtures/second.go"}, 237 | []string{"filepathfixtures"}, 238 | }, 239 | { 240 | []string{"filepathfixtures/second.go"}, 241 | []string(nil), 242 | []string{"filepathfixtures"}, 243 | }, 244 | { 245 | []string{"./filepathfixtures/second.go"}, 246 | []string(nil), 247 | []string{"./filepathfixtures"}, 248 | }, 249 | // directories 250 | { 251 | []string{"./filepathfixtures/..."}, 252 | []string{ 253 | "filepathfixtures/first.go", 254 | "filepathfixtures/second.go", 255 | "filepathfixtures/third.go", 256 | }, 257 | []string{"filepathfixtures/secondfixturespackage"}, 258 | }, 259 | { 260 | []string{"./filepathfixtures/..."}, 261 | []string(nil), 262 | []string{"filepathfixtures"}, 263 | }, 264 | { 265 | []string{"./filepathfixtures"}, 266 | []string(nil), 267 | []string{"filepathfixtures"}, 268 | }, 269 | { 270 | []string{"./filepathfixtures"}, 271 | []string{ 272 | "filepathfixtures/first.go", 273 | "filepathfixtures/second.go", 274 | "filepathfixtures/third.go", 275 | }, 276 | []string(nil), 277 | }, 278 | 279 | //packages 280 | { 281 | []string{"github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/..."}, 282 | []string{ 283 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/first.go", 284 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/second.go", 285 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/third.go", 286 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/secondfixturespackage/fourth.go", 287 | }, 288 | []string{"filepathfixtures"}, 289 | }, 290 | { 291 | []string{"github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/..."}, 292 | []string{ 293 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/first.go", 294 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/second.go", 295 | p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/third.go", 296 | }, 297 | []string{p + "github.com/avito-tech/go-mutesting/internal/importing/filepathfixtures/secondfixturespackage/"}, 298 | }, 299 | } { 300 | var opts = &models.Options{} 301 | opts.Config.ExcludeDirs = test.config 302 | 303 | got := FilesOfArgs(test.args, opts) 304 | 305 | assert.Equal(t, test.expect, got, fmt.Sprintf("With args: %#v", test.args)) 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /internal/importing/filepathfixtures/first.go: -------------------------------------------------------------------------------- 1 | package filepathfixtures 2 | -------------------------------------------------------------------------------- /internal/importing/filepathfixtures/second.go: -------------------------------------------------------------------------------- 1 | package filepathfixtures 2 | -------------------------------------------------------------------------------- /internal/importing/filepathfixtures/second_test.go: -------------------------------------------------------------------------------- 1 | package filepathfixtures 2 | -------------------------------------------------------------------------------- /internal/importing/filepathfixtures/secondfixturespackage/fourth.go: -------------------------------------------------------------------------------- 1 | package secondfixturespackage 2 | -------------------------------------------------------------------------------- /internal/importing/filepathfixtures/third.go: -------------------------------------------------------------------------------- 1 | package filepathfixtures 2 | -------------------------------------------------------------------------------- /internal/importing/filepathfixtures/third_test.go: -------------------------------------------------------------------------------- 1 | // +build fixtures 2 | 3 | package filepathfixtures 4 | -------------------------------------------------------------------------------- /internal/importing/import.go: -------------------------------------------------------------------------------- 1 | package importing 2 | 3 | /* 4 | 5 | This file holds a direct copy of the import path matching code of 6 | https://code.google.com/p/go/source/browse/src/cmd/go/main.go. It can be 7 | replaced when https://code.google.com/p/go/issues/detail?id=8768 is resolved. 8 | 9 | */ 10 | 11 | import ( 12 | "fmt" 13 | "go/build" 14 | "log" 15 | "os" 16 | "path" 17 | "path/filepath" 18 | "regexp" 19 | "runtime" 20 | "strings" 21 | ) 22 | 23 | var buildContext = build.Default 24 | 25 | var ( 26 | goroot = filepath.Clean(runtime.GOROOT()) 27 | gorootSrcPkg = filepath.Join(goroot, "src/pkg") 28 | ) 29 | 30 | // importPathsNoDotExpansion returns the import paths to use for the given 31 | // command line, but it does no ... expansion. 32 | func importPathsNoDotExpansion(args []string) []string { 33 | if len(args) == 0 { 34 | return []string{"."} 35 | } 36 | var out []string 37 | for _, a := range args { 38 | // Arguments are supposed to be import paths, but 39 | // as a courtesy to Windows developers, rewrite \ to / 40 | // in command-line arguments. Handles .\... and so on. 41 | if filepath.Separator == '\\' { 42 | a = strings.Replace(a, `\`, `/`, -1) 43 | } 44 | 45 | // Put argument in canonical form, but preserve leading ./. 46 | if strings.HasPrefix(a, "./") { 47 | a = "./" + path.Clean(a) 48 | if a == "./." { 49 | a = "." 50 | } 51 | } else { 52 | a = path.Clean(a) 53 | } 54 | if a == "all" || a == "std" { 55 | out = append(out, allPackages(a)...) 56 | continue 57 | } 58 | out = append(out, a) 59 | } 60 | return out 61 | } 62 | 63 | // importPaths returns the import paths to use for the given command line. 64 | func importPaths(args []string) []string { 65 | args = importPathsNoDotExpansion(args) 66 | var out []string 67 | for _, a := range args { 68 | if strings.Contains(a, "...") { 69 | if build.IsLocalImport(a) { 70 | out = append(out, allPackagesInFS(a)...) 71 | } else { 72 | out = append(out, allPackages(a)...) 73 | } 74 | continue 75 | } 76 | out = append(out, a) 77 | } 78 | return out 79 | } 80 | 81 | // matchPattern(pattern)(name) reports whether 82 | // name matches pattern. Pattern is a limited glob 83 | // pattern in which '...' means 'any string' and there 84 | // is no other special syntax. 85 | func matchPattern(pattern string) func(name string) bool { 86 | re := regexp.QuoteMeta(pattern) 87 | re = strings.Replace(re, `\.\.\.`, `.*`, -1) 88 | // Special case: foo/... matches foo too. 89 | if strings.HasSuffix(re, `/.*`) { 90 | re = re[:len(re)-len(`/.*`)] + `(/.*)?` 91 | } 92 | reg := regexp.MustCompile(`^` + re + `$`) 93 | return func(name string) bool { 94 | return reg.MatchString(name) 95 | } 96 | } 97 | 98 | // hasPathPrefix reports whether the path s begins with the 99 | // elements in prefix. 100 | func hasPathPrefix(s, prefix string) bool { 101 | switch { 102 | default: 103 | return false 104 | case len(s) == len(prefix): 105 | return s == prefix 106 | case len(s) > len(prefix): 107 | if prefix != "" && prefix[len(prefix)-1] == '/' { 108 | return strings.HasPrefix(s, prefix) 109 | } 110 | return s[len(prefix)] == '/' && s[:len(prefix)] == prefix 111 | } 112 | } 113 | 114 | // treeCanMatchPattern(pattern)(name) reports whether 115 | // name or children of name can possibly match pattern. 116 | // Pattern is the same limited glob accepted by matchPattern. 117 | func treeCanMatchPattern(pattern string) func(name string) bool { 118 | wildCard := false 119 | if i := strings.Index(pattern, "..."); i >= 0 { 120 | wildCard = true 121 | pattern = pattern[:i] 122 | } 123 | return func(name string) bool { 124 | return len(name) <= len(pattern) && hasPathPrefix(pattern, name) || 125 | wildCard && strings.HasPrefix(name, pattern) 126 | } 127 | } 128 | 129 | // allPackages returns all the packages that can be found 130 | // under the $GOPATH directories and $GOROOT matching pattern. 131 | // The pattern is either "all" (all packages), "std" (standard packages) 132 | // or a path including "...". 133 | func allPackages(pattern string) []string { 134 | pkgs := matchPackages(pattern) 135 | if len(pkgs) == 0 { 136 | fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern) 137 | } 138 | return pkgs 139 | } 140 | 141 | func matchPackages(pattern string) []string { 142 | match := func(string) bool { return true } 143 | treeCanMatch := func(string) bool { return true } 144 | if pattern != "all" && pattern != "std" { 145 | match = matchPattern(pattern) 146 | treeCanMatch = treeCanMatchPattern(pattern) 147 | } 148 | 149 | have := map[string]bool{ 150 | "builtin": true, // ignore pseudo-package that exists only for documentation 151 | } 152 | if !buildContext.CgoEnabled { 153 | have["runtime/cgo"] = true // ignore during walk 154 | } 155 | var pkgs []string 156 | 157 | // Commands 158 | cmd := filepath.Join(goroot, "src/cmd") + string(filepath.Separator) 159 | err := filepath.Walk(cmd, func(path string, fi os.FileInfo, err error) error { 160 | if err != nil || !fi.IsDir() || path == cmd { 161 | return nil 162 | } 163 | name := path[len(cmd):] 164 | if !treeCanMatch(name) { 165 | return filepath.SkipDir 166 | } 167 | // Commands are all in cmd/, not in subdirectories. 168 | if strings.Contains(name, string(filepath.Separator)) { 169 | return filepath.SkipDir 170 | } 171 | 172 | // We use, e.g., cmd/gofmt as the pseudo import path for gofmt. 173 | name = "cmd/" + name 174 | if have[name] { 175 | return nil 176 | } 177 | have[name] = true 178 | if !match(name) { 179 | return nil 180 | } 181 | _, err = buildContext.ImportDir(path, 0) 182 | if err != nil { 183 | if _, noGo := err.(*build.NoGoError); !noGo { 184 | log.Print(err) 185 | } 186 | return nil 187 | } 188 | pkgs = append(pkgs, name) 189 | return nil 190 | }) 191 | if err != nil { 192 | log.Print(err) 193 | 194 | return nil 195 | } 196 | 197 | for _, src := range buildContext.SrcDirs() { 198 | if pattern == "std" && src != gorootSrcPkg { 199 | continue 200 | } 201 | src = filepath.Clean(src) + string(filepath.Separator) 202 | err := filepath.Walk(src, func(path string, fi os.FileInfo, err error) error { 203 | if err != nil || !fi.IsDir() || path == src { 204 | return nil 205 | } 206 | 207 | // Avoid .foo, _foo, and testdata directory trees. 208 | _, elem := filepath.Split(path) 209 | if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" { 210 | return filepath.SkipDir 211 | } 212 | 213 | name := filepath.ToSlash(path[len(src):]) 214 | if pattern == "std" && strings.Contains(name, ".") { 215 | return filepath.SkipDir 216 | } 217 | if !treeCanMatch(name) { 218 | return filepath.SkipDir 219 | } 220 | if have[name] { 221 | return nil 222 | } 223 | have[name] = true 224 | if !match(name) { 225 | return nil 226 | } 227 | _, err = buildContext.ImportDir(path, 0) 228 | if err != nil { 229 | if _, noGo := err.(*build.NoGoError); noGo { //nolint:gosimple 230 | return nil 231 | } 232 | } 233 | pkgs = append(pkgs, name) 234 | return nil 235 | }) 236 | if err != nil { 237 | log.Print(err) 238 | 239 | return nil 240 | } 241 | } 242 | return pkgs 243 | } 244 | 245 | // allPackagesInFS is like allPackages but is passed a pattern 246 | // beginning ./ or ../, meaning it should scan the tree rooted 247 | // at the given directory. There are ... in the pattern too. 248 | func allPackagesInFS(pattern string) []string { 249 | pkgs := matchPackagesInFS(pattern) 250 | if len(pkgs) == 0 { 251 | fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern) 252 | } 253 | return pkgs 254 | } 255 | 256 | func matchPackagesInFS(pattern string) []string { 257 | // Find directory to begin the scan. 258 | // Could be smarter but this one optimization 259 | // is enough for now, since ... is usually at the 260 | // end of a path. 261 | i := strings.Index(pattern, "...") 262 | dir, _ := path.Split(pattern[:i]) 263 | 264 | // pattern begins with ./ or ../. 265 | // path.Clean will discard the ./ but not the ../. 266 | // We need to preserve the ./ for pattern matching 267 | // and in the returned import paths. 268 | prefix := "" 269 | if strings.HasPrefix(pattern, "./") { 270 | prefix = "./" 271 | } 272 | match := matchPattern(pattern) 273 | 274 | var pkgs []string 275 | err := filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error { 276 | if err != nil || !fi.IsDir() { 277 | return nil 278 | } 279 | if path == dir { 280 | // filepath.Walk starts at dir and recurses. For the recursive case, 281 | // the path is the result of filepath.Join, which calls filepath.Clean. 282 | // The initial case is not Cleaned, though, so we do this explicitly. 283 | // 284 | // This converts a path like "./io/" to "io". Without this step, running 285 | // "cd $GOROOT/src/pkg; go list ./io/..." would incorrectly skip the io 286 | // package, because prepending the prefix "./" to the unclean path would 287 | // result in "././io", and match("././io") returns false. 288 | path = filepath.Clean(path) 289 | } 290 | 291 | // Avoid .foo, _foo, and testdata directory trees, but do not avoid "." or "..". 292 | _, elem := filepath.Split(path) 293 | dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".." 294 | if dot || strings.HasPrefix(elem, "_") || elem == "testdata" { 295 | return filepath.SkipDir 296 | } 297 | 298 | name := prefix + filepath.ToSlash(path) 299 | if !match(name) { 300 | return nil 301 | } 302 | if _, err = build.ImportDir(path, 0); err != nil { 303 | if _, noGo := err.(*build.NoGoError); !noGo { 304 | log.Print(err) 305 | } 306 | return nil 307 | } 308 | pkgs = append(pkgs, name) 309 | return nil 310 | }) 311 | if err != nil { 312 | log.Print(err) 313 | 314 | return nil 315 | } 316 | 317 | return pkgs 318 | } 319 | -------------------------------------------------------------------------------- /internal/models/.coverignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avito-tech/go-mutesting/3ce278f4e19feff59232667ce0f8f7fa702c59ba/internal/models/.coverignore -------------------------------------------------------------------------------- /internal/models/options.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Options Main config structure 4 | type Options struct { 5 | General struct { 6 | Debug bool `long:"debug" description:"Debug log output"` 7 | DoNotRemoveTmpFolder bool `long:"do-not-remove-tmp-folder" description:"Do not remove the tmp folder where all mutations are saved to"` 8 | Help bool `long:"help" description:"Show this help message"` 9 | Verbose bool `long:"verbose" description:"Verbose log output"` 10 | Config string `long:"config" description:"Path to config file"` 11 | } `group:"General options"` 12 | 13 | Files struct { 14 | Blacklist []string `long:"blacklist" description:"List of MD5 checksums of mutations which should be ignored. Each checksum must end with a new line character."` 15 | ListFiles bool `long:"list-files" description:"List found files"` 16 | PrintAST bool `long:"print-ast" description:"Print the ASTs of all given files and exit"` 17 | } `group:"File options"` 18 | 19 | Mutator struct { 20 | DisableMutators []string `long:"disable" description:"Disable mutator by their name or using * as a suffix pattern (in order to check remaining enabled mutators use --verbose option)"` 21 | ListMutators bool `long:"list-mutators" description:"List all available mutators (including disabled)"` 22 | } `group:"Mutator options"` 23 | 24 | Filter struct { 25 | Match string `long:"match" description:"Only functions are mutated that confirm to the arguments regex"` 26 | } `group:"Filter options"` 27 | 28 | Exec struct { 29 | Exec string `long:"exec" description:"Execute this command for every mutation (by default the built-in exec command is used)"` 30 | NoExec bool `long:"no-exec" description:"Skip the built-in exec command and just generate the mutations"` 31 | Timeout uint `long:"exec-timeout" description:"Sets a timeout for the command execution (in seconds)" default:"10"` 32 | } `group:"Exec options"` 33 | 34 | Test struct { 35 | Recursive bool `long:"test-recursive" description:"Defines if the executer should test recursively"` 36 | } `group:"Test options"` 37 | 38 | Remaining struct { 39 | Targets []string `description:"Packages, directories and files even with patterns (by default the current directory)"` 40 | } `positional-args:"true" required:"true"` 41 | 42 | Config struct { 43 | SkipFileWithoutTest bool `yaml:"skip_without_test"` 44 | SkipFileWithBuildTag bool `yaml:"skip_with_build_tags"` 45 | JSONOutput bool `yaml:"json_output"` 46 | SilentMode bool `yaml:"silent_mode"` 47 | ExcludeDirs []string `yaml:"exclude_dirs"` 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/models/report.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // ReportFileName File name for json report 4 | var ReportFileName string = "report.json" 5 | 6 | // Report Structure for mutation report 7 | type Report struct { 8 | Stats Stats `json:"stats"` 9 | Escaped []Mutant `json:"escaped"` 10 | Timeouted []Mutant `json:"timeouted"` 11 | Killed []Mutant `json:"killed"` 12 | Errored []Mutant `json:"errored"` 13 | } 14 | 15 | // Stats There is stats for mutations 16 | type Stats struct { 17 | TotalMutantsCount int64 `json:"totalMutantsCount"` 18 | KilledCount int64 `json:"killedCount"` 19 | NotCoveredCount int64 `json:"notCoveredCount"` 20 | EscapedCount int64 `json:"escapedCount"` 21 | ErrorCount int64 `json:"errorCount"` 22 | SkippedCount int64 `json:"skippedCount"` 23 | TimeOutCount int64 `json:"timeOutCount"` 24 | Msi float64 `json:"msi"` 25 | MutationCodeCoverage int64 `json:"mutationCodeCoverage"` 26 | CoveredCodeMsi float64 `json:"coveredCodeMsi"` 27 | DuplicatedCount int64 `json:"-"` 28 | } 29 | 30 | // Mutant report by mutant for one mutation on one file 31 | type Mutant struct { 32 | Mutator Mutator `json:"mutator"` 33 | Diff string `json:"diff"` 34 | ProcessOutput string `json:"processOutput,omitempty"` 35 | } 36 | 37 | // Mutator mutator and changes in file 38 | type Mutator struct { 39 | MutatorName string `json:"mutatorName"` 40 | OriginalSourceCode string `json:"originalSourceCode"` 41 | MutatedSourceCode string `json:"mutatedSourceCode"` 42 | OriginalFilePath string `json:"originalFilePath"` 43 | OriginalStartLine int64 `json:"originalStartLine"` 44 | } 45 | 46 | // Calculate calculation for final report 47 | func (report *Report) Calculate() { 48 | report.Stats.Msi = report.MsiScore() 49 | report.Stats.TotalMutantsCount = report.TotalCount() 50 | } 51 | 52 | // MsiScore msi score calculation 53 | func (report *Report) MsiScore() float64 { 54 | total := report.TotalCount() 55 | 56 | if total == 0 { 57 | return 0.0 58 | } 59 | 60 | return float64(report.Stats.KilledCount+report.Stats.ErrorCount+report.Stats.SkippedCount) / float64(total) 61 | } 62 | 63 | // TotalCount total mutations count 64 | func (report *Report) TotalCount() int64 { 65 | return report.Stats.KilledCount + report.Stats.EscapedCount + report.Stats.ErrorCount + report.Stats.SkippedCount 66 | } 67 | -------------------------------------------------------------------------------- /internal/parser/diff.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | ) 8 | 9 | const ( 10 | fallbackLine int64 = 0 11 | diffContextLines = 3 12 | ) 13 | 14 | var ( 15 | diffRegex *regexp.Regexp 16 | ) 17 | 18 | func init() { 19 | var err error 20 | diffRegex, err = regexp.Compile(`@@ -(\d+),?\d* \+(\d+),?\d* @@`) 21 | if err != nil { 22 | panic(fmt.Sprintf("failed to compile diff regex: %v", err)) 23 | } 24 | } 25 | 26 | // ParseDiffOutput parses the unified diff (-u) output to extract the line numbers where changes occurred. 27 | // The `-u` flag provides exactly 3 lines of context around changes, so the actual changed line 28 | // can be derived by adjusting the reported line number from the diff header. 29 | func ParseDiffOutput(diff string) []int64 { 30 | lines := make([]int64, 0) 31 | 32 | matches := diffRegex.FindAllStringSubmatch(diff, -1) 33 | for _, match := range matches { 34 | line, err := strconv.ParseInt(match[1], 10, 64) 35 | if err != nil { 36 | lines = append(lines, fallbackLine) 37 | continue 38 | } 39 | 40 | actualLine := line + diffContextLines 41 | lines = append(lines, actualLine) 42 | } 43 | 44 | return lines 45 | } 46 | 47 | // FindOriginalStartLine attempts to find the original line number where a mutation occurred. 48 | func FindOriginalStartLine(diff []byte) int64 { 49 | changedLines := ParseDiffOutput(string(diff)) 50 | 51 | if len(changedLines) == 0 || len(changedLines) > 1 { 52 | return fallbackLine 53 | } 54 | 55 | return changedLines[0] 56 | } 57 | -------------------------------------------------------------------------------- /internal/parser/diff_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParseDiffOutput(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | input string 11 | expected int64 12 | }{ 13 | { 14 | name: "single line change 1", 15 | input: `--- Original 16 | +++ New 17 | @@ -20,7 +20,7 @@ 18 | } 19 | 20 | func doo() { 21 | - ddd := 6 22 | + ddd := 5 23 | slog.Info(strconv.Itoa(ddd)) 24 | fmt.Println("doo") 25 | }`, 26 | expected: 23, 27 | }, 28 | { 29 | name: "single line change 2", 30 | input: `--- Original 31 | +++ New 32 | @@ -14,7 +14,7 @@ 33 | func foo() { 34 | jjj := 6 35 | - slog.Info(strconv.Itoa(jjj)) 36 | + _, _, _ = slog.Info, strconv.Itoa, jjj 37 | 38 | fmt.Println("foo") 39 | }`, 40 | expected: 17, 41 | }, 42 | { 43 | name: "multiple changes should return fallback", 44 | input: `--- Original 45 | +++ New 46 | @@ -14,7 +14,7 @@ 47 | func foo() { 48 | jjj := 6 49 | - slog.Info(strconv.Itoa(jjj)) 50 | + _, _, _ = slog.Info, strconv.Itoa, jjj 51 | 52 | fmt.Println("foo") 53 | } 54 | @@ -20,7 +20,7 @@ 55 | } 56 | 57 | func doo() { 58 | - ddd := 6 59 | + ddd := 5 60 | slog.Info(strconv.Itoa(ddd)) 61 | fmt.Println("doo") 62 | }`, 63 | expected: 0, 64 | }, 65 | { 66 | name: "empty input", 67 | input: "", 68 | expected: 0, 69 | }, 70 | { 71 | name: "invalid line numbers", 72 | input: `@@ -abc +def @@ 73 | -garbage 74 | +garbage`, 75 | expected: 0, 76 | }, 77 | } 78 | 79 | for _, tt := range tests { 80 | t.Run(tt.name, func(t *testing.T) { 81 | got := FindOriginalStartLine([]byte(tt.input)) 82 | if got != tt.expected { 83 | t.Errorf("FindOriginalStartLine() = %v, want %v", got, tt.expected) 84 | } 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /internal/parser/parse.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/build" 7 | "go/parser" 8 | "go/token" 9 | "go/types" 10 | "os" 11 | "path/filepath" 12 | 13 | "golang.org/x/tools/go/loader" //nolint:staticcheck 14 | 15 | "github.com/avito-tech/go-mutesting/internal/filter" 16 | ) 17 | 18 | // ParseFile parses the content of the given file and returns the corresponding ast.File node and its file set for positional information. 19 | // If a fatal error is encountered the error return argument is not nil. 20 | func ParseFile(file string) (*ast.File, *token.FileSet, error) { 21 | data, err := os.ReadFile(file) 22 | if err != nil { 23 | return nil, nil, err 24 | } 25 | 26 | return ParseSource(data) 27 | } 28 | 29 | // ParseSource parses the given source and returns the corresponding ast.File node and its file set for positional information. 30 | // If a fatal error is encountered the error return argument is not nil. 31 | func ParseSource(data interface{}) (*ast.File, *token.FileSet, error) { 32 | fset := token.NewFileSet() 33 | 34 | src, err := parser.ParseFile(fset, "", data, parser.ParseComments|parser.AllErrors) 35 | if err != nil { 36 | return nil, nil, err 37 | } 38 | 39 | return src, fset, err 40 | } 41 | 42 | // ParseAndTypeCheckFile parses and type-checks the given file, and returns everything interesting about the file. 43 | // If a fatal error is encountered the error return argument is not nil. 44 | func ParseAndTypeCheckFile(file string, collectors []filter.NodeCollector) (*ast.File, *token.FileSet, *types.Package, *types.Info, error) { 45 | fileAbs, err := filepath.Abs(file) 46 | if err != nil { 47 | return nil, nil, nil, nil, fmt.Errorf("Could not absolute the file path of %q: %v", file, err) 48 | } 49 | dir := filepath.Dir(fileAbs) 50 | 51 | buildPkg, err := build.ImportDir(dir, build.FindOnly) 52 | if err != nil { 53 | return nil, nil, nil, nil, fmt.Errorf("Could not create build package of %q: %v", file, err) 54 | } 55 | 56 | var conf = loader.Config{ 57 | ParserMode: parser.AllErrors | parser.ParseComments, 58 | } 59 | 60 | if buildPkg.ImportPath != "." { 61 | conf.Import(buildPkg.ImportPath) 62 | } else { 63 | // This is most definitely the case for files inside a "testdata" package 64 | conf.CreateFromFilenames(dir, fileAbs) 65 | } 66 | 67 | conf.AllowErrors = true 68 | prog, err := conf.Load() 69 | if err != nil { 70 | return nil, nil, nil, nil, fmt.Errorf("Could not load package of file %q: %v", file, err) 71 | } 72 | 73 | pkgInfo := prog.InitialPackages()[0] 74 | 75 | var src *ast.File 76 | for _, f := range pkgInfo.Files { 77 | if prog.Fset.Position(f.Pos()).Filename == fileAbs { 78 | src = f 79 | 80 | break 81 | } 82 | } 83 | 84 | if src != nil { 85 | for _, c := range collectors { 86 | c.Collect(src, prog.Fset, fileAbs) 87 | } 88 | } 89 | 90 | return src, prog.Fset, pkgInfo.Pkg, &pkgInfo.Info, nil 91 | } 92 | -------------------------------------------------------------------------------- /internal/parser/parse_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/avito-tech/go-mutesting/internal/annotation" 9 | "github.com/avito-tech/go-mutesting/internal/filter" 10 | ) 11 | 12 | func TestParseAndTypeCheckFileTypeCheckWholePackage(t *testing.T) { 13 | annotationProcessor := annotation.NewProcessor() 14 | skipFilterProcessor := filter.NewSkipMakeArgsFilter() 15 | 16 | collectors := []filter.NodeCollector{ 17 | annotationProcessor, 18 | skipFilterProcessor, 19 | } 20 | _, _, _, _, err := ParseAndTypeCheckFile("../../astutil/create.go", collectors) 21 | assert.Nil(t, err) 22 | } 23 | -------------------------------------------------------------------------------- /mutator/arithmetic/assign_invert.go: -------------------------------------------------------------------------------- 1 | package arithmetic 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | 8 | "github.com/avito-tech/go-mutesting/mutator" 9 | ) 10 | 11 | func init() { 12 | mutator.Register("arithmetic/assign_invert", MutatorArithmeticAssignInvert) 13 | } 14 | 15 | var assignInvertMutations = map[token.Token]token.Token{ 16 | token.ADD_ASSIGN: token.SUB_ASSIGN, 17 | token.SUB_ASSIGN: token.ADD_ASSIGN, 18 | token.MUL_ASSIGN: token.QUO_ASSIGN, 19 | token.QUO_ASSIGN: token.MUL_ASSIGN, 20 | token.REM_ASSIGN: token.MUL_ASSIGN, 21 | } 22 | 23 | // MutatorArithmeticAssignInvert implements a mutator to invert change assign statements. 24 | func MutatorArithmeticAssignInvert(_ *types.Package, _ *types.Info, node ast.Node) []mutator.Mutation { 25 | n, ok := node.(*ast.AssignStmt) 26 | if !ok { 27 | return nil 28 | } 29 | 30 | original := n.Tok 31 | mutated, ok := assignInvertMutations[n.Tok] 32 | if !ok { 33 | return nil 34 | } 35 | 36 | return []mutator.Mutation{ 37 | { 38 | Change: func() { 39 | n.Tok = mutated 40 | }, 41 | Reset: func() { 42 | n.Tok = original 43 | }, 44 | }, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /mutator/arithmetic/assign_invert_test.go: -------------------------------------------------------------------------------- 1 | package arithmetic 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/avito-tech/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorArithmeticAssignInvert(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorArithmeticAssignInvert, 13 | "../../testdata/arithmetic/assign_invert.go", 14 | 5, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /mutator/arithmetic/assignment.go: -------------------------------------------------------------------------------- 1 | package arithmetic 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | 8 | "github.com/avito-tech/go-mutesting/mutator" 9 | ) 10 | 11 | func init() { 12 | mutator.Register("arithmetic/assignment", MutatorArithmeticAssignment) 13 | } 14 | 15 | var assignmentMutations = map[token.Token]token.Token{ 16 | token.ADD_ASSIGN: token.ASSIGN, 17 | token.SUB_ASSIGN: token.ASSIGN, 18 | token.MUL_ASSIGN: token.ASSIGN, 19 | token.QUO_ASSIGN: token.ASSIGN, 20 | token.REM_ASSIGN: token.ASSIGN, 21 | token.AND_ASSIGN: token.ASSIGN, 22 | token.OR_ASSIGN: token.ASSIGN, 23 | token.XOR_ASSIGN: token.ASSIGN, 24 | token.SHL_ASSIGN: token.ASSIGN, 25 | token.SHR_ASSIGN: token.ASSIGN, 26 | token.AND_NOT_ASSIGN: token.ASSIGN, 27 | } 28 | 29 | // MutatorArithmeticAssignment implements a mutator to change base assign logic. 30 | func MutatorArithmeticAssignment(_ *types.Package, _ *types.Info, node ast.Node) []mutator.Mutation { 31 | n, ok := node.(*ast.AssignStmt) 32 | if !ok { 33 | return nil 34 | } 35 | 36 | original := n.Tok 37 | mutated, ok := assignmentMutations[n.Tok] 38 | if !ok { 39 | return nil 40 | } 41 | 42 | return []mutator.Mutation{ 43 | { 44 | Change: func() { 45 | n.Tok = mutated 46 | }, 47 | Reset: func() { 48 | n.Tok = original 49 | }, 50 | }, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /mutator/arithmetic/assignment_test.go: -------------------------------------------------------------------------------- 1 | package arithmetic 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/avito-tech/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorArithmeticAssignment(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorArithmeticAssignment, 13 | "../../testdata/arithmetic/assignment.go", 14 | 11, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /mutator/arithmetic/base.go: -------------------------------------------------------------------------------- 1 | package arithmetic 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | 8 | "github.com/avito-tech/go-mutesting/mutator" 9 | ) 10 | 11 | func init() { 12 | mutator.Register("arithmetic/base", MutatorArithmeticBase) 13 | } 14 | 15 | var arithmeticMutations = map[token.Token]token.Token{ 16 | token.ADD: token.SUB, 17 | token.SUB: token.ADD, 18 | token.MUL: token.QUO, 19 | token.QUO: token.MUL, 20 | token.REM: token.MUL, 21 | } 22 | 23 | // MutatorArithmeticBase implements a mutator to change base arithmetic. 24 | func MutatorArithmeticBase(_ *types.Package, _ *types.Info, node ast.Node) []mutator.Mutation { 25 | n, ok := node.(*ast.BinaryExpr) 26 | if !ok { 27 | return nil 28 | } 29 | 30 | original := n.Op 31 | mutated, ok := arithmeticMutations[n.Op] 32 | if !ok { 33 | return nil 34 | } 35 | 36 | return []mutator.Mutation{ 37 | { 38 | Change: func() { 39 | n.Op = mutated 40 | }, 41 | Reset: func() { 42 | n.Op = original 43 | }, 44 | }, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /mutator/arithmetic/base_test.go: -------------------------------------------------------------------------------- 1 | package arithmetic 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/avito-tech/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorArithmeticBase(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorArithmeticBase, 13 | "../../testdata/arithmetic/base.go", 14 | 5, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /mutator/arithmetic/bitwise.go: -------------------------------------------------------------------------------- 1 | package arithmetic 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | 8 | "github.com/avito-tech/go-mutesting/mutator" 9 | ) 10 | 11 | func init() { 12 | mutator.Register("arithmetic/bitwise", MutatorArithmeticBitwise) 13 | } 14 | 15 | var bitwiseMutations = map[token.Token]token.Token{ 16 | token.AND: token.OR, 17 | token.OR: token.AND, 18 | token.XOR: token.AND, 19 | token.AND_NOT: token.AND, 20 | token.SHL: token.SHR, 21 | token.SHR: token.SHL, 22 | } 23 | 24 | // MutatorArithmeticBitwise implements a mutator to change bitwise arithmetic. 25 | func MutatorArithmeticBitwise(_ *types.Package, _ *types.Info, node ast.Node) []mutator.Mutation { 26 | n, ok := node.(*ast.BinaryExpr) 27 | if !ok { 28 | return nil 29 | } 30 | 31 | original := n.Op 32 | mutated, ok := bitwiseMutations[n.Op] 33 | if !ok { 34 | return nil 35 | } 36 | 37 | return []mutator.Mutation{ 38 | { 39 | Change: func() { 40 | n.Op = mutated 41 | }, 42 | Reset: func() { 43 | n.Op = original 44 | }, 45 | }, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /mutator/arithmetic/bitwise_test.go: -------------------------------------------------------------------------------- 1 | package arithmetic 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/avito-tech/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorArithmeticBitwise(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorArithmeticBitwise, 13 | "../../testdata/arithmetic/bitwise.go", 14 | 6, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /mutator/branch/mutatecase.go: -------------------------------------------------------------------------------- 1 | package branch 2 | 3 | import ( 4 | "go/ast" 5 | "go/types" 6 | 7 | "github.com/avito-tech/go-mutesting/astutil" 8 | "github.com/avito-tech/go-mutesting/mutator" 9 | ) 10 | 11 | func init() { 12 | mutator.Register("branch/case", MutatorCase) 13 | } 14 | 15 | // MutatorCase implements a mutator for case clauses. 16 | func MutatorCase(pkg *types.Package, info *types.Info, node ast.Node) []mutator.Mutation { 17 | n, ok := node.(*ast.CaseClause) 18 | if !ok { 19 | return nil 20 | } 21 | 22 | old := n.Body 23 | 24 | return []mutator.Mutation{ 25 | { 26 | Change: func() { 27 | n.Body = []ast.Stmt{ 28 | astutil.CreateNoopOfStatements(pkg, info, n.Body), 29 | } 30 | }, 31 | Reset: func() { 32 | n.Body = old 33 | }, 34 | }, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /mutator/branch/mutatecase_test.go: -------------------------------------------------------------------------------- 1 | package branch 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/avito-tech/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorCase(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorCase, 13 | "../../testdata/branch/mutatecase.go", 14 | 3, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /mutator/branch/mutateelse.go: -------------------------------------------------------------------------------- 1 | package branch 2 | 3 | import ( 4 | "go/ast" 5 | "go/types" 6 | 7 | "github.com/avito-tech/go-mutesting/astutil" 8 | "github.com/avito-tech/go-mutesting/mutator" 9 | ) 10 | 11 | func init() { 12 | mutator.Register("branch/else", MutatorElse) 13 | } 14 | 15 | // MutatorElse implements a mutator for else branches. 16 | func MutatorElse(pkg *types.Package, info *types.Info, node ast.Node) []mutator.Mutation { 17 | n, ok := node.(*ast.IfStmt) 18 | if !ok { 19 | return nil 20 | } 21 | // We ignore else ifs and nil blocks 22 | _, ok = n.Else.(*ast.IfStmt) 23 | if ok || n.Else == nil { 24 | return nil 25 | } 26 | 27 | old := n.Else 28 | 29 | return []mutator.Mutation{ 30 | { 31 | Change: func() { 32 | n.Else = astutil.CreateNoopOfStatement(pkg, info, old) 33 | }, 34 | Reset: func() { 35 | n.Else = old 36 | }, 37 | }, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /mutator/branch/mutateelse_test.go: -------------------------------------------------------------------------------- 1 | package branch 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/avito-tech/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorElse(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorElse, 13 | "../../testdata/branch/mutateelse.go", 14 | 1, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /mutator/branch/mutateif.go: -------------------------------------------------------------------------------- 1 | package branch 2 | 3 | import ( 4 | "go/ast" 5 | "go/types" 6 | 7 | "github.com/avito-tech/go-mutesting/astutil" 8 | "github.com/avito-tech/go-mutesting/mutator" 9 | ) 10 | 11 | func init() { 12 | mutator.Register("branch/if", MutatorIf) 13 | } 14 | 15 | // MutatorIf implements a mutator for if and else if branches. 16 | func MutatorIf(pkg *types.Package, info *types.Info, node ast.Node) []mutator.Mutation { 17 | n, ok := node.(*ast.IfStmt) 18 | if !ok { 19 | return nil 20 | } 21 | 22 | old := n.Body.List 23 | 24 | return []mutator.Mutation{ 25 | { 26 | Change: func() { 27 | n.Body.List = []ast.Stmt{ 28 | astutil.CreateNoopOfStatement(pkg, info, n.Body), 29 | } 30 | }, 31 | Reset: func() { 32 | n.Body.List = old 33 | }, 34 | }, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /mutator/branch/mutateif_test.go: -------------------------------------------------------------------------------- 1 | package branch 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/avito-tech/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorIf(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorIf, 13 | "../../testdata/branch/mutateif.go", 14 | 2, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /mutator/conditional/negated.go: -------------------------------------------------------------------------------- 1 | package conditional 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | 8 | "github.com/avito-tech/go-mutesting/mutator" 9 | ) 10 | 11 | func init() { 12 | mutator.Register("conditional/negated", MutatorConditionalNegated) 13 | } 14 | 15 | var negatedMutations = map[token.Token]token.Token{ 16 | token.GTR: token.LEQ, 17 | token.LSS: token.GEQ, 18 | token.GEQ: token.LSS, 19 | token.LEQ: token.GTR, 20 | token.EQL: token.NEQ, 21 | token.NEQ: token.EQL, 22 | } 23 | 24 | // MutatorConditionalNegated implements a mutator to improved comparison changes. 25 | func MutatorConditionalNegated(_ *types.Package, _ *types.Info, node ast.Node) []mutator.Mutation { 26 | n, ok := node.(*ast.BinaryExpr) 27 | if !ok { 28 | return nil 29 | } 30 | 31 | original := n.Op 32 | mutated, ok := negatedMutations[n.Op] 33 | if !ok { 34 | return nil 35 | } 36 | 37 | return []mutator.Mutation{ 38 | { 39 | Change: func() { 40 | n.Op = mutated 41 | }, 42 | Reset: func() { 43 | n.Op = original 44 | }, 45 | }, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /mutator/conditional/negated_test.go: -------------------------------------------------------------------------------- 1 | package conditional 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/avito-tech/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorConditionalNegated(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorConditionalNegated, 13 | "../../testdata/conditional/negated.go", 14 | 6, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /mutator/expression/comparison.go: -------------------------------------------------------------------------------- 1 | package expression 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | 8 | "github.com/avito-tech/go-mutesting/mutator" 9 | ) 10 | 11 | func init() { 12 | mutator.Register("expression/comparison", MutatorComparison) 13 | } 14 | 15 | var comparisonMutations = map[token.Token]token.Token{ 16 | token.LSS: token.LEQ, 17 | token.LEQ: token.LSS, 18 | token.GTR: token.GEQ, 19 | token.GEQ: token.GTR, 20 | } 21 | 22 | // MutatorComparison implements a mutator to change comparisons. 23 | func MutatorComparison(pkg *types.Package, info *types.Info, node ast.Node) []mutator.Mutation { 24 | n, ok := node.(*ast.BinaryExpr) 25 | if !ok { 26 | return nil 27 | } 28 | 29 | o := n.Op 30 | r, ok := comparisonMutations[n.Op] 31 | if !ok { 32 | return nil 33 | } 34 | 35 | return []mutator.Mutation{ 36 | { 37 | Change: func() { 38 | n.Op = r 39 | }, 40 | Reset: func() { 41 | n.Op = o 42 | }, 43 | }, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /mutator/expression/comparison_test.go: -------------------------------------------------------------------------------- 1 | package expression 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/avito-tech/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorComparison(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorComparison, 13 | "../../testdata/expression/comparison.go", 14 | 4, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /mutator/expression/remove.go: -------------------------------------------------------------------------------- 1 | package expression 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | 8 | "github.com/avito-tech/go-mutesting/mutator" 9 | ) 10 | 11 | func init() { 12 | mutator.Register("expression/remove", MutatorRemoveTerm) 13 | } 14 | 15 | // MutatorRemoveTerm implements a mutator to remove expression terms. 16 | func MutatorRemoveTerm(pkg *types.Package, info *types.Info, node ast.Node) []mutator.Mutation { 17 | n, ok := node.(*ast.BinaryExpr) 18 | if !ok { 19 | return nil 20 | } 21 | if n.Op != token.LAND && n.Op != token.LOR { 22 | return nil 23 | } 24 | 25 | var r *ast.Ident 26 | 27 | switch n.Op { 28 | case token.LAND: 29 | r = ast.NewIdent("true") 30 | case token.LOR: 31 | r = ast.NewIdent("false") 32 | } 33 | 34 | x := n.X 35 | y := n.Y 36 | 37 | return []mutator.Mutation{ 38 | { 39 | Change: func() { 40 | n.X = r 41 | }, 42 | Reset: func() { 43 | n.X = x 44 | }, 45 | }, 46 | { 47 | Change: func() { 48 | n.Y = r 49 | }, 50 | Reset: func() { 51 | n.Y = y 52 | }, 53 | }, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /mutator/expression/remove_test.go: -------------------------------------------------------------------------------- 1 | package expression 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/avito-tech/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorRemoveTerm(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorRemoveTerm, 13 | "../../testdata/expression/remove.go", 14 | 6, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /mutator/loop/break.go: -------------------------------------------------------------------------------- 1 | package loop 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | 8 | "github.com/avito-tech/go-mutesting/mutator" 9 | ) 10 | 11 | func init() { 12 | mutator.Register("loop/break", MutatorLoopBreak) 13 | } 14 | 15 | var breakMutations = map[token.Token]token.Token{ 16 | token.CONTINUE: token.BREAK, 17 | token.BREAK: token.CONTINUE, 18 | } 19 | 20 | // MutatorLoopBreak implements a mutator to change continue to break and break to continue. 21 | func MutatorLoopBreak(_ *types.Package, _ *types.Info, node ast.Node) []mutator.Mutation { 22 | n, ok := node.(*ast.BranchStmt) 23 | if !ok { 24 | return nil 25 | } 26 | 27 | original := n.Tok 28 | mutated, ok := breakMutations[n.Tok] 29 | if !ok { 30 | return nil 31 | } 32 | 33 | return []mutator.Mutation{ 34 | { 35 | Change: func() { 36 | n.Tok = mutated 37 | }, 38 | Reset: func() { 39 | n.Tok = original 40 | }, 41 | }, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /mutator/loop/break_test.go: -------------------------------------------------------------------------------- 1 | package loop 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/avito-tech/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorLoopBreak(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorLoopBreak, 13 | "../../testdata/loop/break.go", 14 | 2, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /mutator/loop/condition.go: -------------------------------------------------------------------------------- 1 | package loop 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | 8 | "github.com/avito-tech/go-mutesting/mutator" 9 | ) 10 | 11 | func init() { 12 | mutator.Register("loop/condition", MutatorLoopCondition) 13 | } 14 | 15 | // MutatorLoopCondition implements a mutator to change loop condition to always false. 16 | func MutatorLoopCondition(_ *types.Package, _ *types.Info, node ast.Node) []mutator.Mutation { 17 | n, ok := node.(*ast.ForStmt) 18 | if !ok { 19 | return nil 20 | } 21 | 22 | condition, ok := n.Cond.(*ast.BinaryExpr) 23 | if !ok { 24 | return nil 25 | } 26 | 27 | originalX := condition.X 28 | originalOp := condition.Op 29 | originalY := condition.Y 30 | 31 | return []mutator.Mutation{ 32 | { 33 | Change: func() { 34 | condition.X = ast.NewIdent("1") 35 | condition.Op = token.LSS 36 | condition.Y = ast.NewIdent("1") 37 | }, 38 | Reset: func() { 39 | condition.X = originalX 40 | condition.Op = originalOp 41 | condition.Y = originalY 42 | }, 43 | }, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /mutator/loop/condition_test.go: -------------------------------------------------------------------------------- 1 | package loop 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/avito-tech/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorLoopCondition(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorLoopCondition, 13 | "../../testdata/loop/condition.go", 14 | 2, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /mutator/loop/range_break.go: -------------------------------------------------------------------------------- 1 | package loop 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | 8 | "github.com/avito-tech/go-mutesting/mutator" 9 | ) 10 | 11 | func init() { 12 | mutator.Register("loop/range_break", MutatorLoopRangeBreak) 13 | } 14 | 15 | // MutatorLoopRangeBreak implements a mutator to add a break to range-loop body. 16 | func MutatorLoopRangeBreak(_ *types.Package, _ *types.Info, node ast.Node) []mutator.Mutation { 17 | n, ok := node.(*ast.RangeStmt) 18 | if !ok { 19 | return nil 20 | } 21 | 22 | newBody := &ast.BlockStmt{ 23 | List: []ast.Stmt{}, 24 | } 25 | oldBody := n.Body 26 | 27 | newBreakStmt := &ast.BranchStmt{Tok: token.BREAK} 28 | newBody.List = append(newBody.List, newBreakStmt) 29 | newBody.List = append(newBody.List, n.Body.List...) 30 | 31 | return []mutator.Mutation{ 32 | { 33 | Change: func() { 34 | n.Body = newBody 35 | }, 36 | Reset: func() { 37 | n.Body = oldBody 38 | }, 39 | }, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /mutator/loop/range_break_test.go: -------------------------------------------------------------------------------- 1 | package loop 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/avito-tech/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorLoopRangeBreak(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorLoopRangeBreak, 13 | "../../testdata/loop/range_break.go", 14 | 2, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /mutator/mutation.go: -------------------------------------------------------------------------------- 1 | package mutator 2 | 3 | // Mutation defines the behavior of one mutation 4 | type Mutation struct { 5 | // Change is called before executing the exec command. 6 | Change func() 7 | // Reset is called after executing the exec command. 8 | Reset func() 9 | } 10 | -------------------------------------------------------------------------------- /mutator/mutator.go: -------------------------------------------------------------------------------- 1 | package mutator 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/types" 7 | "sort" 8 | ) 9 | 10 | // Mutator defines a mutator for mutation testing by returning a list of possible mutations for the given node. 11 | type Mutator func(pkg *types.Package, info *types.Info, node ast.Node) []Mutation 12 | 13 | var mutatorLookup = make(map[string]Mutator) 14 | 15 | // New returns a new mutator instance given the registered name of the mutator. 16 | // The error return argument is not nil, if the name does not exist in the registered mutator list. 17 | func New(name string) (Mutator, error) { 18 | mutator, ok := mutatorLookup[name] 19 | if !ok { 20 | return nil, fmt.Errorf("unknown mutator %q", name) 21 | } 22 | 23 | return mutator, nil 24 | } 25 | 26 | // List returns a list of all registered mutator names. 27 | func List() []string { 28 | keyMutatorLookup := make([]string, 0, len(mutatorLookup)) 29 | 30 | for key := range mutatorLookup { 31 | keyMutatorLookup = append(keyMutatorLookup, key) 32 | } 33 | 34 | sort.Strings(keyMutatorLookup) 35 | 36 | return keyMutatorLookup 37 | } 38 | 39 | // Register registers a mutator instance function with the given name. 40 | func Register(name string, mutator Mutator) { 41 | if mutator == nil { 42 | panic("mutator function is nil") 43 | } 44 | 45 | if _, ok := mutatorLookup[name]; ok { 46 | panic(fmt.Sprintf("mutator %q already registered", name)) 47 | } 48 | 49 | mutatorLookup[name] = mutator 50 | } 51 | -------------------------------------------------------------------------------- /mutator/mutator_test.go: -------------------------------------------------------------------------------- 1 | package mutator 2 | 3 | import ( 4 | "go/ast" 5 | "go/types" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func mockMutator(pkg *types.Package, info *types.Info, node ast.Node) []Mutation { 12 | // Do nothing 13 | 14 | return nil 15 | } 16 | 17 | func TestMockMutator(t *testing.T) { 18 | // Mock is not registered 19 | for _, name := range List() { 20 | if name == "mock" { 21 | assert.Fail(t, "mock should not be in the mutator list yet") 22 | } 23 | } 24 | 25 | m, err := New("mock") 26 | assert.Nil(t, m) 27 | assert.NotNil(t, err) 28 | 29 | // Register mock 30 | Register("mock", mockMutator) 31 | 32 | // Mock is registered 33 | found := false 34 | for _, name := range List() { 35 | if name == "mock" { 36 | found = true 37 | 38 | break 39 | } 40 | } 41 | assert.True(t, found) 42 | 43 | m, err = New("mock") 44 | assert.NotNil(t, m) 45 | assert.Nil(t, err) 46 | 47 | // Register mock a second time 48 | caught := false 49 | func() { 50 | defer func() { 51 | if r := recover(); r != nil { 52 | caught = true 53 | } 54 | }() 55 | 56 | Register("mock", mockMutator) 57 | }() 58 | assert.True(t, caught) 59 | 60 | // Register nil function 61 | caught = false 62 | func() { 63 | defer func() { 64 | if r := recover(); r != nil { 65 | caught = true 66 | } 67 | }() 68 | 69 | Register("mockachino", nil) 70 | }() 71 | assert.True(t, caught) 72 | } 73 | -------------------------------------------------------------------------------- /mutator/numbers/decrementer.go: -------------------------------------------------------------------------------- 1 | package numbers 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | "strconv" 8 | 9 | "github.com/avito-tech/go-mutesting/mutator" 10 | ) 11 | 12 | func init() { 13 | mutator.Register("numbers/decrementer", MutatorNumbersDecrementer) 14 | } 15 | 16 | // MutatorNumbersDecrementer implements a mutator to decrement int and float. 17 | func MutatorNumbersDecrementer(_ *types.Package, _ *types.Info, node ast.Node) []mutator.Mutation { 18 | n, ok := node.(*ast.BasicLit) 19 | if !ok { 20 | return nil 21 | } 22 | 23 | if n.Kind == token.INT { 24 | original := n.Value 25 | originalInt, err := strconv.Atoi(n.Value) 26 | if err != nil { 27 | return nil 28 | } 29 | 30 | originalInt-- 31 | mutated := strconv.Itoa(originalInt) 32 | 33 | return []mutator.Mutation{ 34 | { 35 | Change: func() { 36 | n.Value = mutated 37 | }, 38 | Reset: func() { 39 | n.Value = original 40 | }, 41 | }, 42 | } 43 | } 44 | 45 | if n.Kind == token.FLOAT { 46 | original := n.Value 47 | originalFloat, err := strconv.ParseFloat(n.Value, 64) 48 | if err != nil { 49 | return nil 50 | } 51 | 52 | originalFloat-- 53 | mutated := strconv.FormatFloat(originalFloat, 'f', -1, 64) 54 | 55 | return []mutator.Mutation{ 56 | { 57 | Change: func() { 58 | n.Value = mutated 59 | }, 60 | Reset: func() { 61 | n.Value = original 62 | }, 63 | }, 64 | } 65 | } 66 | 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /mutator/numbers/decrementer_test.go: -------------------------------------------------------------------------------- 1 | package numbers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/avito-tech/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorNumbersDecrementer(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorNumbersDecrementer, 13 | "../../testdata/numbers/decrementer.go", 14 | 2, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /mutator/numbers/incrementer.go: -------------------------------------------------------------------------------- 1 | package numbers 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | "strconv" 8 | 9 | "github.com/avito-tech/go-mutesting/mutator" 10 | ) 11 | 12 | func init() { 13 | mutator.Register("numbers/incrementer", MutatorNumbersIncrementer) 14 | } 15 | 16 | // MutatorNumbersIncrementer implements a mutator to increment int and float. 17 | func MutatorNumbersIncrementer(_ *types.Package, _ *types.Info, node ast.Node) []mutator.Mutation { 18 | n, ok := node.(*ast.BasicLit) 19 | if !ok { 20 | return nil 21 | } 22 | 23 | if n.Kind == token.INT { 24 | original := n.Value 25 | originalInt, err := strconv.Atoi(n.Value) 26 | if err != nil { 27 | return nil 28 | } 29 | 30 | originalInt++ 31 | mutated := strconv.Itoa(originalInt) 32 | 33 | return []mutator.Mutation{ 34 | { 35 | Change: func() { 36 | n.Value = mutated 37 | }, 38 | Reset: func() { 39 | n.Value = original 40 | }, 41 | }, 42 | } 43 | } 44 | 45 | if n.Kind == token.FLOAT { 46 | original := n.Value 47 | originalFloat, err := strconv.ParseFloat(n.Value, 64) 48 | if err != nil { 49 | return nil 50 | } 51 | 52 | originalFloat++ 53 | mutated := strconv.FormatFloat(originalFloat, 'f', -1, 64) 54 | 55 | return []mutator.Mutation{ 56 | { 57 | Change: func() { 58 | n.Value = mutated 59 | }, 60 | Reset: func() { 61 | n.Value = original 62 | }, 63 | }, 64 | } 65 | } 66 | 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /mutator/numbers/incrementer_test.go: -------------------------------------------------------------------------------- 1 | package numbers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/avito-tech/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorNumbersIncrementer(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorNumbersIncrementer, 13 | "../../testdata/numbers/incrementer.go", 14 | 2, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /mutator/statement/remove.go: -------------------------------------------------------------------------------- 1 | package statement 2 | 3 | import ( 4 | "github.com/avito-tech/go-mutesting/internal/annotation" 5 | "go/ast" 6 | "go/token" 7 | "go/types" 8 | 9 | "github.com/avito-tech/go-mutesting/astutil" 10 | "github.com/avito-tech/go-mutesting/mutator" 11 | ) 12 | 13 | func init() { 14 | mutator.Register("statement/remove", MutatorRemoveStatement) 15 | } 16 | 17 | func checkRemoveStatement(node ast.Stmt) bool { 18 | skip := annotation.HandleBlockStmt(node) 19 | if skip { 20 | return false 21 | } 22 | 23 | switch n := node.(type) { 24 | case *ast.AssignStmt: 25 | if n.Tok != token.DEFINE { 26 | return true 27 | } 28 | case *ast.ExprStmt, *ast.IncDecStmt: 29 | return true 30 | } 31 | 32 | return false 33 | } 34 | 35 | // MutatorRemoveStatement implements a mutator to remove statements. 36 | func MutatorRemoveStatement(pkg *types.Package, info *types.Info, node ast.Node) []mutator.Mutation { 37 | var l []ast.Stmt 38 | 39 | switch n := node.(type) { 40 | case *ast.BlockStmt: 41 | l = n.List 42 | case *ast.CaseClause: 43 | l = n.Body 44 | } 45 | 46 | var mutations []mutator.Mutation 47 | 48 | for i, ni := range l { 49 | if checkRemoveStatement(ni) { 50 | li := i 51 | old := l[li] 52 | 53 | mutations = append(mutations, mutator.Mutation{ 54 | Change: func() { 55 | l[li] = astutil.CreateNoopOfStatement(pkg, info, old) 56 | }, 57 | Reset: func() { 58 | l[li] = old 59 | }, 60 | }) 61 | } 62 | } 63 | 64 | return mutations 65 | } 66 | -------------------------------------------------------------------------------- /mutator/statement/remove_test.go: -------------------------------------------------------------------------------- 1 | package statement 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/avito-tech/go-mutesting/test" 7 | ) 8 | 9 | func TestMutatorRemoveStatement(t *testing.T) { 10 | test.Mutator( 11 | t, 12 | MutatorRemoveStatement, 13 | "../../testdata/statement/remove.go", 14 | 17, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /scripts/ci/errcheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z ${PKG+x} ]; then echo "PKG is not set"; exit 1; fi 4 | if [ -z ${ROOT_DIR+x} ]; then echo "ROOT_DIR is not set"; exit 1; fi 5 | 6 | echo "errcheck:" 7 | OUT=$(errcheck $PKG/... 2>&1 | grep --invert-match -E "(/example)") 8 | if [ -n "$OUT" ]; then echo "$OUT"; PROBLEM=1; fi 9 | 10 | if [ -n "$PROBLEM" ]; then exit 1; fi 11 | -------------------------------------------------------------------------------- /scripts/ci/gofmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z ${PKG+x} ]; then echo "PKG is not set"; exit 1; fi 4 | if [ -z ${ROOT_DIR+x} ]; then echo "ROOT_DIR is not set"; exit 1; fi 5 | 6 | echo "gofmt:" 7 | OUT=$(gofmt -l -s $ROOT_DIR 2>&1 | grep --invert-match -E "(/example)" | grep --invert-match -E "(/testdata)" | grep --invert-match -E "(fixtures)") 8 | if [ -n "$OUT" ]; then echo "$OUT"; PROBLEM=1; fi 9 | 10 | if [ -n "$PROBLEM" ]; then exit 1; fi 11 | -------------------------------------------------------------------------------- /scripts/ci/govet.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z ${PKG+x} ]; then echo "PKG is not set"; exit 1; fi 4 | if [ -z ${ROOT_DIR+x} ]; then echo "ROOT_DIR is not set"; exit 1; fi 5 | 6 | echo "go vet:" 7 | OUT=$(go vet -all=true ./... 2>&1 | grep --invert-match -E "(Checking file|\%p of wrong type|can't check non-constant format|/example)") 8 | if [ -n "$OUT" ]; then echo "$OUT"; PROBLEM=1; fi 9 | 10 | if [ -n "$PROBLEM" ]; then exit 1; fi 11 | -------------------------------------------------------------------------------- /scripts/ci/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z ${PKG+x} ]; then echo "PKG is not set"; exit 1; fi 4 | if [ -z ${ROOT_DIR+x} ]; then echo "ROOT_DIR is not set"; exit 1; fi 5 | 6 | echo "golint:" 7 | OUT=$(golint $PKG/... 2>&1 | grep --invert-match -E "(/example)") 8 | if [ -n "$OUT" ]; then echo "$OUT"; PROBLEM=1; fi 9 | 10 | if [ -n "$PROBLEM" ]; then exit 1; fi 11 | -------------------------------------------------------------------------------- /scripts/exec/test-current-directory.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This exec script implements 4 | # - the replacement of the original file with the mutation, 5 | # - the execution of all tests originating from the current directory, 6 | # - and the reporting if the mutation was killed. 7 | 8 | if [ -z ${MUTATE_CHANGED+x} ]; then echo "MUTATE_CHANGED is not set"; exit 1; fi 9 | if [ -z ${MUTATE_ORIGINAL+x} ]; then echo "MUTATE_ORIGINAL is not set"; exit 1; fi 10 | if [ -z ${MUTATE_PACKAGE+x} ]; then echo "MUTATE_PACKAGE is not set"; exit 1; fi 11 | 12 | function clean_up { 13 | if [ -f $MUTATE_ORIGINAL.tmp ]; 14 | then 15 | mv $MUTATE_ORIGINAL.tmp $MUTATE_ORIGINAL 16 | fi 17 | } 18 | 19 | function sig_handler { 20 | clean_up 21 | 22 | exit $GOMUTESTING_RESULT 23 | } 24 | trap sig_handler SIGHUP SIGINT SIGTERM 25 | 26 | export GOMUTESTING_DIFF=$(diff -u $MUTATE_ORIGINAL $MUTATE_CHANGED) 27 | 28 | mv $MUTATE_ORIGINAL $MUTATE_ORIGINAL.tmp 29 | cp $MUTATE_CHANGED $MUTATE_ORIGINAL 30 | 31 | export MUTATE_TIMEOUT=${MUTATE_TIMEOUT:-10} 32 | 33 | if [ -n "$TEST_RECURSIVE" ]; then 34 | TEST_RECURSIVE="/..." 35 | fi 36 | 37 | GOMUTESTING_TEST=$(go test -timeout $(printf '%ds' $MUTATE_TIMEOUT) .$TEST_RECURSIVE 2>&1) 38 | export GOMUTESTING_RESULT=$? 39 | 40 | if [ "$MUTATE_DEBUG" = true ] ; then 41 | echo "$GOMUTESTING_TEST" 42 | fi 43 | 44 | clean_up 45 | 46 | case $GOMUTESTING_RESULT in 47 | 0) # tests passed -> FAIL 48 | echo "$GOMUTESTING_DIFF" 49 | 50 | exit 1 51 | ;; 52 | 1) # tests failed -> PASS 53 | if [ "$MUTATE_DEBUG" = true ] ; then 54 | echo "$GOMUTESTING_DIFF" 55 | fi 56 | 57 | exit 0 58 | ;; 59 | 2) # did not compile -> SKIP 60 | if [ "$MUTATE_VERBOSE" = true ] ; then 61 | echo "Mutation did not compile" 62 | fi 63 | 64 | if [ "$MUTATE_DEBUG" = true ] ; then 65 | echo "$GOMUTESTING_DIFF" 66 | fi 67 | 68 | exit 2 69 | ;; 70 | *) # Unkown exit code -> SKIP 71 | echo "Unknown exit code" 72 | echo "$GOMUTESTING_DIFF" 73 | 74 | exit $GOMUTESTING_RESULT 75 | ;; 76 | esac 77 | -------------------------------------------------------------------------------- /scripts/exec/test-mutated-package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This exec script implements 4 | # - the replacement of the original file with the mutation, 5 | # - the execution of all tests originating from the package of the mutated file, 6 | # - and the reporting if the mutation was killed. 7 | 8 | if [ -z ${MUTATE_CHANGED+x} ]; then echo "MUTATE_CHANGED is not set"; exit 1; fi 9 | if [ -z ${MUTATE_ORIGINAL+x} ]; then echo "MUTATE_ORIGINAL is not set"; exit 1; fi 10 | if [ -z ${MUTATE_PACKAGE+x} ]; then echo "MUTATE_PACKAGE is not set"; exit 1; fi 11 | 12 | function clean_up { 13 | if [ -f $MUTATE_ORIGINAL.tmp ]; 14 | then 15 | mv $MUTATE_ORIGINAL.tmp $MUTATE_ORIGINAL 16 | fi 17 | } 18 | 19 | function sig_handler { 20 | clean_up 21 | 22 | exit $GOMUTESTING_RESULT 23 | } 24 | trap sig_handler SIGHUP SIGINT SIGTERM 25 | 26 | export GOMUTESTING_DIFF=$(diff -u $MUTATE_ORIGINAL $MUTATE_CHANGED) 27 | 28 | mv $MUTATE_ORIGINAL $MUTATE_ORIGINAL.tmp 29 | cp $MUTATE_CHANGED $MUTATE_ORIGINAL 30 | 31 | export MUTATE_TIMEOUT=${MUTATE_TIMEOUT:-10} 32 | 33 | if [ -n "$TEST_RECURSIVE" ]; then 34 | TEST_RECURSIVE="/..." 35 | fi 36 | 37 | GOMUTESTING_TEST=$(go test -timeout $(printf '%ds' $MUTATE_TIMEOUT) $MUTATE_PACKAGE$TEST_RECURSIVE 2>&1) 38 | export GOMUTESTING_RESULT=$? 39 | 40 | if [ "$MUTATE_DEBUG" = true ] ; then 41 | echo "$GOMUTESTING_TEST" 42 | fi 43 | 44 | clean_up 45 | 46 | case $GOMUTESTING_RESULT in 47 | 0) # tests passed -> FAIL 48 | echo "$GOMUTESTING_DIFF" 49 | 50 | exit 1 51 | ;; 52 | 1) # tests failed -> PASS 53 | if [ "$MUTATE_DEBUG" = true ] ; then 54 | echo "$GOMUTESTING_DIFF" 55 | fi 56 | 57 | exit 0 58 | ;; 59 | 2) # did not compile -> SKIP 60 | if [ "$MUTATE_VERBOSE" = true ] ; then 61 | echo "Mutation did not compile" 62 | fi 63 | 64 | if [ "$MUTATE_DEBUG" = true ] ; then 65 | echo "$GOMUTESTING_DIFF" 66 | fi 67 | 68 | exit 2 69 | ;; 70 | *) # Unkown exit code -> SKIP 71 | echo "Unknown exit code" 72 | echo "$GOMUTESTING_DIFF" 73 | 74 | exit $GOMUTESTING_RESULT 75 | ;; 76 | esac 77 | -------------------------------------------------------------------------------- /test/mutator.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/avito-tech/go-mutesting/internal/annotation" 7 | "github.com/avito-tech/go-mutesting/internal/filter" 8 | "github.com/avito-tech/go-mutesting/internal/parser" 9 | "go/printer" 10 | "os" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/assert" 14 | 15 | "github.com/avito-tech/go-mutesting" 16 | "github.com/avito-tech/go-mutesting/mutator" 17 | ) 18 | 19 | // Mutator tests a mutator. 20 | // It mutates the given original file with the given mutator. Every mutation is then validated with the given changed file. The mutation overall count is validated with the given count. 21 | func Mutator(t *testing.T, m mutator.Mutator, testFile string, count int) { 22 | // Test if mutator is not nil 23 | assert.NotNil(t, m) 24 | 25 | annotationProcessor := annotation.NewProcessor() 26 | skipFilterProcessor := filter.NewSkipMakeArgsFilter() 27 | 28 | collectors := []filter.NodeCollector{ 29 | annotationProcessor, 30 | skipFilterProcessor, 31 | } 32 | 33 | // Read the origianl source code 34 | data, err := os.ReadFile(testFile) 35 | assert.Nil(t, err) 36 | 37 | // Parse and type-check the original source code 38 | src, fset, pkg, info, err := parser.ParseAndTypeCheckFile(testFile, collectors) 39 | assert.Nil(t, err) 40 | 41 | // Mutate a non relevant node 42 | assert.Nil(t, m(pkg, info, src)) 43 | 44 | // Count the actual mutations 45 | n := mutesting.CountWalk(pkg, info, src, m) 46 | assert.Equal(t, count, n) 47 | 48 | // Mutate all relevant nodes -> test whole mutation process 49 | changed := mutesting.MutateWalk(pkg, info, src, m) 50 | 51 | for i := 0; i < count; i++ { 52 | assert.True(t, <-changed) 53 | 54 | buf := new(bytes.Buffer) 55 | err = printer.Fprint(buf, fset, src) 56 | assert.Nil(t, err) 57 | 58 | changedFilename := fmt.Sprintf("%s.%d.go", testFile, i) 59 | changedFile, err := os.ReadFile(changedFilename) 60 | assert.Nil(t, err) 61 | 62 | if !assert.Equal(t, string(changedFile), buf.String(), fmt.Sprintf("For change file %q", changedFilename)) { 63 | err = os.WriteFile(fmt.Sprintf("%s.%d.go.new", testFile, i), buf.Bytes(), 0644) 64 | assert.Nil(t, err) 65 | } 66 | 67 | changed <- true 68 | 69 | assert.True(t, <-changed) 70 | 71 | buf = new(bytes.Buffer) 72 | err = printer.Fprint(buf, fset, src) 73 | assert.Nil(t, err) 74 | 75 | assert.Equal(t, string(data), buf.String()) 76 | 77 | changed <- true 78 | } 79 | 80 | _, ok := <-changed 81 | assert.False(t, ok) 82 | } 83 | -------------------------------------------------------------------------------- /testdata/annotation/collect.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | // mutator-disable-func 9 | func myFunction(a int) int { 10 | return a * a 11 | } 12 | 13 | func regexFunction() { 14 | fmt.Println("This is a test") 15 | } 16 | 17 | func lineFunction() { 18 | // mutator-disable-next-line numbers/incrementer 19 | var y = 10 20 | 21 | fmt.Println(y) 22 | fmt.Println("Line annotation") 23 | } 24 | 25 | // mutator-disable-regexp fmt\.Println * 26 | -------------------------------------------------------------------------------- /testdata/annotation/empty.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | -------------------------------------------------------------------------------- /testdata/annotation/regex.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "log/slog" 9 | "strings" 10 | ) 11 | 12 | var xx = 42 13 | var xxx = 43 14 | 15 | func main() { 16 | fmt.Println(xx) 17 | fmt.Println(xxx) 18 | 19 | s := MyStruct{name: "Go"} 20 | s.Method() 21 | 22 | if strings.Contains("hello", "he") { 23 | fmt.Println("contains!") 24 | } 25 | } 26 | 27 | type MyStruct struct { 28 | name string 29 | } 30 | 31 | func (m MyStruct) Method() { 32 | callMe(3, 5) 33 | fmt.Println("method:", m.name) 34 | } 35 | 36 | func callMe(a int, b int) int { 37 | test := 8 38 | fmt.Println(test) 39 | return a + b 40 | } 41 | 42 | type Greeter interface { 43 | Greet() string 44 | } 45 | 46 | type Person struct{} 47 | 48 | func (p Person) Greet() string { 49 | slog.Info("structured log") 50 | 51 | return "Hi!" 52 | } 53 | -------------------------------------------------------------------------------- /testdata/arithmetic/assign_invert.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i += 10 12 | i -= 20 13 | i *= 2 14 | i /= 5 15 | i %= 10 16 | 17 | fmt.Println(i) 18 | } 19 | -------------------------------------------------------------------------------- /testdata/arithmetic/assign_invert.go.0.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i -= 10 12 | i -= 20 13 | i *= 2 14 | i /= 5 15 | i %= 10 16 | 17 | fmt.Println(i) 18 | } 19 | -------------------------------------------------------------------------------- /testdata/arithmetic/assign_invert.go.1.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i += 10 12 | i += 20 13 | i *= 2 14 | i /= 5 15 | i %= 10 16 | 17 | fmt.Println(i) 18 | } 19 | -------------------------------------------------------------------------------- /testdata/arithmetic/assign_invert.go.2.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i += 10 12 | i -= 20 13 | i /= 2 14 | i /= 5 15 | i %= 10 16 | 17 | fmt.Println(i) 18 | } 19 | -------------------------------------------------------------------------------- /testdata/arithmetic/assign_invert.go.3.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i += 10 12 | i -= 20 13 | i *= 2 14 | i *= 5 15 | i %= 10 16 | 17 | fmt.Println(i) 18 | } 19 | -------------------------------------------------------------------------------- /testdata/arithmetic/assign_invert.go.4.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i += 10 12 | i -= 20 13 | i *= 2 14 | i /= 5 15 | i *= 10 16 | 17 | fmt.Println(i) 18 | } 19 | -------------------------------------------------------------------------------- /testdata/arithmetic/assignment.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i += 10 12 | i -= 20 13 | i *= 2 14 | i /= 2 15 | i %= 10000 16 | 17 | i &= 1 18 | i |= 1 19 | i ^= 1 20 | i <<= 1 21 | i >>= 1 22 | i &^= 1 23 | 24 | fmt.Println(i) 25 | } 26 | -------------------------------------------------------------------------------- /testdata/arithmetic/assignment.go.0.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i = 10 12 | i -= 20 13 | i *= 2 14 | i /= 2 15 | i %= 10000 16 | 17 | i &= 1 18 | i |= 1 19 | i ^= 1 20 | i <<= 1 21 | i >>= 1 22 | i &^= 1 23 | 24 | fmt.Println(i) 25 | } 26 | -------------------------------------------------------------------------------- /testdata/arithmetic/assignment.go.1.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i += 10 12 | i = 20 13 | i *= 2 14 | i /= 2 15 | i %= 10000 16 | 17 | i &= 1 18 | i |= 1 19 | i ^= 1 20 | i <<= 1 21 | i >>= 1 22 | i &^= 1 23 | 24 | fmt.Println(i) 25 | } 26 | -------------------------------------------------------------------------------- /testdata/arithmetic/assignment.go.10.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i += 10 12 | i -= 20 13 | i *= 2 14 | i /= 2 15 | i %= 10000 16 | 17 | i &= 1 18 | i |= 1 19 | i ^= 1 20 | i <<= 1 21 | i >>= 1 22 | i = 1 23 | 24 | fmt.Println(i) 25 | } 26 | -------------------------------------------------------------------------------- /testdata/arithmetic/assignment.go.2.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i += 10 12 | i -= 20 13 | i = 2 14 | i /= 2 15 | i %= 10000 16 | 17 | i &= 1 18 | i |= 1 19 | i ^= 1 20 | i <<= 1 21 | i >>= 1 22 | i &^= 1 23 | 24 | fmt.Println(i) 25 | } 26 | -------------------------------------------------------------------------------- /testdata/arithmetic/assignment.go.3.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i += 10 12 | i -= 20 13 | i *= 2 14 | i = 2 15 | i %= 10000 16 | 17 | i &= 1 18 | i |= 1 19 | i ^= 1 20 | i <<= 1 21 | i >>= 1 22 | i &^= 1 23 | 24 | fmt.Println(i) 25 | } 26 | -------------------------------------------------------------------------------- /testdata/arithmetic/assignment.go.4.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i += 10 12 | i -= 20 13 | i *= 2 14 | i /= 2 15 | i = 10000 16 | 17 | i &= 1 18 | i |= 1 19 | i ^= 1 20 | i <<= 1 21 | i >>= 1 22 | i &^= 1 23 | 24 | fmt.Println(i) 25 | } 26 | -------------------------------------------------------------------------------- /testdata/arithmetic/assignment.go.5.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i += 10 12 | i -= 20 13 | i *= 2 14 | i /= 2 15 | i %= 10000 16 | 17 | i = 1 18 | i |= 1 19 | i ^= 1 20 | i <<= 1 21 | i >>= 1 22 | i &^= 1 23 | 24 | fmt.Println(i) 25 | } 26 | -------------------------------------------------------------------------------- /testdata/arithmetic/assignment.go.6.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i += 10 12 | i -= 20 13 | i *= 2 14 | i /= 2 15 | i %= 10000 16 | 17 | i &= 1 18 | i = 1 19 | i ^= 1 20 | i <<= 1 21 | i >>= 1 22 | i &^= 1 23 | 24 | fmt.Println(i) 25 | } 26 | -------------------------------------------------------------------------------- /testdata/arithmetic/assignment.go.7.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i += 10 12 | i -= 20 13 | i *= 2 14 | i /= 2 15 | i %= 10000 16 | 17 | i &= 1 18 | i |= 1 19 | i = 1 20 | i <<= 1 21 | i >>= 1 22 | i &^= 1 23 | 24 | fmt.Println(i) 25 | } 26 | -------------------------------------------------------------------------------- /testdata/arithmetic/assignment.go.8.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i += 10 12 | i -= 20 13 | i *= 2 14 | i /= 2 15 | i %= 10000 16 | 17 | i &= 1 18 | i |= 1 19 | i ^= 1 20 | i = 1 21 | i >>= 1 22 | i &^= 1 23 | 24 | fmt.Println(i) 25 | } 26 | -------------------------------------------------------------------------------- /testdata/arithmetic/assignment.go.9.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i += 10 12 | i -= 20 13 | i *= 2 14 | i /= 2 15 | i %= 10000 16 | 17 | i &= 1 18 | i |= 1 19 | i ^= 1 20 | i <<= 1 21 | i = 1 22 | i &^= 1 23 | 24 | fmt.Println(i) 25 | } 26 | -------------------------------------------------------------------------------- /testdata/arithmetic/base.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i = i + 10 12 | i = i - 20 13 | i = i * 2 14 | i = i / 2 15 | i = i % 10 16 | 17 | fmt.Println(i) 18 | } 19 | -------------------------------------------------------------------------------- /testdata/arithmetic/base.go.0.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i = i - 10 12 | i = i - 20 13 | i = i * 2 14 | i = i / 2 15 | i = i % 10 16 | 17 | fmt.Println(i) 18 | } 19 | -------------------------------------------------------------------------------- /testdata/arithmetic/base.go.1.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i = i + 10 12 | i = i + 20 13 | i = i * 2 14 | i = i / 2 15 | i = i % 10 16 | 17 | fmt.Println(i) 18 | } 19 | -------------------------------------------------------------------------------- /testdata/arithmetic/base.go.2.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i = i + 10 12 | i = i - 20 13 | i = i / 2 14 | i = i / 2 15 | i = i % 10 16 | 17 | fmt.Println(i) 18 | } 19 | -------------------------------------------------------------------------------- /testdata/arithmetic/base.go.3.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i = i + 10 12 | i = i - 20 13 | i = i * 2 14 | i = i * 2 15 | i = i % 10 16 | 17 | fmt.Println(i) 18 | } 19 | -------------------------------------------------------------------------------- /testdata/arithmetic/base.go.4.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | 11 | i = i + 10 12 | i = i - 20 13 | i = i * 2 14 | i = i / 2 15 | i = i * 10 16 | 17 | fmt.Println(i) 18 | } 19 | -------------------------------------------------------------------------------- /testdata/arithmetic/bitwise.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | j := 200 11 | 12 | a := i & j 13 | a = i | j 14 | a = i ^ j 15 | a = i &^ j 16 | a = i << 1 17 | a = i >> 1 18 | 19 | fmt.Println(a) 20 | } 21 | -------------------------------------------------------------------------------- /testdata/arithmetic/bitwise.go.0.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | j := 200 11 | 12 | a := i | j 13 | a = i | j 14 | a = i ^ j 15 | a = i &^ j 16 | a = i << 1 17 | a = i >> 1 18 | 19 | fmt.Println(a) 20 | } 21 | -------------------------------------------------------------------------------- /testdata/arithmetic/bitwise.go.1.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | j := 200 11 | 12 | a := i & j 13 | a = i & j 14 | a = i ^ j 15 | a = i &^ j 16 | a = i << 1 17 | a = i >> 1 18 | 19 | fmt.Println(a) 20 | } 21 | -------------------------------------------------------------------------------- /testdata/arithmetic/bitwise.go.2.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | j := 200 11 | 12 | a := i & j 13 | a = i | j 14 | a = i & j 15 | a = i &^ j 16 | a = i << 1 17 | a = i >> 1 18 | 19 | fmt.Println(a) 20 | } 21 | -------------------------------------------------------------------------------- /testdata/arithmetic/bitwise.go.3.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | j := 200 11 | 12 | a := i & j 13 | a = i | j 14 | a = i ^ j 15 | a = i & j 16 | a = i << 1 17 | a = i >> 1 18 | 19 | fmt.Println(a) 20 | } 21 | -------------------------------------------------------------------------------- /testdata/arithmetic/bitwise.go.4.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | j := 200 11 | 12 | a := i & j 13 | a = i | j 14 | a = i ^ j 15 | a = i &^ j 16 | a = i >> 1 17 | a = i >> 1 18 | 19 | fmt.Println(a) 20 | } 21 | -------------------------------------------------------------------------------- /testdata/arithmetic/bitwise.go.5.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 100 10 | j := 200 11 | 12 | a := i & j 13 | a = i | j 14 | a = i ^ j 15 | a = i &^ j 16 | a = i << 1 17 | a = i << 1 18 | 19 | fmt.Println(a) 20 | } 21 | -------------------------------------------------------------------------------- /testdata/branch/mutatecase.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | func main() { 11 | i := 1 12 | 13 | for i != 4 { 14 | switch { 15 | case i == 1: 16 | fmt.Println(i) 17 | case i == 2: 18 | fmt.Println(i * 2) 19 | default: 20 | fmt.Println(i * 3) 21 | } 22 | 23 | i++ 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /testdata/branch/mutatecase.go.0.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | func main() { 11 | i := 1 12 | 13 | for i != 4 { 14 | switch { 15 | case i == 1: 16 | _, _ = fmt.Println, i 17 | case i == 2: 18 | fmt.Println(i * 2) 19 | default: 20 | fmt.Println(i * 3) 21 | } 22 | 23 | i++ 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /testdata/branch/mutatecase.go.1.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | func main() { 11 | i := 1 12 | 13 | for i != 4 { 14 | switch { 15 | case i == 1: 16 | fmt.Println(i) 17 | case i == 2: 18 | _, _ = fmt.Println, i 19 | default: 20 | fmt.Println(i * 3) 21 | } 22 | 23 | i++ 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /testdata/branch/mutatecase.go.2.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | func main() { 11 | i := 1 12 | 13 | for i != 4 { 14 | switch { 15 | case i == 1: 16 | fmt.Println(i) 17 | case i == 2: 18 | fmt.Println(i * 2) 19 | default: 20 | _, _ = fmt.Println, i 21 | } 22 | 23 | i++ 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /testdata/branch/mutateelse.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | func main() { 11 | i := 1 12 | 13 | for i != 4 { 14 | if i == 1 { 15 | fmt.Println(i) 16 | } else if i == 2 { 17 | fmt.Println(i * 2) 18 | } else { 19 | fmt.Println(i * 3) 20 | } 21 | 22 | i++ 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/branch/mutateelse.go.0.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | func main() { 11 | i := 1 12 | 13 | for i != 4 { 14 | if i == 1 { 15 | fmt.Println(i) 16 | } else if i == 2 { 17 | fmt.Println(i * 2) 18 | } else { 19 | _, _ = fmt.Println, i 20 | } 21 | 22 | i++ 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/branch/mutateif.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | func main() { 11 | i := 1 12 | 13 | for i != 4 { 14 | if i == 1 { 15 | fmt.Println(i) 16 | } else if i == 2 { 17 | fmt.Println(i * 2) 18 | } else { 19 | fmt.Println(i * 3) 20 | } 21 | 22 | i++ 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/branch/mutateif.go.0.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | func main() { 11 | i := 1 12 | 13 | for i != 4 { 14 | if i == 1 { 15 | _, _ = fmt.Println, i 16 | } else if i == 2 { 17 | fmt.Println(i * 2) 18 | } else { 19 | fmt.Println(i * 3) 20 | } 21 | 22 | i++ 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/branch/mutateif.go.1.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | func main() { 11 | i := 1 12 | 13 | for i != 4 { 14 | if i == 1 { 15 | fmt.Println(i) 16 | } else if i == 2 { 17 | _, _ = fmt.Println, i 18 | } else { 19 | fmt.Println(i * 3) 20 | } 21 | 22 | i++ 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/conditional/negated.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 1 10 | j := 2 11 | 12 | if i > j { 13 | fmt.Println("1") 14 | } 15 | if i < j { 16 | fmt.Println("2") 17 | } 18 | if i >= j { 19 | fmt.Println("3") 20 | } 21 | if i <= j { 22 | fmt.Println("4") 23 | } 24 | if i == j { 25 | fmt.Println("5") 26 | } 27 | if i != j { 28 | fmt.Println("6") 29 | } 30 | fmt.Println("done") 31 | } 32 | -------------------------------------------------------------------------------- /testdata/conditional/negated.go.0.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 1 10 | j := 2 11 | 12 | if i <= j { 13 | fmt.Println("1") 14 | } 15 | if i < j { 16 | fmt.Println("2") 17 | } 18 | if i >= j { 19 | fmt.Println("3") 20 | } 21 | if i <= j { 22 | fmt.Println("4") 23 | } 24 | if i == j { 25 | fmt.Println("5") 26 | } 27 | if i != j { 28 | fmt.Println("6") 29 | } 30 | fmt.Println("done") 31 | } 32 | -------------------------------------------------------------------------------- /testdata/conditional/negated.go.1.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 1 10 | j := 2 11 | 12 | if i > j { 13 | fmt.Println("1") 14 | } 15 | if i >= j { 16 | fmt.Println("2") 17 | } 18 | if i >= j { 19 | fmt.Println("3") 20 | } 21 | if i <= j { 22 | fmt.Println("4") 23 | } 24 | if i == j { 25 | fmt.Println("5") 26 | } 27 | if i != j { 28 | fmt.Println("6") 29 | } 30 | fmt.Println("done") 31 | } 32 | -------------------------------------------------------------------------------- /testdata/conditional/negated.go.2.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 1 10 | j := 2 11 | 12 | if i > j { 13 | fmt.Println("1") 14 | } 15 | if i < j { 16 | fmt.Println("2") 17 | } 18 | if i < j { 19 | fmt.Println("3") 20 | } 21 | if i <= j { 22 | fmt.Println("4") 23 | } 24 | if i == j { 25 | fmt.Println("5") 26 | } 27 | if i != j { 28 | fmt.Println("6") 29 | } 30 | fmt.Println("done") 31 | } 32 | -------------------------------------------------------------------------------- /testdata/conditional/negated.go.3.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 1 10 | j := 2 11 | 12 | if i > j { 13 | fmt.Println("1") 14 | } 15 | if i < j { 16 | fmt.Println("2") 17 | } 18 | if i >= j { 19 | fmt.Println("3") 20 | } 21 | if i > j { 22 | fmt.Println("4") 23 | } 24 | if i == j { 25 | fmt.Println("5") 26 | } 27 | if i != j { 28 | fmt.Println("6") 29 | } 30 | fmt.Println("done") 31 | } 32 | -------------------------------------------------------------------------------- /testdata/conditional/negated.go.4.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 1 10 | j := 2 11 | 12 | if i > j { 13 | fmt.Println("1") 14 | } 15 | if i < j { 16 | fmt.Println("2") 17 | } 18 | if i >= j { 19 | fmt.Println("3") 20 | } 21 | if i <= j { 22 | fmt.Println("4") 23 | } 24 | if i != j { 25 | fmt.Println("5") 26 | } 27 | if i != j { 28 | fmt.Println("6") 29 | } 30 | fmt.Println("done") 31 | } 32 | -------------------------------------------------------------------------------- /testdata/conditional/negated.go.5.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | i := 1 10 | j := 2 11 | 12 | if i > j { 13 | fmt.Println("1") 14 | } 15 | if i < j { 16 | fmt.Println("2") 17 | } 18 | if i >= j { 19 | fmt.Println("3") 20 | } 21 | if i <= j { 22 | fmt.Println("4") 23 | } 24 | if i == j { 25 | fmt.Println("5") 26 | } 27 | if i == j { 28 | fmt.Println("6") 29 | } 30 | fmt.Println("done") 31 | } 32 | -------------------------------------------------------------------------------- /testdata/configs/configForJson.yml.test: -------------------------------------------------------------------------------- 1 | skip_without_test: true 2 | skip_with_build_tags: true 3 | json_output: true 4 | silent_mode: false 5 | -------------------------------------------------------------------------------- /testdata/configs/configSkipWithoutTest.yml.test: -------------------------------------------------------------------------------- 1 | skip_without_test: true 2 | skip_with_build_tags: true 3 | json_output: false 4 | silent_mode: false 5 | 6 | -------------------------------------------------------------------------------- /testdata/expression/comparison.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | if 1 > 2 { 10 | fmt.Printf("1 is greater than 2!") 11 | } 12 | 13 | if 1 < 2 { 14 | fmt.Printf("1 is less than 2!") 15 | } 16 | 17 | if 1 <= 2 { 18 | fmt.Printf("1 is less than or equal to 2!") 19 | } 20 | 21 | if 1 >= 2 { 22 | fmt.Printf("1 is greater than or equal to 2!") 23 | } 24 | 25 | if 1 == 2 { 26 | fmt.Print("1 is equal to 2!") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /testdata/expression/comparison.go.0.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | if 1 >= 2 { 10 | fmt.Printf("1 is greater than 2!") 11 | } 12 | 13 | if 1 < 2 { 14 | fmt.Printf("1 is less than 2!") 15 | } 16 | 17 | if 1 <= 2 { 18 | fmt.Printf("1 is less than or equal to 2!") 19 | } 20 | 21 | if 1 >= 2 { 22 | fmt.Printf("1 is greater than or equal to 2!") 23 | } 24 | 25 | if 1 == 2 { 26 | fmt.Print("1 is equal to 2!") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /testdata/expression/comparison.go.1.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | if 1 > 2 { 10 | fmt.Printf("1 is greater than 2!") 11 | } 12 | 13 | if 1 <= 2 { 14 | fmt.Printf("1 is less than 2!") 15 | } 16 | 17 | if 1 <= 2 { 18 | fmt.Printf("1 is less than or equal to 2!") 19 | } 20 | 21 | if 1 >= 2 { 22 | fmt.Printf("1 is greater than or equal to 2!") 23 | } 24 | 25 | if 1 == 2 { 26 | fmt.Print("1 is equal to 2!") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /testdata/expression/comparison.go.2.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | if 1 > 2 { 10 | fmt.Printf("1 is greater than 2!") 11 | } 12 | 13 | if 1 < 2 { 14 | fmt.Printf("1 is less than 2!") 15 | } 16 | 17 | if 1 < 2 { 18 | fmt.Printf("1 is less than or equal to 2!") 19 | } 20 | 21 | if 1 >= 2 { 22 | fmt.Printf("1 is greater than or equal to 2!") 23 | } 24 | 25 | if 1 == 2 { 26 | fmt.Print("1 is equal to 2!") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /testdata/expression/comparison.go.3.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | if 1 > 2 { 10 | fmt.Printf("1 is greater than 2!") 11 | } 12 | 13 | if 1 < 2 { 14 | fmt.Printf("1 is less than 2!") 15 | } 16 | 17 | if 1 <= 2 { 18 | fmt.Printf("1 is less than or equal to 2!") 19 | } 20 | 21 | if 1 > 2 { 22 | fmt.Printf("1 is greater than or equal to 2!") 23 | } 24 | 25 | if 1 == 2 { 26 | fmt.Print("1 is equal to 2!") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /testdata/expression/remove.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | func main() { 11 | i := 1 12 | 13 | for i != 4 { 14 | if i >= 1 && i <= 1 { 15 | fmt.Println(i) 16 | } else if (i >= 2 && i <= 2) || i*1 == 1+1 { 17 | fmt.Println(i * 2) 18 | } else { 19 | 20 | } 21 | 22 | i++ 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/expression/remove.go.0.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | func main() { 11 | i := 1 12 | 13 | for i != 4 { 14 | if true && i <= 1 { 15 | fmt.Println(i) 16 | } else if (i >= 2 && i <= 2) || i*1 == 1+1 { 17 | fmt.Println(i * 2) 18 | } else { 19 | 20 | } 21 | 22 | i++ 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/expression/remove.go.1.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | func main() { 11 | i := 1 12 | 13 | for i != 4 { 14 | if i >= 1 && true { 15 | fmt.Println(i) 16 | } else if (i >= 2 && i <= 2) || i*1 == 1+1 { 17 | fmt.Println(i * 2) 18 | } else { 19 | 20 | } 21 | 22 | i++ 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/expression/remove.go.2.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | func main() { 11 | i := 1 12 | 13 | for i != 4 { 14 | if i >= 1 && i <= 1 { 15 | fmt.Println(i) 16 | } else if false || i*1 == 1+1 { 17 | fmt.Println(i * 2) 18 | } else { 19 | 20 | } 21 | 22 | i++ 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/expression/remove.go.3.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | func main() { 11 | i := 1 12 | 13 | for i != 4 { 14 | if i >= 1 && i <= 1 { 15 | fmt.Println(i) 16 | } else if (i >= 2 && i <= 2) || false { 17 | fmt.Println(i * 2) 18 | } else { 19 | 20 | } 21 | 22 | i++ 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/expression/remove.go.4.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | func main() { 11 | i := 1 12 | 13 | for i != 4 { 14 | if i >= 1 && i <= 1 { 15 | fmt.Println(i) 16 | } else if (true && i <= 2) || i*1 == 1+1 { 17 | fmt.Println(i * 2) 18 | } else { 19 | 20 | } 21 | 22 | i++ 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/expression/remove.go.5.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | func main() { 11 | i := 1 12 | 13 | for i != 4 { 14 | if i >= 1 && i <= 1 { 15 | fmt.Println(i) 16 | } else if (i >= 2 && true) || i*1 == 1+1 { 17 | fmt.Println(i * 2) 18 | } else { 19 | 20 | } 21 | 22 | i++ 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/loop/break.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | k := 0 10 | 11 | for i := 0; i < 100; i++ { 12 | if i%2 == 1 { 13 | k += i 14 | continue 15 | } 16 | } 17 | 18 | for j := 0; j < 400; j++ { 19 | if j%2 == 1 { 20 | k += j 21 | break 22 | } 23 | } 24 | 25 | fmt.Println(k) 26 | } 27 | -------------------------------------------------------------------------------- /testdata/loop/break.go.0.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | k := 0 10 | 11 | for i := 0; i < 100; i++ { 12 | if i%2 == 1 { 13 | k += i 14 | break 15 | } 16 | } 17 | 18 | for j := 0; j < 400; j++ { 19 | if j%2 == 1 { 20 | k += j 21 | break 22 | } 23 | } 24 | 25 | fmt.Println(k) 26 | } 27 | -------------------------------------------------------------------------------- /testdata/loop/break.go.1.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | k := 0 10 | 11 | for i := 0; i < 100; i++ { 12 | if i%2 == 1 { 13 | k += i 14 | continue 15 | } 16 | } 17 | 18 | for j := 0; j < 400; j++ { 19 | if j%2 == 1 { 20 | k += j 21 | continue 22 | } 23 | } 24 | 25 | fmt.Println(k) 26 | } 27 | -------------------------------------------------------------------------------- /testdata/loop/condition.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | k := 0 10 | 11 | for k < 100 { 12 | k = k + 1 13 | } 14 | 15 | for i := 0; i < 5; i++ { 16 | k = k + 2 17 | } 18 | 19 | fmt.Println(k) 20 | } 21 | -------------------------------------------------------------------------------- /testdata/loop/condition.go.0.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | k := 0 10 | 11 | for 1 < 1 { 12 | k = k + 1 13 | } 14 | 15 | for i := 0; i < 5; i++ { 16 | k = k + 2 17 | } 18 | 19 | fmt.Println(k) 20 | } 21 | -------------------------------------------------------------------------------- /testdata/loop/condition.go.1.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | k := 0 10 | 11 | for k < 100 { 12 | k = k + 1 13 | } 14 | 15 | for i := 0; 1 < 1; i++ { 16 | k = k + 2 17 | } 18 | 19 | fmt.Println(k) 20 | } 21 | -------------------------------------------------------------------------------- /testdata/loop/range_break.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "runtime" 9 | ) 10 | 11 | func main() { 12 | var pow = []int{1, 2, 4, 8, 16, 32, 64, 128} 13 | 14 | for i, v := range pow { 15 | fmt.Printf("2**%d = %d\n", i, v) 16 | } 17 | 18 | switch os := runtime.GOOS; os { 19 | case "darwin": 20 | var cow = []float64{1.0, 2.0, 3.0} 21 | for _, v := range cow { 22 | fmt.Print(v) 23 | } 24 | default: 25 | fmt.Print(":(") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /testdata/loop/range_break.go.0.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "runtime" 9 | ) 10 | 11 | func main() { 12 | var pow = []int{1, 2, 4, 8, 16, 32, 64, 128} 13 | 14 | for i, v := range pow { 15 | break 16 | fmt.Printf("2**%d = %d\n", i, v) 17 | } 18 | 19 | switch os := runtime.GOOS; os { 20 | case "darwin": 21 | var cow = []float64{1.0, 2.0, 3.0} 22 | for _, v := range cow { 23 | fmt.Print(v) 24 | } 25 | default: 26 | fmt.Print(":(") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /testdata/loop/range_break.go.1.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "runtime" 9 | ) 10 | 11 | func main() { 12 | var pow = []int{1, 2, 4, 8, 16, 32, 64, 128} 13 | 14 | for i, v := range pow { 15 | fmt.Printf("2**%d = %d\n", i, v) 16 | } 17 | 18 | switch os := runtime.GOOS; os { 19 | case "darwin": 20 | var cow = []float64{1.0, 2.0, 3.0} 21 | for _, v := range cow { 22 | break 23 | fmt.Print(v) 24 | } 25 | 26 | default: 27 | fmt.Print(":(") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /testdata/numbers/decrementer.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | k := 100 10 | m := 10.1 11 | 12 | fmt.Println(k) 13 | fmt.Println(m) 14 | } 15 | -------------------------------------------------------------------------------- /testdata/numbers/decrementer.go.0.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | k := 99 10 | m := 10.1 11 | 12 | fmt.Println(k) 13 | fmt.Println(m) 14 | } 15 | -------------------------------------------------------------------------------- /testdata/numbers/decrementer.go.1.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | k := 100 10 | m := 9.1 11 | 12 | fmt.Println(k) 13 | fmt.Println(m) 14 | } 15 | -------------------------------------------------------------------------------- /testdata/numbers/incrementer.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | k := 100 10 | m := 10.1 11 | 12 | fmt.Println(k) 13 | fmt.Println(m) 14 | } 15 | -------------------------------------------------------------------------------- /testdata/numbers/incrementer.go.0.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | k := 101 10 | m := 10.1 11 | 12 | fmt.Println(k) 13 | fmt.Println(m) 14 | } 15 | -------------------------------------------------------------------------------- /testdata/numbers/incrementer.go.1.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | func main() { 9 | k := 100 10 | m := 11.1 11 | 12 | fmt.Println(k) 13 | fmt.Println(m) 14 | } 15 | -------------------------------------------------------------------------------- /testdata/statement/remove.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package example 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | func foo() int { 12 | n := 1 13 | 14 | for i := 0; i < 3; i++ { 15 | if i == 0 { 16 | n++ 17 | } else if i == 1 { 18 | n += 2 19 | } else { 20 | n += 3 21 | } 22 | 23 | n++ 24 | } 25 | 26 | if n < 0 { 27 | n = 0 28 | } 29 | 30 | n++ 31 | 32 | n += bar() 33 | 34 | bar() 35 | bar() 36 | 37 | switch { 38 | case n < 20: 39 | n++ 40 | case n > 20: 41 | n-- 42 | default: 43 | n = 0 44 | fmt.Println(n) 45 | func() {}() 46 | } 47 | 48 | var x = 0 49 | x++ 50 | 51 | return n 52 | } 53 | 54 | func bar() int { 55 | return 4 56 | } 57 | 58 | func statementRemoveStructInitialization() (a http.Header, b error) { 59 | var err error 60 | 61 | a, b = http.Header{}, err 62 | 63 | return 64 | } 65 | 66 | func statementRemoveStringArrayMap() map[string][]string { 67 | hash := "ok" 68 | var hdr = make(map[string][]string) 69 | 70 | hdr["Hash"] = []string{hash} 71 | 72 | return hdr 73 | } 74 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.0.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package example 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | func foo() int { 12 | n := 1 13 | 14 | for i := 0; i < 3; i++ { 15 | if i == 0 { 16 | n++ 17 | } else if i == 1 { 18 | n += 2 19 | } else { 20 | n += 3 21 | } 22 | 23 | n++ 24 | } 25 | 26 | if n < 0 { 27 | n = 0 28 | } 29 | _ = n 30 | 31 | n += bar() 32 | 33 | bar() 34 | bar() 35 | 36 | switch { 37 | case n < 20: 38 | n++ 39 | case n > 20: 40 | n-- 41 | default: 42 | n = 0 43 | fmt.Println(n) 44 | func() {}() 45 | } 46 | 47 | var x = 0 48 | x++ 49 | 50 | return n 51 | } 52 | 53 | func bar() int { 54 | return 4 55 | } 56 | 57 | func statementRemoveStructInitialization() (a http.Header, b error) { 58 | var err error 59 | 60 | a, b = http.Header{}, err 61 | 62 | return 63 | } 64 | 65 | func statementRemoveStringArrayMap() map[string][]string { 66 | hash := "ok" 67 | var hdr = make(map[string][]string) 68 | 69 | hdr["Hash"] = []string{hash} 70 | 71 | return hdr 72 | } 73 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.1.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package example 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | func foo() int { 12 | n := 1 13 | 14 | for i := 0; i < 3; i++ { 15 | if i == 0 { 16 | n++ 17 | } else if i == 1 { 18 | n += 2 19 | } else { 20 | n += 3 21 | } 22 | 23 | n++ 24 | } 25 | 26 | if n < 0 { 27 | n = 0 28 | } 29 | 30 | n++ 31 | _ = n 32 | 33 | bar() 34 | bar() 35 | 36 | switch { 37 | case n < 20: 38 | n++ 39 | case n > 20: 40 | n-- 41 | default: 42 | n = 0 43 | fmt.Println(n) 44 | func() {}() 45 | } 46 | 47 | var x = 0 48 | x++ 49 | 50 | return n 51 | } 52 | 53 | func bar() int { 54 | return 4 55 | } 56 | 57 | func statementRemoveStructInitialization() (a http.Header, b error) { 58 | var err error 59 | 60 | a, b = http.Header{}, err 61 | 62 | return 63 | } 64 | 65 | func statementRemoveStringArrayMap() map[string][]string { 66 | hash := "ok" 67 | var hdr = make(map[string][]string) 68 | 69 | hdr["Hash"] = []string{hash} 70 | 71 | return hdr 72 | } 73 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.10.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package example 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | func foo() int { 12 | n := 1 13 | 14 | for i := 0; i < 3; i++ { 15 | if i == 0 { 16 | n++ 17 | } else if i == 1 { 18 | n += 2 19 | } else { 20 | n += 3 21 | } 22 | 23 | n++ 24 | } 25 | 26 | if n < 0 { 27 | n = 0 28 | } 29 | 30 | n++ 31 | 32 | n += bar() 33 | 34 | bar() 35 | bar() 36 | 37 | switch { 38 | case n < 20: 39 | _ = n 40 | 41 | case n > 20: 42 | n-- 43 | default: 44 | n = 0 45 | fmt.Println(n) 46 | func() {}() 47 | } 48 | 49 | var x = 0 50 | x++ 51 | 52 | return n 53 | } 54 | 55 | func bar() int { 56 | return 4 57 | } 58 | 59 | func statementRemoveStructInitialization() (a http.Header, b error) { 60 | var err error 61 | 62 | a, b = http.Header{}, err 63 | 64 | return 65 | } 66 | 67 | func statementRemoveStringArrayMap() map[string][]string { 68 | hash := "ok" 69 | var hdr = make(map[string][]string) 70 | 71 | hdr["Hash"] = []string{hash} 72 | 73 | return hdr 74 | } 75 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.11.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package example 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | func foo() int { 12 | n := 1 13 | 14 | for i := 0; i < 3; i++ { 15 | if i == 0 { 16 | n++ 17 | } else if i == 1 { 18 | n += 2 19 | } else { 20 | n += 3 21 | } 22 | 23 | n++ 24 | } 25 | 26 | if n < 0 { 27 | n = 0 28 | } 29 | 30 | n++ 31 | 32 | n += bar() 33 | 34 | bar() 35 | bar() 36 | 37 | switch { 38 | case n < 20: 39 | n++ 40 | case n > 20: 41 | _ = n 42 | 43 | default: 44 | n = 0 45 | fmt.Println(n) 46 | func() {}() 47 | } 48 | 49 | var x = 0 50 | x++ 51 | 52 | return n 53 | } 54 | 55 | func bar() int { 56 | return 4 57 | } 58 | 59 | func statementRemoveStructInitialization() (a http.Header, b error) { 60 | var err error 61 | 62 | a, b = http.Header{}, err 63 | 64 | return 65 | } 66 | 67 | func statementRemoveStringArrayMap() map[string][]string { 68 | hash := "ok" 69 | var hdr = make(map[string][]string) 70 | 71 | hdr["Hash"] = []string{hash} 72 | 73 | return hdr 74 | } 75 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.12.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package example 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | func foo() int { 12 | n := 1 13 | 14 | for i := 0; i < 3; i++ { 15 | if i == 0 { 16 | n++ 17 | } else if i == 1 { 18 | n += 2 19 | } else { 20 | n += 3 21 | } 22 | 23 | n++ 24 | } 25 | 26 | if n < 0 { 27 | n = 0 28 | } 29 | 30 | n++ 31 | 32 | n += bar() 33 | 34 | bar() 35 | bar() 36 | 37 | switch { 38 | case n < 20: 39 | n++ 40 | case n > 20: 41 | n-- 42 | default: 43 | _ = n 44 | 45 | fmt.Println(n) 46 | func() {}() 47 | } 48 | 49 | var x = 0 50 | x++ 51 | 52 | return n 53 | } 54 | 55 | func bar() int { 56 | return 4 57 | } 58 | 59 | func statementRemoveStructInitialization() (a http.Header, b error) { 60 | var err error 61 | 62 | a, b = http.Header{}, err 63 | 64 | return 65 | } 66 | 67 | func statementRemoveStringArrayMap() map[string][]string { 68 | hash := "ok" 69 | var hdr = make(map[string][]string) 70 | 71 | hdr["Hash"] = []string{hash} 72 | 73 | return hdr 74 | } 75 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.13.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package example 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | func foo() int { 12 | n := 1 13 | 14 | for i := 0; i < 3; i++ { 15 | if i == 0 { 16 | n++ 17 | } else if i == 1 { 18 | n += 2 19 | } else { 20 | n += 3 21 | } 22 | 23 | n++ 24 | } 25 | 26 | if n < 0 { 27 | n = 0 28 | } 29 | 30 | n++ 31 | 32 | n += bar() 33 | 34 | bar() 35 | bar() 36 | 37 | switch { 38 | case n < 20: 39 | n++ 40 | case n > 20: 41 | n-- 42 | default: 43 | n = 0 44 | _, _ = fmt.Println, n 45 | func() {}() 46 | } 47 | 48 | var x = 0 49 | x++ 50 | 51 | return n 52 | } 53 | 54 | func bar() int { 55 | return 4 56 | } 57 | 58 | func statementRemoveStructInitialization() (a http.Header, b error) { 59 | var err error 60 | 61 | a, b = http.Header{}, err 62 | 63 | return 64 | } 65 | 66 | func statementRemoveStringArrayMap() map[string][]string { 67 | hash := "ok" 68 | var hdr = make(map[string][]string) 69 | 70 | hdr["Hash"] = []string{hash} 71 | 72 | return hdr 73 | } 74 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.14.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package example 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | func foo() int { 12 | n := 1 13 | 14 | for i := 0; i < 3; i++ { 15 | if i == 0 { 16 | n++ 17 | } else if i == 1 { 18 | n += 2 19 | } else { 20 | n += 3 21 | } 22 | 23 | n++ 24 | } 25 | 26 | if n < 0 { 27 | n = 0 28 | } 29 | 30 | n++ 31 | 32 | n += bar() 33 | 34 | bar() 35 | bar() 36 | 37 | switch { 38 | case n < 20: 39 | n++ 40 | case n > 20: 41 | n-- 42 | default: 43 | n = 0 44 | fmt.Println(n) 45 | 46 | } 47 | 48 | var x = 0 49 | x++ 50 | 51 | return n 52 | } 53 | 54 | func bar() int { 55 | return 4 56 | } 57 | 58 | func statementRemoveStructInitialization() (a http.Header, b error) { 59 | var err error 60 | 61 | a, b = http.Header{}, err 62 | 63 | return 64 | } 65 | 66 | func statementRemoveStringArrayMap() map[string][]string { 67 | hash := "ok" 68 | var hdr = make(map[string][]string) 69 | 70 | hdr["Hash"] = []string{hash} 71 | 72 | return hdr 73 | } 74 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.15.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package example 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | func foo() int { 12 | n := 1 13 | 14 | for i := 0; i < 3; i++ { 15 | if i == 0 { 16 | n++ 17 | } else if i == 1 { 18 | n += 2 19 | } else { 20 | n += 3 21 | } 22 | 23 | n++ 24 | } 25 | 26 | if n < 0 { 27 | n = 0 28 | } 29 | 30 | n++ 31 | 32 | n += bar() 33 | 34 | bar() 35 | bar() 36 | 37 | switch { 38 | case n < 20: 39 | n++ 40 | case n > 20: 41 | n-- 42 | default: 43 | n = 0 44 | fmt.Println(n) 45 | func() {}() 46 | } 47 | 48 | var x = 0 49 | x++ 50 | 51 | return n 52 | } 53 | 54 | func bar() int { 55 | return 4 56 | } 57 | 58 | func statementRemoveStructInitialization() (a http.Header, b error) { 59 | var err error 60 | _, _, _, _ = a, b, http.Header{}, err 61 | 62 | return 63 | } 64 | 65 | func statementRemoveStringArrayMap() map[string][]string { 66 | hash := "ok" 67 | var hdr = make(map[string][]string) 68 | 69 | hdr["Hash"] = []string{hash} 70 | 71 | return hdr 72 | } 73 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.16.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package example 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | func foo() int { 12 | n := 1 13 | 14 | for i := 0; i < 3; i++ { 15 | if i == 0 { 16 | n++ 17 | } else if i == 1 { 18 | n += 2 19 | } else { 20 | n += 3 21 | } 22 | 23 | n++ 24 | } 25 | 26 | if n < 0 { 27 | n = 0 28 | } 29 | 30 | n++ 31 | 32 | n += bar() 33 | 34 | bar() 35 | bar() 36 | 37 | switch { 38 | case n < 20: 39 | n++ 40 | case n > 20: 41 | n-- 42 | default: 43 | n = 0 44 | fmt.Println(n) 45 | func() {}() 46 | } 47 | 48 | var x = 0 49 | x++ 50 | 51 | return n 52 | } 53 | 54 | func bar() int { 55 | return 4 56 | } 57 | 58 | func statementRemoveStructInitialization() (a http.Header, b error) { 59 | var err error 60 | 61 | a, b = http.Header{}, err 62 | 63 | return 64 | } 65 | 66 | func statementRemoveStringArrayMap() map[string][]string { 67 | hash := "ok" 68 | var hdr = make(map[string][]string) 69 | _, _ = hdr, hash 70 | 71 | return hdr 72 | } 73 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.2.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package example 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | func foo() int { 12 | n := 1 13 | 14 | for i := 0; i < 3; i++ { 15 | if i == 0 { 16 | n++ 17 | } else if i == 1 { 18 | n += 2 19 | } else { 20 | n += 3 21 | } 22 | 23 | n++ 24 | } 25 | 26 | if n < 0 { 27 | n = 0 28 | } 29 | 30 | n++ 31 | 32 | n += bar() 33 | 34 | bar() 35 | 36 | switch { 37 | case n < 20: 38 | n++ 39 | case n > 20: 40 | n-- 41 | default: 42 | n = 0 43 | fmt.Println(n) 44 | func() {}() 45 | } 46 | 47 | var x = 0 48 | x++ 49 | 50 | return n 51 | } 52 | 53 | func bar() int { 54 | return 4 55 | } 56 | 57 | func statementRemoveStructInitialization() (a http.Header, b error) { 58 | var err error 59 | 60 | a, b = http.Header{}, err 61 | 62 | return 63 | } 64 | 65 | func statementRemoveStringArrayMap() map[string][]string { 66 | hash := "ok" 67 | var hdr = make(map[string][]string) 68 | 69 | hdr["Hash"] = []string{hash} 70 | 71 | return hdr 72 | } 73 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.3.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package example 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | func foo() int { 12 | n := 1 13 | 14 | for i := 0; i < 3; i++ { 15 | if i == 0 { 16 | n++ 17 | } else if i == 1 { 18 | n += 2 19 | } else { 20 | n += 3 21 | } 22 | 23 | n++ 24 | } 25 | 26 | if n < 0 { 27 | n = 0 28 | } 29 | 30 | n++ 31 | 32 | n += bar() 33 | 34 | bar() 35 | 36 | switch { 37 | case n < 20: 38 | n++ 39 | case n > 20: 40 | n-- 41 | default: 42 | n = 0 43 | fmt.Println(n) 44 | func() {}() 45 | } 46 | 47 | var x = 0 48 | x++ 49 | 50 | return n 51 | } 52 | 53 | func bar() int { 54 | return 4 55 | } 56 | 57 | func statementRemoveStructInitialization() (a http.Header, b error) { 58 | var err error 59 | 60 | a, b = http.Header{}, err 61 | 62 | return 63 | } 64 | 65 | func statementRemoveStringArrayMap() map[string][]string { 66 | hash := "ok" 67 | var hdr = make(map[string][]string) 68 | 69 | hdr["Hash"] = []string{hash} 70 | 71 | return hdr 72 | } 73 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.4.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package example 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | func foo() int { 12 | n := 1 13 | 14 | for i := 0; i < 3; i++ { 15 | if i == 0 { 16 | n++ 17 | } else if i == 1 { 18 | n += 2 19 | } else { 20 | n += 3 21 | } 22 | 23 | n++ 24 | } 25 | 26 | if n < 0 { 27 | n = 0 28 | } 29 | 30 | n++ 31 | 32 | n += bar() 33 | 34 | bar() 35 | bar() 36 | 37 | switch { 38 | case n < 20: 39 | n++ 40 | case n > 20: 41 | n-- 42 | default: 43 | n = 0 44 | fmt.Println(n) 45 | func() {}() 46 | } 47 | 48 | var x = 0 49 | _ = x 50 | 51 | return n 52 | } 53 | 54 | func bar() int { 55 | return 4 56 | } 57 | 58 | func statementRemoveStructInitialization() (a http.Header, b error) { 59 | var err error 60 | 61 | a, b = http.Header{}, err 62 | 63 | return 64 | } 65 | 66 | func statementRemoveStringArrayMap() map[string][]string { 67 | hash := "ok" 68 | var hdr = make(map[string][]string) 69 | 70 | hdr["Hash"] = []string{hash} 71 | 72 | return hdr 73 | } 74 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.5.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package example 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | func foo() int { 12 | n := 1 13 | 14 | for i := 0; i < 3; i++ { 15 | if i == 0 { 16 | n++ 17 | } else if i == 1 { 18 | n += 2 19 | } else { 20 | n += 3 21 | } 22 | _ = n 23 | 24 | } 25 | 26 | if n < 0 { 27 | n = 0 28 | } 29 | 30 | n++ 31 | 32 | n += bar() 33 | 34 | bar() 35 | bar() 36 | 37 | switch { 38 | case n < 20: 39 | n++ 40 | case n > 20: 41 | n-- 42 | default: 43 | n = 0 44 | fmt.Println(n) 45 | func() {}() 46 | } 47 | 48 | var x = 0 49 | x++ 50 | 51 | return n 52 | } 53 | 54 | func bar() int { 55 | return 4 56 | } 57 | 58 | func statementRemoveStructInitialization() (a http.Header, b error) { 59 | var err error 60 | 61 | a, b = http.Header{}, err 62 | 63 | return 64 | } 65 | 66 | func statementRemoveStringArrayMap() map[string][]string { 67 | hash := "ok" 68 | var hdr = make(map[string][]string) 69 | 70 | hdr["Hash"] = []string{hash} 71 | 72 | return hdr 73 | } 74 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.6.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package example 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | func foo() int { 12 | n := 1 13 | 14 | for i := 0; i < 3; i++ { 15 | if i == 0 { 16 | _ = n 17 | 18 | } else if i == 1 { 19 | n += 2 20 | } else { 21 | n += 3 22 | } 23 | 24 | n++ 25 | } 26 | 27 | if n < 0 { 28 | n = 0 29 | } 30 | 31 | n++ 32 | 33 | n += bar() 34 | 35 | bar() 36 | bar() 37 | 38 | switch { 39 | case n < 20: 40 | n++ 41 | case n > 20: 42 | n-- 43 | default: 44 | n = 0 45 | fmt.Println(n) 46 | func() {}() 47 | } 48 | 49 | var x = 0 50 | x++ 51 | 52 | return n 53 | } 54 | 55 | func bar() int { 56 | return 4 57 | } 58 | 59 | func statementRemoveStructInitialization() (a http.Header, b error) { 60 | var err error 61 | 62 | a, b = http.Header{}, err 63 | 64 | return 65 | } 66 | 67 | func statementRemoveStringArrayMap() map[string][]string { 68 | hash := "ok" 69 | var hdr = make(map[string][]string) 70 | 71 | hdr["Hash"] = []string{hash} 72 | 73 | return hdr 74 | } 75 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.7.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package example 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | func foo() int { 12 | n := 1 13 | 14 | for i := 0; i < 3; i++ { 15 | if i == 0 { 16 | n++ 17 | } else if i == 1 { 18 | _ = n 19 | 20 | } else { 21 | n += 3 22 | } 23 | 24 | n++ 25 | } 26 | 27 | if n < 0 { 28 | n = 0 29 | } 30 | 31 | n++ 32 | 33 | n += bar() 34 | 35 | bar() 36 | bar() 37 | 38 | switch { 39 | case n < 20: 40 | n++ 41 | case n > 20: 42 | n-- 43 | default: 44 | n = 0 45 | fmt.Println(n) 46 | func() {}() 47 | } 48 | 49 | var x = 0 50 | x++ 51 | 52 | return n 53 | } 54 | 55 | func bar() int { 56 | return 4 57 | } 58 | 59 | func statementRemoveStructInitialization() (a http.Header, b error) { 60 | var err error 61 | 62 | a, b = http.Header{}, err 63 | 64 | return 65 | } 66 | 67 | func statementRemoveStringArrayMap() map[string][]string { 68 | hash := "ok" 69 | var hdr = make(map[string][]string) 70 | 71 | hdr["Hash"] = []string{hash} 72 | 73 | return hdr 74 | } 75 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.8.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package example 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | func foo() int { 12 | n := 1 13 | 14 | for i := 0; i < 3; i++ { 15 | if i == 0 { 16 | n++ 17 | } else if i == 1 { 18 | n += 2 19 | } else { 20 | _ = n 21 | 22 | } 23 | 24 | n++ 25 | } 26 | 27 | if n < 0 { 28 | n = 0 29 | } 30 | 31 | n++ 32 | 33 | n += bar() 34 | 35 | bar() 36 | bar() 37 | 38 | switch { 39 | case n < 20: 40 | n++ 41 | case n > 20: 42 | n-- 43 | default: 44 | n = 0 45 | fmt.Println(n) 46 | func() {}() 47 | } 48 | 49 | var x = 0 50 | x++ 51 | 52 | return n 53 | } 54 | 55 | func bar() int { 56 | return 4 57 | } 58 | 59 | func statementRemoveStructInitialization() (a http.Header, b error) { 60 | var err error 61 | 62 | a, b = http.Header{}, err 63 | 64 | return 65 | } 66 | 67 | func statementRemoveStringArrayMap() map[string][]string { 68 | hash := "ok" 69 | var hdr = make(map[string][]string) 70 | 71 | hdr["Hash"] = []string{hash} 72 | 73 | return hdr 74 | } 75 | -------------------------------------------------------------------------------- /testdata/statement/remove.go.9.go: -------------------------------------------------------------------------------- 1 | //go:build examplemain 2 | // +build examplemain 3 | 4 | package example 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | func foo() int { 12 | n := 1 13 | 14 | for i := 0; i < 3; i++ { 15 | if i == 0 { 16 | n++ 17 | } else if i == 1 { 18 | n += 2 19 | } else { 20 | n += 3 21 | } 22 | 23 | n++ 24 | } 25 | 26 | if n < 0 { 27 | _ = n 28 | 29 | } 30 | 31 | n++ 32 | 33 | n += bar() 34 | 35 | bar() 36 | bar() 37 | 38 | switch { 39 | case n < 20: 40 | n++ 41 | case n > 20: 42 | n-- 43 | default: 44 | n = 0 45 | fmt.Println(n) 46 | func() {}() 47 | } 48 | 49 | var x = 0 50 | x++ 51 | 52 | return n 53 | } 54 | 55 | func bar() int { 56 | return 4 57 | } 58 | 59 | func statementRemoveStructInitialization() (a http.Header, b error) { 60 | var err error 61 | 62 | a, b = http.Header{}, err 63 | 64 | return 65 | } 66 | 67 | func statementRemoveStringArrayMap() map[string][]string { 68 | hash := "ok" 69 | var hdr = make(map[string][]string) 70 | 71 | hdr["Hash"] = []string{hash} 72 | 73 | return hdr 74 | } 75 | -------------------------------------------------------------------------------- /walk.go: -------------------------------------------------------------------------------- 1 | package mutesting 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/types" 7 | "strings" 8 | 9 | "github.com/avito-tech/go-mutesting/mutator" 10 | ) 11 | 12 | // CountWalk returns the number of corresponding mutations for a given mutator. 13 | // It traverses the AST of the given node and calls the method Check of the given mutator for every node and sums up the returned counts. After completion of the traversal the final counter is returned. 14 | func CountWalk(pkg *types.Package, info *types.Info, node ast.Node, m mutator.Mutator) int { 15 | w := &countWalk{ 16 | count: 0, 17 | mutator: m, 18 | pkg: pkg, 19 | info: info, 20 | } 21 | 22 | ast.Walk(w, node) 23 | 24 | return w.count 25 | } 26 | 27 | type countWalk struct { 28 | count int 29 | mutator mutator.Mutator 30 | pkg *types.Package 31 | info *types.Info 32 | } 33 | 34 | // Visit implements the Visit method of the ast.Visitor interface 35 | func (w *countWalk) Visit(node ast.Node) ast.Visitor { 36 | if node == nil { 37 | return w 38 | } 39 | 40 | w.count += len(w.mutator(w.pkg, w.info, node)) 41 | 42 | return w 43 | } 44 | 45 | // MutateWalk mutates the given node with the given mutator returning a channel to control the mutation steps. 46 | // It traverses the AST of the given node and calls the method Check of the given mutator to verify that a node can be mutated by the mutator. If a node can be mutated the method Mutate of the given mutator is executed with the node and the control channel. After completion of the traversal the control channel is closed. 47 | func MutateWalk(pkg *types.Package, info *types.Info, node ast.Node, m mutator.Mutator) chan bool { 48 | w := &mutateWalk{ 49 | changed: make(chan bool), 50 | mutator: m, 51 | pkg: pkg, 52 | info: info, 53 | } 54 | 55 | go func() { 56 | ast.Walk(w, node) 57 | 58 | close(w.changed) 59 | }() 60 | 61 | return w.changed 62 | } 63 | 64 | type mutateWalk struct { 65 | changed chan bool 66 | mutator mutator.Mutator 67 | pkg *types.Package 68 | info *types.Info 69 | } 70 | 71 | // Visit implements the Visit method of the ast.Visitor interface 72 | func (w *mutateWalk) Visit(node ast.Node) ast.Visitor { 73 | if node == nil { 74 | return w 75 | } 76 | 77 | for _, m := range w.mutator(w.pkg, w.info, node) { 78 | m.Change() 79 | w.changed <- true 80 | <-w.changed 81 | 82 | m.Reset() 83 | w.changed <- true 84 | <-w.changed 85 | } 86 | 87 | return w 88 | } 89 | 90 | // PrintWalk traverses the AST of the given node and prints every node to STDOUT. 91 | func PrintWalk(node ast.Node) { 92 | w := &printWalk{ 93 | level: 0, 94 | } 95 | 96 | ast.Walk(w, node) 97 | } 98 | 99 | type printWalk struct { 100 | level int 101 | } 102 | 103 | // Visit implements the Visit method of the ast.Visitor interface 104 | func (w *printWalk) Visit(node ast.Node) ast.Visitor { 105 | if node != nil { 106 | w.level++ 107 | 108 | fmt.Printf("%s(%p)%#v\n", strings.Repeat("\t", w.level), node, node) 109 | } else { 110 | w.level-- 111 | } 112 | 113 | return w 114 | } 115 | --------------------------------------------------------------------------------