| // 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 fix -a -d %s:#%d refactor.inline", | |
| 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) | |
| } |