Skip to content

Commit c1ff179

Browse files
adonovangopherbot
authored andcommitted
gopls/internal/golang: Implementations: support generics
This CL adds support for generic types to the local (go/types-based) implementation of Implementations. Instead of using types.AssignableTo(C, I), which doesn't consider generics, we check that C has all the methods of I and that the signatures of each corresponding pair match when type parameters are allowed to stand for any type at all. A more precise implementation would use true unification, ensuring that only consistent substitutions are considered to match, but this is good enough for now. Perhaps when go/types adds a Unify API (#63982) this will be easier. See CL 634197 for the corresponding change to the global (fingerprint-based) implementation. Fixes golang/go#59224 Updates golang/go#63982 Change-Id: I6959f855b69436d52cafbe4de07bcf24b1e0f280 Reviewed-on: https://go-review.googlesource.com/c/tools/+/634596 Reviewed-by: Robert Findley <[email protected]> Auto-Submit: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent dd4bf11 commit c1ff179

File tree

5 files changed

+208
-30
lines changed

5 files changed

+208
-30
lines changed

gopls/internal/golang/completion/completion.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -266,9 +266,10 @@ type completer struct {
266266
// matcher matches the candidates against the surrounding prefix.
267267
matcher matcher
268268

269-
// methodSetCache caches the types.NewMethodSet call, which is relatively
269+
// methodSetCache caches the [types.NewMethodSet] call, which is relatively
270270
// expensive and can be called many times for the same type while searching
271271
// for deep completions.
272+
// TODO(adonovan): use [typeutil.MethodSetCache], which exists for this purpose.
272273
methodSetCache map[methodSetKey]*types.MethodSet
273274

274275
// tooNewSymbolsCache is a cache of

gopls/internal/golang/implementation.go

+166-18
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"sync"
1818

1919
"golang.org/x/sync/errgroup"
20+
"golang.org/x/tools/go/types/typeutil"
2021
"golang.org/x/tools/gopls/internal/cache"
2122
"golang.org/x/tools/gopls/internal/cache/metadata"
2223
"golang.org/x/tools/gopls/internal/cache/methodsets"
@@ -345,6 +346,8 @@ func implementsObj(ctx context.Context, snapshot *cache.Snapshot, uri protocol.D
345346
func localImplementations(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, queryType types.Type, method *types.Func) ([]protocol.Location, error) {
346347
queryType = methodsets.EnsurePointer(queryType)
347348

349+
var msets typeutil.MethodSetCache
350+
348351
// Scan through all type declarations in the syntax.
349352
var locs []protocol.Location
350353
var methodLocs []methodsets.Location
@@ -366,16 +369,16 @@ func localImplementations(ctx context.Context, snapshot *cache.Snapshot, pkg *ca
366369
// The historical behavior enshrined by this
367370
// function rejects cases where both are
368371
// (nontrivial) interface types?
369-
// That seems like useful information.
372+
// That seems like useful information; see #68641.
370373
// TODO(adonovan): UX: report I/I pairs too?
371374
// The same question appears in the global algorithm (methodsets).
372-
if !concreteImplementsIntf(candidateType, queryType) {
375+
if !concreteImplementsIntf(&msets, candidateType, queryType) {
373376
return true // not assignable
374377
}
375378

376379
// Ignore types with empty method sets.
377380
// (No point reporting that every type satisfies 'any'.)
378-
mset := types.NewMethodSet(candidateType)
381+
mset := msets.MethodSet(candidateType)
379382
if mset.Len() == 0 {
380383
return true
381384
}
@@ -449,28 +452,173 @@ func errorLocation(ctx context.Context, snapshot *cache.Snapshot) (protocol.Loca
449452
return protocol.Location{}, fmt.Errorf("built-in error type not found")
450453
}
451454

452-
// concreteImplementsIntf returns true if a is an interface type implemented by
453-
// concrete type b, or vice versa.
454-
func concreteImplementsIntf(a, b types.Type) bool {
455-
aIsIntf, bIsIntf := types.IsInterface(a), types.IsInterface(b)
455+
// concreteImplementsIntf reports whether x is an interface type
456+
// implemented by concrete type y, or vice versa.
457+
//
458+
// If one or both types are generic, the result indicates whether the
459+
// interface may be implemented under some instantiation.
460+
func concreteImplementsIntf(msets *typeutil.MethodSetCache, x, y types.Type) bool {
461+
xiface := types.IsInterface(x)
462+
yiface := types.IsInterface(y)
456463

457464
// Make sure exactly one is an interface type.
458-
if aIsIntf == bIsIntf {
465+
// TODO(adonovan): rescind this policy choice and report
466+
// I/I relationships. See CL 619719 + issue #68641.
467+
if xiface == yiface {
459468
return false
460469
}
461470

462-
// Rearrange if needed so "a" is the concrete type.
463-
if aIsIntf {
464-
a, b = b, a
471+
// Rearrange if needed so x is the concrete type.
472+
if xiface {
473+
x, y = y, x
465474
}
475+
// Inv: y is an interface type.
466476

467-
// TODO(adonovan): this should really use GenericAssignableTo
468-
// to report (e.g.) "ArrayList[T] implements List[T]", but
469-
// GenericAssignableTo doesn't work correctly on pointers to
470-
// generic named types. Thus the legacy implementation and the
471-
// "local" part of implementations fail to report generics.
472-
// The global algorithm based on subsets does the right thing.
473-
return types.AssignableTo(a, b)
477+
// For each interface method of y, check that x has it too.
478+
// It is not necessary to compute x's complete method set.
479+
//
480+
// If y is a constraint interface (!y.IsMethodSet()), we
481+
// ignore non-interface terms, leading to occasional spurious
482+
// matches. We could in future filter based on them, but it
483+
// would lead to divergence with the global (fingerprint-based)
484+
// algorithm, which operates only on methodsets.
485+
ymset := msets.MethodSet(y)
486+
for i := range ymset.Len() {
487+
ym := ymset.At(i).Obj().(*types.Func)
488+
489+
xobj, _, _ := types.LookupFieldOrMethod(x, false, ym.Pkg(), ym.Name())
490+
xm, ok := xobj.(*types.Func)
491+
if !ok {
492+
return false // x lacks a method of y
493+
}
494+
if !unify(xm.Signature(), ym.Signature()) {
495+
return false // signatures do not match
496+
}
497+
}
498+
return true // all methods found
499+
}
500+
501+
// unify reports whether the types of x and y match, allowing free
502+
// type parameters to stand for anything at all, without regard to
503+
// consistency of substitutions.
504+
//
505+
// TODO(adonovan): implement proper unification (#63982), finding the
506+
// most general unifier across all the interface methods.
507+
//
508+
// See also: unify in cache/methodsets/fingerprint, which uses a
509+
// similar ersatz unification approach on type fingerprints, for
510+
// the global index.
511+
func unify(x, y types.Type) bool {
512+
x = types.Unalias(x)
513+
y = types.Unalias(y)
514+
515+
// For now, allow a type parameter to match anything,
516+
// without regard to consistency of substitutions.
517+
if is[*types.TypeParam](x) || is[*types.TypeParam](y) {
518+
return true
519+
}
520+
521+
if reflect.TypeOf(x) != reflect.TypeOf(y) {
522+
return false // mismatched types
523+
}
524+
525+
switch x := x.(type) {
526+
case *types.Array:
527+
y := y.(*types.Array)
528+
return x.Len() == y.Len() &&
529+
unify(x.Elem(), y.Elem())
530+
531+
case *types.Basic:
532+
y := y.(*types.Basic)
533+
return x.Kind() == y.Kind()
534+
535+
case *types.Chan:
536+
y := y.(*types.Chan)
537+
return x.Dir() == y.Dir() &&
538+
unify(x.Elem(), y.Elem())
539+
540+
case *types.Interface:
541+
y := y.(*types.Interface)
542+
// TODO(adonovan): fix: for correctness, we must check
543+
// that both interfaces have the same set of methods
544+
// modulo type parameters, while avoiding the risk of
545+
// unbounded interface recursion.
546+
//
547+
// Since non-empty interface literals are vanishingly
548+
// rare in methods signatures, we ignore this for now.
549+
// If more precision is needed we could compare method
550+
// names and arities, still without full recursion.
551+
return x.NumMethods() == y.NumMethods()
552+
553+
case *types.Map:
554+
y := y.(*types.Map)
555+
return unify(x.Key(), y.Key()) &&
556+
unify(x.Elem(), y.Elem())
557+
558+
case *types.Named:
559+
y := y.(*types.Named)
560+
if x.Origin() != y.Origin() {
561+
return false // different named types
562+
}
563+
xtargs := x.TypeArgs()
564+
ytargs := y.TypeArgs()
565+
if xtargs.Len() != ytargs.Len() {
566+
return false // arity error (ill-typed)
567+
}
568+
for i := range xtargs.Len() {
569+
if !unify(xtargs.At(i), ytargs.At(i)) {
570+
return false // mismatched type args
571+
}
572+
}
573+
return true
574+
575+
case *types.Pointer:
576+
y := y.(*types.Pointer)
577+
return unify(x.Elem(), y.Elem())
578+
579+
case *types.Signature:
580+
y := y.(*types.Signature)
581+
return x.Variadic() == y.Variadic() &&
582+
unify(x.Params(), y.Params()) &&
583+
unify(x.Results(), y.Results())
584+
585+
case *types.Slice:
586+
y := y.(*types.Slice)
587+
return unify(x.Elem(), y.Elem())
588+
589+
case *types.Struct:
590+
y := y.(*types.Struct)
591+
if x.NumFields() != y.NumFields() {
592+
return false
593+
}
594+
for i := range x.NumFields() {
595+
xf := x.Field(i)
596+
yf := y.Field(i)
597+
if xf.Embedded() != yf.Embedded() ||
598+
xf.Name() != yf.Name() ||
599+
x.Tag(i) != y.Tag(i) ||
600+
!xf.Exported() && xf.Pkg() != yf.Pkg() ||
601+
!unify(xf.Type(), yf.Type()) {
602+
return false
603+
}
604+
}
605+
return true
606+
607+
case *types.Tuple:
608+
y := y.(*types.Tuple)
609+
if x.Len() != y.Len() {
610+
return false
611+
}
612+
for i := range x.Len() {
613+
if !unify(x.At(i).Type(), y.At(i).Type()) {
614+
return false
615+
}
616+
}
617+
return true
618+
619+
default: // incl. *Union, *TypeParam
620+
panic(fmt.Sprintf("unexpected Type %#v", x))
621+
}
474622
}
475623

476624
var (

gopls/internal/golang/references.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525

2626
"golang.org/x/sync/errgroup"
2727
"golang.org/x/tools/go/types/objectpath"
28+
"golang.org/x/tools/go/types/typeutil"
2829
"golang.org/x/tools/gopls/internal/cache"
2930
"golang.org/x/tools/gopls/internal/cache/metadata"
3031
"golang.org/x/tools/gopls/internal/cache/methodsets"
@@ -579,6 +580,8 @@ func localReferences(pkg *cache.Package, targets map[types.Object]bool, correspo
579580
}
580581
}
581582

583+
var msets typeutil.MethodSetCache
584+
582585
// matches reports whether obj either is or corresponds to a target.
583586
// (Correspondence is defined as usual for interface methods.)
584587
matches := func(obj types.Object) bool {
@@ -588,7 +591,7 @@ func localReferences(pkg *cache.Package, targets map[types.Object]bool, correspo
588591
if methodRecvs != nil && obj.Name() == methodName {
589592
if orecv := effectiveReceiver(obj); orecv != nil {
590593
for _, mrecv := range methodRecvs {
591-
if concreteImplementsIntf(orecv, mrecv) {
594+
if concreteImplementsIntf(&msets, orecv, mrecv) {
592595
return true
593596
}
594597
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
Test of special case of 'implementation' query: aliases of basic types
2+
(rune vs int32) in the "tricky" (=generic) algorithm for unifying
3+
method signatures.
4+
5+
We test both the local (intra-package) and global (cross-package)
6+
algorithms.
7+
8+
-- go.mod --
9+
module example.com
10+
go 1.18
11+
12+
-- a/a.go --
13+
package a
14+
15+
type C[T any] struct{}
16+
func (C[T]) F(rune, T) {} //@ loc(aCF, "F"), implementation("F", aIF, bIF)
17+
18+
type I[T any] interface{ F(int32, T) } //@ loc(aIF, "F"), implementation("F", aCF, bCF)
19+
20+
-- b/b.go --
21+
package b
22+
23+
type C[T any] struct{}
24+
func (C[T]) F(rune, T) {} //@ loc(bCF, "F"), implementation("F", aIF, bIF)
25+
26+
type I[T any] interface{ F(int32, T) } //@ loc(bIF, "F"), implementation("F", aCF, bCF)

gopls/internal/test/marker/testdata/implementation/generics.txt

+10-10
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,25 @@ go 1.18
77
-- implementation/implementation.go --
88
package implementation
99

10-
type GenIface[T any] interface { //@loc(GenIface, "GenIface"),implementation("GenIface", GC)
11-
F(int, string, T) //@loc(GenIfaceF, "F"),implementation("F", GCF)
10+
type GenIface[T any] interface { //@loc(GenIface, "GenIface"),implementation("GenIface", GC, GenConc, GenConcString)
11+
F(int, string, T) //@loc(GenIfaceF, "F"),implementation("F", GCF, GenConcF)
1212
}
1313

14-
type GenConc[U any] int //@loc(GenConc, "GenConc"),implementation("GenConc", GI, GIString)
14+
type GenConc[U any] int //@loc(GenConc, "GenConc"),implementation("GenConc", GI, GIString, GenIface)
1515

16-
func (GenConc[V]) F(int, string, V) {} //@loc(GenConcF, "F"),implementation("F", GIF)
16+
func (GenConc[V]) F(int, string, V) {} //@loc(GenConcF, "F"),implementation("F", GIF, GenIfaceF)
1717

18-
type GenConcString struct{ GenConc[string] } //@loc(GenConcString, "GenConcString"),implementation(GenConcString, GIString, GI)
18+
type GenConcString struct{ GenConc[string] } //@loc(GenConcString, "GenConcString"),implementation(GenConcString, GIString, GI, GenIface)
1919

2020
-- other/other.go --
2121
package other
2222

23-
type GI[T any] interface { //@loc(GI, "GI"),implementation("GI", GenConc, GenConcString)
24-
F(int, string, T) //@loc(GIF, "F"),implementation("F", GenConcF)
23+
type GI[T any] interface { //@loc(GI, "GI"),implementation("GI", GenConc, GenConcString, GC)
24+
F(int, string, T) //@loc(GIF, "F"),implementation("F", GenConcF, GCF)
2525
}
2626

27-
type GIString GI[string] //@loc(GIString, "GIString"),implementation("GIString", GenConcString, GenConc)
27+
type GIString GI[string] //@loc(GIString, "GIString"),implementation("GIString", GenConcString, GenConc, GC)
2828

29-
type GC[U any] int //@loc(GC, "GC"),implementation("GC", GenIface)
29+
type GC[U any] int //@loc(GC, "GC"),implementation("GC", GenIface, GI, GIString)
3030

31-
func (GC[V]) F(int, string, V) {} //@loc(GCF, "F"),implementation("F", GenIfaceF)
31+
func (GC[V]) F(int, string, V) {} //@loc(GCF, "F"),implementation("F", GenIfaceF, GIF)

0 commit comments

Comments
 (0)