From b8a9ef4fac272339aec2a47fb1f0c7294ffb4880 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Sun, 1 Nov 2026 03:03:22 -0600 Subject: [PATCH] fix(go): an implementer's methods must take and return the interface's types goImplementsEdges matched method names and, since PR #2447, parameter and result counts. Go wants the signatures identical, so a type with the right number of the wrong types passed: caddy's TLS matchers, with `Match(*tls.ClientHelloInfo) bool`, "implemented" its HTTP `RequestMatcher`, whose `Match` takes an `*http.Request`, and every HTTP matcher its TLS `ConnectionMatcher`. Each false entry also sent calls through the interface to a method they never reach, and took a place under the per-interface cap. The fix: at the counts that fit, typesFit compares each parameter's and result's type, read off the stored signature (goItemTypes, goComparableType). A type compares by its name with the package left off, since the two declarations may write `Context` and `caddy.Context` from different packages. Nothing is ruled out by a true alias, by a type parameter of the type or of what it embeds, by a `func`/`struct`/`interface` literal type, or by a method a standard-library embedding brings, whose types the table doesn't hold. A gRPC server keeps crossing to its client interface at the server's counts. Validation: - go-implements-types.test.ts: 4 of 9 cases fail on main, all pass. go-promoted-dispatch.test.ts pinned the old behaviour (two series sets implementing each other's interface by name) and now says what Go says. - A/B against main 4499d007, implementers dropped: caddy 24, prometheus 261, etcd 36, hugo 169. A separate judge reads each dropped pair's signatures through embedded types with qualifiers and names removed: 490 of 490 differ. Calls dropped with them: caddy 24, prometheus 636, etcd 36, hugo 98. Added: prometheus 8 implementers and 30 moved dispatch edges, real implementers the false ones had pushed past the cap (`blockSeriesSet`, `seriesSetAdapter`). gin and cobra byte-identical; controls flask and express identical. - REG's 7 false entries on caddy are gone. The 11 real implementers caddy's `Provisioner` and `Unmarshaler` still lose against 1.6.2 are the cap's: both arms keep 40 real ones, of more than 40, in file order. That isn't changed here; raising the cap to 400 adds 239 implementers and 467 edges on caddy. - CPU, median of 3 interleaved: hugo 22.75 to 22.44 s, etcd 21.19 to 22.00 s (wall 7.23 to 7.14 s). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/go-implements-types.test.ts | 277 +++++++++++++++++++++++++ __tests__/go-promoted-dispatch.test.ts | 16 +- src/resolution/callback-synthesizer.ts | 139 ++++++++++++- 4 files changed, 419 insertions(+), 14 deletions(-) create mode 100644 __tests__/go-implements-types.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 106f2518a9..863e2441d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -323,6 +323,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In Go, the methods a type gets from an embedded standard-library type, like `io.Closer`, `http.ResponseWriter`, `testing.TB`, `*os.File` or `sync.Mutex`, or from the built-in `error`, now count when working out which types implement an interface, each with its parameters and results. Before, they were left out: prometheus's `TBRun`, which is all of `testing.TB` plus `Run`, was listed as implemented by a type that only has a `Run`, an interface made only of standard-library ones, like `slog.Handler` plus `io.Closer`, had no implementations at all, and a struct embedding `sync.Mutex` didn't implement an interface of your own that asks for `Lock` and `Unlock`. An interface that embeds a type from another module, whose methods can't be seen, is now implemented only by a type that embeds the same one or by the only type with all its other methods, not by every type that shares a method name with it. Re-index Go projects after upgrading. - In Go, a call through an interface now reaches a method that the implementing struct gets from a type it embeds: prometheus's `targetScraper` gets `Report` from the `*Target` it embeds, so a call through the `scraper` interface now reaches `Target.Report`, and etcd's `Hasher.MemberID` reaches `EtcdServer.MemberID` through `hasherAdapter`. Before, the call reached only structs that declare the method themselves, often just a test double, so `codegraph_explore`'s call path stopped at the interface. The method is picked the way Go picks it, from the nearest embedded type, and none is linked when two embedded types at the same depth both have it or it comes from an embedded interface. Re-index Go projects after upgrading. - In Go, a type defined over a slice, map, function or basic type now counts as implementing the interfaces its methods satisfy, just as a struct does: prometheus's `type staticDiscoverer []*targetgroup.Group` is a `Discoverer`, gin's `type formSource map[string][]string` a `setter`, etcd's `type Int64Comparable int64` a `Comparable`, and an adapter like `type HandlerFunc func(…)` a `Handler`. Before, only structs were considered, so these types were missing from an interface's implementations in `codegraph_explore`, impact and the type hierarchy, and a call through the interface, like `Discoverer.Run` or `setter.TrySet`, stopped there instead of reaching their methods. Re-index Go projects after upgrading. +- In Go, a type now counts as implementing an interface only when its methods take and return the interface's types, not just as many of them: caddy's TLS matchers, whose `Match` takes a `*tls.ClientHelloInfo`, no longer implement its HTTP `RequestMatcher`, whose `Match` takes an `*http.Request`. Before, each false match also sent a call through the interface to a method it never reaches, and could push a real implementer off the list. Re-index Go projects after upgrading. - In Go, a type alias like `type Event = mvccpb.Event`, alone or inside a `type ( … )` group, is now indexed and links to the type it names, so impact on that type reaches the code that uses the alias. Before, aliases were missing: a use written through the alias's package, like `clientv3.Event` or prometheus's `storage.AOptions{…}`, linked to nothing, and one inside the package, like `&PrometheusVersion{}` in prometheus's `web` package, linked to whichever type of that name the project had, there a test helper's struct. A method called on a value of an alias's type now reaches the aliased type's method rather than a namesake's, and a generic alias like `type Items[T any] = List[T]` links to `List`, so `xs.Len()` on one reaches `List.Len` instead of nothing. Re-index Go projects after upgrading. - In Go, a struct that embeds another is no longer treated as overriding the methods it redeclares. gin's `Engine` embeds `RouterGroup` and has a `Use` of its own, but calling `Use` on a `RouterGroup` only ever runs `RouterGroup.Use`; before, `codegraph_explore`'s call path and impact went on from it into `Engine.Use` and the code only the engine runs. The same held for every struct or defined type embedded in another that redeclares one of its methods, like etcd's `Mutex` inside `lockerMutex`. A call through an interface still reaches each type that implements it, the embedding struct included. Re-index Go projects after upgrading. - In JavaScript and TypeScript, a subclass's `static` method no longer counts as overriding an instance method of the same name in its base class, or the reverse, since calling one never runs the other. In webpack, `codegraph_explore`'s call path and impact went on from `Module`'s `deserialize(context)` into the `static deserialize(context)` of `NormalModule` and other modules. A static method still overrides its base's static method and an instance method its base's instance method, and JavaScript class members are now marked static, as TypeScript ones already were. Re-index JavaScript and TypeScript projects after upgrading. Thanks @inth3shadows for the report and @yang0228 for the pull request. (#2761) diff --git a/__tests__/go-implements-types.test.ts b/__tests__/go-implements-types.test.ts new file mode 100644 index 0000000000..0684b30f73 --- /dev/null +++ b/__tests__/go-implements-types.test.ts @@ -0,0 +1,277 @@ +/** + * Go wants an implementer's methods identical to the interface's, and the + * parameter and result counts alone let a type with the right number of the + * wrong types through: + * + * type RequestMatcher interface { Match(*http.Request) bool } // caddyhttp + * func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool // caddytls + * + * so caddy's TLS matchers "implemented" its HTTP one, and each false entry also + * took a place under the per-interface cap. The pass now compares the types the + * text can settle: each by its name, with the package left off, since the two + * declarations may sit in different packages and write `Context` and + * `caddy.Context`. A true alias, a type parameter and a type that holds a list + * of its own say nothing, and a gRPC server still crosses to its client + * interface at the server's counts. + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import CodeGraph from '../src/index'; +import type { Node } from '../src/types'; + +const FILES: Record = { + 'go.mod': 'module example.com/app\n\ngo 1.22\n', + // caddy's two matcher interfaces, and a matcher of each. + 'httpx/matcher.go': `package httpx + +import "net/http" + +type RequestMatcher interface { + Match(*http.Request) bool +} + +type MatchHost []string + +func (m MatchHost) Match(r *http.Request) bool { return false } + +type Encode struct{} + +func (e *Encode) Match(rw *responseWriter) bool { return false } + +type responseWriter struct{} +`, + 'tlsx/matcher.go': `package tlsx + +import "crypto/tls" + +type ConnectionMatcher interface { + Match(*tls.ClientHelloInfo) bool +} + +type MatchServerName []string + +func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool { return false } +`, + // A type its own package writes bare and another through its package. + 'core/modules.go': `package core + +type Context struct{} + +type Provisioner interface { + Provision(Context) error +} + +type Own struct{} + +func (o *Own) Provision(ctx Context) error { return nil } +`, + 'plugin/plugin.go': `package plugin + +import ( + "example.com/app/core" + mod "example.com/app/core" +) + +type Plugin struct{} + +func (p *Plugin) Provision(ctx core.Context) error { return nil } + +type Renamed struct{} + +func (r *Renamed) Provision(ctx mod.Context) error { return nil } +`, + // A grouped list on one side, none on the other; the results are named on one. + 'lists/lists.go': `package lists + +type Copier interface { + Copy(string, string) (int, error) +} + +type Grouped struct{} + +func (g *Grouped) Copy(from, to string) (n int, err error) { return 0, nil } + +type Bytes struct{} + +func (b *Bytes) Copy(from []byte, to string) (int, error) { return 0, nil } +`, + // interface{} and any, uint8 and byte are one type. + 'spelled/spelled.go': `package spelled + +type Sink interface { + Put(v any, b []byte) +} + +type Old struct{} + +func (o *Old) Put(v interface{}, b []uint8) {} +`, + // A true alias stands for another name; a defined type does not. + 'aliased/aliased.go': `package aliased + +type Handle = uint64 + +type Store interface { + Get(id uint64) string +} + +type ByAlias struct{} + +func (a *ByAlias) Get(id Handle) string { return "" } + +type Other uint64 + +type ByDefined struct{} + +func (d *ByDefined) Get(id Other) string { return "" } +`, + // A generic declaration's parameters stand for any type, its other types don't. + 'generic/generic.go': `package generic + +type Keyed interface { + Key() string + Value() int +} + +type Pair[K comparable, V any] struct{} + +func (p *Pair[K, V]) Key() K { var k K; return k } +func (p *Pair[K, V]) Value() V { var v V; return v } + +type Tagged[T any] struct{} + +func (t *Tagged[T]) Key() T { var k T; return k } +func (t *Tagged[T]) Value() int64 { return 0 } + +type Getter[T any] interface { + Get() T +} + +type Text struct{} + +func (t *Text) Get() string { return "" } +`, + // What a generic base passes on is read with its parameters. + 'promoted/promoted.go': `package promoted + +type Base[T any] struct{} + +func (b *Base[T]) Load() T { var v T; return v } + +type Loader interface { + Load() string +} + +type Holder struct { + Base[string] +} +`, + // A type that holds a list of its own isn't compared. + 'funcs/funcs.go': `package funcs + +import "context" + +type Runner interface { + Run(fn func(context.Context) error) +} + +type Job struct{} + +func (j *Job) Run(fn func(ctx context.Context) error) {} +`, + // A gRPC server and its generated client interface, at the server's counts. + 'pb/rpc_grpc.pb.go': `package pb + +import ( + "context" + + "google.golang.org/grpc" +) + +type RangeRequest struct{} +type RangeResponse struct{} + +type KVClient interface { + Range(ctx context.Context, in *RangeRequest, opts ...grpc.CallOption) (*RangeResponse, error) +} +`, + 'server/kv.go': `package server + +import ( + "context" + + pb "example.com/app/pb" +) + +type kvServer struct{} + +func (s *kvServer) Range(ctx context.Context, r *pb.RangeRequest) (*pb.RangeResponse, error) { + return nil, nil +} +`, +}; + +describe('go-implements compares what the signatures say of the types', () => { + let dir: string; + let cg: CodeGraph; + const implementers = (iface: string): string[] => { + const node = cg.getNodesByKind('interface').find((n: Node) => n.name === iface); + expect(node, iface).toBeDefined(); + return cg.getIncomingEdges(node!.id).filter((e) => e.kind === 'implements') + .map((e) => cg.getNode(e.source)!.name).sort(); + }; + + beforeAll(async () => { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-impl-types-')); + for (const [rel, text] of Object.entries(FILES)) { + fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true }); + fs.writeFileSync(path.join(dir, rel), text); + } + cg = await CodeGraph.init(dir, { index: true }); + }, 60_000); + + afterAll(() => { + cg?.close(); + if (dir) fs.rmSync(dir, { recursive: true, force: true }); + }); + + it('keeps a matcher of another parameter type out', () => { + expect(implementers('RequestMatcher')).toEqual(['MatchHost']); + expect(implementers('ConnectionMatcher')).toEqual(['MatchServerName']); + }); + + it('reads a type by its name, however its package is written', () => { + expect(implementers('Provisioner')).toEqual(['Own', 'Plugin', 'Renamed']); + }); + + it('reads a grouped list, a named result and an unnamed list alike', () => { + expect(implementers('Copier')).toEqual(['Grouped']); + }); + + it('takes `interface{}` for `any` and `uint8` for `byte`', () => { + expect(implementers('Sink')).toEqual(['Old']); + }); + + it('lets a true alias stand for its type, and no defined type', () => { + expect(implementers('Store')).toEqual(['ByAlias']); + }); + + it('reads a declaration\'s type parameters as any type, and the rest as written', () => { + expect(implementers('Keyed')).toEqual(['Pair']); + expect(implementers('Getter')).toEqual(['Text']); + }); + + it('reads what a generic base promotes with its parameters', () => { + expect(implementers('Loader')).toEqual(['Base', 'Holder']); + }); + + it('leaves a type with a list of its own uncompared', () => { + expect(implementers('Runner')).toEqual(['Job']); + }); + + it('still crosses from a gRPC client interface to the server of its service', () => { + expect(implementers('KVClient')).toEqual(['kvServer']); + }); +}); diff --git a/__tests__/go-promoted-dispatch.test.ts b/__tests__/go-promoted-dispatch.test.ts index 76b5d6243f..64b6728fb0 100644 --- a/__tests__/go-promoted-dispatch.test.ts +++ b/__tests__/go-promoted-dispatch.test.ts @@ -276,14 +276,14 @@ describe('a call through a Go interface reaches the method embedding promotes', it('links the interface method to the embedded type that declares it', () => { const seriesSet = one('SeriesSet', 'storage/interface.go'); const chunkSet = one('ChunkSeriesSet', 'storage/interface.go'); - // blockSeriesSet's embedding, the first of the two. - const embedding = at('tsdb/querier.go', '\tblockBaseSeriesSet'); - // Both structs satisfy both interfaces by name, and both get Next from - // the same base: one edge, through the first of them. - for (const iface of [seriesSet, chunkSet]) { - expect(dispatch(cg, methodOf(cg, iface, 'Next'))).toEqual([`blockBaseSeriesSet::Next <- blockSeriesSet @${embedding}`]); - expect(dispatch(cg, methodOf(cg, iface, 'Err'))).toEqual([`blockBaseSeriesSet::Err <- blockSeriesSet @${embedding}`]); - expect(dispatch(cg, methodOf(cg, iface, 'At'))).toEqual(['blockChunkSeriesSet::At', 'blockSeriesSet::At']); + const lines = FILES['tsdb/querier.go']!.split('\n'); + const embedded = lines.flatMap((text, i) => (text === '\tblockBaseSeriesSet' ? [`tsdb/querier.go:${i + 1}`] : [])); + // Each struct's `At` returns its own interface's type, and each gets Next + // from the same base, through its own embedding. + for (const [iface, type, embedding] of [[seriesSet, 'blockSeriesSet', embedded[0]], [chunkSet, 'blockChunkSeriesSet', embedded[1]]] as const) { + expect(dispatch(cg, methodOf(cg, iface, 'Next'))).toEqual([`blockBaseSeriesSet::Next <- ${type} @${embedding}`]); + expect(dispatch(cg, methodOf(cg, iface, 'Err'))).toEqual([`blockBaseSeriesSet::Err <- ${type} @${embedding}`]); + expect(dispatch(cg, methodOf(cg, iface, 'At'))).toEqual([`${type}::At`]); } }); diff --git a/src/resolution/callback-synthesizer.ts b/src/resolution/callback-synthesizer.ts index 08f0ac3f34..b69341030e 100644 --- a/src/resolution/callback-synthesizer.ts +++ b/src/resolution/callback-synthesizer.ts @@ -983,6 +983,63 @@ export function goSignatureArity(signature: string | undefined): string | null { return parts ? `${parts.params.length}/${parts.results.length}` : null; } +/** Words that start a Go type, in a parameter that could otherwise read as a name and its type. */ +const GO_TYPE_WORDS: ReadonlySet = new Set(['func', 'chan', 'map', 'struct', 'interface']); + +/** + * The types of a Go parameter or result list's items: `ctx context.Context` + * is a `context.Context`, the `a` of `a, b int` an `int`, and in a list with + * no names each item is a type. + */ +function goItemTypes(items: readonly string[]): string[] { + const named = items.map((item) => { + const m = /^([A-Za-z_]\w*)\s+(\S[\s\S]*)$/.exec(item); + return m && !GO_TYPE_WORDS.has(m[1]!) ? m[2]! : null; + }); + if (named.every((type) => type === null)) return [...items]; + const types: string[] = []; + let type = ''; + for (let i = items.length - 1; i >= 0; i--) types[i] = type = named[i] ?? type; + return types; +} + +/** + * A Go type as two declarations can compare it: with no spaces or package + * qualifiers, as the two may sit in different packages and write `Context` and + * `caddy.Context`. Null for a type that holds a list of its own (a `func`, a + * `struct`), whose text differs by more than that. + */ +function goComparableType(written: string): string | null { + const flat = written.replace(/\s+/g, '').replace(/interface\{\}/g, 'any'); + if (/\bfunc\(|\bstruct\{|\binterface\{/.test(flat)) return null; + return flat.replace(/[A-Za-z_]\w*\./g, '').replace(/\buint8\b/g, 'byte').replace(/\bint32\b/g, 'rune'); +} + +/** A Go method's parameter and result types, each as goComparableType gives it, one list. Null when the signature doesn't read. */ +function goMethodTypes(signature: string | undefined): Array | null { + const parts = goSignatureParts(signature); + return parts ? [...goItemTypes(parts.params), ...goItemTypes(parts.results)].map(goComparableType) : null; +} + +/** The source a Go type declaration starts with: its line, and the next two for a list that wraps. */ +function goDeclarationText(n: Node, ctx: ResolutionContext): string { + const lines = ctx.getFileLines?.(n.filePath) ?? ctx.readFile(n.filePath)?.split(/\r?\n/) ?? []; + return lines.slice(n.startLine - 1, n.startLine + 2).join('\n'); +} + +/** The type parameters a Go type declares: `K` and `V` of `type Cache[K comparable, V any] struct`. */ +function goTypeParameterNames(n: Node, ctx: ResolutionContext): string[] { + const text = goDeclarationText(n, ctx); + const open = /^\s*(?:type\s+)?[A-Za-z_]\w*\s*\[/.exec(text); + const list = open ? goListItems(text, open[0].length - 1) : null; + return list ? list.items.flatMap((item) => /^[A-Za-z_]\w*/.exec(item)?.[0] ?? []) : []; +} + +/** Whether a Go `type_alias` node is a true alias, `type A = B`, rather than a defined type. */ +function isGoTrueAlias(n: Node, ctx: ResolutionContext): boolean { + return /^\s*(?:type\s+)?[A-Za-z_]\w*\s*(?:\[[^\]]*\]\s*)?=/.test(goDeclarationText(n, ctx)); +} + /** * The arity a gRPC server implements an RPC with, read off the signature the * generated client has for it. A unary call (`Range(ctx, in *RangeRequest, @@ -1058,11 +1115,19 @@ function goGrpcServerArity(signature: string | undefined): string | null { * (see `wantedArities`), the bridge a client's call crosses to reach its * handler. * - * Types are not compared, and a promoted name counts even where Go finds it - * ambiguous or hidden by a field — over-approximation accepted, in line with - * the other dispatch synthesizers; capped per interface. Empty interfaces - * (`any`) are skipped so they don't match every type, and a struct that - * embeds the interface keeps the edge its embedding already is. + * At those counts its types must be the interface's too, each by its name with + * the package left off, since the two may sit in different packages (see + * `typesFit`): caddy's TLS `MatchServerName`, with `Match(*tls.ClientHelloInfo) + * bool`, is no HTTP `RequestMatcher`, whose `Match` takes an `*http.Request`. + * A true alias, a declaration's type parameter and a type that holds a list of + * its own rule nothing out, nor does a method a standard-library embedding + * brings. + * + * A promoted name counts even where Go finds it ambiguous or hidden by a field + * — over-approximation accepted, in line with the other dispatch synthesizers; + * capped per interface. Empty interfaces (`any`) are skipped so they don't + * match every type, and a struct that embeds the interface keeps the edge its + * embedding already is. */ async function goImplementsEdges(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield): Promise { let scanned255 = 0; @@ -1287,6 +1352,68 @@ async function goImplementsEdges(queries: QueryBuilder, ctx: ResolutionContext, return true; }; + // Each method in a type's method set → the types of its declarations there; + // null when one of them doesn't read or a standard-library embedding brings + // it, whose types the table doesn't hold. Built only for the pairs whose + // counts already fit. + const typesMemo = new Map> | null>>(); + const methodTypes = (id: string): Map> | null> => { + let types = typesMemo.get(id); + if (types) return types; + types = new Map(); + for (const t of embedClosure(id)) { + ownMethods(t); + for (const [name, signatures] of ownSignatures.get(t) ?? []) { + let known = types.get(name); + if (known === null) continue; + if (!known) types.set(name, (known = [])); + for (const signature of signatures) { + const read = goMethodTypes(signature); + if (!read) { + types.set(name, null); + break; + } + known.push(read); + } + } + for (const methods of outside.get(t)?.stdlib ?? []) for (const name of methods.keys()) types.set(name, null); + } + typesMemo.set(id, types); + return types; + }; + // The names that stand for some other type: the project's true aliases, and + // the type parameters of a type or of what it embeds. + let aliases: Set | undefined; + const parameterMemo = new Map(); + const typeParameters = (id: string): string[] => { + let names = parameterMemo.get(id); + if (!names) { + const type = queries.getNodeById(id); + parameterMemo.set(id, (names = type ? goTypeParameterNames(type, ctx) : [])); + } + return names; + }; + // Go wants each method's types to be the interface's. A declaration at + // another count is the gRPC server `wantedArities` lets through: the + // parameters are its service's, not the client's. + const typesFit = (s: Node, iface: Node): boolean => { + aliases ??= new Set(goImplementers.filter((n) => n.kind === 'type_alias' && isGoTrueAlias(n, ctx)).map((n) => n.name)); + const known = aliases; + const parameters = new Set([...embedClosure(s.id), ...embedClosure(iface.id)].flatMap(typeParameters)); + const open = (type: string | null): boolean => type === null || + type.split(/\W+/).some((word) => known.has(word) || parameters.has(word)); + const sameType = (a: string | null, b: string | null): boolean => a === b || open(a) || open(b); + const have = methodTypes(s.id); + for (const [name, wantedTypes] of methodTypes(iface.id)) { + const got = have.get(name); + if (!wantedTypes || !got) continue; + const fits = wantedTypes.some((want) => got.some((type) => + type.length !== want.length || type.every((t, i) => sameType(t, want[i]!)))); + if (!fits) return false; + } + return true; + }; + // A type without a method, or an embedded type nothing describes, satisfies // no interface this pass looks at. const candidates: Node[] = []; @@ -1334,7 +1461,7 @@ async function goImplementsEdges(queries: QueryBuilder, ctx: ResolutionContext, const have = candidateMethods.get(s.id)!; if (!covers(have.methods, want.methods)) continue; if (!wanted) wanted = wantedArities(iface); - if (!aritiesFit(s.id, wanted)) continue; + if (!aritiesFit(s.id, wanted) || !typesFit(s, iface)) continue; covering++; if (embeds(s.id).includes(iface.id)) continue; // declared by embedding it if (!coversOpaque(have, want)) {