blob: a73a0967ca5869ce4fdc49911024e70b868ff864 [file] [log] [blame]
// Copyright 2019 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 (
"fmt"
"go/ast"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/analysistest"
"golang.org/x/tools/go/analysis/internal/checker"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/testenv"
"golang.org/x/tools/internal/testfiles"
"golang.org/x/tools/txtar"
)
func TestApplyFixes(t *testing.T) {
testenv.NeedsGoPackages(t)
files := map[string]string{
"rename/test.go": `package rename
func Foo() {
bar := 12
_ = bar
}
// the end
`}
want := `package rename
func Foo() {
baz := 12
_ = baz
}
// the end
`
testdata, cleanup, err := analysistest.WriteFiles(files)
if err != nil {
t.Fatal(err)
}
path := filepath.Join(testdata, "src/rename/test.go")
checker.Fix = true
checker.Run([]string{"file=" + path}, []*analysis.Analyzer{renameAnalyzer})
contents, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
got := string(contents)
if got != want {
t.Errorf("contents of rewritten file\ngot: %s\nwant: %s", got, want)
}
defer cleanup()
}
var renameAnalyzer = &analysis.Analyzer{
Name: "rename",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
Doc: "renames symbols named bar to baz",
RunDespiteErrors: true,
}
var otherAnalyzer = &analysis.Analyzer{ // like analyzer but with a different Name.
Name: "other",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
Doc: "renames symbols named bar to baz only in package 'other'",
}
func run(pass *analysis.Pass) (interface{}, error) {
const (
from = "bar"
to = "baz"
conflict = "conflict" // add conflicting edits to package conflict.
duplicate = "duplicate" // add duplicate edits to package conflict.
other = "other" // add conflicting edits to package other from different analyzers.
)
if pass.Analyzer.Name == other {
if pass.Pkg.Name() != other {
return nil, nil // only apply Analyzer other to packages named other
}
}
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{(*ast.Ident)(nil)}
inspect.Preorder(nodeFilter, func(n ast.Node) {
ident := n.(*ast.Ident)
if ident.Name == from {
msg := fmt.Sprintf("renaming %q to %q", from, to)
edits := []analysis.TextEdit{
{Pos: ident.Pos(), End: ident.End(), NewText: []byte(to)},
}
switch pass.Pkg.Name() {
case conflict:
edits = append(edits, []analysis.TextEdit{
{Pos: ident.Pos() - 1, End: ident.End(), NewText: []byte(to)},
{Pos: ident.Pos(), End: ident.End() - 1, NewText: []byte(to)},
{Pos: ident.Pos(), End: ident.End(), NewText: []byte("lorem ipsum")},
}...)
case duplicate:
edits = append(edits, edits...)
case other:
if pass.Analyzer.Name == other {
edits[0].Pos = edits[0].Pos + 1 // shift by one to mismatch analyzer and other
}
}
pass.Report(analysis.Diagnostic{
Pos: ident.Pos(),
End: ident.End(),
Message: msg,
SuggestedFixes: []analysis.SuggestedFix{{Message: msg, TextEdits: edits}}})
}
})
return nil, nil
}
func TestRunDespiteErrors(t *testing.T) {
testenv.NeedsGoPackages(t)
files := map[string]string{
"rderr/test.go": `package rderr
// Foo deliberately has a type error
func Foo(s string) int {
return s + 1
}
`,
"cperr/test.go": `package copyerr
import "sync"
func bar() { } // for renameAnalyzer
type T struct{ mu sync.Mutex }
type T1 struct{ t *T }
func NewT1() *T1 { return &T1{T} }
`,
}
testdata, cleanup, err := analysistest.WriteFiles(files)
if err != nil {
t.Fatal(err)
}
defer cleanup()
rderrFile := "file=" + filepath.Join(testdata, "src/rderr/test.go")
cperrFile := "file=" + filepath.Join(testdata, "src/cperr/test.go")
// A no-op analyzer that should finish regardless of
// parse or type errors in the code.
noop := &analysis.Analyzer{
Name: "noop",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: func(pass *analysis.Pass) (interface{}, error) {
return nil, nil
},
RunDespiteErrors: true,
}
// A no-op analyzer, with facts, that should finish
// regardless of parse or type errors in the code.
noopWithFact := &analysis.Analyzer{
Name: "noopfact",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: func(pass *analysis.Pass) (interface{}, error) {
return nil, nil
},
RunDespiteErrors: true,
FactTypes: []analysis.Fact{&EmptyFact{}},
}
for _, test := range []struct {
name string
pattern []string
analyzers []*analysis.Analyzer
code int
}{
// parse/type errors
{name: "skip-error", pattern: []string{rderrFile}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: 1},
// RunDespiteErrors allows a driver to run an Analyzer even after parse/type errors.
//
// The noop analyzer doesn't use facts, so the driver loads only the root
// package from source. For the rest, it asks 'go list' for export data,
// which fails because the compiler encounters the type error. Since the
// errors come from 'go list', the driver doesn't run the analyzer.
{name: "despite-error", pattern: []string{rderrFile}, analyzers: []*analysis.Analyzer{noop}, code: 1},
// The noopfact analyzer does use facts, so the driver loads source for
// all dependencies, does type checking itself, recognizes the error as a
// type error, and runs the analyzer.
{name: "despite-error-fact", pattern: []string{rderrFile}, analyzers: []*analysis.Analyzer{noopWithFact}, code: 1},
// combination of parse/type errors and no errors
{name: "despite-error-and-no-error", pattern: []string{rderrFile, "sort"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1},
// non-existing package error
{name: "no-package", pattern: []string{"xyz"}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: 1},
{name: "no-package-despite-error", pattern: []string{"abc"}, analyzers: []*analysis.Analyzer{noop}, code: 1},
{name: "no-multi-package-despite-error", pattern: []string{"xyz", "abc"}, analyzers: []*analysis.Analyzer{noop}, code: 1},
// combination of type/parsing and different errors
{name: "different-errors", pattern: []string{rderrFile, "xyz"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1},
// non existing dir error
{name: "no-match-dir", pattern: []string{"file=non/existing/dir"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 1},
// no errors
{name: "no-errors", pattern: []string{"sort"}, analyzers: []*analysis.Analyzer{renameAnalyzer, noop}, code: 0},
// duplicate list error with no findings
{name: "list-error", pattern: []string{cperrFile}, analyzers: []*analysis.Analyzer{noop}, code: 1},
// duplicate list errors with findings (issue #67790)
{name: "list-error-findings", pattern: []string{cperrFile}, analyzers: []*analysis.Analyzer{renameAnalyzer}, code: 3},
} {
if test.name == "despite-error" && testenv.Go1Point() < 20 {
// The behavior in the comment on the despite-error test only occurs for Go 1.20+.
continue
}
if got := checker.Run(test.pattern, test.analyzers); got != test.code {
t.Errorf("got incorrect exit code %d for test %s; want %d", got, test.name, test.code)
}
}
}
type EmptyFact struct{}
func (f *EmptyFact) AFact() {}
func TestURL(t *testing.T) {
// TestURL test that URLs get forwarded to diagnostics by internal/checker.
testenv.NeedsGoPackages(t)
files := map[string]string{
"p/test.go": `package p // want "package name is p"`,
}
pkgname := &analysis.Analyzer{
Name: "pkgname",
Doc: "trivial analyzer that reports package names",
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/internal/checker",
Run: func(p *analysis.Pass) (interface{}, error) {
for _, f := range p.Files {
p.ReportRangef(f.Name, "package name is %s", f.Name.Name)
}
return nil, nil
},
}
testdata, cleanup, err := analysistest.WriteFiles(files)
if err != nil {
t.Fatal(err)
}
defer cleanup()
path := filepath.Join(testdata, "src/p/test.go")
results := analysistest.Run(t, testdata, pkgname, "file="+path)
var urls []string
for _, r := range results {
for _, d := range r.Diagnostics {
urls = append(urls, d.URL)
}
}
want := []string{"https://pkg.go.dev/golang.org/x/tools/go/analysis/internal/checker"}
if !reflect.DeepEqual(urls, want) {
t.Errorf("Expected Diagnostics.URLs %v. got %v", want, urls)
}
}
// TestPassReadFile exercises the Pass.ReadFile function.
func TestPassReadFile(t *testing.T) {
cwd, _ := os.Getwd()
const src = `
-- go.mod --
module example.com
-- p/file.go --
package p
-- p/ignored.go --
//go:build darwin && mips64
package p
hello from ignored
-- p/other.s --
hello from other
`
// Expand archive into tmp tree.
fs, err := txtar.FS(txtar.Parse([]byte(src)))
if err != nil {
t.Fatal(err)
}
tmpdir := testfiles.CopyToTmp(t, fs)
ran := false
a := &analysis.Analyzer{
Name: "a",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Doc: "doc",
Run: func(pass *analysis.Pass) (any, error) {
if len(pass.OtherFiles)+len(pass.IgnoredFiles) == 0 {
t.Errorf("OtherFiles and IgnoredFiles are empty")
return nil, nil
}
for _, test := range []struct {
filename string
want string // substring of file content or error message
}{
{
pass.OtherFiles[0], // [other.s]
"hello from other",
},
{
pass.IgnoredFiles[0], // [ignored.go]
"hello from ignored",
},
{
"nonesuch",
"nonesuch is not among OtherFiles, ", // etc
},
{
filepath.Join(cwd, "checker_test.go"),
"checker_test.go is not among OtherFiles, ", // etc
},
} {
content, err := pass.ReadFile(test.filename)
var got string
if err != nil {
got = err.Error()
} else {
got = string(content)
if len(got) > 100 {
got = got[:100] + "..."
}
}
if !strings.Contains(got, test.want) {
t.Errorf("Pass.ReadFile(%q) did not contain %q; got:\n%s",
test.filename, test.want, got)
}
}
ran = true
return nil, nil
},
}
analysistest.Run(t, tmpdir, a, "example.com/p")
if !ran {
t.Error("analyzer did not run")
}
}