| // Copyright 2023 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 inline_test |
| |
| import ( |
| "flag" |
| "fmt" |
| "go/ast" |
| "go/parser" |
| "go/types" |
| "log" |
| "os" |
| "path/filepath" |
| "strings" |
| "testing" |
| |
| "golang.org/x/tools/go/packages" |
| "golang.org/x/tools/go/types/typeutil" |
| "golang.org/x/tools/internal/diff" |
| "golang.org/x/tools/internal/refactor/inline" |
| "golang.org/x/tools/internal/testenv" |
| ) |
| |
| var packagesFlag = flag.String("packages", "", "set of packages for TestEverything") |
| |
| // TestEverything invokes the inliner on every single call site in a |
| // given package. and checks that it produces either a reasonable |
| // error, or output that parses and type-checks. |
| // |
| // It does nothing during ordinary testing, but may be used to find |
| // inlining bugs in large corpora. |
| // |
| // Use this command to inline everything in golang.org/x/tools: |
| // |
| // $ go test ./internal/refactor/inline/ -run=Everything -packages=../../../ |
| // |
| // And these commands to inline everything in the kubernetes repository: |
| // |
| // $ go test -c -o /tmp/everything ./internal/refactor/inline/ |
| // $ (cd kubernetes && /tmp/everything -test.run=Everything -packages=./...) |
| // |
| // TODO(adonovan): |
| // - report counters (number of attempts, failed AnalyzeCallee, failed |
| // Inline, etc.) |
| // - Make a pretty log of the entire output so that we can peruse it |
| // for opportunities for systematic improvement. |
| func TestEverything(t *testing.T) { |
| testenv.NeedsGoPackages(t) |
| if testing.Short() { |
| t.Skipf("skipping slow test in -short mode") |
| } |
| if *packagesFlag == "" { |
| return |
| } |
| |
| // Load this package plus dependencies from typed syntax. |
| cfg := &packages.Config{ |
| Mode: packages.LoadAllSyntax, |
| Env: append(os.Environ(), |
| "GO111MODULES=on", |
| "GOPATH=", |
| "GOWORK=off", |
| "GOPROXY=off"), |
| } |
| pkgs, err := packages.Load(cfg, *packagesFlag) |
| if err != nil { |
| t.Errorf("Load: %v", err) |
| } |
| // Report parse/type errors. |
| // Also, build transitive dependency mapping. |
| deps := make(map[string]*packages.Package) // key is PkgPath |
| packages.Visit(pkgs, nil, func(pkg *packages.Package) { |
| deps[pkg.Types.Path()] = pkg |
| for _, err := range pkg.Errors { |
| t.Fatal(err) |
| } |
| }) |
| |
| // Memoize repeated calls for same file. |
| fileContent := make(map[string][]byte) |
| readFile := func(filename string) ([]byte, error) { |
| content, ok := fileContent[filename] |
| if !ok { |
| var err error |
| content, err = os.ReadFile(filename) |
| if err != nil { |
| return nil, err |
| } |
| fileContent[filename] = content |
| } |
| return content, nil |
| } |
| |
| for _, callerPkg := range pkgs { |
| // Find all static function calls in the package. |
| for _, callerFile := range callerPkg.Syntax { |
| noMutCheck := checkNoMutation(callerFile) |
| ast.Inspect(callerFile, func(n ast.Node) bool { |
| call, ok := n.(*ast.CallExpr) |
| if !ok { |
| return true |
| } |
| fn := typeutil.StaticCallee(callerPkg.TypesInfo, call) |
| if fn == nil { |
| return true |
| } |
| |
| // Prepare caller info. |
| callPosn := callerPkg.Fset.PositionFor(call.Lparen, false) |
| callerContent, err := readFile(callPosn.Filename) |
| if err != nil { |
| t.Fatal(err) |
| } |
| caller := &inline.Caller{ |
| Fset: callerPkg.Fset, |
| Types: callerPkg.Types, |
| Info: callerPkg.TypesInfo, |
| File: callerFile, |
| Call: call, |
| Content: callerContent, |
| } |
| |
| // Analyze callee. |
| calleePkg, ok := deps[fn.Pkg().Path()] |
| if !ok { |
| t.Fatalf("missing package for callee %v", fn) |
| } |
| calleePosn := callerPkg.Fset.PositionFor(fn.Pos(), false) |
| calleeDecl, err := findFuncByPosition(calleePkg, calleePosn) |
| if err != nil { |
| t.Fatal(err) |
| } |
| calleeContent, err := readFile(calleePosn.Filename) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Create a subtest for each inlining operation. |
| name := fmt.Sprintf("%s@%v", fn.Name(), filepath.Base(callPosn.String())) |
| t.Run(name, func(t *testing.T) { |
| // TODO(adonovan): add a panic handler. |
| |
| t.Logf("callee declared at %v", |
| filepath.Base(calleePosn.String())) |
| |
| t.Logf("run this command to reproduce locally:\n$ gopls codeaction -kind=refactor.inline -exec -diff %s:#%d", |
| callPosn.Filename, callPosn.Offset) |
| |
| callee, err := inline.AnalyzeCallee( |
| t.Logf, |
| calleePkg.Fset, |
| calleePkg.Types, |
| calleePkg.TypesInfo, |
| calleeDecl, |
| calleeContent) |
| if err != nil { |
| // Ignore the expected kinds of errors. |
| for _, ignore := range []string{ |
| "has no body", |
| "type parameters are not yet", |
| "line directives", |
| "cgo-generated", |
| } { |
| if strings.Contains(err.Error(), ignore) { |
| return |
| } |
| } |
| t.Fatalf("AnalyzeCallee: %v", err) |
| } |
| if err := checkTranscode(callee); err != nil { |
| t.Fatal(err) |
| } |
| |
| res, err := inline.Inline(caller, callee, &inline.Options{ |
| Logf: t.Logf, |
| }) |
| if err != nil { |
| // Write error to a log, but this ok. |
| t.Log(err) |
| return |
| } |
| got := res.Content |
| |
| // Print the diff. |
| t.Logf("Got diff:\n%s", |
| diff.Unified("old", "new", string(callerContent), string(res.Content))) |
| |
| // Parse and type-check the transformed source. |
| f, err := parser.ParseFile(caller.Fset, callPosn.Filename, got, parser.SkipObjectResolution) |
| if err != nil { |
| t.Fatalf("transformed source does not parse: %v", err) |
| } |
| // Splice into original file list. |
| syntax := append([]*ast.File(nil), callerPkg.Syntax...) |
| for i := range callerPkg.Syntax { |
| if syntax[i] == callerFile { |
| syntax[i] = f |
| break |
| } |
| } |
| |
| var typeErrors []string |
| conf := &types.Config{ |
| Error: func(err error) { |
| typeErrors = append(typeErrors, err.Error()) |
| }, |
| Importer: importerFunc(func(importPath string) (*types.Package, error) { |
| // Note: deps is properly keyed by package path, |
| // not import path, but we can't assume |
| // Package.Imports[importPath] exists in the |
| // case of newly added imports of indirect |
| // dependencies. Seems not to matter to this test. |
| dep, ok := deps[importPath] |
| if ok { |
| return dep.Types, nil |
| } |
| return nil, fmt.Errorf("missing package: %q", importPath) |
| }), |
| } |
| if _, err := conf.Check("p", caller.Fset, syntax, nil); err != nil { |
| t.Fatalf("transformed package has type errors:\n\n%s\n\nTransformed file:\n\n%s", |
| strings.Join(typeErrors, "\n"), |
| got) |
| } |
| }) |
| return true |
| }) |
| noMutCheck() |
| } |
| } |
| log.Printf("Analyzed %d packages", len(pkgs)) |
| } |
| |
| type importerFunc func(path string) (*types.Package, error) |
| |
| func (f importerFunc) Import(path string) (*types.Package, error) { |
| return f(path) |
| } |