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  }