golang.org/x/tools@v0.21.0/go/analysis/internal/checker/fix_test.go (about) 1 // Copyright 2022 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 "flag" 9 "fmt" 10 "go/token" 11 "log" 12 "os" 13 "os/exec" 14 "path" 15 "regexp" 16 "strings" 17 "testing" 18 19 "golang.org/x/tools/go/analysis" 20 "golang.org/x/tools/go/analysis/analysistest" 21 "golang.org/x/tools/go/analysis/multichecker" 22 "golang.org/x/tools/internal/testenv" 23 ) 24 25 // These are the analyzers available to the multichecker. 26 // (Tests may add more in init functions as needed.) 27 var candidates = map[string]*analysis.Analyzer{ 28 renameAnalyzer.Name: renameAnalyzer, 29 otherAnalyzer.Name: otherAnalyzer, 30 } 31 32 func TestMain(m *testing.M) { 33 // If the ANALYZERS=a,..,z environment is set, then this 34 // process should behave like a multichecker with the 35 // named analyzers. 36 if s, ok := os.LookupEnv("ANALYZERS"); ok { 37 var analyzers []*analysis.Analyzer 38 for _, name := range strings.Split(s, ",") { 39 a := candidates[name] 40 if a == nil { 41 log.Fatalf("no such analyzer: %q", name) 42 } 43 analyzers = append(analyzers, a) 44 } 45 multichecker.Main(analyzers...) 46 panic("unreachable") 47 } 48 49 // ordinary test 50 flag.Parse() 51 os.Exit(m.Run()) 52 } 53 54 const ( 55 exitCodeSuccess = 0 // success (no diagnostics) 56 exitCodeFailed = 1 // analysis failed to run 57 exitCodeDiagnostics = 3 // diagnostics were reported 58 ) 59 60 // fix runs a multichecker subprocess with -fix in the specified 61 // directory, applying the comma-separated list of named analyzers to 62 // the packages matching the patterns. It returns the CombinedOutput. 63 func fix(t *testing.T, dir, analyzers string, wantExit int, patterns ...string) string { 64 testenv.NeedsExec(t) 65 testenv.NeedsTool(t, "go") 66 67 cmd := exec.Command(os.Args[0], "-fix") 68 cmd.Args = append(cmd.Args, patterns...) 69 cmd.Env = append(os.Environ(), 70 "ANALYZERS="+analyzers, 71 "GOPATH="+dir, 72 "GO111MODULE=off", 73 "GOPROXY=off") 74 75 clean := func(s string) string { 76 return strings.ReplaceAll(s, os.TempDir(), "os.TempDir/") 77 } 78 outBytes, err := cmd.CombinedOutput() 79 out := clean(string(outBytes)) 80 t.Logf("$ %s\n%s", clean(fmt.Sprint(cmd)), out) 81 if err, ok := err.(*exec.ExitError); !ok { 82 t.Fatalf("failed to execute multichecker: %v", err) 83 } else if err.ExitCode() != wantExit { 84 t.Errorf("exit code was %d, want %d", err.ExitCode(), wantExit) 85 } 86 return out 87 } 88 89 // TestFixes ensures that checker.Run applies fixes correctly. 90 // This test fork/execs the main function above. 91 func TestFixes(t *testing.T) { 92 files := map[string]string{ 93 "rename/foo.go": `package rename 94 95 func Foo() { 96 bar := 12 97 _ = bar 98 } 99 100 // the end 101 `, 102 "rename/intestfile_test.go": `package rename 103 104 func InTestFile() { 105 bar := 13 106 _ = bar 107 } 108 109 // the end 110 `, 111 "rename/foo_test.go": `package rename_test 112 113 func Foo() { 114 bar := 14 115 _ = bar 116 } 117 118 // the end 119 `, 120 "duplicate/dup.go": `package duplicate 121 122 func Foo() { 123 bar := 14 124 _ = bar 125 } 126 127 // the end 128 `, 129 } 130 fixed := map[string]string{ 131 "rename/foo.go": `package rename 132 133 func Foo() { 134 baz := 12 135 _ = baz 136 } 137 138 // the end 139 `, 140 "rename/intestfile_test.go": `package rename 141 142 func InTestFile() { 143 baz := 13 144 _ = baz 145 } 146 147 // the end 148 `, 149 "rename/foo_test.go": `package rename_test 150 151 func Foo() { 152 baz := 14 153 _ = baz 154 } 155 156 // the end 157 `, 158 "duplicate/dup.go": `package duplicate 159 160 func Foo() { 161 baz := 14 162 _ = baz 163 } 164 165 // the end 166 `, 167 } 168 dir, cleanup, err := analysistest.WriteFiles(files) 169 if err != nil { 170 t.Fatalf("Creating test files failed with %s", err) 171 } 172 defer cleanup() 173 174 fix(t, dir, "rename,other", exitCodeDiagnostics, "rename", "duplicate") 175 176 for name, want := range fixed { 177 path := path.Join(dir, "src", name) 178 contents, err := os.ReadFile(path) 179 if err != nil { 180 t.Errorf("error reading %s: %v", path, err) 181 } 182 if got := string(contents); got != want { 183 t.Errorf("contents of %s file did not match expectations. got=%s, want=%s", path, got, want) 184 } 185 } 186 } 187 188 // TestConflict ensures that checker.Run detects conflicts correctly. 189 // This test fork/execs the main function above. 190 func TestConflict(t *testing.T) { 191 files := map[string]string{ 192 "conflict/foo.go": `package conflict 193 194 func Foo() { 195 bar := 12 196 _ = bar 197 } 198 199 // the end 200 `, 201 } 202 dir, cleanup, err := analysistest.WriteFiles(files) 203 if err != nil { 204 t.Fatalf("Creating test files failed with %s", err) 205 } 206 defer cleanup() 207 208 out := fix(t, dir, "rename,other", exitCodeFailed, "conflict") 209 210 pattern := `conflicting edits from rename and rename on .*foo.go` 211 matched, err := regexp.MatchString(pattern, out) 212 if err != nil { 213 t.Errorf("error matching pattern %s: %v", pattern, err) 214 } else if !matched { 215 t.Errorf("output did not match pattern: %s", pattern) 216 } 217 218 // No files updated 219 for name, want := range files { 220 path := path.Join(dir, "src", name) 221 contents, err := os.ReadFile(path) 222 if err != nil { 223 t.Errorf("error reading %s: %v", path, err) 224 } 225 if got := string(contents); got != want { 226 t.Errorf("contents of %s file updated. got=%s, want=%s", path, got, want) 227 } 228 } 229 } 230 231 // TestOther ensures that checker.Run reports conflicts from 232 // distinct actions correctly. 233 // This test fork/execs the main function above. 234 func TestOther(t *testing.T) { 235 files := map[string]string{ 236 "other/foo.go": `package other 237 238 func Foo() { 239 bar := 12 240 _ = bar 241 } 242 243 // the end 244 `, 245 } 246 dir, cleanup, err := analysistest.WriteFiles(files) 247 if err != nil { 248 t.Fatalf("Creating test files failed with %s", err) 249 } 250 defer cleanup() 251 252 out := fix(t, dir, "rename,other", exitCodeFailed, "other") 253 254 pattern := `.*conflicting edits from other and rename on .*foo.go` 255 matched, err := regexp.MatchString(pattern, out) 256 if err != nil { 257 t.Errorf("error matching pattern %s: %v", pattern, err) 258 } else if !matched { 259 t.Errorf("output did not match pattern: %s", pattern) 260 } 261 262 // No files updated 263 for name, want := range files { 264 path := path.Join(dir, "src", name) 265 contents, err := os.ReadFile(path) 266 if err != nil { 267 t.Errorf("error reading %s: %v", path, err) 268 } 269 if got := string(contents); got != want { 270 t.Errorf("contents of %s file updated. got=%s, want=%s", path, got, want) 271 } 272 } 273 } 274 275 // TestNoEnd tests that a missing SuggestedFix.End position is 276 // correctly interpreted as if equal to SuggestedFix.Pos (see issue #64199). 277 func TestNoEnd(t *testing.T) { 278 files := map[string]string{ 279 "a/a.go": "package a\n\nfunc F() {}", 280 } 281 dir, cleanup, err := analysistest.WriteFiles(files) 282 if err != nil { 283 t.Fatalf("Creating test files failed with %s", err) 284 } 285 defer cleanup() 286 287 fix(t, dir, "noend", exitCodeDiagnostics, "a") 288 289 got, err := os.ReadFile(path.Join(dir, "src/a/a.go")) 290 if err != nil { 291 t.Fatal(err) 292 } 293 const want = "package a\n\n/*hello*/\nfunc F() {}\n" 294 if string(got) != want { 295 t.Errorf("new file contents were <<%s>>, want <<%s>>", got, want) 296 } 297 } 298 299 func init() { 300 candidates["noend"] = &analysis.Analyzer{ 301 Name: "noend", 302 Doc: "inserts /*hello*/ before first decl", 303 Run: func(pass *analysis.Pass) (any, error) { 304 decl := pass.Files[0].Decls[0] 305 pass.Report(analysis.Diagnostic{ 306 Pos: decl.Pos(), 307 End: token.NoPos, 308 Message: "say hello", 309 SuggestedFixes: []analysis.SuggestedFix{{ 310 Message: "say hello", 311 TextEdits: []analysis.TextEdit{ 312 { 313 Pos: decl.Pos(), 314 End: token.NoPos, 315 NewText: []byte("/*hello*/"), 316 }, 317 }, 318 }}, 319 }) 320 return nil, nil 321 }, 322 } 323 }