go/types/objectpath: add support for type parameters

This CL adds an initial draft of support for type parameters in the
go/types/objectpath package. For now, introduce two new operators:
 - the 'T' operator (type->type), which requires an integer operand and
   goes from *Named and *Signature types to the type parameter at the
   given index.
 - the 'C' operator (type->type), which goes from a *TypeParam type to
   its constraint.

Along the way, reorganize the path tests and update some errors messages
to consistently format the expected type with %T.

Fixes golang/go#48588

Change-Id: Ibdf03a86b7d8e24a8faa1f2fc42f2be8db20ca75
Reviewed-on: https://go-review.googlesource.com/c/tools/+/350148
Trust: Robert Findley <rfindley@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
Reviewed-by: Tim King <taking@google.com>
diff --git a/go/types/objectpath/objectpath.go b/go/types/objectpath/objectpath.go
index 81e8fdc..aa374c5 100644
--- a/go/types/objectpath/objectpath.go
+++ b/go/types/objectpath/objectpath.go
@@ -27,6 +27,8 @@
 	"strings"
 
 	"go/types"
+
+	"golang.org/x/tools/internal/typeparams"
 )
 
 // A Path is an opaque name that identifies a types.Object
@@ -57,7 +59,9 @@
 // - The only PO operator is Package.Scope.Lookup, which requires an identifier.
 // - The only OT operator is Object.Type,
 //   which we encode as '.' because dot cannot appear in an identifier.
-// - The TT operators are encoded as [EKPRU].
+// - The TT operators are encoded as [EKPRUTC];
+//   one of these (TypeParam) requires an integer operand,
+//   which is encoded as a string of decimal digits.
 // - The TO operators are encoded as [AFMO];
 //   three of these (At,Field,Method) require an integer operand,
 //   which is encoded as a string of decimal digits.
@@ -89,17 +93,19 @@
 	opType = '.' // .Type()		  (Object)
 
 	// type->type operators
-	opElem       = 'E' // .Elem()		(Pointer, Slice, Array, Chan, Map)
-	opKey        = 'K' // .Key()		(Map)
-	opParams     = 'P' // .Params()		(Signature)
-	opResults    = 'R' // .Results()	(Signature)
-	opUnderlying = 'U' // .Underlying()	(Named)
+	opElem       = 'E' // .Elem()		        (Pointer, Slice, Array, Chan, Map)
+	opKey        = 'K' // .Key()		        (Map)
+	opParams     = 'P' // .Params()		      (Signature)
+	opResults    = 'R' // .Results()	      (Signature)
+	opUnderlying = 'U' // .Underlying()	    (Named)
+	opTypeParam  = 'T' // .TypeParams.At(i) (Named, Signature)
+	opConstraint = 'C' // .Constraint()     (TypeParam)
 
 	// type->object operators
-	opAt     = 'A' // .At(i)		(Tuple)
-	opField  = 'F' // .Field(i)		(Struct)
-	opMethod = 'M' // .Method(i)		(Named or Interface; not Struct: "promoted" names are ignored)
-	opObj    = 'O' // .Obj()		(Named)
+	opAt     = 'A' // .At(i)		 (Tuple)
+	opField  = 'F' // .Field(i)	 (Struct)
+	opMethod = 'M' // .Method(i) (Named or Interface; not Struct: "promoted" names are ignored)
+	opObj    = 'O' // .Obj()		 (Named, TypeParam)
 )
 
 // The For function returns the path to an object relative to its package,
@@ -190,10 +196,15 @@
 	// 3. Not a package-level object.
 	//    Reject obviously non-viable cases.
 	switch obj := obj.(type) {
+	case *types.TypeName:
+		if _, ok := obj.Type().(*typeparams.TypeParam); !ok {
+			// With the exception of type parameters, only package-level type names
+			// have a path.
+			return "", fmt.Errorf("no path for %v", obj)
+		}
 	case *types.Const, // Only package-level constants have a path.
-		*types.TypeName, // Only package-level types have a path.
-		*types.Label,    // Labels are function-local.
-		*types.PkgName:  // PkgNames are file-local.
+		*types.Label,   // Labels are function-local.
+		*types.PkgName: // PkgNames are file-local.
 		return "", fmt.Errorf("no path for %v", obj)
 
 	case *types.Var:
@@ -245,6 +256,12 @@
 				return Path(r), nil
 			}
 		} else {
+			if named, _ := T.(*types.Named); named != nil {
+				if r := findTypeParam(obj, typeparams.ForNamed(named), path); r != nil {
+					// generic named type
+					return Path(r), nil
+				}
+			}
 			// defined (named) type
 			if r := find(obj, T.Underlying(), append(path, opUnderlying)); r != nil {
 				return Path(r), nil
@@ -313,6 +330,9 @@
 		}
 		return find(obj, T.Elem(), append(path, opElem))
 	case *types.Signature:
+		if r := findTypeParam(obj, typeparams.ForSignature(T), path); r != nil {
+			return r
+		}
 		if r := find(obj, T.Params(), append(path, opParams)); r != nil {
 			return r
 		}
@@ -353,10 +373,30 @@
 			}
 		}
 		return nil
+	case *typeparams.TypeParam:
+		name := T.Obj()
+		if name == obj {
+			return append(path, opObj)
+		}
+		if r := find(obj, T.Constraint(), append(path, opConstraint)); r != nil {
+			return r
+		}
+		return nil
 	}
 	panic(T)
 }
 
+func findTypeParam(obj types.Object, list *typeparams.TypeParamList, path []byte) []byte {
+	for i := 0; i < list.Len(); i++ {
+		tparam := list.At(i)
+		path2 := appendOpArg(path, opTypeParam, i)
+		if r := find(obj, tparam, path2); r != nil {
+			return r
+		}
+	}
+	return nil
+}
+
 // Object returns the object denoted by path p within the package pkg.
 func Object(pkg *types.Package, p Path) (types.Object, error) {
 	if p == "" {
@@ -386,6 +426,14 @@
 		Method(int) *types.Func
 		NumMethods() int
 	}
+	// abstraction of *types.{Named,Signature}
+	type hasTypeParams interface {
+		TypeParams() *typeparams.TypeParamList
+	}
+	// abstraction of *types.{Named,TypeParam}
+	type hasObj interface {
+		Obj() *types.TypeName
+	}
 
 	// The loop state is the pair (t, obj),
 	// exactly one of which is non-nil, initially obj.
@@ -401,7 +449,7 @@
 		// Codes [AFM] have an integer operand.
 		var index int
 		switch code {
-		case opAt, opField, opMethod:
+		case opAt, opField, opMethod, opTypeParam:
 			rest := strings.TrimLeft(suffix, "0123456789")
 			numerals := suffix[:len(suffix)-len(rest)]
 			suffix = rest
@@ -466,14 +514,32 @@
 		case opUnderlying:
 			named, ok := t.(*types.Named)
 			if !ok {
-				return nil, fmt.Errorf("cannot apply %q to %s (got %s, want named)", code, t, t)
+				return nil, fmt.Errorf("cannot apply %q to %s (got %T, want named)", code, t, t)
 			}
 			t = named.Underlying()
 
+		case opTypeParam:
+			hasTypeParams, ok := t.(hasTypeParams) // Named, Signature
+			if !ok {
+				return nil, fmt.Errorf("cannot apply %q to %s (got %T, want named or signature)", code, t, t)
+			}
+			tparams := hasTypeParams.TypeParams()
+			if n := tparams.Len(); index >= n {
+				return nil, fmt.Errorf("tuple index %d out of range [0-%d)", index, n)
+			}
+			t = tparams.At(index)
+
+		case opConstraint:
+			tparam, ok := t.(*typeparams.TypeParam)
+			if !ok {
+				return nil, fmt.Errorf("cannot apply %q to %s (got %T, want type parameter)", code, t, t)
+			}
+			t = tparam.Constraint()
+
 		case opAt:
 			tuple, ok := t.(*types.Tuple)
 			if !ok {
-				return nil, fmt.Errorf("cannot apply %q to %s (got %s, want tuple)", code, t, t)
+				return nil, fmt.Errorf("cannot apply %q to %s (got %T, want tuple)", code, t, t)
 			}
 			if n := tuple.Len(); index >= n {
 				return nil, fmt.Errorf("tuple index %d out of range [0-%d)", index, n)
@@ -495,7 +561,7 @@
 		case opMethod:
 			hasMethods, ok := t.(hasMethods) // Interface or Named
 			if !ok {
-				return nil, fmt.Errorf("cannot apply %q to %s (got %s, want interface or named)", code, t, t)
+				return nil, fmt.Errorf("cannot apply %q to %s (got %T, want interface or named)", code, t, t)
 			}
 			if n := hasMethods.NumMethods(); index >= n {
 				return nil, fmt.Errorf("method index %d out of range [0-%d)", index, n)
@@ -504,11 +570,11 @@
 			t = nil
 
 		case opObj:
-			named, ok := t.(*types.Named)
+			hasObj, ok := t.(hasObj)
 			if !ok {
-				return nil, fmt.Errorf("cannot apply %q to %s (got %s, want named)", code, t, t)
+				return nil, fmt.Errorf("cannot apply %q to %s (got %T, want named or type param)", code, t, t)
 			}
-			obj = named.Obj()
+			obj = hasObj.Obj()
 			t = nil
 
 		default:
diff --git a/go/types/objectpath/objectpath_go118_test.go b/go/types/objectpath/objectpath_go118_test.go
new file mode 100644
index 0000000..253b062
--- /dev/null
+++ b/go/types/objectpath/objectpath_go118_test.go
@@ -0,0 +1,93 @@
+// Copyright 2021 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.
+
+//go:build go1.18
+// +build go1.18
+
+package objectpath_test
+
+import (
+	"go/types"
+	"testing"
+
+	"golang.org/x/tools/go/buildutil"
+	"golang.org/x/tools/go/loader"
+	"golang.org/x/tools/go/types/objectpath"
+)
+
+func TestGenericPaths(t *testing.T) {
+	pkgs := map[string]map[string]string{
+		"b": {"b.go": `
+package b
+
+const C int = 1
+
+type T[TP0 any, TP1 interface{ M0(); M1() }] struct{}
+
+func (T[RP0, RP1]) M() {}
+
+type N int
+
+func (N) M0()
+func (N) M1()
+
+type A = T[int, N]
+
+func F[FP0, FP1 any](FP0, FP1) {}
+`},
+	}
+	paths := []pathTest{
+		// Good paths
+		{"b", "T", "type b.T[b.TP0 interface{}, b.TP1 interface{M0(); M1()}] struct{}", ""},
+		{"b", "T.O", "type b.T[b.TP0 interface{}, b.TP1 interface{M0(); M1()}] struct{}", ""},
+		{"b", "T.M0", "func (b.T[b.RP0, b.RP1]).M()", ""},
+		{"b", "T.T0O", "type TP0 = b.TP0", ""},
+		{"b", "T.T1O", "type TP1 = b.TP1", ""},
+		{"b", "T.T1CM0", "func (interface).M0()", ""},
+		// Obj of an instance is the generic declaration.
+		{"b", "A.O", "type b.T[b.TP0 interface{}, b.TP1 interface{M0(); M1()}] struct{}", ""},
+		{"b", "A.M0", "func (b.T[int, b.N]).M()", ""},
+
+		// Bad paths
+		{"b", "N.C", "", "invalid path: ends with 'C', want [AFMO]"},
+		{"b", "N.CO", "", "cannot apply 'C' to b.N (got *types.Named, want type parameter)"},
+		{"b", "N.T", "", `invalid path: bad numeric operand "" for code 'T'`},
+		{"b", "N.T0", "", "tuple index 0 out of range [0-0)"},
+		{"b", "T.T2O", "", "tuple index 2 out of range [0-2)"},
+		{"b", "T.T1M0", "", "cannot apply 'M' to b.TP1 (got *types.TypeParam, want interface or named)"},
+		{"b", "C.T0", "", "cannot apply 'T' to int (got *types.Basic, want named or signature)"},
+	}
+
+	conf := loader.Config{Build: buildutil.FakeContext(pkgs)}
+	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
+	for _, test := range []struct {
+		obj     types.Object
+		wantErr string
+	}{
+		{types.Universe.Lookup("any"), "predeclared type any = interface{} has no path"},
+		{types.Universe.Lookup("comparable"), "predeclared type comparable interface{} has no path"},
+	} {
+		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
+		}
+	}
+}
diff --git a/go/types/objectpath/objectpath_test.go b/go/types/objectpath/objectpath_test.go
index 16b6123..1e335ef 100644
--- a/go/types/objectpath/objectpath_test.go
+++ b/go/types/objectpath/objectpath_test.go
@@ -6,6 +6,7 @@
 
 import (
 	"bytes"
+	"fmt"
 	"go/ast"
 	"go/importer"
 	"go/parser"
@@ -55,6 +56,50 @@
 
 `},
 	}
+	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{}", ""},
+		{"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"},
+	}
 	conf := loader.Config{Build: buildutil.FakeContext(pkgs)}
 	conf.Import("a")
 	conf.Import("b")
@@ -62,9 +107,45 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	a := prog.Imported["a"].Pkg
-	b := prog.Imported["b"].Pkg
 
+	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.
 	//
@@ -80,133 +161,63 @@
 	// The downside is that the test depends on the path encoding.
 	// The upside is that the test exercises the encoding.
 
-	// good paths
-	for _, test := range []struct {
-		pkg     *types.Package
-		path    objectpath.Path
-		wantobj string
-	}{
-		{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{}"},
-		{a, "T", "type a.T struct{x int; y int}"},
-		{a, "T.UF0", "field x int"},
-	} {
-		// check path -> object
-		obj, err := objectpath.Object(test.pkg, test.path)
-		if err != nil {
-			t.Errorf("Object(%s, %q) failed: %v",
-				test.pkg.Path(), test.path, err)
-			continue
+	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)
 		}
-		if obj.String() != test.wantobj {
-			t.Errorf("Object(%s, %q) = %v, want %s",
-				test.pkg.Path(), test.path, obj, test.wantobj)
-			continue
-		}
-		if obj.Pkg() != test.pkg {
-			t.Errorf("Object(%s, %q) = %v, which belongs to package %s",
-				test.pkg.Path(), test.path, obj, obj.Pkg().Path())
-			continue
-		}
+		return nil
+	}
+	// Inv: err == nil
 
-		// check object -> path
-		path2, err := objectpath.For(obj)
-		if err != nil {
-			t.Errorf("For(%v) failed: %v, want %q", obj, err, test.path)
-			continue
-		}
-		// 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(test.pkg, path2)
-		if err != nil {
-			t.Errorf("Object(%s, %q) failed: %v (roundtrip from %q)",
-				test.pkg.Path(), path2, err, test.path)
-			continue
-		}
-		if obj2 != obj {
-			t.Errorf("Object(%s, For(obj)) != obj: got %s, obj is %s (path1=%q, path2=%q)",
-				test.pkg.Path(), obj2, obj, test.path, path2)
-			continue
-		}
+	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())
 	}
 
-	// bad paths (all relative to package b)
-	for _, test := range []struct {
-		pkg     *types.Package
-		path    objectpath.Path
-		wantErr string
-	}{
-		{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 struct{x int}, want named)"},
-		{b, "A.UF0", "cannot apply 'U' to struct{x int} (got struct{x int}, 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"},
-	} {
-		obj, err := objectpath.Object(test.pkg, test.path)
-		if err == nil {
-			t.Errorf("Object(%s, %q) = %s, want error",
-				test.pkg.Path(), test.path, obj)
-			continue
-		}
-		if err.Error() != test.wantErr {
-			t.Errorf("Object(%s, %q) error was %q, want %q",
-				test.pkg.Path(), test.path, err, test.wantErr)
-			continue
-		}
+	// 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
+}
 
-	// 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.Info.Implicits[bInfo.Files[0].Imports[0]], "no path for package a"}, // import "a"
-		{b.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
+// 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
 		}
-		if err.Error() != test.wantErr {
-			t.Errorf("Object(%s) error was %q, want %q", test.obj, err, test.wantErr)
-			continue
-		}
+		runes = append(runes, r)
 	}
+	return string(runes)
 }
 
 // TestSourceAndExportData uses objectpath to compute a correspondence