// Copyright 2022 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package checker_test import ( "flag" "fmt" "go/token" "log" "os" "os/exec" "path" "regexp" "strings" "testing" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/go/analysis/multichecker" "golang.org/x/tools/internal/testenv" ) // These are the analyzers available to the multichecker. // (Tests may add more in init functions as needed.) var candidates = map[string]*analysis.Analyzer{ renameAnalyzer.Name: renameAnalyzer, otherAnalyzer.Name: otherAnalyzer, } func TestMain(m *testing.M) { // If the ANALYZERS=a,..,z environment is set, then this // process should behave like a multichecker with the // named analyzers. if s, ok := os.LookupEnv("ANALYZERS"); ok { var analyzers []*analysis.Analyzer for _, name := range strings.Split(s, ",") { a := candidates[name] if a == nil { log.Fatalf("no such analyzer: %q", name) } analyzers = append(analyzers, a) } multichecker.Main(analyzers...) panic("unreachable") } // ordinary test flag.Parse() os.Exit(m.Run()) } const ( exitCodeSuccess = 0 // success (no diagnostics) exitCodeFailed = 1 // analysis failed to run exitCodeDiagnostics = 3 // diagnostics were reported ) // fix runs a multichecker subprocess with -fix in the specified // directory, applying the comma-separated list of named analyzers to // the packages matching the patterns. It returns the CombinedOutput. func fix(t *testing.T, dir, analyzers string, wantExit int, patterns ...string) string { testenv.NeedsExec(t) testenv.NeedsTool(t, "go") cmd := exec.Command(os.Args[0], "-fix") cmd.Args = append(cmd.Args, patterns...) cmd.Env = append(os.Environ(), "ANALYZERS="+analyzers, "GOPATH="+dir, "GO111MODULE=off", "GOPROXY=off") clean := func(s string) string { return strings.ReplaceAll(s, os.TempDir(), "os.TempDir/") } outBytes, err := cmd.CombinedOutput() out := clean(string(outBytes)) t.Logf("$ %s\n%s", clean(fmt.Sprint(cmd)), out) if err, ok := err.(*exec.ExitError); !ok { t.Fatalf("failed to execute multichecker: %v", err) } else if err.ExitCode() != wantExit { t.Errorf("exit code was %d, want %d", err.ExitCode(), wantExit) } return out } // TestFixes ensures that checker.Run applies fixes correctly. // This test fork/execs the main function above. func TestFixes(t *testing.T) { files := map[string]string{ "rename/foo.go": `package rename func Foo() { bar := 12 _ = bar } // the end `, "rename/intestfile_test.go": `package rename func InTestFile() { bar := 13 _ = bar } // the end `, "rename/foo_test.go": `package rename_test func Foo() { bar := 14 _ = bar } // the end `, "duplicate/dup.go": `package duplicate func Foo() { bar := 14 _ = bar } // the end `, } fixed := map[string]string{ "rename/foo.go": `package rename func Foo() { baz := 12 _ = baz } // the end `, "rename/intestfile_test.go": `package rename func InTestFile() { baz := 13 _ = baz } // the end `, "rename/foo_test.go": `package rename_test func Foo() { baz := 14 _ = baz } // the end `, "duplicate/dup.go": `package duplicate func Foo() { baz := 14 _ = baz } // the end `, } dir, cleanup, err := analysistest.WriteFiles(files) if err != nil { t.Fatalf("Creating test files failed with %s", err) } defer cleanup() fix(t, dir, "rename,other", exitCodeDiagnostics, "rename", "duplicate") for name, want := range fixed { path := path.Join(dir, "src", name) contents, err := os.ReadFile(path) if err != nil { t.Errorf("error reading %s: %v", path, err) } if got := string(contents); got != want { t.Errorf("contents of %s file did not match expectations. got=%s, want=%s", path, got, want) } } } // TestConflict ensures that checker.Run detects conflicts correctly. // This test fork/execs the main function above. func TestConflict(t *testing.T) { files := map[string]string{ "conflict/foo.go": `package conflict func Foo() { bar := 12 _ = bar } // the end `, } dir, cleanup, err := analysistest.WriteFiles(files) if err != nil { t.Fatalf("Creating test files failed with %s", err) } defer cleanup() out := fix(t, dir, "rename,other", exitCodeFailed, "conflict") pattern := `conflicting edits from rename and rename on .*foo.go` matched, err := regexp.MatchString(pattern, out) if err != nil { t.Errorf("error matching pattern %s: %v", pattern, err) } else if !matched { t.Errorf("output did not match pattern: %s", pattern) } // No files updated for name, want := range files { path := path.Join(dir, "src", name) contents, err := os.ReadFile(path) if err != nil { t.Errorf("error reading %s: %v", path, err) } if got := string(contents); got != want { t.Errorf("contents of %s file updated. got=%s, want=%s", path, got, want) } } } // TestOther ensures that checker.Run reports conflicts from // distinct actions correctly. // This test fork/execs the main function above. func TestOther(t *testing.T) { files := map[string]string{ "other/foo.go": `package other func Foo() { bar := 12 _ = bar } // the end `, } dir, cleanup, err := analysistest.WriteFiles(files) if err != nil { t.Fatalf("Creating test files failed with %s", err) } defer cleanup() out := fix(t, dir, "rename,other", exitCodeFailed, "other") pattern := `.*conflicting edits from other and rename on .*foo.go` matched, err := regexp.MatchString(pattern, out) if err != nil { t.Errorf("error matching pattern %s: %v", pattern, err) } else if !matched { t.Errorf("output did not match pattern: %s", pattern) } // No files updated for name, want := range files { path := path.Join(dir, "src", name) contents, err := os.ReadFile(path) if err != nil { t.Errorf("error reading %s: %v", path, err) } if got := string(contents); got != want { t.Errorf("contents of %s file updated. got=%s, want=%s", path, got, want) } } } // TestNoEnd tests that a missing SuggestedFix.End position is // correctly interpreted as if equal to SuggestedFix.Pos (see issue #64199). func TestNoEnd(t *testing.T) { files := map[string]string{ "a/a.go": "package a\n\nfunc F() {}", } dir, cleanup, err := analysistest.WriteFiles(files) if err != nil { t.Fatalf("Creating test files failed with %s", err) } defer cleanup() fix(t, dir, "noend", exitCodeDiagnostics, "a") got, err := os.ReadFile(path.Join(dir, "src/a/a.go")) if err != nil { t.Fatal(err) } const want = "package a\n\n/*hello*/\nfunc F() {}\n" if string(got) != want { t.Errorf("new file contents were <<%s>>, want <<%s>>", got, want) } } func init() { candidates["noend"] = &analysis.Analyzer{ Name: "noend", Doc: "inserts /*hello*/ before first decl", Run: func(pass *analysis.Pass) (any, error) { decl := pass.Files[0].Decls[0] pass.Report(analysis.Diagnostic{ Pos: decl.Pos(), End: token.NoPos, Message: "say hello", SuggestedFixes: []analysis.SuggestedFix{{ Message: "say hello", TextEdits: []analysis.TextEdit{ { Pos: decl.Pos(), End: token.NoPos, NewText: []byte("/*hello*/"), }, }, }}, }) return nil, nil }, } }