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  }