blob: 042784082cc12311900f8a7f1f6c35ce244088b1 [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 (
"bytes"
"encoding/gob"
"fmt"
"go/ast"
"go/token"
"os"
"path/filepath"
"regexp"
"testing"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/expect"
"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/txtar"
)
// Test executes test scenarios specified by files in testdata/*.txtar.
func Test(t *testing.T) {
files, err := filepath.Glob("testdata/*.txtar")
if err != nil {
t.Fatal(err)
}
for _, file := range files {
file := file
t.Run(filepath.Base(file), func(t *testing.T) {
t.Parallel()
// Extract archive to temporary tree.
ar, err := txtar.ParseFile(file)
if err != nil {
t.Fatal(err)
}
dir := t.TempDir()
if err := extractTxtar(ar, dir); err != nil {
t.Fatal(err)
}
// Load packages.
cfg := &packages.Config{
Dir: dir,
Mode: packages.LoadAllSyntax,
Env: append(os.Environ(),
"GO111MODULES=on",
"GOPATH=",
"GOWORK=off",
"GOPROXY=off"),
}
pkgs, err := packages.Load(cfg, "./...")
if err != nil {
t.Errorf("Load: %v", err)
}
// Report parse/type errors; they may be benign.
packages.Visit(pkgs, nil, func(pkg *packages.Package) {
for _, err := range pkg.Errors {
t.Log(err)
}
})
// Process @inline notes in comments in initial packages.
for _, pkg := range pkgs {
for _, file := range pkg.Syntax {
// Read file content (for @inline regexp, and inliner).
content, err := os.ReadFile(pkg.Fset.File(file.Pos()).Name())
if err != nil {
t.Error(err)
continue
}
// Read and process @inline notes.
notes, err := expect.ExtractGo(pkg.Fset, file)
if err != nil {
t.Errorf("parsing notes in %q: %v", pkg.Fset.File(file.Pos()).Name(), err)
continue
}
for _, note := range notes {
posn := pkg.Fset.Position(note.Pos)
if note.Name != "inline" {
t.Errorf("%s: invalid marker @%s", posn, note.Name)
continue
}
if nargs := len(note.Args); nargs != 2 {
t.Errorf("@inline: want 2 args, got %d", nargs)
continue
}
pattern, ok := note.Args[0].(*regexp.Regexp)
if !ok {
t.Errorf("%s: @inline(rx, want): want regular expression rx", posn)
continue
}
// want is a []byte (success) or *Regexp (failure)
var want any
switch x := note.Args[1].(type) {
case string, expect.Identifier:
for _, file := range ar.Files {
if file.Name == fmt.Sprint(x) {
want = file.Data
break
}
}
if want == nil {
t.Errorf("%s: @inline(rx, want): archive entry %q not found", posn, x)
continue
}
case *regexp.Regexp:
want = x
default:
t.Errorf("%s: @inline(rx, want): want file name (to assert success) or error message regexp (to assert failure)", posn)
continue
}
t.Log("doInlineNote", posn)
if err := doInlineNote(pkg, file, content, pattern, posn, want); err != nil {
t.Errorf("%s: @inline(%v, %v): %v", posn, note.Args[0], note.Args[1], err)
continue
}
}
}
}
})
}
}
// doInlineNote executes an assertion specified by a single
// @inline(re"pattern", want) note in a comment. It finds the first
// match of regular expression 'pattern' on the same line, finds the
// innermost enclosing CallExpr, and inlines it.
//
// Finally it checks that, on success, the transformed file is equal
// to want (a []byte), or on failure that the error message matches
// want (a *Regexp).
func doInlineNote(pkg *packages.Package, file *ast.File, content []byte, pattern *regexp.Regexp, posn token.Position, want any) error {
// Find extent of pattern match within commented line.
var startPos, endPos token.Pos
{
tokFile := pkg.Fset.File(file.Pos())
lineStartOffset := int(tokFile.LineStart(posn.Line)) - tokFile.Base()
line := content[lineStartOffset:]
if i := bytes.IndexByte(line, '\n'); i >= 0 {
line = line[:i]
}
matches := pattern.FindSubmatchIndex(line)
var start, end int // offsets
switch len(matches) {
case 2:
// no subgroups: return the range of the regexp expression
start, end = matches[0], matches[1]
case 4:
// one subgroup: return its range
start, end = matches[2], matches[3]
default:
return fmt.Errorf("invalid location regexp %q: expect either 0 or 1 subgroups, got %d",
pattern, len(matches)/2-1)
}
startPos = tokFile.Pos(lineStartOffset + start)
endPos = tokFile.Pos(lineStartOffset + end)
}
// Find innermost call enclosing the pattern match.
var caller *inline.Caller
{
path, _ := astutil.PathEnclosingInterval(file, startPos, endPos)
for _, n := range path {
if call, ok := n.(*ast.CallExpr); ok {
caller = &inline.Caller{
Fset: pkg.Fset,
Types: pkg.Types,
Info: pkg.TypesInfo,
File: file,
Call: call,
Content: content,
}
break
}
}
if caller == nil {
return fmt.Errorf("no enclosing call")
}
}
// Is it a static function call?
fn := typeutil.StaticCallee(caller.Info, caller.Call)
if fn == nil {
return fmt.Errorf("cannot inline: not a static call")
}
// Find callee function.
var (
calleePkg *packages.Package
calleeDecl *ast.FuncDecl
)
{
var same func(*ast.FuncDecl) bool
// Is the call within the package?
if fn.Pkg() == caller.Types {
calleePkg = pkg // same as caller
same = func(decl *ast.FuncDecl) bool {
return decl.Name.Pos() == fn.Pos()
}
} else {
// Different package. Load it now.
// (The primary load loaded all dependencies,
// but we choose to load it again, with
// a distinct token.FileSet and types.Importer,
// to keep the implementation honest.)
cfg := &packages.Config{
// TODO(adonovan): get the original module root more cleanly
Dir: filepath.Dir(filepath.Dir(pkg.GoFiles[0])),
Fset: token.NewFileSet(),
Mode: packages.LoadSyntax,
}
roots, err := packages.Load(cfg, fn.Pkg().Path())
if err != nil {
return fmt.Errorf("loading callee package: %v", err)
}
if packages.PrintErrors(roots) > 0 {
return fmt.Errorf("callee package had errors") // (see log)
}
calleePkg = roots[0]
posn := caller.Fset.Position(fn.Pos()) // callee posn wrt caller package
same = func(decl *ast.FuncDecl) bool {
// We can't rely on columns in export data:
// some variants replace it with 1.
// We can't expect file names to have the same prefix.
// export data for go1.20 std packages have $GOROOT written in
// them, so how are we supposed to find the source? Yuck!
// Ugh. need to samefile? Nope $GOROOT just won't work
// This is highly client specific anyway.
posn2 := calleePkg.Fset.Position(decl.Name.Pos())
return posn.Filename == posn2.Filename &&
posn.Line == posn2.Line
}
}
for _, file := range calleePkg.Syntax {
for _, decl := range file.Decls {
if decl, ok := decl.(*ast.FuncDecl); ok && same(decl) {
calleeDecl = decl
goto found
}
}
}
return fmt.Errorf("can't find FuncDecl for callee") // can't happen?
found:
}
// Do the inlining. For the purposes of the test,
// AnalyzeCallee and Inline are a single operation.
got, err := func() ([]byte, error) {
filename := calleePkg.Fset.File(calleeDecl.Pos()).Name()
content, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
callee, err := inline.AnalyzeCallee(
calleePkg.Fset,
calleePkg.Types,
calleePkg.TypesInfo,
calleeDecl,
content)
if err != nil {
return nil, err
}
// Perform Gob transcoding so that it is exercised by the test.
var enc bytes.Buffer
if err := gob.NewEncoder(&enc).Encode(callee); err != nil {
return nil, fmt.Errorf("internal error: gob encoding failed: %v", err)
}
*callee = inline.Callee{}
if err := gob.NewDecoder(&enc).Decode(callee); err != nil {
return nil, fmt.Errorf("internal error: gob decoding failed: %v", err)
}
return inline.Inline(caller, callee)
}()
if err != nil {
if wantRE, ok := want.(*regexp.Regexp); ok {
if !wantRE.MatchString(err.Error()) {
return fmt.Errorf("Inline failed with wrong error: %v (want error matching %q)", err, want)
}
return nil // expected error
}
return fmt.Errorf("Inline failed: %v", err) // success was expected
}
// Inline succeeded.
if want, ok := want.([]byte); ok {
got = append(bytes.TrimSpace(got), '\n')
want = append(bytes.TrimSpace(want), '\n')
if diff := diff.Unified("want", "got", string(want), string(got)); diff != "" {
return fmt.Errorf("Inline returned wrong output:\n%s\nWant:\n%s\nDiff:\n%s",
got, want, diff)
}
return nil
}
return fmt.Errorf("Inline succeeded unexpectedly: want error matching %q, got <<%s>>", want, got)
}
// TODO(adonovan): publish this a helper (#61386).
func extractTxtar(ar *txtar.Archive, dir string) error {
for _, file := range ar.Files {
name := filepath.Join(dir, file.Name)
if err := os.MkdirAll(filepath.Dir(name), 0777); err != nil {
return err
}
if err := os.WriteFile(name, file.Data, 0666); err != nil {
return err
}
}
return nil
}