From b4eb6bf54123ef26bf1e372c9ba7f37c3b7d73db Mon Sep 17 00:00:00 2001 From: Alok Kumar Date: Thu, 29 Oct 2026 00:55:45 +0530 Subject: [PATCH] implementations: fall back to a method-set fingerprint when types.Implements fails loader.addImportsToPkgs keeps the white-box test variant `p [p.test]` of a package that has an in-package _test.go, so implementation extraction sees that variant's *types.Named while a cross-package implementer was checked against the plain `p`. types.Implements then fails for any interface method whose signature names a type declared in `p`, and the is_implementation relationship is silently dropped. When types.Implements fails, compare method-set identity (each interface method's Id + signature fingerprint present in the type's method set). Types with type parameters stay on types.Implements only, so the fallback can only add edges. Co-Authored-By: Claude Opus 5.5 --- internal/implementations/extractor.go | 1 + internal/implementations/implementations.go | 38 ++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/internal/implementations/extractor.go b/internal/implementations/extractor.go index a8b0a40b..cbabbbb3 100644 --- a/internal/implementations/extractor.go +++ b/internal/implementations/extractor.go @@ -142,6 +142,7 @@ func (e *Extractor) classify( HasUnexported: hasUnexportedMethods(methods), PkgPath: pkgPath, } + impl.Sigs, impl.Tricky = methodSigs(methods) if types.IsInterface(named) { interfaces[impl.Symbol.Symbol] = impl } else { diff --git a/internal/implementations/implementations.go b/internal/implementations/implementations.go index 107a82b4..37a19d70 100644 --- a/internal/implementations/implementations.go +++ b/internal/implementations/implementations.go @@ -26,6 +26,41 @@ type ImplDef struct { MethodCount int HasUnexported bool // true if any method is unexported PkgPath string // package path, used to skip cross-package checks for unexported methods + + // Sigs holds each method's Id + signature fingerprint; Tricky is set when any fingerprint + // involves type parameters (then only types.Implements decides). + Sigs map[string]bool + Tricky bool +} + +// methodSigs returns the Id+fingerprint set of a method set, the identity gopls's methodsets +// index matches on across separately type-checked packages. +func methodSigs(methods []*types.Selection) (map[string]bool, bool) { + sigs, tricky := make(map[string]bool, len(methods)), false + for _, m := range methods { + fn := m.Obj().(*types.Func) + fp, tr := fingerprint.Encode(fn.Signature()) + tricky = tricky || tr + sigs[fn.Id()+"\x00"+fp] = true + } + return sigs, tricky +} + +// implementsBySignature reports whether every method of iface appears in ty with the same +// fingerprint. It answers when the two sides come from different type-checking universes: +// a package and its white-box test variant ("p [p.test]") share a symbol but not a +// *types.Named, so types.Implements fails for an implementer in another package whose +// signatures name a type of the interface's own package. +func implementsBySignature(ty, iface ImplDef) bool { + if ty.Tricky || iface.Tricky { + return false + } + for sig := range iface.Sigs { + if !ty.Sigs[sig] { + return false + } + } + return true } // methodMask computes a 64-bit bitmask from a set of methods, using @@ -89,7 +124,8 @@ func findImplementations(concreteTypes map[string]ImplDef, interfaces map[string // Use types.Implements to correctly check interface satisfaction, // handling all edge cases (embedded types, generics, unexported methods). if !types.Implements(ty.Named, ifaceType) && - !types.Implements(types.NewPointer(ty.Named), ifaceType) { + !types.Implements(types.NewPointer(ty.Named), ifaceType) && + !implementsBySignature(ty, iface) { continue }