blob: 39e7b1bcdf6975965c8b3799e425383b3da80631 [file] [log] [blame] [edit]
// Copyright 2018 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 objectpath_test
import (
"bytes"
"fmt"
"go/ast"
"go/importer"
"go/parser"
"go/token"
"go/types"
"strings"
"testing"
"golang.org/x/tools/go/buildutil"
"golang.org/x/tools/go/gcexportdata"
"golang.org/x/tools/go/loader"
"golang.org/x/tools/go/types/objectpath"
)
func TestPaths(t *testing.T) {
pkgs := map[string]map[string]string{
"b": {"b.go": `
package b
import "a"
const C = a.Int(0)
func F(a, b, c int, d a.T)
type T struct{ A int; b int; a.T }
func (T) M() *interface{ f() }
type U T
type A = struct{ x int }
var V []*a.T
type M map[struct{x int}]struct{y int}
func unexportedFunc()
type unexportedType struct{}
type S struct{t struct{x int}}
type R []struct{y int}
type Q [2]struct{z int}
`},
"a": {"a.go": `
package a
type Int int
type T struct{x, y int}
`},
}
paths := []pathTest{
// Good paths
{"b", "C", "const b.C a.Int", ""},
{"b", "F", "func b.F(a int, b int, c int, d a.T)", ""},
{"b", "F.PA0", "var a int", ""},
{"b", "F.PA1", "var b int", ""},
{"b", "F.PA2", "var c int", ""},
{"b", "F.PA3", "var d a.T", ""},
{"b", "T", "type b.T struct{A int; b int; a.T}", ""},
{"b", "T.O", "type b.T struct{A int; b int; a.T}", ""},
{"b", "T.UF0", "field A int", ""},
{"b", "T.UF1", "field b int", ""},
{"b", "T.UF2", "field T a.T", ""},
{"b", "U.UF2", "field T a.T", ""}, // U.U... are aliases for T.U...
{"b", "A", "type b.A = struct{x int}", ""},
{"b", "A.F0", "field x int", ""},
{"b", "V", "var b.V []*a.T", ""},
{"b", "M", "type b.M map[struct{x int}]struct{y int}", ""},
{"b", "M.UKF0", "field x int", ""},
{"b", "M.UEF0", "field y int", ""},
{"b", "T.M0", "func (b.T).M() *interface{f()}", ""}, // concrete method
{"b", "T.M0.RA0", "var *interface{f()}", ""}, // parameter
{"b", "T.M0.RA0.EM0", "func (interface).f()", ""}, // interface method
{"b", "unexportedType", "type b.unexportedType struct{}", ""},
{"b", "S.UF0.F0", "field x int", ""},
{"b", "R.UEF0", "field y int", ""},
{"b", "Q.UEF0", "field z int", ""},
{"a", "T", "type a.T struct{x int; y int}", ""},
{"a", "T.UF0", "field x int", ""},
// Bad paths
{"b", "", "", "empty path"},
{"b", "missing", "", `package b does not contain "missing"`},
{"b", "F.U", "", "invalid path: ends with 'U', want [AFMO]"},
{"b", "F.PA3.O", "", "path denotes type a.T struct{x int; y int}, which belongs to a different package"},
{"b", "F.PA!", "", `invalid path: bad numeric operand "" for code 'A'`},
{"b", "F.PA3.UF0", "", "path denotes field x int, which belongs to a different package"},
{"b", "F.PA3.UF5", "", "field index 5 out of range [0-2)"},
{"b", "V.EE", "", "invalid path: ends with 'E', want [AFMO]"},
{"b", "F..O", "", "invalid path: unexpected '.' in type context"},
{"b", "T.OO", "", "invalid path: code 'O' in object context"},
{"b", "T.EO", "", "cannot apply 'E' to b.T (got *types.Named, want pointer, slice, array, chan or map)"},
{"b", "A.O", "", "cannot apply 'O' to struct{x int} (got *types.Struct, want named or type param)"},
{"b", "A.UF0", "", "cannot apply 'U' to struct{x int} (got *types.Struct, want named)"},
{"b", "M.UPO", "", "cannot apply 'P' to map[struct{x int}]struct{y int} (got *types.Map, want signature)"},
{"b", "C.O", "", "path denotes type a.Int int, which belongs to a different package"},
{"b", "T.M9", "", "method index 9 out of range [0-1)"},
{"b", "M.UF0", "", "cannot apply 'F' to map[struct{x int}]struct{y int} (got *types.Map, want struct)"},
{"b", "V.KO", "", "cannot apply 'K' to []*a.T (got *types.Slice, want map)"},
{"b", "V.A4", "", "cannot apply 'A' to []*a.T (got *types.Slice, want tuple)"},
{"b", "V.RA0", "", "cannot apply 'R' to []*a.T (got *types.Slice, want signature)"},
{"b", "F.PA4", "", "tuple index 4 out of range [0-4)"},
{"b", "F.XO", "", "invalid path: unknown code 'X'"},
}
conf := loader.Config{Build: buildutil.FakeContext(pkgs)}
conf.Import("a")
conf.Import("b")
prog, err := conf.Load()
if err != nil {
t.Fatal(err)
}
for _, test := range paths {
if err := testPath(prog, test); err != nil {
t.Error(err)
}
}
// bad objects
bInfo := prog.Imported["b"]
for _, test := range []struct {
obj types.Object
wantErr string
}{
{types.Universe.Lookup("nil"), "predeclared nil has no path"},
{types.Universe.Lookup("len"), "predeclared builtin len has no path"},
{types.Universe.Lookup("int"), "predeclared type int has no path"},
{bInfo.Implicits[bInfo.Files[0].Imports[0]], "no path for package a"}, // import "a"
{bInfo.Pkg.Scope().Lookup("unexportedFunc"), "no path for non-exported func b.unexportedFunc()"},
} {
path, err := objectpath.For(test.obj)
if err == nil {
t.Errorf("Object(%s) = %q, want error", test.obj, path)
continue
}
if err.Error() != test.wantErr {
t.Errorf("Object(%s) error was %q, want %q", test.obj, err, test.wantErr)
continue
}
}
}
type pathTest struct {
pkg string
path objectpath.Path
wantobj string
wantErr string
}
func testPath(prog *loader.Program, test pathTest) error {
// We test objectpath by enumerating a set of paths
// and ensuring that Path(pkg, Object(pkg, path)) == path.
//
// It might seem more natural to invert the test:
// identify a set of objects and for each one,
// ensure that Object(pkg, Path(pkg, obj)) == obj.
// However, for most interesting test cases there is no
// easy way to identify the object short of applying
// a series of destructuring operations to pkg---which
// is essentially what objectpath.Object does.
// (We do a little of that when testing bad paths, below.)
//
// The downside is that the test depends on the path encoding.
// The upside is that the test exercises the encoding.
pkg := prog.Imported[test.pkg].Pkg
// check path -> object
obj, err := objectpath.Object(pkg, test.path)
if (test.wantErr != "") != (err != nil) {
return fmt.Errorf("Object(%s, %q) returned error %q, want %q", pkg.Path(), test.path, err, test.wantErr)
}
if test.wantErr != "" {
if got := stripSubscripts(err.Error()); got != test.wantErr {
return fmt.Errorf("Object(%s, %q) error was %q, want %q",
pkg.Path(), test.path, got, test.wantErr)
}
return nil
}
// Inv: err == nil
if objString := stripSubscripts(obj.String()); objString != test.wantobj {
return fmt.Errorf("Object(%s, %q) = %s, want %s", pkg.Path(), test.path, objString, test.wantobj)
}
if obj.Pkg() != pkg {
return fmt.Errorf("Object(%s, %q) = %v, which belongs to package %s",
pkg.Path(), test.path, obj, obj.Pkg().Path())
}
// check object -> path
path2, err := objectpath.For(obj)
if err != nil {
return fmt.Errorf("For(%v) failed: %v, want %q", obj, err, test.path)
}
// We do not require that test.path == path2. Aliases are legal.
// But we do require that Object(path2) finds the same object.
obj2, err := objectpath.Object(pkg, path2)
if err != nil {
return fmt.Errorf("Object(%s, %q) failed: %v (roundtrip from %q)", pkg.Path(), path2, err, test.path)
}
if obj2 != obj {
return fmt.Errorf("Object(%s, For(obj)) != obj: got %s, obj is %s (path1=%q, path2=%q)", pkg.Path(), obj2, obj, test.path, path2)
}
return nil
}
// stripSubscripts removes type parameter id subscripts.
//
// TODO(rfindley): remove this function once subscripts are removed from the
// type parameter type string.
func stripSubscripts(s string) string {
var runes []rune
for _, r := range s {
// For debugging/uniqueness purposes, TypeString on a type parameter adds a
// subscript corresponding to the type parameter's unique id. This is going
// to be removed, but in the meantime we skip the subscript runes to get a
// deterministic output.
if '₀' <= r && r < '₀'+10 {
continue // trim type parameter subscripts
}
runes = append(runes, r)
}
return string(runes)
}
// TestSourceAndExportData uses objectpath to compute a correspondence
// of objects between two versions of the same package, one loaded from
// source, the other from export data.
func TestSourceAndExportData(t *testing.T) {
const src = `
package p
type I int
func (I) F() *struct{ X, Y int } {
return nil
}
type Foo interface {
Method() (string, func(int) struct{ X int })
}
var X chan struct{ Z int }
var Z map[string]struct{ A int }
`
// Parse source file and type-check it as a package, "src".
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "src.go", src, 0)
if err != nil {
t.Fatal(err)
}
conf := types.Config{Importer: importer.For("source", nil)}
info := &types.Info{
Defs: make(map[*ast.Ident]types.Object),
}
srcpkg, err := conf.Check("src/p", fset, []*ast.File{f}, info)
if err != nil {
t.Fatal(err)
}
// Export binary export data then reload it as a new package, "bin".
var buf bytes.Buffer
if err := gcexportdata.Write(&buf, fset, srcpkg); err != nil {
t.Fatal(err)
}
imports := make(map[string]*types.Package)
binpkg, err := gcexportdata.Read(&buf, fset, imports, "bin/p")
if err != nil {
t.Fatal(err)
}
// Now find the correspondences between them.
for _, srcobj := range info.Defs {
if srcobj == nil {
continue // e.g. package declaration
}
if _, ok := srcobj.(*types.PkgName); ok {
continue // PkgName has no objectpath
}
path, err := objectpath.For(srcobj)
if err != nil {
t.Errorf("For(%v): %v", srcobj, err)
continue
}
binobj, err := objectpath.Object(binpkg, path)
if err != nil {
t.Errorf("Object(%s, %q): %v", binpkg.Path(), path, err)
continue
}
// Check the object strings match.
// (We can't check that types are identical because the
// objects belong to different type-checker realms.)
srcstr := objectString(srcobj)
binstr := objectString(binobj)
if srcstr != binstr {
t.Errorf("ObjectStrings do not match: Object(For(%q)) = %s, want %s",
path, srcstr, binstr)
continue
}
}
}
func objectString(obj types.Object) string {
s := types.ObjectString(obj, (*types.Package).Name)
// The printing of interface methods changed in go1.11.
// This work-around makes the specific test pass with earlier versions.
s = strings.Replace(s, "func (interface).Method", "func (p.Foo).Method", -1)
return s
}
// TestOrdering uses objectpath over two Named types with the same method
// names but in a different source order and checks that objectpath is the
// same for methods with the same name.
func TestOrdering(t *testing.T) {
pkgs := map[string]map[string]string{
"p": {"p.go": `
package p
type T struct{ A int }
func (T) M() { }
func (T) N() { }
func (T) X() { }
func (T) Y() { }
`},
"q": {"q.go": `
package q
type T struct{ A int }
func (T) N() { }
func (T) M() { }
func (T) Y() { }
func (T) X() { }
`}}
conf := loader.Config{Build: buildutil.FakeContext(pkgs)}
conf.Import("p")
conf.Import("q")
prog, err := conf.Load()
if err != nil {
t.Fatal(err)
}
p := prog.Imported["p"].Pkg
q := prog.Imported["q"].Pkg
// From here, the objectpaths generated for p and q should be the
// same. If they are not, then we are generating an ordering that is
// dependent on the declaration of the types within the file.
for _, test := range []struct {
path objectpath.Path
}{
{"T.M0"},
{"T.M1"},
{"T.M2"},
{"T.M3"},
} {
pobj, err := objectpath.Object(p, test.path)
if err != nil {
t.Errorf("Object(%s) failed in a1: %v", test.path, err)
continue
}
qobj, err := objectpath.Object(q, test.path)
if err != nil {
t.Errorf("Object(%s) failed in a2: %v", test.path, err)
continue
}
if pobj.Name() != pobj.Name() {
t.Errorf("Objects(%s) not equal, got a1 = %v, a2 = %v", test.path, pobj.Name(), qobj.Name())
}
}
}