(queries: QueryBuilder, onYield: MaybeYield)
| 1041 | } |
| 1042 | |
| 1043 | async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield): Promise<Edge[]> { |
| 1044 | let scanned255 = 0; |
| 1045 | const edges: Edge[] = []; |
| 1046 | const seen = new Set<string>(); |
| 1047 | // Memoized: a popular base interface's method list is otherwise re-fetched |
| 1048 | // once per implementer (dubbo-style hub interfaces have hundreds), and the |
| 1049 | // memo only ever serves reads. Same rows, same order — byte-identical. |
| 1050 | const methodsMemo = new Map<string, Node[]>(); |
| 1051 | const methodsOf = (classId: string): Node[] => { |
| 1052 | const hit = methodsMemo.get(classId); |
| 1053 | if (hit) return hit; |
| 1054 | const methods = queries |
| 1055 | .getOutgoingEdges(classId, ['contains']) |
| 1056 | .map((e) => queries.getNodeById(e.target)) |
| 1057 | .filter((n): n is Node => !!n && n.kind === 'method'); |
| 1058 | methodsMemo.set(classId, methods); |
| 1059 | return methods; |
| 1060 | }; |
| 1061 | // Concrete-side kinds vary by language: `class` covers Java / Kotlin / |
| 1062 | // C# / TS / Swift-classes / Scala-classes; `struct` covers Swift value |
| 1063 | // types that conform to protocols. Iterate both. |
| 1064 | const concreteKinds = ['class', 'struct'] as const; |
| 1065 | for (const kind of concreteKinds) { |
| 1066 | for (const cls of queries.iterateNodesByKind(kind)) { |
| 1067 | if ((++scanned255 & 63) === 0) await onYield(); |
| 1068 | // A class can only emit here if it HAS a supertype edge — check that |
| 1069 | // (one edge query) before materializing its methods: most classes in a |
| 1070 | // typical graph extend/implement nothing and skip in one hop. |
| 1071 | const sups = queries.getOutgoingEdges(cls.id, ['implements', 'extends']); |
| 1072 | if (sups.length === 0) continue; |
| 1073 | const implMethods = methodsOf(cls.id).filter((n) => IFACE_OVERRIDE_LANGS.has(n.language)); |
| 1074 | if (implMethods.length === 0) continue; |
| 1075 | for (const sup of sups) { |
| 1076 | const base = queries.getNodeById(sup.target); |
| 1077 | if (!base || !IFACE_OVERRIDE_LANGS.has(base.language) || base.id === cls.id) continue; |
| 1078 | // Group impl methods by name to handle OVERLOADS: an interface `list()` and |
| 1079 | // `list(params)` are distinct nodes and a call may resolve to either, so |
| 1080 | // link every base overload → every same-name impl overload (keying by name |
| 1081 | // alone would drop all but one and miss the resolved overload). |
| 1082 | const implByName = new Map<string, Node[]>(); |
| 1083 | for (const m of implMethods) { |
| 1084 | const arr = implByName.get(m.name); |
| 1085 | if (arr) arr.push(m); else implByName.set(m.name, [m]); |
| 1086 | } |
| 1087 | let added = 0; |
| 1088 | for (const bm of methodsOf(base.id)) { |
| 1089 | if (added >= MAX_CALLBACKS_PER_CHANNEL) break; |
| 1090 | for (const m of implByName.get(bm.name) ?? []) { |
| 1091 | if (added >= MAX_CALLBACKS_PER_CHANNEL) break; |
| 1092 | if (bm.id === m.id) continue; |
| 1093 | const key = `${bm.id}>${m.id}`; |
| 1094 | if (seen.has(key)) continue; |
| 1095 | seen.add(key); |
| 1096 | edges.push({ |
| 1097 | source: bm.id, |
| 1098 | target: m.id, |
| 1099 | kind: 'calls', |
| 1100 | line: bm.startLine, |
no test coverage detected