Skip to content

Commit

Permalink
go/types/objectpath: support parameterized type aliases
Browse files Browse the repository at this point in the history
This change caused objectpath to treat Alias nodes more
like Named types: as first-class nodes, with type parameters,
and a destructuring operation (Alias.Rhs(), opRhs, encoded 'a')
access the RHS type.

A number of historical bugs made this trickier than it should
have been:
- go1.22 prints Alias wrongly, requiring a workaround in the test.
- aliases.Enabled is too expensive to call in the decoder,
  so we must trust that when we see an opRhs and we don't
  have an alias, it's because !Enabled(), not a bug.
- legacy aliases still need to be handled, and order matters.
- the test of parameterized aliases can't be added until
  the GOEXPERIMENT has gone away (soon).

Updates golang/go#46477

Change-Id: Ia903f81e29fb7dbb6e17d1e6a962fad73b3e1f7b
Reviewed-on: https://go-review.googlesource.com/c/tools/+/601235
LUCI-TryBot-Result: Go LUCI <[email protected]>
Auto-Submit: Alan Donovan <[email protected]>
Reviewed-by: Tim King <[email protected]>
Commit-Queue: Alan Donovan <[email protected]>
  • Loading branch information
adonovan committed Jul 26, 2024
1 parent 12d2c34 commit 8b51d66
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 18 deletions.
40 changes: 28 additions & 12 deletions go/types/objectpath/objectpath.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ type Path string
//
// PO package->object Package.Scope.Lookup
// OT object->type Object.Type
// TT type->type Type.{Elem,Key,{,{,Recv}Type}Params,Results,Underlying} [EKPRUTrC]
// TT type->type Type.{Elem,Key,{,{,Recv}Type}Params,Results,Underlying,Rhs} [EKPRUTrCa]
// TO type->object Type.{At,Field,Method,Obj} [AFMO]
//
// All valid paths start with a package and end at an object
Expand All @@ -63,7 +63,7 @@ type Path string
// - 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 [EKPRUTrC];
// - The TT operators are encoded as [EKPRUTrCa];
// two of these ({,Recv}TypeParams) require an integer operand,
// which is encoded as a string of decimal digits.
// - The TO operators are encoded as [AFMO];
Expand Down Expand Up @@ -106,6 +106,7 @@ const (
opTypeParam = 'T' // .TypeParams.At(i) (Named, Signature)
opRecvTypeParam = 'r' // .RecvTypeParams.At(i) (Signature)
opConstraint = 'C' // .Constraint() (TypeParam)
opRhs = 'a' // .Rhs() (Alias)

// type->object operators
opAt = 'A' // .At(i) (Tuple)
Expand Down Expand Up @@ -279,21 +280,26 @@ func (enc *Encoder) For(obj types.Object) (Path, error) {
path = append(path, opType)

T := o.Type()
if alias, ok := T.(*aliases.Alias); ok {
if r := findTypeParam(obj, aliases.TypeParams(alias), path, opTypeParam, nil); r != nil {
return Path(r), nil
}
if r := find(obj, aliases.Rhs(alias), append(path, opRhs), nil); r != nil {
return Path(r), nil
}

if tname.IsAlias() {
// type alias
} else if tname.IsAlias() {
// legacy alias
if r := find(obj, T, path, nil); r != nil {
return Path(r), nil
}
} else {
if named, _ := T.(*types.Named); named != nil {
if r := findTypeParam(obj, named.TypeParams(), path, opTypeParam, nil); r != nil {
// generic named type
return Path(r), nil
}
}

} else if named, ok := T.(*types.Named); ok {
// defined (named) type
if r := find(obj, T.Underlying(), append(path, opUnderlying), nil); r != nil {
if r := findTypeParam(obj, named.TypeParams(), path, opTypeParam, nil); r != nil {
return Path(r), nil
}
if r := find(obj, named.Underlying(), append(path, opUnderlying), nil); r != nil {
return Path(r), nil
}
}
Expand Down Expand Up @@ -657,6 +663,16 @@ func Object(pkg *types.Package, p Path) (types.Object, error) {
}
t = named.Underlying()

case opRhs:
if alias, ok := t.(*aliases.Alias); ok {
t = aliases.Rhs(alias)
} else if false && aliases.Enabled() {
// The Enabled check is too expensive, so for now we
// simply assume that aliases are not enabled.
// TODO(adonovan): replace with "if true {" when go1.24 is assured.
return nil, fmt.Errorf("cannot apply %q to %s (got %T, want alias)", code, t, t)
}

case opTypeParam:
hasTypeParams, ok := t.(hasTypeParams) // Named, Signature
if !ok {
Expand Down
1 change: 1 addition & 0 deletions go/types/objectpath/objectpath_go118_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"golang.org/x/tools/go/types/objectpath"
)

// TODO(adonovan): merge this back into objectpath_test.go.
func TestGenericPaths(t *testing.T) {
pkgs := map[string]map[string]string{
"b": {"b.go": `
Expand Down
54 changes: 52 additions & 2 deletions go/types/objectpath/objectpath_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"bytes"
"fmt"
"go/ast"
"go/build"
"go/importer"
"go/parser"
"go/token"
Expand All @@ -19,9 +20,21 @@ import (
"golang.org/x/tools/go/gcexportdata"
"golang.org/x/tools/go/loader"
"golang.org/x/tools/go/types/objectpath"
"golang.org/x/tools/internal/aliases"
)

func TestPaths(t *testing.T) {
for _, aliases := range []int{0, 1} {
t.Run(fmt.Sprint(aliases), func(t *testing.T) {
testPaths(t, aliases)
})
}
}

func testPaths(t *testing.T, gotypesalias int) {
// override default set by go1.19 in go.mod
t.Setenv("GODEBUG", fmt.Sprintf("gotypesalias=%d", gotypesalias))

pkgs := map[string]map[string]string{
"b": {"b.go": `
package b
Expand All @@ -40,6 +53,11 @@ type U T
type A = struct{ x int }
type unexported2 struct { z int }
type AN = unexported2 // alias of named
// type GA[T any] = T // see below
var V []*a.T
type M map[struct{x int}]struct{y int}
Expand Down Expand Up @@ -75,9 +93,17 @@ type T struct{x, y int}
{"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", "U.UF2", "field T a.T", ""}, // U.U... are aliases for T.U...
{"b", "A", "type b.A = struct{x int}", ""}, // go1.22/alias=1: "type b.A = b.A"
{"b", "A.aF0", "field x int", ""},
{"b", "A.F0", "field x int", ""},
{"b", "AN", "type b.AN = b.unexported2", ""}, // go1.22/alias=1: "type b.AN = b.AN"
{"b", "AN.UF0", "field z int", ""},
{"b", "AN.aO", "type b.unexported2 struct{z int}", ""},
{"b", "AN.O", "type b.unexported2 struct{z int}", ""},
{"b", "AN.aUF0", "field z int", ""},
{"b", "AN.UF0", "field z int", ""},
// {"b", "GA", "type parameter b.GA = T", ""}, // TODO(adonovan): enable once GOEXPERIMENT=aliastypeparams has gone, and only when gotypesalias=1
{"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", ""},
Expand Down Expand Up @@ -126,6 +152,20 @@ type T struct{x, y int}
}

for _, test := range paths {
// go1.22 gotypesalias=1 prints aliases wrong: "type A = A".
// (Fixed by https://go.dev/cl/574716.)
// Work around it here by updating the expectation.
if slicesContains(build.Default.ReleaseTags, "go1.22") &&
!slicesContains(build.Default.ReleaseTags, "go1.23") &&
aliases.Enabled() {
if test.pkg == "b" && test.path == "A" {
test.wantobj = "type b.A = b.A"
}
if test.pkg == "b" && test.path == "AN" {
test.wantobj = "type b.AN = b.AN"
}
}

if err := testPath(prog, test); err != nil {
t.Error(err)
}
Expand Down Expand Up @@ -387,3 +427,13 @@ func (T) X() { }
}
}
}

// TODO(adonovan): use go1.21 slices.Contains.
func slicesContains[S ~[]E, E comparable](slice S, x E) bool {
for _, elem := range slice {
if elem == x {
return true
}
}
return false
}
9 changes: 5 additions & 4 deletions internal/aliases/aliases_go121.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ import (
// It will never be created by go/types.
type Alias struct{}

func (*Alias) String() string { panic("unreachable") }
func (*Alias) Underlying() types.Type { panic("unreachable") }
func (*Alias) Obj() *types.TypeName { panic("unreachable") }
func Rhs(alias *Alias) types.Type { panic("unreachable") }
func (*Alias) String() string { panic("unreachable") }
func (*Alias) Underlying() types.Type { panic("unreachable") }
func (*Alias) Obj() *types.TypeName { panic("unreachable") }
func Rhs(alias *Alias) types.Type { panic("unreachable") }
func TypeParams(alias *Alias) *types.TypeParamList { panic("unreachable") }

// Unalias returns the type t for go <=1.21.
func Unalias(t types.Type) types.Type { return t }
Expand Down
8 changes: 8 additions & 0 deletions internal/aliases/aliases_go122.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ func Rhs(alias *Alias) types.Type {
return Unalias(alias)
}

// TypeParams returns the type parameter list of the alias.
func TypeParams(alias *Alias) *types.TypeParamList {
if alias, ok := any(alias).(interface{ TypeParams() *types.TypeParamList }); ok {
return alias.TypeParams() // go1.23+
}
return nil
}

// Unalias is a wrapper of types.Unalias.
func Unalias(t types.Type) types.Type { return types.Unalias(t) }

Expand Down

0 comments on commit 8b51d66

Please sign in to comment.