blob: 12b9ba47f21a9eccbb53632a3e68a3225304d334 [file] [log] [blame]
// 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)
}