diff --git a/gopls/internal/golang/completion/completion.go b/gopls/internal/golang/completion/completion.go index 36d382dfc78..f02f7e26f1d 100644 --- a/gopls/internal/golang/completion/completion.go +++ b/gopls/internal/golang/completion/completion.go @@ -2936,6 +2936,13 @@ func (ci *candidateInference) candTypeMatches(cand *candidate) bool { for _, expType := range expTypes { if isEmptyInterface(expType) { + // If any type matches the expected type, fall back to other + // considerations below. + // + // TODO(rfindley): can this be expressed via scoring, rather than a boolean? + // Why is it the case that we break ties for the empty interface, but + // not for other expected types that may be satisfied by a lot of + // types, such as fmt.Stringer? continue } diff --git a/gopls/internal/golang/completion/util.go b/gopls/internal/golang/completion/util.go index 1261d417080..ad4ee5e09fc 100644 --- a/gopls/internal/golang/completion/util.go +++ b/gopls/internal/golang/completion/util.go @@ -142,10 +142,17 @@ func isPkgName(obj types.Object) bool { return is[*types.PkgName](obj) } // TODO(adonovan): shouldn't this use CoreType(T)? func isPointer(T types.Type) bool { return is[*types.Pointer](aliases.Unalias(T)) } +// isEmptyInterface whether T is a (possibly Named or Alias) empty interface +// type, such that every type is assignable to T. +// +// isEmptyInterface returns false for type parameters, since they have +// different assignability rules. func isEmptyInterface(T types.Type) bool { - // TODO(adonovan): shouldn't this use Underlying? - intf, _ := T.(*types.Interface) - return intf != nil && intf.NumMethods() == 0 && intf.IsMethodSet() + if _, ok := T.(*types.TypeParam); ok { + return false + } + intf, _ := T.Underlying().(*types.Interface) + return intf != nil && intf.Empty() } func isUntyped(T types.Type) bool {