├── .gitignore ├── testdata ├── importname │ ├── importname.go │ ├── go.mod │ └── go.sum ├── rangestmt │ └── rangestmt.go ├── revert │ └── revert.go └── general │ └── general.go ├── go.mod ├── Makefile ├── go.sum ├── .github └── workflows │ └── ci.yaml ├── LICENSE ├── README.md ├── quickfix_test.go ├── cmd └── goquickfix │ └── main.go └── quickfix.go /.gitignore: -------------------------------------------------------------------------------- 1 | /goquickfix 2 | *.exe 3 | *.test 4 | *.out 5 | -------------------------------------------------------------------------------- /testdata/importname/importname.go: -------------------------------------------------------------------------------- 1 | package importname 2 | 3 | import "github.com/motemen/go-quickfix" 4 | 5 | func F() { 6 | } 7 | -------------------------------------------------------------------------------- /testdata/rangestmt/rangestmt.go: -------------------------------------------------------------------------------- 1 | package rangestmt 2 | 3 | func F() { 4 | for range []any{} { 5 | } 6 | 7 | for i, x := range []any{} { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/motemen/go-quickfix 2 | 3 | go 1.22.0 4 | 5 | require golang.org/x/tools v0.30.0 6 | 7 | require ( 8 | golang.org/x/mod v0.23.0 // indirect 9 | golang.org/x/sync v0.11.0 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /testdata/importname/go.mod: -------------------------------------------------------------------------------- 1 | module gomod 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/motemen/go-quickfix v0.0.0-20160413151302-5c522febc679 // indirect 7 | golang.org/x/tools v0.0.0-20191230220329-2aa90c603ae3 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /testdata/revert/revert.go: -------------------------------------------------------------------------------- 1 | package revert 2 | 3 | import _ "fmt" 4 | 5 | import _ "image/png" 6 | 7 | func F() { 8 | var x, y, z int 9 | _ = y 10 | _ = z 11 | 12 | if true { 13 | _ = x 14 | } 15 | 16 | for i := range []int{} { 17 | _ = i 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /testdata/general/general.go: -------------------------------------------------------------------------------- 1 | package general 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func F() { 9 | a := 1 10 | b := a + 1 11 | 12 | var s string 13 | 14 | b := 3 15 | 16 | switch b { 17 | default: 18 | var x any 19 | } 20 | 21 | select { 22 | case u := <-make(chan struct{}): 23 | var y int 24 | } 25 | 26 | for i, n := range []int{} { 27 | } 28 | 29 | os.Exit(0) 30 | } 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN := goquickfix 2 | GOBIN ?= $(shell go env GOPATH)/bin 3 | 4 | .PHONY: all 5 | all: build 6 | 7 | .PHONY: build 8 | build: 9 | go build -o $(BIN) ./cmd/$(BIN) 10 | 11 | .PHONY: install 12 | install: 13 | go install ./cmd/$(BIN) 14 | 15 | .PHONY: test 16 | test: build 17 | go test -v -race ./... 18 | 19 | .PHONY: lint 20 | lint: $(GOBIN)/staticcheck 21 | go vet ./... 22 | staticcheck -checks all ./... 23 | 24 | $(GOBIN)/staticcheck: 25 | go install honnef.co/go/tools/cmd/staticcheck@latest 26 | 27 | .PHONY: clean 28 | clean: 29 | rm -f $(BIN) 30 | go clean 31 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= 4 | golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 5 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 6 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 7 | golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= 8 | golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | name: Test 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | go: [1.24.x, 1.23.x, 1.22.x] 19 | fail-fast: false 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v4 23 | - name: Setup Go 24 | uses: actions/setup-go@v5 25 | with: 26 | go-version: ${{ matrix.go }} 27 | - name: Build 28 | run: make build 29 | - name: Test 30 | run: make test 31 | - name: Lint 32 | run: make lint 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Hiroshi SHIBAMURA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /testdata/importname/go.sum: -------------------------------------------------------------------------------- 1 | github.com/motemen/go-quickfix v0.0.0-20160413151302-5c522febc679 h1:UkBp8kp/ChZOCNV1nyiKpfufVkTdYU32saV0Y6iQgKk= 2 | github.com/motemen/go-quickfix v0.0.0-20160413151302-5c522febc679/go.mod h1:MIkbIO1YK5LNJNfBzYUstnQr/eQTEFKzlBuRsOkknUI= 3 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 4 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 5 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 6 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 7 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 8 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 9 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 10 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 11 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 12 | golang.org/x/tools v0.0.0-20191230220329-2aa90c603ae3 h1:2+KluhQfJ1YhW+TB1KrISS2SfiG1pLEoseB0D4VF/bo= 13 | golang.org/x/tools v0.0.0-20191230220329-2aa90c603ae3/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 14 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # goquickfix 2 | [![CI Status](https://github.com/motemen/go-quickfix/actions/workflows/ci.yaml/badge.svg?branch=master)](https://github.com/motemen/go-quickfix/actions?query=branch:master) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/motemen/go-quickfix)](https://goreportcard.com/report/github.com/motemen/go-quickfix) 4 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/motemen/go-quickfix/blob/master/LICENSE) 5 | [![release](https://img.shields.io/github/release/motemen/go-quickfix/all.svg)](https://github.com/motemen/go-quickfix/releases) 6 | [![pkg.go.dev](https://pkg.go.dev/badge/github.com/motemen/go-quickfix)](https://pkg.go.dev/github.com/motemen/go-quickfix) 7 | 8 | The `goquickfix` command quickly fixes Go source that is well typed but 9 | Go refuses to compile e.g. `declared and not used: x`. 10 | 11 | ## Installation 12 | 13 | ```sh 14 | go install github.com/motemen/go-quickfix/cmd/goquickfix@latest 15 | ``` 16 | 17 | ## Usage 18 | 19 | ``` 20 | goquickfix [-w] [-revert] ... 21 | 22 | Flags: 23 | -revert: try to revert possible quickfixes introduced by goquickfix 24 | -w: write result to (source) file instead of stdout 25 | ``` 26 | 27 | ### Description 28 | 29 | While coding, you may write a Go program that is completely well typed 30 | but `go build` (or `run` or `test`) refuses to build, like this: 31 | 32 | ```go 33 | package main 34 | 35 | import ( 36 | "fmt" 37 | "log" 38 | ) 39 | 40 | func main() { 41 | nums := []int{3, 1, 4, 1, 5} 42 | for i, n := range nums { 43 | fmt.Println(n) 44 | } 45 | } 46 | ``` 47 | 48 | The Go compiler will complain: 49 | 50 | ``` 51 | main.go:5:2: "log" imported and not used 52 | main.go:10:6: declared and not used: i 53 | ``` 54 | 55 | Do we have to bother to comment out the import line or remove 56 | the unused identifier one by one for the Go compiler? Of course no, 57 | `goquickfix` does the work instead of you. 58 | 59 | Run 60 | 61 | ``` 62 | goquickfix -w main.go 63 | ``` 64 | 65 | and you will get the source rewritten so that Go compiles it well without 66 | changing the semantics: 67 | 68 | ```go 69 | package main 70 | 71 | import ( 72 | "fmt" 73 | _ "log" 74 | ) 75 | 76 | func main() { 77 | nums := []int{3, 1, 4, 1, 5} 78 | for i, n := range nums { 79 | fmt.Println(n) 80 | _ = i 81 | } 82 | } 83 | ``` 84 | 85 | Now, you can `go run` or `go test` your code successfully. 86 | -------------------------------------------------------------------------------- /quickfix_test.go: -------------------------------------------------------------------------------- 1 | package quickfix 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/ast" 7 | "go/parser" 8 | "go/printer" 9 | "go/token" 10 | "go/types" 11 | "path/filepath" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | func TestQuickFix_General(t *testing.T) { 17 | checkCorrectness(t, "general") 18 | } 19 | 20 | func TestQuickFix_RangeStmt(t *testing.T) { 21 | checkCorrectness(t, "rangestmt") 22 | } 23 | 24 | func TestRevertQuickFix_BlankAssign(t *testing.T) { 25 | fset, files, _, err := loadTestData("revert") 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | err = RevertQuickFix(fset, files) 31 | if err != nil { 32 | t.Fatalf("RevertQuickFix(): %s", err) 33 | } 34 | 35 | if strings.Contains(fileContent(fset, files[0]), `_ = `) { 36 | t.Fatal("assignments to blank identifiers should be removed") 37 | } 38 | 39 | if !strings.Contains(fileContent(fset, files[0]), `import "fmt"`) { 40 | t.Fatal("quickfixes to blank imports should be reverted") 41 | } 42 | 43 | if !strings.Contains(fileContent(fset, files[0]), `import _ "image/png"`) { 44 | t.Fatal("imports of packages with side effects should not be considered as quickfixed") 45 | } 46 | 47 | logFiles(t, fset, files) 48 | } 49 | 50 | func TestImportName(t *testing.T) { 51 | checkCorrectness(t, "importname") 52 | } 53 | 54 | func loadTestData(pkgName string) (*token.FileSet, []*ast.File, string, error) { 55 | fset := token.NewFileSet() 56 | dir := filepath.Join("testdata", pkgName) 57 | pkgs, err := parser.ParseDir(fset, dir, nil, parser.Mode(0)) 58 | if err != nil { 59 | return nil, nil, "", err 60 | } 61 | 62 | pkg, ok := pkgs[pkgName] 63 | if !ok { 64 | return nil, nil, "", fmt.Errorf("package %s not found: %v", pkgName, pkgs) 65 | } 66 | 67 | files := make([]*ast.File, 0, len(pkg.Files)) 68 | for _, f := range pkg.Files { 69 | files = append(files, f) 70 | } 71 | 72 | return fset, files, dir, nil 73 | } 74 | 75 | func checkCorrectness(t *testing.T, testName string) { 76 | fset, files, dir, err := loadTestData(testName) 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | 81 | qfconfig := Config{ 82 | Fset: fset, 83 | Files: files, 84 | Dir: dir, 85 | MaxTries: 10, 86 | } 87 | err = qfconfig.QuickFix() 88 | if err != nil { 89 | t.Fatalf("QuickFix(): %s", err) 90 | } 91 | 92 | logFiles(t, fset, files) 93 | 94 | config := &types.Config{Importer: pkgsImporter{dir: dir}} 95 | _, err = config.Check(dir, fset, files, nil) 96 | if err != nil { 97 | t.Fatalf("should pass type checking: %s", err) 98 | } 99 | } 100 | 101 | func logFiles(t *testing.T, fset *token.FileSet, files []*ast.File) { 102 | for _, f := range files { 103 | t.Log("#", fset.File(f.Pos()).Name()) 104 | t.Log(fileContent(fset, f)) 105 | } 106 | } 107 | 108 | func fileContent(fset *token.FileSet, f *ast.File) string { 109 | var buf bytes.Buffer 110 | printer.Fprint(&buf, fset, f) 111 | return buf.String() 112 | } 113 | -------------------------------------------------------------------------------- /cmd/goquickfix/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | The goquickfix command quick fixes Go source that is well typed but go refuses 3 | to compile e.g. "foo imported but not used". 4 | 5 | Run with -help flag for usage information. 6 | */ 7 | package main 8 | 9 | import ( 10 | "bytes" 11 | "errors" 12 | "flag" 13 | "fmt" 14 | "go/ast" 15 | "go/parser" 16 | "go/printer" 17 | "go/token" 18 | "os" 19 | "path/filepath" 20 | "strings" 21 | 22 | "github.com/motemen/go-quickfix" 23 | ) 24 | 25 | var ( 26 | flagWrite = flag.Bool("w", false, "write result to (source) file instead of stdout") 27 | flagRevert = flag.Bool("revert", false, "try to revert possible quickfixes introduced by goquickfix") 28 | ) 29 | 30 | func usage() { 31 | fmt.Fprintln(os.Stderr, `Usage: 32 | goquickfix [-w] [-revert] ... 33 | 34 | Flags:`) 35 | flag.PrintDefaults() 36 | os.Exit(2) 37 | } 38 | 39 | func main() { 40 | if err := run(); err != nil { 41 | fmt.Fprintln(os.Stderr, err) 42 | os.Exit(1) 43 | } 44 | } 45 | 46 | func run() error { 47 | flag.Usage = usage 48 | flag.Parse() 49 | 50 | if flag.NArg() == 0 { 51 | flag.Usage() 52 | } 53 | 54 | fileContents := map[string]string{} 55 | 56 | fset := token.NewFileSet() 57 | 58 | for i := 0; i < flag.NArg(); i++ { 59 | arg := flag.Arg(i) 60 | fi, err := os.Stat(arg) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | if fi.IsDir() { 66 | if i != 0 { 67 | return errors.New("you can only specify exact one directory") 68 | } 69 | 70 | fis, err := os.ReadDir(arg) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | for _, fi := range fis { 76 | if fi.IsDir() { 77 | continue 78 | } 79 | 80 | name := fi.Name() 81 | if !strings.HasSuffix(name, ".go") { 82 | continue 83 | } 84 | if name[0] == '_' || name[0] == '.' { 85 | continue 86 | } 87 | 88 | filename := filepath.Join(arg, name) 89 | b, err := os.ReadFile(filename) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | fileContents[filename] = string(b) 95 | } 96 | } else { 97 | b, err := os.ReadFile(arg) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | fileContents[arg] = string(b) 103 | } 104 | 105 | } 106 | 107 | ff, err := parseFiles(fset, fileContents) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | if *flagRevert { 113 | err = quickfix.RevertQuickFix(fset, ff) 114 | } else { 115 | err = quickfix.QuickFix(fset, ff) 116 | } 117 | if err != nil { 118 | return err 119 | } 120 | 121 | for _, f := range ff { 122 | filename := fset.File(f.Pos()).Name() 123 | 124 | var buf bytes.Buffer 125 | conf := printer.Config{ 126 | Tabwidth: 8, 127 | Mode: printer.UseSpaces | printer.TabIndent, 128 | } 129 | if err := conf.Fprint(&buf, fset, f); err != nil { 130 | return err 131 | } 132 | 133 | if buf.String() == fileContents[filename] { 134 | // no change, skip this file 135 | continue 136 | } 137 | 138 | out := os.Stdout 139 | if *flagWrite { 140 | if out, err = os.Create(filename); err != nil { 141 | return err 142 | } 143 | } 144 | 145 | buf.WriteTo(out) 146 | } 147 | 148 | return nil 149 | } 150 | 151 | func parseFiles(fset *token.FileSet, fileContents map[string]string) ([]*ast.File, error) { 152 | files := make([]*ast.File, 0, len(fileContents)) 153 | 154 | for filename, content := range fileContents { 155 | f, err := parser.ParseFile(fset, filename, content, parser.ParseComments) 156 | if err != nil { 157 | return nil, err 158 | } 159 | 160 | files = append(files, f) 161 | } 162 | 163 | return files, nil 164 | } 165 | -------------------------------------------------------------------------------- /quickfix.go: -------------------------------------------------------------------------------- 1 | // Package quickfix provides functions for fixing Go ASTs 2 | // that are well typed but "go build" refuses to build. 3 | package quickfix 4 | 5 | import ( 6 | "cmp" 7 | "fmt" 8 | "go/ast" 9 | "go/token" 10 | "go/types" 11 | "regexp" 12 | "strings" 13 | 14 | "golang.org/x/tools/go/ast/astutil" 15 | "golang.org/x/tools/go/packages" 16 | ) 17 | 18 | var ( 19 | declaredNotUsed = regexp.MustCompile(`^(?:(\w+) )?declared and not used(?:: (\w+))?$`) 20 | importedNotUsed = regexp.MustCompile(`^(".+") imported (?:as \w+ )?and not used$`) 21 | noNewVariablesOnDefine = "no new variables on left side of :=" 22 | ) 23 | 24 | // Config for quickfix. 25 | type Config struct { 26 | Fset *token.FileSet 27 | Files []*ast.File 28 | Dir string 29 | TypeInfo *types.Info 30 | MaxTries int 31 | } 32 | 33 | // QuickFix a file set. 34 | func QuickFix(fset *token.FileSet, files []*ast.File) error { 35 | config := Config{ 36 | Fset: fset, 37 | Files: files, 38 | MaxTries: 10, 39 | } 40 | return config.QuickFix() 41 | } 42 | 43 | // QuickFix rewrites AST files of same package so that they pass go build. 44 | // For example: 45 | // 46 | // declared and not used: v -> append `_ = v` 47 | // "p" imported and not used -> rewrite to `import _ "p"` 48 | // no new variables on left side of := -> rewrite `:=` to `=` 49 | // 50 | // TODO implement hardMode, which removes errorneous code rather than adding 51 | func (c Config) QuickFix() (err error) { 52 | maxTries := cmp.Or(c.MaxTries, 10) 53 | for i := 0; i < maxTries; i++ { 54 | var foundError bool 55 | foundError, err = c.QuickFixOnce() 56 | if !foundError { 57 | return nil 58 | } 59 | } 60 | 61 | return 62 | } 63 | 64 | type tracedVisitor struct { 65 | path []ast.Node 66 | visit func(ast.Node, []ast.Node) bool 67 | } 68 | 69 | func (v tracedVisitor) Visit(node ast.Node) ast.Visitor { 70 | if v.visit(node, v.path) { 71 | return tracedVisitor{ 72 | path: append([]ast.Node{node}, v.path...), 73 | visit: v.visit, 74 | } 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func traverseAST(node ast.Node, visit func(ast.Node, []ast.Node) bool) { 81 | v := tracedVisitor{ 82 | visit: visit, 83 | } 84 | ast.Walk(v, node) 85 | } 86 | 87 | // pkgsWithSideEffect are set of packages which are known to provide APIs by 88 | // blank identifier import (import _ "p"). 89 | var pkgsWithSideEffect = map[string]bool{} 90 | 91 | func init() { 92 | for _, path := range []string{ 93 | "expvar", 94 | "image/gif", 95 | "image/jpeg", 96 | "image/png", 97 | "net/http/pprof", 98 | "unsafe", 99 | "golang.org/x/image/bmp", 100 | "golang.org/x/image/tiff", 101 | "golang.org/x/image/vp8", 102 | "golang.org/x/image/vp81", 103 | "golang.org/x/image/webp", 104 | "golang.org/x/tools/go/gcimporter", 105 | } { 106 | pkgsWithSideEffect[`"`+path+`"`] = true 107 | } 108 | } 109 | 110 | // RevertQuickFix a file set. 111 | func RevertQuickFix(fset *token.FileSet, files []*ast.File) error { 112 | config := Config{ 113 | Fset: fset, 114 | Files: files, 115 | MaxTries: 10, 116 | } 117 | return config.RevertQuickFix() 118 | } 119 | 120 | // RevertQuickFix reverts possible quickfixes introduced by QuickFix. 121 | // This may result to non-buildable source, and cannot reproduce the original 122 | // code before prior QuickFix. 123 | // For example: 124 | // 125 | // `_ = v` -> removed 126 | // `import _ "p"` -> rewritten to `import "p"` 127 | func (c Config) RevertQuickFix() (err error) { 128 | fset := c.Fset 129 | files := c.Files 130 | 131 | nodeToRemove := map[ast.Node]bool{} 132 | 133 | for _, f := range files { 134 | ast.Inspect(f, func(node ast.Node) bool { 135 | if assign, ok := node.(*ast.AssignStmt); ok { 136 | if len(assign.Lhs) == 1 && isBlankIdent(assign.Lhs[0]) && 137 | len(assign.Rhs) == 1 && isIdent(assign.Rhs[0]) { 138 | // The statement is `_ = v` 139 | nodeToRemove[node] = true 140 | } 141 | 142 | return false 143 | } else if imp, ok := node.(*ast.ImportSpec); ok { 144 | if isBlankIdent(imp.Name) && !pkgsWithSideEffect[imp.Path.Value] { 145 | // The spec is `import _ "p"` and p is not a package that 146 | // provides "side effects" 147 | imp.Name = nil 148 | } 149 | 150 | return false 151 | } 152 | 153 | return true 154 | }) 155 | 156 | for len(nodeToRemove) > 0 { 157 | traverseAST(f, func(node ast.Node, nodepath []ast.Node) bool { 158 | if nodeToRemove[node] { 159 | parent := nodepath[0] 160 | if !removeChildNode(node, parent) { 161 | err = fmt.Errorf( 162 | "BUG: could not remove node: %s (in: %s)", 163 | fset.Position(node.Pos()), 164 | fset.Position(parent.Pos()), 165 | ) 166 | } 167 | delete(nodeToRemove, node) 168 | return false 169 | } 170 | 171 | return true 172 | }) 173 | } 174 | } 175 | 176 | return 177 | } 178 | 179 | type pkgsImporter struct { 180 | dir string 181 | } 182 | 183 | func (i pkgsImporter) Import(path string) (*types.Package, error) { 184 | pkgs, err := packages.Load(&packages.Config{ 185 | Mode: packages.NeedTypes | packages.NeedDeps, 186 | Dir: i.dir, 187 | }, path) 188 | if err != nil { 189 | return nil, err 190 | } 191 | 192 | if len(pkgs) == 0 { 193 | return nil, fmt.Errorf("path %s not found", path) 194 | } 195 | 196 | return pkgs[0].Types, nil 197 | } 198 | 199 | // QuickFixOnce apply the fixes once. 200 | func (c Config) QuickFixOnce() (bool, error) { 201 | fset := c.Fset 202 | files := c.Files 203 | 204 | errs := []error{} 205 | config := &types.Config{ 206 | Error: func(err error) { 207 | errs = append(errs, err) 208 | }, 209 | Importer: pkgsImporter{dir: c.Dir}, 210 | } 211 | 212 | _, err := config.Check("_quickfix", fset, files, c.TypeInfo) 213 | if err == nil { 214 | return false, nil 215 | } 216 | 217 | // apply fixes on AST later so that we won't break funcs that inspect AST by positions 218 | fixes := map[error]func() bool{} 219 | unhandled := ErrorList{} 220 | 221 | foundError := len(errs) > 0 222 | 223 | for _, err := range errs { 224 | err, ok := err.(types.Error) 225 | if !ok { 226 | unhandled = append(unhandled, err) 227 | continue 228 | } 229 | 230 | f := findFile(c.Files, err.Pos) 231 | if f == nil { 232 | e := ErrCouldNotLocate{ 233 | Err: err, 234 | Fset: fset, 235 | } 236 | unhandled = append(unhandled, e) 237 | continue 238 | } 239 | 240 | nodepath, _ := astutil.PathEnclosingInterval(f, err.Pos, err.Pos) 241 | 242 | var fix func() bool 243 | 244 | if m := declaredNotUsed.FindStringSubmatch(err.Msg); m != nil { 245 | identName := cmp.Or(m[1], m[2]) 246 | fix = func() bool { 247 | return fixDeclaredNotUsed(nodepath, identName) 248 | } 249 | } else if m := importedNotUsed.FindStringSubmatch(err.Msg); m != nil { 250 | pkgPath := m[1] // quoted string, but it's okay because this will be compared to ast.BasicLit.Value. 251 | fix = func() bool { 252 | return fixImportedNotUsed(nodepath, pkgPath) 253 | } 254 | } else if err.Msg == noNewVariablesOnDefine { 255 | fix = func() bool { 256 | return fixNoNewVariables(nodepath) 257 | } 258 | } else { 259 | unhandled = append(unhandled, err) 260 | } 261 | 262 | if fix != nil { 263 | fixes[err] = fix 264 | } 265 | } 266 | 267 | for err, fix := range fixes { 268 | if !fix() { 269 | unhandled = append(unhandled, err) 270 | } 271 | } 272 | 273 | return foundError, unhandled.any() 274 | } 275 | 276 | func fixDeclaredNotUsed(nodepath []ast.Node, identName string) bool { 277 | // insert "_ = x" to suppress "declared and not used" error 278 | stmt := &ast.AssignStmt{ 279 | Lhs: []ast.Expr{ast.NewIdent("_")}, 280 | Tok: token.ASSIGN, 281 | Rhs: []ast.Expr{ast.NewIdent(identName)}, 282 | } 283 | return appendStmt(nodepath, stmt) 284 | } 285 | 286 | func fixImportedNotUsed(nodepath []ast.Node, pkgPath string) bool { 287 | for _, node := range nodepath { 288 | if f, ok := node.(*ast.File); ok { 289 | for _, imp := range f.Imports { 290 | if imp.Path.Value == pkgPath { 291 | // make this import spec anonymous one 292 | imp.Name = ast.NewIdent("_") 293 | return true 294 | } 295 | } 296 | } 297 | } 298 | return false 299 | } 300 | 301 | func fixNoNewVariables(nodepath []ast.Node) bool { 302 | for _, node := range nodepath { 303 | switch node := node.(type) { 304 | case *ast.AssignStmt: 305 | if node.Tok == token.DEFINE { 306 | node.Tok = token.ASSIGN 307 | return true 308 | } 309 | 310 | case *ast.RangeStmt: 311 | if node.Tok == token.DEFINE { 312 | node.Tok = token.ASSIGN 313 | return true 314 | } 315 | } 316 | } 317 | return false 318 | } 319 | 320 | // ErrorList represents a collection of errors. 321 | type ErrorList []error 322 | 323 | func (errs ErrorList) any() error { 324 | if len(errs) == 0 { 325 | return nil 326 | } 327 | 328 | return errs 329 | } 330 | 331 | func (errs ErrorList) Error() string { 332 | s := []string{fmt.Sprintf("%d error(s):", len(errs))} 333 | for _, e := range errs { 334 | s = append(s, fmt.Sprintf("- %s", e)) 335 | } 336 | return strings.Join(s, "\n") 337 | } 338 | 339 | func appendStmt(nodepath []ast.Node, stmt ast.Stmt) bool { 340 | for _, node := range nodepath { 341 | switch node := node.(type) { 342 | case *ast.BlockStmt: 343 | if node.List == nil { 344 | node.List = []ast.Stmt{} 345 | } 346 | node.List = append(node.List, stmt) 347 | 348 | case *ast.CaseClause: 349 | if node.Body == nil { 350 | node.Body = []ast.Stmt{} 351 | } 352 | node.Body = append(node.Body, stmt) 353 | 354 | case *ast.CommClause: 355 | if node.Body == nil { 356 | node.Body = []ast.Stmt{} 357 | } 358 | node.Body = append(node.Body, stmt) 359 | 360 | case *ast.RangeStmt: 361 | if node.Body == nil { 362 | node.Body = &ast.BlockStmt{} 363 | } 364 | if node.Body.List == nil { 365 | node.Body.List = []ast.Stmt{} 366 | } 367 | node.Body.List = append(node.Body.List, stmt) 368 | 369 | default: 370 | continue 371 | } 372 | 373 | return true 374 | } 375 | 376 | return false 377 | } 378 | 379 | func removeChildNode(child, parent ast.Node) bool { 380 | switch parent := parent.(type) { 381 | case *ast.BlockStmt: 382 | removeFromStmtList(child, parent.List) 383 | return true 384 | case *ast.CaseClause: 385 | removeFromStmtList(child, parent.Body) 386 | return true 387 | case *ast.CommClause: 388 | removeFromStmtList(child, parent.Body) 389 | return true 390 | case *ast.RangeStmt: 391 | removeFromStmtList(child, parent.Body.List) 392 | return true 393 | } 394 | 395 | return false 396 | } 397 | 398 | // removeFromStmtList remove node from slice of statements list. This function 399 | // modifies list in-place and pads rest of the slice with ast.EmptyStmt. 400 | func removeFromStmtList(node ast.Node, list []ast.Stmt) bool { 401 | for i, s := range list { 402 | if s == node { 403 | for ; i < len(list)-1; i++ { 404 | list[i] = list[i+1] 405 | } 406 | list[len(list)-1] = &ast.EmptyStmt{} 407 | return true 408 | } 409 | } 410 | 411 | return false 412 | } 413 | 414 | func findFile(files []*ast.File, pos token.Pos) *ast.File { 415 | for _, f := range files { 416 | if f.Pos() <= pos && pos < f.End() { 417 | return f 418 | } 419 | } 420 | 421 | return nil 422 | } 423 | 424 | func isIdent(node ast.Node) bool { 425 | if node == nil { 426 | return false 427 | } 428 | 429 | _, ok := node.(*ast.Ident) 430 | return ok 431 | } 432 | 433 | func isBlankIdent(node ast.Node) bool { 434 | if node == nil { 435 | return false 436 | } 437 | 438 | ident, ok := node.(*ast.Ident) 439 | return ok && ident != nil && ident.Name == "_" 440 | } 441 | 442 | // ErrCouldNotLocate represents a file not found error. 443 | type ErrCouldNotLocate struct { 444 | Err types.Error 445 | Fset *token.FileSet 446 | } 447 | 448 | func (e ErrCouldNotLocate) Error() string { 449 | return fmt.Sprintf("cannot find file for error %q: %s (%d)", 450 | e.Err.Error(), e.Fset.Position(e.Err.Pos), e.Err.Pos) 451 | } 452 | --------------------------------------------------------------------------------