golang.org/x/tools@v0.21.0/go/analysis/internal/checker/checker_test.go (about) 1 // Copyright 2019 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package checker_test 6 7 import ( 8 "fmt" 9 "go/ast" 10 "os" 11 "path/filepath" 12 "reflect" 13 "strings" 14 "testing" 15 16 "golang.org/x/tools/go/analysis" 17 "golang.org/x/tools/go/analysis/analysistest" 18 "golang.org/x/tools/go/analysis/internal/checker" 19 "golang.org/x/tools/go/analysis/passes/inspect" 20 "golang.org/x/tools/go/ast/inspector" 21 "golang.org/x/tools/internal/testenv" 22 "golang.org/x/tools/internal/testfiles" 23 "golang.org/x/tools/txtar" 24 ) 25 26 func TestApplyFixes(t *testing.T) { 27 testenv.NeedsGoPackages(t) 28 29 files := map[string]string{ 30 "rename/test.go": `package rename 31 32 func Foo() { 33 bar := 12 34 _ = bar 35 } 36 37 // the end 38 `} 39 want := `package rename 40 41 func Foo() { 42 baz := 12 43 _ = baz 44 } 45 46 // the end 47 ` 48 49 testdata, cleanup, err := analysistest.WriteFiles(files) 50 if err != nil { 51 t.Fatal(err) 52 } 53 path := filepath.Join(testdata, "src/rename/test.go") 54 checker.Fix = true 55 checker.Run([]string{"file=" + path}, []*analysis.Analyzer{renameAnalyzer}) 56 57 contents, err := os.ReadFile(path) 58 if err != nil { 59 t.Fatal(err) 60 } 61 62 got := string(contents) 63 if got != want { 64 t.Errorf("contents of rewritten file\ngot: %s\nwant: %s", got, want) 65 } 66 67 defer cleanup() 68 } 69 70 var renameAnalyzer = &analysis.Analyzer{ 71 Name: "rename", 72 Requires: []*analysis.Analyzer{inspect.Analyzer}, 73 Run: run, 74 Doc: "renames symbols named bar to baz", 75 } 76 77 var otherAnalyzer = &analysis.Analyzer{ // like analyzer but with a different Name. 78 Name: "other", 79 Requires: []*analysis.Analyzer{inspect.Analyzer}, 80 Run: run, 81 Doc: "renames symbols named bar to baz only in package 'other'", 82 } 83 84 func run(pass *analysis.Pass) (interface{}, error) { 85 const ( 86 from = "bar" 87 to = "baz" 88 conflict = "conflict" // add conflicting edits to package conflict. 89 duplicate = "duplicate" // add duplicate edits to package conflict. 90 other = "other" // add conflicting edits to package other from different analyzers. 91 ) 92 93 if pass.Analyzer.Name == other { 94 if pass.Pkg.Name() != other { 95 return nil, nil // only apply Analyzer other to packages named other 96 } 97 } 98 99 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 100 nodeFilter := []ast.Node{(*ast.Ident)(nil)} 101 inspect.Preorder(nodeFilter, func(n ast.Node) { 102 ident := n.(*ast.Ident) 103 if ident.Name == from { 104 msg := fmt.Sprintf("renaming %q to %q", from, to) 105 edits := []analysis.TextEdit{ 106 {Pos: ident.Pos(), End: ident.End(), NewText: []byte(to)}, 107 } 108 switch pass.Pkg.Name() { 109 case conflict: 110 edits = append(edits, []analysis.TextEdit{ 111 {Pos: ident.Pos() - 1, End: ident.End(), NewText: []byte(to)}, 112 {Pos: ident.Pos(), End: ident.End() - 1, NewText: []byte(to)}, 113 {Pos: ident.Pos(), End: ident.End(), NewText: []byte("lorem ipsum")}, 114 }...) 115 case duplicate: 116 edits = append(edits, edits...) 117 case other: 118 if pass.Analyzer.Name == other { 119 edits[0].Pos = edits[0].Pos + 1 // shift by one to mismatch analyzer and other 120 } 121 } 122 pass.Report(analysis.Diagnostic{ 123 Pos: ident.Pos(), 124 End: ident.End(), 125 Message: msg, 126 SuggestedFixes: []analysis.SuggestedFix{{Message: msg, TextEdits: edits}}}) 127 } 128 }) 129 130 return nil, nil 131 } 132 133 func TestRunDespiteErrors(t *testing.T) { 134 testenv.NeedsGoPackages(t) 135 136 files := map[string]string{ 137 "rderr/test.go": `package rderr 138 139 // Foo deliberately has a type error 140 func Foo(s string) int { 141 return s + 1 142 } 143 `} 144 145 testdata, cleanup, err := analysistest.WriteFiles(files) 146 if err != nil { 147 t.Fatal(err) 148 } 149 path := filepath.Join(testdata, "src/rderr/test.go") 150 151 // A no-op analyzer that should finish regardless of 152 // parse or type errors in the code. 153 noop := &analysis.Analyzer{ 154 Name: "noop", 155 Requires: []*analysis.Analyzer{inspect.Analyzer}, 156 Run: func(pass *analysis.Pass) (interface{}, error) { 157 return nil, nil 158 }, 159 RunDespiteErrors: true, 160 } 161 162 // A no-op analyzer that should finish regardless of 163 // parse or type errors in the code. 164 noopWithFact := &analysis.Analyzer{ 165 Name: "noopfact", 166 Requires: []*analysis.Analyzer{inspect.Analyzer}, 167 Run: func(pass *analysis.Pass) (interface{}, error) { 168 return nil, nil 169 }, 170 RunDespiteErrors: true, 171 FactTypes: []analysis.Fact{&EmptyFact{}}, 172 } 173 174 for _, test := range []struct { 175 name string 176 pattern []string 177 analyzers []*analysis.Analyzer 178 code int 179 }{ 180 // parse/type errors 181 {name: "skip-error", pattern: []string{"file=" + path}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: 1}, 182 // RunDespiteErrors allows a driver to run an Analyzer even after parse/type errors. 183 // 184 // The noop analyzer doesn't use facts, so the driver loads only the root 185 // package from source. For the rest, it asks 'go list' for export data, 186 // which fails because the compiler encounters the type error. Since the 187 // errors come from 'go list', the driver doesn't run the analyzer. 188 {name: "despite-error", pattern: []string{"file=" + path}, analyzers: []*analysis.Analyzer{noop}, code: 1}, 189 // The noopfact analyzer does use facts, so the driver loads source for 190 // all dependencies, does type checking itself, recognizes the error as a 191 // type error, and runs the analyzer. 192 {name: "despite-error-fact", pattern: []string{"file=" + path}, analyzers: []*analysis.Analyzer{noopWithFact}, code: 0}, 193 // combination of parse/type errors and no errors 194 {name: "despite-error-and-no-error", pattern: []string{"file=" + path, "sort"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1}, 195 // non-existing package error 196 {name: "no-package", pattern: []string{"xyz"}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: 1}, 197 {name: "no-package-despite-error", pattern: []string{"abc"}, analyzers: []*analysis.Analyzer{noop}, code: 1}, 198 {name: "no-multi-package-despite-error", pattern: []string{"xyz", "abc"}, analyzers: []*analysis.Analyzer{noop}, code: 1}, 199 // combination of type/parsing and different errors 200 {name: "different-errors", pattern: []string{"file=" + path, "xyz"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1}, 201 // non existing dir error 202 {name: "no-match-dir", pattern: []string{"file=non/existing/dir"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1}, 203 // no errors 204 {name: "no-errors", pattern: []string{"sort"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 0}, 205 } { 206 if test.name == "despite-error" && testenv.Go1Point() < 20 { 207 // The behavior in the comment on the despite-error test only occurs for Go 1.20+. 208 continue 209 } 210 if got := checker.Run(test.pattern, test.analyzers); got != test.code { 211 t.Errorf("got incorrect exit code %d for test %s; want %d", got, test.name, test.code) 212 } 213 } 214 215 defer cleanup() 216 } 217 218 type EmptyFact struct{} 219 220 func (f *EmptyFact) AFact() {} 221 222 func TestURL(t *testing.T) { 223 // TestURL test that URLs get forwarded to diagnostics by internal/checker. 224 testenv.NeedsGoPackages(t) 225 226 files := map[string]string{ 227 "p/test.go": `package p // want "package name is p"`, 228 } 229 pkgname := &analysis.Analyzer{ 230 Name: "pkgname", 231 Doc: "trivial analyzer that reports package names", 232 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/internal/checker", 233 Run: func(p *analysis.Pass) (interface{}, error) { 234 for _, f := range p.Files { 235 p.ReportRangef(f.Name, "package name is %s", f.Name.Name) 236 } 237 return nil, nil 238 }, 239 } 240 241 testdata, cleanup, err := analysistest.WriteFiles(files) 242 if err != nil { 243 t.Fatal(err) 244 } 245 defer cleanup() 246 path := filepath.Join(testdata, "src/p/test.go") 247 results := analysistest.Run(t, testdata, pkgname, "file="+path) 248 249 var urls []string 250 for _, r := range results { 251 for _, d := range r.Diagnostics { 252 urls = append(urls, d.URL) 253 } 254 } 255 want := []string{"https://pkg.go.dev/golang.org/x/tools/go/analysis/internal/checker"} 256 if !reflect.DeepEqual(urls, want) { 257 t.Errorf("Expected Diagnostics.URLs %v. got %v", want, urls) 258 } 259 } 260 261 // TestPassReadFile exercises the Pass.ReadFile function. 262 func TestPassReadFile(t *testing.T) { 263 cwd, _ := os.Getwd() 264 265 const src = ` 266 -- go.mod -- 267 module example.com 268 269 -- p/file.go -- 270 package p 271 272 -- p/ignored.go -- 273 //go:build darwin && mips64 274 275 package p 276 277 hello from ignored 278 279 -- p/other.s -- 280 hello from other 281 ` 282 283 // Expand archive into tmp tree. 284 tmpdir := t.TempDir() 285 if err := testfiles.ExtractTxtar(tmpdir, txtar.Parse([]byte(src))); err != nil { 286 t.Fatal(err) 287 } 288 289 ran := false 290 a := &analysis.Analyzer{ 291 Name: "a", 292 Requires: []*analysis.Analyzer{inspect.Analyzer}, 293 Doc: "doc", 294 Run: func(pass *analysis.Pass) (any, error) { 295 if len(pass.OtherFiles)+len(pass.IgnoredFiles) == 0 { 296 t.Errorf("OtherFiles and IgnoredFiles are empty") 297 return nil, nil 298 } 299 300 for _, test := range []struct { 301 filename string 302 want string // substring of file content or error message 303 }{ 304 { 305 pass.OtherFiles[0], // [other.s] 306 "hello from other", 307 }, 308 { 309 pass.IgnoredFiles[0], // [ignored.go] 310 "hello from ignored", 311 }, 312 { 313 "nonesuch", 314 "nonesuch is not among OtherFiles, ", // etc 315 }, 316 { 317 filepath.Join(cwd, "checker_test.go"), 318 "checker_test.go is not among OtherFiles, ", // etc 319 }, 320 } { 321 content, err := pass.ReadFile(test.filename) 322 var got string 323 if err != nil { 324 got = err.Error() 325 } else { 326 got = string(content) 327 if len(got) > 100 { 328 got = got[:100] + "..." 329 } 330 } 331 if !strings.Contains(got, test.want) { 332 t.Errorf("Pass.ReadFile(%q) did not contain %q; got:\n%s", 333 test.filename, test.want, got) 334 } 335 } 336 ran = true 337 return nil, nil 338 }, 339 } 340 341 analysistest.Run(t, tmpdir, a, "example.com/p") 342 343 if !ran { 344 t.Error("analyzer did not run") 345 } 346 }