зеркало из https://github.com/golang/tools.git
241 строка
6.7 KiB
Go
241 строка
6.7 KiB
Go
// 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)
|
|
}
|