type-hierarchy.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. /**
  2. * The type hierarchy — one derivation of "what is above this type, what is
  3. * below it, and what a call through it can land on".
  4. *
  5. * Three surfaces ask that question. The viewer draws it as a tree above the
  6. * members outline (design spec §3.10). `codegraph_explore` announces it as an
  7. * interface-dispatch boundary ("`execute` → runtime dispatch to **611** types
  8. * implementing `INodeType`"). `codegraph_node` shows the same relations as
  9. * chips. Three derivations would eventually disagree about the ONE number that
  10. * matters — how many implementations a call can reach — and a reader holding
  11. * two of them has no way to tell which is lying. So the walk lives here once,
  12. * and each caller renders it: `src/ui-server/api/node.ts` turns it into
  13. * `WireHierarchy`, `ToolHandler.buildPolymorphicBoundaries` into prose.
  14. *
  15. * Everything here is query-time and read-only. No edge is invented: the tree is
  16. * exactly the `extends`/`implements` edges the graph holds, and the one thing
  17. * that is *derived* — which members override an ancestor's — is derived by name
  18. * within a chain the graph already links, and is labelled as a match rather
  19. * than as an `overrides` edge (nothing in the engine emits one).
  20. *
  21. * ## Why the fan is the interesting direction
  22. *
  23. * Ancestors are a fact about the code you are reading: `class X extends Y` is
  24. * written on line 1. Descendants are a fact you cannot get from the file at
  25. * all — the implementations of an interface live anywhere in the repo, and they
  26. * are precisely what a call through that interface dispatches to. Go makes this
  27. * sharpest: `System` and `Fixed` satisfy `Clock` without either file naming the
  28. * other, and the `implements` edge that links them is synthesized by the
  29. * resolver (`synthesizedBy: 'go-implements'`). So the fan carries its own
  30. * provenance and the caller draws a synthesized hop differently — the same
  31. * honesty rule the Flow strip's dashed connectors follow.
  32. */
  33. import type CodeGraph from '../index';
  34. import type { Edge, EdgeKind, Node, NodeKind } from '../types';
  35. /** The two edge kinds that make a type hierarchy. Nothing else is a subtype. */
  36. export const HIERARCHY_EDGE_KINDS: readonly EdgeKind[] = ['extends', 'implements'];
  37. /**
  38. * Kinds that can sit in a type hierarchy.
  39. *
  40. * `type_alias` is in deliberately — TypeScript's `interface A extends B` and
  41. * Rust's associated types both land here, and an alias with subtypes is a real
  42. * hierarchy however it was spelled. `enum` is in for Java/Kotlin/Swift, where an
  43. * enum implements interfaces.
  44. */
  45. export const HIERARCHY_KINDS: ReadonlySet<NodeKind> = new Set<NodeKind>([
  46. 'class',
  47. 'interface',
  48. 'struct',
  49. 'trait',
  50. 'protocol',
  51. 'enum',
  52. 'type_alias',
  53. 'union',
  54. ]);
  55. /** Member kinds an override can be declared on. */
  56. const OVERRIDABLE_KINDS: ReadonlySet<NodeKind> = new Set<NodeKind>([
  57. 'method',
  58. 'function',
  59. 'property',
  60. 'field',
  61. ]);
  62. /** Levels walked upward. A chain deeper than this is a generated-code artefact. */
  63. export const MAX_ANCESTOR_DEPTH = 8;
  64. /** Levels walked downward. Depth, not breadth — the fan itself is capped separately. */
  65. export const MAX_DESCENDANT_DEPTH = 6;
  66. /**
  67. * Subtypes returned across the whole downward walk.
  68. *
  69. * A framework base class can have thousands, and the caller caps again for
  70. * display; this bound is what stops the *query* from walking them. When it
  71. * bites, {@link TypeHierarchy.bounded} says so — a fan that quietly stopped at
  72. * 400 would read as a complete answer.
  73. */
  74. export const MAX_DESCENDANTS = 400;
  75. /** Ancestors whose members are read when matching overrides. */
  76. const MAX_OVERRIDE_ANCESTORS = 12;
  77. /**
  78. * Implementations at or above which a call through the type cannot be resolved
  79. * statically at all — the same threshold `codegraph_explore` uses before it
  80. * announces an interface-dispatch boundary.
  81. */
  82. export const DISPATCH_MIN_IMPLEMENTERS = 8;
  83. // =============================================================================
  84. // Shapes
  85. // =============================================================================
  86. /** How a subtype is tied to the type above it. */
  87. export type HierarchyRelation = 'extends' | 'implements';
  88. /** One type in the tree, and the single edge that puts it there. */
  89. export interface HierarchyEntry {
  90. node: Node;
  91. /** Steps from the focus. 1 = declared directly on the focus (either way). */
  92. depth: number;
  93. /**
  94. * The entry one step NEARER the focus — the row this one hangs off when the
  95. * tree is drawn. The focus's own id for a depth-1 entry.
  96. */
  97. parentId: string;
  98. relation: HierarchyRelation;
  99. /** The edge itself, always oriented subtype → supertype as the code declares it. */
  100. edge: Edge;
  101. /**
  102. * The edge was synthesized rather than parsed — Go's implicit interface
  103. * satisfaction, a framework registry. Drawn dashed, with its wiring site.
  104. */
  105. synthesized: boolean;
  106. /** Direct subtypes this entry has that are NOT in the returned set. */
  107. hiddenSubtypes: number;
  108. }
  109. /** A member of the focus that redeclares a member of one of its ancestors. */
  110. export interface OverrideMatch {
  111. /** The member on the focus. */
  112. memberId: string;
  113. /** The member it redeclares. */
  114. baseId: string;
  115. /** The ancestor type that declares {@link baseId}. */
  116. baseTypeId: string;
  117. baseTypeName: string;
  118. /** How the focus reaches that ancestor — `implements` reads as "satisfies". */
  119. relation: HierarchyRelation;
  120. }
  121. /** What is above a type, what is below it, and what a call through it reaches. */
  122. export interface TypeHierarchy {
  123. focus: Node;
  124. /** Supertypes, nearest first. Ordered so the focus's own parents lead. */
  125. ancestors: HierarchyEntry[];
  126. /** Subtypes, breadth-first, so depth 1 is complete before depth 2 begins. */
  127. descendants: HierarchyEntry[];
  128. /** True number of DIRECT subtypes, whatever `descendants` was capped to. */
  129. directSubtypes: number;
  130. /** Of {@link directSubtypes}, the ones tied by `implements`. */
  131. directImplementers: number;
  132. /**
  133. * The downward walk hit {@link MAX_DESCENDANTS} or {@link MAX_DESCENDANT_DEPTH}
  134. * — subtypes exist that are not in `descendants`.
  135. */
  136. bounded: boolean;
  137. /**
  138. * A call through this type dispatches at runtime rather than to one target.
  139. * `directImplementers >= DISPATCH_MIN_IMPLEMENTERS`.
  140. */
  141. polymorphic: boolean;
  142. /** Members of the focus that redeclare an ancestor's, keyed by member id. */
  143. overrides: Map<string, OverrideMatch>;
  144. }
  145. // =============================================================================
  146. // The walk
  147. // =============================================================================
  148. /**
  149. * Whether a node could have a hierarchy at all.
  150. *
  151. * Cheap enough to gate on before doing any work: a function never has one, and
  152. * the overwhelming majority of symbols a reader opens are functions.
  153. */
  154. export function canHaveHierarchy(node: Node): boolean {
  155. return HIERARCHY_KINDS.has(node.kind);
  156. }
  157. /**
  158. * The whole hierarchy of one type.
  159. *
  160. * Cost is one query per level in each direction plus one batched member read,
  161. * never one per node — a base class with 400 subtypes is 2–3 queries, not 400.
  162. *
  163. * Returns `null` when the node cannot have a hierarchy or has no
  164. * `extends`/`implements` edge in either direction, so a caller can gate on the
  165. * return value rather than on the emptiness of three lists.
  166. */
  167. export function buildTypeHierarchy(
  168. cg: CodeGraph,
  169. focus: Node,
  170. options: { overrides?: boolean } = {}
  171. ): TypeHierarchy | null {
  172. if (!canHaveHierarchy(focus)) return null;
  173. const ancestors = walkAncestors(cg, focus);
  174. const down = walkDescendants(cg, focus);
  175. if (ancestors.length === 0 && down.entries.length === 0) return null;
  176. return {
  177. focus,
  178. ancestors,
  179. descendants: down.entries,
  180. directSubtypes: down.directTotal,
  181. directImplementers: down.directImplementers,
  182. bounded: down.bounded,
  183. polymorphic: down.directImplementers >= DISPATCH_MIN_IMPLEMENTERS,
  184. overrides: options.overrides === false ? new Map() : matchOverrides(cg, focus, ancestors),
  185. };
  186. }
  187. /**
  188. * Walk up. Multiple direct parents are normal (a class extends one and
  189. * implements three), so this is a BFS rather than a chain, ordered nearest
  190. * first and — within a level — `extends` before `implements`, because the one
  191. * that carries the implementation is the one a reader wants adjacent.
  192. */
  193. function walkAncestors(cg: CodeGraph, focus: Node): HierarchyEntry[] {
  194. const out: HierarchyEntry[] = [];
  195. const seen = new Set<string>([focus.id]);
  196. let frontier = [focus.id];
  197. for (let depth = 1; depth <= MAX_ANCESTOR_DEPTH && frontier.length > 0; depth++) {
  198. const edges = hierarchyEdges(cg, frontier, 'up');
  199. if (edges.length === 0) break;
  200. const nodes = cg.getNodesByIds(edges.map((e) => e.target));
  201. const level: HierarchyEntry[] = [];
  202. for (const edge of edges) {
  203. const node = nodes.get(edge.target);
  204. if (!node || seen.has(node.id)) continue;
  205. seen.add(node.id);
  206. level.push(toEntry(node, depth, edge.source, edge));
  207. }
  208. sortLevel(level);
  209. out.push(...level);
  210. frontier = level.map((e) => e.node.id);
  211. }
  212. return out;
  213. }
  214. /**
  215. * Walk down — the fan. Breadth-first so the cap always trims the deepest,
  216. * least-relevant end: a reader looking at an interface wants its direct
  217. * implementations complete before a subclass of a subclass appears at all.
  218. */
  219. function walkDescendants(cg: CodeGraph, focus: Node): {
  220. entries: HierarchyEntry[];
  221. directTotal: number;
  222. directImplementers: number;
  223. bounded: boolean;
  224. } {
  225. const entries: HierarchyEntry[] = [];
  226. const byId = new Map<string, HierarchyEntry>();
  227. const seen = new Set<string>([focus.id]);
  228. let frontier = [focus.id];
  229. let directTotal = 0;
  230. let directImplementers = 0;
  231. let bounded = false;
  232. for (let depth = 1; depth <= MAX_DESCENDANT_DEPTH && frontier.length > 0; depth++) {
  233. const edges = hierarchyEdges(cg, frontier, 'down');
  234. if (edges.length === 0) break;
  235. const nodes = cg.getNodesByIds(edges.map((e) => e.source));
  236. // One row per subtype, not per edge: a class tied to its supertype by both
  237. // a parsed `extends` and a synthesized `implements` is ONE implementation.
  238. // `extends` wins the relation because it is the one written in the file.
  239. const level: HierarchyEntry[] = [];
  240. const overflow = new Map<string, number>();
  241. const levelSeen = new Set<string>();
  242. for (const edge of edges) {
  243. const node = nodes.get(edge.source);
  244. if (!node || seen.has(node.id)) continue;
  245. const existing = levelSeen.has(node.id)
  246. ? level.find((e) => e.node.id === node.id)
  247. : undefined;
  248. if (existing) {
  249. if (existing.relation === 'implements' && edge.kind === 'extends') {
  250. existing.relation = 'extends';
  251. existing.edge = edge;
  252. existing.synthesized = edge.provenance === 'heuristic';
  253. }
  254. continue;
  255. }
  256. if (depth === 1) {
  257. directTotal++;
  258. if (edge.kind === 'implements') directImplementers++;
  259. }
  260. if (entries.length + level.length >= MAX_DESCENDANTS) {
  261. // Stop materialising rows, but keep counting depth 1 so
  262. // `directSubtypes` stays the true number.
  263. bounded = true;
  264. overflow.set(edge.target, (overflow.get(edge.target) ?? 0) + 1);
  265. levelSeen.add(node.id);
  266. continue;
  267. }
  268. levelSeen.add(node.id);
  269. level.push(toEntry(node, depth, edge.target, edge));
  270. }
  271. for (const entry of level) seen.add(entry.node.id);
  272. sortLevel(level);
  273. for (const entry of level) {
  274. entries.push(entry);
  275. byId.set(entry.node.id, entry);
  276. }
  277. for (const [parentId, count] of overflow) {
  278. const parent = byId.get(parentId);
  279. if (parent) parent.hiddenSubtypes += count;
  280. }
  281. if (bounded) break;
  282. frontier = level.map((e) => e.node.id);
  283. if (depth === MAX_DESCENDANT_DEPTH && frontier.length > 0) {
  284. // A level exists below the one we are about to stop at. Say so rather
  285. // than letting the deepest row read as a leaf.
  286. for (const edge of hierarchyEdges(cg, frontier, 'down')) {
  287. if (seen.has(edge.source)) continue;
  288. bounded = true;
  289. const parent = byId.get(edge.target);
  290. if (parent) parent.hiddenSubtypes++;
  291. }
  292. }
  293. }
  294. return { entries, directTotal, directImplementers, bounded };
  295. }
  296. /** One batched edge read per level, filtered to the two hierarchy kinds. */
  297. function hierarchyEdges(cg: CodeGraph, ids: readonly string[], direction: 'up' | 'down'): Edge[] {
  298. const kinds = [...HIERARCHY_EDGE_KINDS];
  299. try {
  300. const edges =
  301. direction === 'up'
  302. ? cg.getOutgoingEdgesFrom(ids, kinds)
  303. : cg.getIncomingEdgesTo(ids, kinds);
  304. // Belt and braces: the kind filter is applied in SQL, but a caller reading
  305. // `entry.relation` must never see a third value.
  306. return edges.filter((e) => e.kind === 'extends' || e.kind === 'implements');
  307. } catch {
  308. return [];
  309. }
  310. }
  311. function toEntry(node: Node, depth: number, parentId: string, edge: Edge): HierarchyEntry {
  312. return {
  313. node,
  314. depth,
  315. parentId,
  316. relation: edge.kind === 'implements' ? 'implements' : 'extends',
  317. edge,
  318. synthesized: edge.provenance === 'heuristic',
  319. hiddenSubtypes: 0,
  320. };
  321. }
  322. /**
  323. * Deterministic order within one level: `extends` first, then by name, then by
  324. * file. Never by insertion — two runs against the same index must draw the same
  325. * tree, and SQLite's row order is not a promise.
  326. */
  327. function sortLevel(level: HierarchyEntry[]): void {
  328. level.sort(
  329. (a, b) =>
  330. (a.relation === b.relation ? 0 : a.relation === 'extends' ? -1 : 1) ||
  331. a.node.name.localeCompare(b.node.name) ||
  332. a.node.filePath.localeCompare(b.node.filePath) ||
  333. a.node.startLine - b.node.startLine
  334. );
  335. }
  336. // =============================================================================
  337. // Overrides
  338. // =============================================================================
  339. /**
  340. * Which of the focus's members redeclare an ancestor's.
  341. *
  342. * Nothing in the engine emits an `overrides` edge (the kind exists in the
  343. * schema and no extractor writes one), so this is a NAME match — but a name
  344. * match inside a chain the graph already established, which is exactly what
  345. * every language's dispatch rule is. It is reported as a match against a named
  346. * base member the reader can open, never as an edge, and it is deliberately
  347. * blind to signatures: an overload set would need type resolution the graph
  348. * does not have, and claiming "overrides" for the wrong overload is worse than
  349. * saying which type also declares this name.
  350. *
  351. * Two batched queries total, whatever the ancestor count.
  352. */
  353. function matchOverrides(
  354. cg: CodeGraph,
  355. focus: Node,
  356. ancestors: readonly HierarchyEntry[]
  357. ): Map<string, OverrideMatch> {
  358. const result = new Map<string, OverrideMatch>();
  359. if (ancestors.length === 0) return result;
  360. const ownMembers = membersOf(cg, [focus.id]);
  361. if (ownMembers.length === 0) return result;
  362. // Nearest ancestors win: a method redeclared two levels up is still reported
  363. // against the type the reader would actually look in.
  364. const chain = ancestors.slice(0, MAX_OVERRIDE_ANCESTORS);
  365. const baseMembers = membersOf(
  366. cg,
  367. chain.map((a) => a.node.id)
  368. );
  369. if (baseMembers.length === 0) return result;
  370. const ancestorById = new Map(chain.map((a) => [a.node.id, a] as const));
  371. const byName = new Map<string, { member: Node; ownerId: string }>();
  372. // `chain` is nearest-first and `membersOf` preserves the order of the ids it
  373. // was given, so the first entry for a name is the nearest declaration.
  374. for (const { member, ownerId } of baseMembers) {
  375. if (!byName.has(member.name)) byName.set(member.name, { member, ownerId });
  376. }
  377. for (const { member } of ownMembers) {
  378. if (!OVERRIDABLE_KINDS.has(member.kind)) continue;
  379. const base = byName.get(member.name);
  380. if (!base || base.member.id === member.id) continue;
  381. const owner = ancestorById.get(base.ownerId);
  382. if (!owner) continue;
  383. result.set(member.id, {
  384. memberId: member.id,
  385. baseId: base.member.id,
  386. baseTypeId: owner.node.id,
  387. baseTypeName: owner.node.name,
  388. relation: owner.relation,
  389. });
  390. }
  391. return result;
  392. }
  393. /** Direct `contains` children of the given containers, in the containers' order. */
  394. function membersOf(
  395. cg: CodeGraph,
  396. containerIds: readonly string[]
  397. ): Array<{ member: Node; ownerId: string }> {
  398. if (containerIds.length === 0) return [];
  399. let edges: Edge[];
  400. try {
  401. edges = cg.getOutgoingEdgesFrom(containerIds, ['contains']);
  402. } catch {
  403. return [];
  404. }
  405. if (edges.length === 0) return [];
  406. const nodes = cg.getNodesByIds(edges.map((e) => e.target));
  407. const rank = new Map(containerIds.map((id, i) => [id, i] as const));
  408. const out: Array<{ member: Node; ownerId: string }> = [];
  409. for (const edge of edges) {
  410. const member = nodes.get(edge.target);
  411. if (member) out.push({ member, ownerId: edge.source });
  412. }
  413. out.sort(
  414. (a, b) =>
  415. (rank.get(a.ownerId) ?? 0) - (rank.get(b.ownerId) ?? 0) ||
  416. a.member.startLine - b.member.startLine
  417. );
  418. return out;
  419. }
  420. // =============================================================================
  421. // The fan, on its own
  422. // =============================================================================
  423. /**
  424. * How many distinct types extend or implement this one — the number
  425. * `codegraph_explore` prints when it announces an interface dispatch and the
  426. * number the viewer's fan draws.
  427. *
  428. * DISTINCT types, not edges: a class tied to a supertype by both an `extends`
  429. * and a synthesized `implements` edge is one implementation, and a count that
  430. * disagrees with the length of the list beside it is the bug this function
  431. * exists to prevent.
  432. */
  433. export function countImplementers(cg: CodeGraph, typeId: string): number {
  434. try {
  435. const edges = cg.getIncomingEdgesTo([typeId], [...HIERARCHY_EDGE_KINDS]);
  436. return new Set(edges.map((e) => e.source)).size;
  437. } catch {
  438. return 0;
  439. }
  440. }