dsh-plugin-codegraph-tree-sitter 0.1.0 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/extract.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * real workspace are of exactly this shape.
8
8
  * @module dsh-plugin-codegraph-tree-sitter/extract
9
9
  */
10
+ import { DECLARATOR_NAME_FIELD, FIRST_CHILD_NAME_FIELD, PHP_ELEMENT_NAME_FIELD, SELF_NAME_FIELD } from "./languages.js";
10
11
  /** A node's named children, with the `null` slots `web-tree-sitter` reserves for missing nodes dropped. */
11
12
  function namedChildren(node) {
12
13
  return node.namedChildren.filter(child => child !== null);
@@ -36,16 +37,219 @@ function hasKeywordChild(node, keyword) {
36
37
  }
37
38
  return false;
38
39
  }
40
+ /**
41
+ * Java's `modifiers` node for a declaration, wherever it sits. Unlike every other grammar this
42
+ * package extracts from, Java never places a modifier keyword (`public`, `static`, `final`, …) as a
43
+ * direct child of the declaration node itself — it wraps them all in one named `modifiers` child. A
44
+ * captured `variable_declarator` (the `field`/`variable` rules in `languages.ts`) carries no
45
+ * `modifiers` of its own at all; its modifiers sit one level up, on the enclosing
46
+ * `field_declaration`/`local_variable_declaration` this function falls back to. Verified against a
47
+ * real parse, not guessed.
48
+ */
49
+ function javaModifiersNode(node) {
50
+ const own = namedChildren(node).find(child => child.type === 'modifiers');
51
+ if (own !== undefined)
52
+ return own;
53
+ // Every node this is called on is a definition the walk captured below the tree's root, so it
54
+ // always has a parent; the null case only satisfies `.parent`'s general `SyntaxNode | null` type.
55
+ /* v8 ignore next */
56
+ if (node.parent === null)
57
+ return undefined;
58
+ return namedChildren(node.parent).find(child => child.type === 'modifiers');
59
+ }
60
+ /** Whether a Java declaration carries `keyword` among its modifiers — see {@link javaModifiersNode}. */
61
+ function javaHasModifier(node, keyword) {
62
+ const modifiers = javaModifiersNode(node);
63
+ return modifiers !== undefined && hasKeywordChild(modifiers, keyword);
64
+ }
65
+ /**
66
+ * Whether a C/C++ declaration carries `keyword` (`static`, `virtual`, …) among its own named children.
67
+ * Unlike every other grammar this package extracts from, C/C++ wraps a storage-class keyword like
68
+ * `static` in its own named `storage_class_specifier` node rather than leaving it as a bare anonymous
69
+ * token on the declaration itself — `hasKeywordChild` alone would never see it. `virtual`, by contrast,
70
+ * is already its own bare named node with no wrapper, so checking each named child's own text (not just
71
+ * `storage_class_specifier`'s) covers both in one pass. Verified against a real parse, not guessed.
72
+ */
73
+ function cHasStorageClassKeyword(node, keyword) {
74
+ return namedChildren(node).some(child => child.text === keyword);
75
+ }
76
+ /**
77
+ * Whether a C# declaration carries `keyword` (`public`, `static`, …) among its own named children. C#
78
+ * wraps each modifier keyword in its own flat, individually-named `modifier` node directly on the
79
+ * declaration — unlike Java's single wrapping `modifiers` collection (see {@link javaModifiersNode}) or
80
+ * C/C++'s `storage_class_specifier`, there is no group to look inside; each keyword is its own sibling.
81
+ * Verified against a real parse, not guessed.
82
+ */
83
+ function csharpHasModifier(node, keyword) {
84
+ return namedChildren(node).some(child => child.type === 'modifier' && child.text === keyword);
85
+ }
86
+ /** C/C++ node types wrapping another `declarator` field one level further down — see {@link declaratorName}. */
87
+ const DECLARATOR_WRAPPER_TYPES = new Set([
88
+ 'pointer_declarator',
89
+ 'init_declarator',
90
+ 'array_declarator',
91
+ 'reference_declarator',
92
+ ]);
93
+ /**
94
+ * `parenthesized_declarator`'s inner declarator — unlike every wrapper in {@link DECLARATOR_WRAPPER_TYPES}
95
+ * plus `function_declarator`, its sole child is purely positional, bound to no field at all
96
+ * (`int (*fp)(void);`'s `parenthesized_declarator` wraps a `pointer_declarator` with no `declarator`
97
+ * field to find it through). Verified against a real parse, not guessed.
98
+ */
99
+ function parenthesizedDeclaratorInner(node) {
100
+ // The grammar never produces an empty `parenthesized_declarator` (`()` alone does not parse as one);
101
+ // the fallback only satisfies `namedChildren`'s general array-access return type.
102
+ /* v8 ignore next */
103
+ return namedChildren(node)[0] ?? null;
104
+ }
105
+ /**
106
+ * The terminal name behind a C/C++ declaration's `declarator` field, however many wrapper layers deep —
107
+ * `int *make(int a)`'s `function_definition.declarator` is a `pointer_declarator` wrapping a
108
+ * `function_declarator` wrapping the `identifier` "make"; a plain `int g = 1;` global's `declaration.declarator`
109
+ * is an `init_declarator` wrapping the `identifier` "g" directly; `int (*fp)(void);`'s is a
110
+ * `function_declarator` wrapping a `parenthesized_declarator` wrapping a `pointer_declarator` wrapping
111
+ * the `identifier` "fp". Every wrapper in {@link DECLARATOR_WRAPPER_TYPES}, plus `function_declarator`,
112
+ * exposes the same `declarator` field down to the next layer; `parenthesized_declarator` alone is
113
+ * unwrapped through {@link parenthesizedDeclaratorInner} instead — see there. Stops at an
114
+ * `identifier`/`field_identifier` (the common case), or at a C++ `destructor_name` (`~C`) kept whole
115
+ * rather than unwrapped to its inner `identifier` — unwrapping would make a destructor's name collide
116
+ * with its constructor's plain class name. A shape this doesn't recognize (a C++ operator-overload
117
+ * declarator's `operator_name`, or a function-pointer typedef) returns `undefined` rather than guess a
118
+ * name from it, the same "don't guess" precedent this file already follows for shapes it does not fully
119
+ * resolve.
120
+ * @param node - the declaration node (`function_definition`, `declaration`, `field_declaration`, …).
121
+ * @returns the terminal name node, or `undefined`.
122
+ */
123
+ function declaratorName(node) {
124
+ // `declaration`/`field_declaration` allow a comma-separated multi-declarator list (`int a, b;`) that
125
+ // shares the same repeated `declarator` field on one node — the same ambiguity Go's `const a, b = 1, 2`
126
+ // has on one `const_spec`; `childForFieldName` below would otherwise silently return only the first.
127
+ // `function_definition`'s `declarator` is never repeated, so this is a no-op there.
128
+ if (soleNamedField(node, 'declarator') === undefined)
129
+ return undefined;
130
+ let current = node.childForFieldName('declarator');
131
+ while (current !== null) {
132
+ if (current.type === 'identifier' || current.type === 'field_identifier' || current.type === 'destructor_name')
133
+ return current;
134
+ if (current.type === 'parenthesized_declarator') {
135
+ current = parenthesizedDeclaratorInner(current);
136
+ continue;
137
+ }
138
+ if (current.type !== 'function_declarator' && !DECLARATOR_WRAPPER_TYPES.has(current.type))
139
+ return undefined;
140
+ current = current.childForFieldName('declarator');
141
+ }
142
+ // Every wrapper type reaching this point (`function_declarator` or a `DECLARATOR_WRAPPER_TYPES`
143
+ // member) is required by the grammar to carry its own nested `declarator`; the loop always returns
144
+ // from inside before `current` could become null.
145
+ /* v8 ignore next */
146
+ return undefined;
147
+ }
148
+ /**
149
+ * The matched node's first named child, when it is an `identifier` — see {@link FIRST_CHILD_NAME_FIELD}.
150
+ * Rejects anything else (e.g. a tuple-deconstructing pattern) rather than name a declaration after a
151
+ * shape this package does not attempt to resolve, the same "don't guess" precedent every other
152
+ * unresolved shape in this file already follows.
153
+ */
154
+ function firstChildName(node) {
155
+ const first = namedChildren(node)[0];
156
+ return first?.type === 'identifier' ? first : undefined;
157
+ }
158
+ /**
159
+ * PHP's bare `name` node — either directly (`const_element`'s first named child) or one level deeper
160
+ * inside a `variable_name` wrapper (`property_element`'s and a closure-binding `assignment_expression`'s
161
+ * first named child) — see {@link PHP_ELEMENT_NAME_FIELD}. Rejects anything else (a destructuring
162
+ * `list_literal` target, a member/subscript assignment target) rather than name a declaration after a
163
+ * shape this package does not attempt to resolve, the same "don't guess" precedent every other
164
+ * unresolved shape in this file already follows. Verified against a real parse, not guessed.
165
+ */
166
+ function phpElementName(node) {
167
+ const first = namedChildren(node)[0];
168
+ // Every node this is called on (`const_element`, `property_element`, a value-guarded
169
+ // `assignment_expression`) is required by the grammar to carry at least one named child; the
170
+ // undefined case only satisfies `namedChildren`'s general array-access return type.
171
+ /* v8 ignore next */
172
+ if (first === undefined)
173
+ return undefined;
174
+ if (first.type === 'name')
175
+ return first;
176
+ if (first.type !== 'variable_name')
177
+ return undefined;
178
+ const inner = namedChildren(first)[0];
179
+ // The grammar's `variable_name` rule always wraps exactly one bare `name`; the fallback only satisfies
180
+ // `namedChildren`'s general array-access return type.
181
+ /* v8 ignore next */
182
+ return inner?.type === 'name' ? inner : undefined;
183
+ }
184
+ /**
185
+ * PHP's own modifier host for `keyword`: a class/method/interface/trait/enum declaration carries its
186
+ * `visibility_modifier`/`static_modifier` directly, but a captured `const_element`/`property_element`
187
+ * carries neither — like Java's `variable_declarator` (see {@link javaModifiersNode}), its modifiers sit
188
+ * one level up, on the enclosing `const_declaration`/`property_declaration` this falls back to.
189
+ */
190
+ function phpModifierHost(node) {
191
+ // Every `const_element`/`property_element` the walk visits sits inside a `const_declaration`/
192
+ // `property_declaration`, never at the tree's root; the `?? node` fallback only satisfies `.parent`'s
193
+ // general `SyntaxNode | null` type.
194
+ /* v8 ignore next */
195
+ if (node.type === 'const_element' || node.type === 'property_element')
196
+ return node.parent ?? node;
197
+ return node;
198
+ }
199
+ /** Whether a PHP declaration carries an explicit `public` (or other) `visibility_modifier` — see
200
+ * {@link phpModifierHost}. Verified against a real parse, not guessed. */
201
+ function phpHasVisibility(node, keyword) {
202
+ return namedChildren(phpModifierHost(node)).some(child => child.type === 'visibility_modifier' && child.text === keyword);
203
+ }
204
+ /** Whether a PHP declaration carries a `static_modifier` — see {@link phpModifierHost}. Verified against
205
+ * a real parse, not guessed. */
206
+ function phpHasStaticModifier(node) {
207
+ return namedChildren(phpModifierHost(node)).some(child => child.type === 'static_modifier');
208
+ }
209
+ /**
210
+ * Whether a Rust declaration carries a bare `pub` `visibility_modifier` among its own named children.
211
+ * Every Rust item this package captures places its visibility keyword there directly, unlike Java's
212
+ * wrapping `modifiers` node or C's `storage_class_specifier` — but `pub(crate)`/`pub(super)`/`pub(self)`
213
+ * restrict visibility to inside the crate rather than truly exporting it, so only the bare, unrestricted
214
+ * keyword counts, mirroring Java's/C#'s explicit-`public`-only convention. Verified against a real parse,
215
+ * not guessed.
216
+ */
217
+ function rustIsPublic(node) {
218
+ return namedChildren(node).some(child => child.type === 'visibility_modifier' && child.text === 'pub');
219
+ }
220
+ /**
221
+ * The lone named child bound to `field`, or `undefined` when zero or more than one is bound. A field
222
+ * ordinarily binds exactly one child (a declaration's name); Go's `const a, b = 1, 2` is the exception
223
+ * — both identifiers share the `name` field on one `const_spec` node — and this package extracts a
224
+ * single declared name per definition, never a silently partial pick from an ambiguous group.
225
+ */
226
+ function soleNamedField(node, field) {
227
+ const named = node.childrenForFieldName(field).filter((child) => child !== null && child.isNamed);
228
+ return named.length === 1 ? named[0] : undefined;
229
+ }
39
230
  /** The definition rule matching `node`, or `undefined` when it introduces no declaration. */
40
231
  function matchDefinition(node, definitions) {
41
232
  for (const rule of definitions) {
42
233
  if (node.type !== rule.nodeType)
43
234
  continue;
235
+ if (rule.parentType !== undefined && node.parent?.type !== rule.parentType)
236
+ continue;
237
+ if (rule.grandparentType !== undefined && node.parent?.parent?.type !== rule.grandparentType)
238
+ continue;
44
239
  if (rule.value !== undefined) {
45
240
  const value = node.childForFieldName(rule.value.field);
46
241
  if (value === null || !rule.value.types.includes(value.type))
47
242
  continue;
48
243
  }
244
+ const nameNode = rule.nameField === SELF_NAME_FIELD ? node
245
+ : rule.nameField === DECLARATOR_NAME_FIELD ? declaratorName(node)
246
+ : rule.nameField === FIRST_CHILD_NAME_FIELD ? firstChildName(node)
247
+ : rule.nameField === PHP_ELEMENT_NAME_FIELD ? phpElementName(node)
248
+ : soleNamedField(node, rule.nameField);
249
+ if (nameNode === undefined)
250
+ continue;
251
+ if (rule.nameNodeTypes !== undefined && !rule.nameNodeTypes.includes(nameNode.type))
252
+ continue;
49
253
  return rule;
50
254
  }
51
255
  return undefined;
@@ -60,15 +264,90 @@ function matchDefinition(node, definitions) {
60
264
  * computed or parenthesized callee, for instance).
61
265
  */
62
266
  function calleeName(callee) {
267
+ // Rust's turbofish call (`collect::<Vec<_>>()`) wraps the real callee expression in its own
268
+ // `generic_function` node, pairing a `function` field with a sibling `type_arguments` field for the
269
+ // explicit generics — unwrap to the inner expression and resolve it the same way as any other call.
270
+ // Verified against a real parse, not guessed.
271
+ if (callee.type === 'generic_function') {
272
+ const inner = callee.childForFieldName('function');
273
+ return inner === null ? undefined : calleeName(inner);
274
+ }
63
275
  if (callee.type === 'identifier')
64
276
  return callee.text;
277
+ // PHP's bare-name node type — the resolved `callFunctionFieldByType` field value for all three of its
278
+ // call shapes (`function_call_expression`'s simple case, `member_call_expression`'s and
279
+ // `scoped_call_expression`'s method name) is this node type directly, never wrapped in a further
280
+ // field. Verified against a real parse, not guessed.
281
+ if (callee.type === 'name')
282
+ return callee.text;
283
+ // PHP's namespaced free-function call (`Foo\bar()`) — `qualified_name` binds neither its namespace
284
+ // prefix nor its final segment to a field of its own (unlike C++'s `qualified_identifier`, below); the
285
+ // final segment is always its last named child. Verified against a real parse, not guessed.
286
+ if (callee.type === 'qualified_name') {
287
+ const last = namedChildren(callee).at(-1);
288
+ // The grammar's `qualified_name` rule always wraps at least one final segment of this type; the
289
+ // fallback only satisfies `Array.prototype.at`'s general return type.
290
+ /* v8 ignore next */
291
+ return last?.type === 'name' ? last.text : undefined;
292
+ }
293
+ // `name` covers C++'s `qualified_identifier` (`ns::func`), C#'s `member_access_expression`
294
+ // (`obj.Method()`), and Rust's `scoped_identifier` (`Type::method()`, `std::mem::swap()`) — no other
295
+ // call-callee shape in this file's language tables binds a field called `name` for anything else,
296
+ // and Rust's `field_expression` (`obj.method()`) binds `field` instead, already covered below.
297
+ // Verified against a real parse, not guessed.
65
298
  const property = callee.childForFieldName('property')
66
299
  ?? callee.childForFieldName('field')
67
- ?? callee.childForFieldName('attribute');
300
+ ?? callee.childForFieldName('attribute')
301
+ ?? callee.childForFieldName('name');
68
302
  if (property !== null && property.type !== 'computed_property_name')
69
303
  return property.text;
70
304
  return undefined;
71
305
  }
306
+ /** Seam language labels using the ECMAScript-family grammars, for CommonJS `require`/export detection. */
307
+ const ECMASCRIPT_LANGUAGES = new Set(['typescript', 'tsx', 'javascript', 'jsx']);
308
+ /** Shared empty set for a non-ECMAScript file, which never has CommonJS export assignments to find. */
309
+ const EMPTY_NAME_SET = new Set();
310
+ /**
311
+ * A CommonJS `require('./foo')` call recognized as an import binding: `const foo = require('./foo')`
312
+ * binds `foo`; a bare `require('./foo')` statement imports for its side effect only. Any other
313
+ * position (a sub-expression, immediate member access on the call result) is not a binding this
314
+ * package attempts to name — the same "don't guess a name" precedent `ecmascriptImports`/
315
+ * `pythonBinding` already follow for shapes they do not fully resolve. Without this, CommonJS code
316
+ * (still common outside pure-ESM projects) parses `require` as an ordinary, always-unresolved call
317
+ * and the workspace gets no `imports` edge for it at all.
318
+ * @param node - a `call_expression` node.
319
+ * @returns the import binding, or `undefined` when `node` is not a bare top-level `require(...)` call.
320
+ */
321
+ function commonJsRequireImport(node) {
322
+ const callee = node.childForFieldName('function');
323
+ // A call node's `function` field is required by the grammar; the null case only satisfies
324
+ // `childForFieldName`'s general return type.
325
+ /* v8 ignore next */
326
+ if (callee === null)
327
+ return undefined;
328
+ if (callee.type !== 'identifier' || callee.text !== 'require')
329
+ return undefined;
330
+ const argsNode = node.childForFieldName('arguments');
331
+ // Likewise required by the grammar, present (empty) even for a zero-argument call.
332
+ /* v8 ignore next */
333
+ if (argsNode === null)
334
+ return undefined;
335
+ const args = namedChildren(argsNode);
336
+ const specifierNode = args.length === 1 ? args[0] : undefined;
337
+ if (specifierNode?.type !== 'string')
338
+ return undefined;
339
+ const specifier = namedChildren(specifierNode)[0]?.text ?? specifierNode.text.slice(1, -1);
340
+ const parent = node.parent;
341
+ if (parent?.type === 'variable_declarator') {
342
+ const name = parent.childForFieldName('name');
343
+ if (name?.type !== 'identifier')
344
+ return undefined;
345
+ return { localName: name.text, importedName: '*', specifier };
346
+ }
347
+ if (parent?.type === 'expression_statement')
348
+ return { localName: '', importedName: '*', specifier };
349
+ return undefined;
350
+ }
72
351
  /** ECMAScript-family import extraction: `import_statement` with a `import_clause` and a `source`. */
73
352
  function ecmascriptImports(node) {
74
353
  // `source` is required by the grammar; the null case only satisfies `childForFieldName`'s general
@@ -116,6 +395,60 @@ function ecmascriptImports(node) {
116
395
  }
117
396
  return imports;
118
397
  }
398
+ /**
399
+ * One Python `decorator` node's name — a bare `@staticmethod` names its own `identifier`; `@app.route`
400
+ * names its dotted `attribute` verbatim (not just the rightmost segment, unlike `calleeName`'s call-site
401
+ * ambiguity — a decorator name is metadata, not something this package resolves, so there is no reason
402
+ * to throw away the qualifying prefix); `@app.route('/x')` first unwraps the `call` to its `function`
403
+ * field, then applies the same rule. Any other shape (e.g. a subscript, `@decorators[0]`) is not named —
404
+ * the "don't guess" precedent this file already follows elsewhere.
405
+ * @param decorator - the `decorator` node.
406
+ * @returns the decorator's name, or `undefined` when its expression is not identifier/attribute-shaped.
407
+ */
408
+ function pythonDecoratorName(decorator) {
409
+ let expr = namedChildren(decorator)[0];
410
+ // A decorator's expression is required by the grammar (`@` alone does not parse); the undefined case
411
+ // only satisfies `namedChildren`'s general array-access return type.
412
+ /* v8 ignore next */
413
+ if (expr === undefined)
414
+ return undefined;
415
+ if (expr.type === 'call') {
416
+ // A call node's `function` field is required by the grammar; the null case only satisfies
417
+ // `childForFieldName`'s general return type.
418
+ const callee = expr.childForFieldName('function');
419
+ /* v8 ignore next */
420
+ if (callee === null)
421
+ return undefined;
422
+ expr = callee;
423
+ }
424
+ if (expr.type === 'identifier' || expr.type === 'attribute')
425
+ return expr.text;
426
+ return undefined;
427
+ }
428
+ /**
429
+ * Every decorator name applied to a Python `function_definition`/`class_definition`, from its enclosing
430
+ * `decorated_definition`'s `decorator` children (a decorated declaration is wrapped one level up by the
431
+ * grammar, not marked on the declaration node itself — verified against a real parse, not guessed).
432
+ * Empty when the language is not Python or the declaration is undecorated.
433
+ * @param node - the `function_definition`/`class_definition` node.
434
+ * @param language - the seam language label the file was parsed as.
435
+ * @returns every decorator name applied, outermost first.
436
+ */
437
+ function pythonDecorators(node, language) {
438
+ if (language !== 'python')
439
+ return [];
440
+ if (node.parent?.type !== 'decorated_definition')
441
+ return [];
442
+ const names = [];
443
+ for (const child of namedChildren(node.parent)) {
444
+ if (child.type !== 'decorator')
445
+ continue;
446
+ const name = pythonDecoratorName(child);
447
+ if (name !== undefined)
448
+ names.push(name);
449
+ }
450
+ return names;
451
+ }
119
452
  /**
120
453
  * One `dotted_name` or `aliased_import` child of a Python import statement, resolved to a binding.
121
454
  * @param child - the `dotted_name` or `aliased_import` node.
@@ -151,10 +484,14 @@ function pythonImports(node) {
151
484
  // by type from an imported symbol written the same way (`from x import y`).
152
485
  .filter(child => !(moduleNode !== null && child.equals(moduleNode)))
153
486
  .filter(child => child.type === 'dotted_name' || child.type === 'aliased_import' || child.type === 'wildcard_import')
154
- .flatMap((child) => {
487
+ .map((child) => {
488
+ // `from x import *` binds no individual symbol this package tracks by name, but the module
489
+ // itself is still imported — recording it (with no `localName`, `importedName: '*'`, matching
490
+ // the namespace-import convention `pythonBinding` already uses) keeps the `imports` edge to
491
+ // `specifier` instead of silently dropping the statement.
155
492
  if (child.type === 'wildcard_import')
156
- return [];
157
- return [pythonBinding(child, true, specifier)];
493
+ return { localName: '', importedName: '*', specifier };
494
+ return pythonBinding(child, true, specifier);
158
495
  });
159
496
  }
160
497
  /** Go import extraction: `import_declaration` wraps one or more `import_spec` nodes, each requiring a `path`. */
@@ -174,6 +511,492 @@ function goImports(node) {
174
511
  return { localName: local, importedName: '*', specifier: raw };
175
512
  });
176
513
  }
514
+ /**
515
+ * Java import extraction: `import_declaration` wraps a `scoped_identifier` (a dotted path,
516
+ * `java.util.List`) or, for a single-segment specifier, a bare `identifier`, plus an optional
517
+ * trailing `asterisk` child for a wildcard import (`import java.util.*;`). The `static` keyword
518
+ * (`import static java.lang.Math.max;`) is an unnamed token the grammar tacks onto the same shape, so
519
+ * no separate handling is needed — the specifier and local name below are extracted identically
520
+ * either way. Verified against a real parse, not guessed.
521
+ * @param node - the `import_declaration` node.
522
+ * @returns the single import binding this statement introduces.
523
+ */
524
+ function javaImports(node) {
525
+ const children = namedChildren(node);
526
+ const path = children.find(child => child.type === 'scoped_identifier' || child.type === 'identifier');
527
+ // `import_declaration` always wraps one of these two node types per the grammar; the undefined case
528
+ // only satisfies `Array.prototype.find`'s general return type.
529
+ /* v8 ignore next */
530
+ if (path === undefined)
531
+ return [];
532
+ const specifier = path.text;
533
+ // A package import binds no individual symbol; resolution never matches on `importedName: '*'`,
534
+ // matching `goImports`'s same precedent for a whole-package/wildcard binding.
535
+ if (children.some(child => child.type === 'asterisk'))
536
+ return [{ localName: '', importedName: '*', specifier }];
537
+ const localName = path.type === 'scoped_identifier' ? fieldText(path, 'name', specifier) : specifier;
538
+ return [{ localName, importedName: '*', specifier }];
539
+ }
540
+ /**
541
+ * C/C++ `#include` extraction. A system include (`#include <stdio.h>`) parses as a `system_lib_string`
542
+ * token holding the whole `<...>` text; a local include (`#include "local.h"`) wraps a `string_literal`
543
+ * around a `string_content` child holding the bare path. Neither binds an individual symbol this
544
+ * package tracks by name — like a Go whole-package import, it is recorded for completeness with no
545
+ * `localName`, matching that same side-effect-only convention. Verified against a real parse, not guessed.
546
+ * @param node - the `preproc_include` node.
547
+ * @returns the single import binding this directive introduces.
548
+ */
549
+ function cIncludeImports(node) {
550
+ const path = namedChildren(node)[0];
551
+ // `preproc_include` always wraps exactly one of `system_lib_string`/`string_literal`/an
552
+ // expanded-macro path per the grammar; the undefined case only satisfies `namedChildren`'s general
553
+ // array-access return type.
554
+ /* v8 ignore next */
555
+ if (path === undefined)
556
+ return [];
557
+ const specifier = path.type === 'system_lib_string' ? path.text.slice(1, -1) : (namedChildren(path)[0]?.text ?? path.text.slice(1, -1));
558
+ return [{ localName: '', importedName: '*', specifier }];
559
+ }
560
+ /**
561
+ * C# `using_directive` extraction. A plain directive (`using System;`) wraps its dotted path
562
+ * (`identifier`/`qualified_name`) directly; an aliased one (`using Alias = System.Text;`) wraps a
563
+ * `name_equals` (the alias) alongside the same dotted-path shape for the target. Neither form binds an
564
+ * individual symbol the way an ECMAScript named import does — a C# `using` imports a whole namespace —
565
+ * so the aliased form's `localName` is the alias itself, matching the namespace-import convention
566
+ * `pythonBinding`/`goImports` already use elsewhere in this file. Verified against a real parse, not
567
+ * guessed.
568
+ * @param node - the `using_directive` node.
569
+ * @returns the single import binding this directive introduces.
570
+ */
571
+ function csharpUsingImports(node) {
572
+ const children = namedChildren(node);
573
+ const alias = children.find(child => child.type === 'name_equals');
574
+ const path = children.find(child => child.type === 'identifier' || child.type === 'qualified_name');
575
+ // `using_directive` always wraps a dotted path per the grammar (a bare `using;` does not parse); the
576
+ // undefined case only satisfies `Array.prototype.find`'s general return type.
577
+ /* v8 ignore next */
578
+ if (path === undefined)
579
+ return [];
580
+ const specifier = path.text;
581
+ // The grammar's `name_equals` rule always wraps exactly one `identifier`; the fallback only satisfies
582
+ // `namedChildren`'s general array-access return type.
583
+ /* v8 ignore next */
584
+ const localName = alias === undefined ? '' : (namedChildren(alias)[0]?.text ?? '');
585
+ return [{ localName, importedName: '*', specifier }];
586
+ }
587
+ /**
588
+ * One `namespace_use_clause`'s bound name and optional alias — `use App\Contracts\Cacheable;` binds a
589
+ * `qualified_name` (its final segment is always the last named child, with no field of its own, matching
590
+ * `calleeName`'s same `qualified_name` handling); a single-segment `use Foo;` binds a bare `name`
591
+ * directly instead. An `as` rename wraps a `namespace_aliasing_clause` sibling. Verified against a real
592
+ * parse, not guessed.
593
+ * @param clause - the `namespace_use_clause` node.
594
+ * @returns the imported name and its local binding, or `undefined` when the clause's target is a shape
595
+ * this package does not name (never observed in practice, but `qualified_name`'s grammar rule allows no
596
+ * other target).
597
+ */
598
+ function phpUseClauseBinding(clause) {
599
+ // `namespace_use_clause` always wraps exactly one of these two node types per the grammar; the
600
+ // fallback (`clause` itself) only satisfies `Array.prototype.find`'s general return type.
601
+ /* v8 ignore next */
602
+ const target = namedChildren(clause).find(child => child.type === 'qualified_name' || child.type === 'name') ?? clause;
603
+ const specifier = target.text;
604
+ // The grammar's `qualified_name` rule always wraps at least one final segment; the fallback only
605
+ // satisfies `Array.prototype.at`'s general return type.
606
+ /* v8 ignore next */
607
+ const importedName = target.type === 'qualified_name' ? (namedChildren(target).at(-1)?.text ?? specifier) : specifier;
608
+ const alias = namedChildren(clause).find(child => child.type === 'namespace_aliasing_clause');
609
+ // The grammar's `namespace_aliasing_clause` rule always wraps exactly one `name`; the fallback only
610
+ // satisfies `namedChildren`'s general array-access return type.
611
+ /* v8 ignore next */
612
+ const localName = alias === undefined ? importedName : (namedChildren(alias)[0]?.text ?? importedName);
613
+ return { importedName, localName, specifier };
614
+ }
615
+ /**
616
+ * PHP `namespace_use_declaration` extraction, covering both its shapes: a comma-separated list of
617
+ * `namespace_use_clause` siblings (`use A\B, C\D as E;`), and a group `use App\{Foo, Bar as Baz};` — the
618
+ * group form's shared prefix sits in a bare `namespace_name` sibling of the `namespace_use_group`, and
619
+ * each `namespace_use_group_clause` inside it repeats the same name-plus-optional-alias shape as a plain
620
+ * clause. The `function`/`const` keyword a `use function …`/`use const …` statement adds is an anonymous
621
+ * token the grammar tacks onto the same shape either way, so no separate handling is needed. Verified
622
+ * against a real parse, not guessed.
623
+ * @param node - the `namespace_use_declaration` node.
624
+ * @returns every import binding this statement introduces.
625
+ */
626
+ function phpImports(node) {
627
+ const group = namedChildren(node).find(child => child.type === 'namespace_use_group');
628
+ if (group !== undefined) {
629
+ // The grammar always pairs a `namespace_use_group` with a preceding `namespace_name` prefix; the
630
+ // undefined case only satisfies `Array.prototype.find`'s general return type.
631
+ const prefix = namedChildren(node).find(child => child.type === 'namespace_name');
632
+ /* v8 ignore next */
633
+ const prefixText = prefix?.text ?? '';
634
+ return namedChildren(group)
635
+ .filter(child => child.type === 'namespace_use_group_clause')
636
+ .map((clause) => {
637
+ // A group clause's own name is always a bare `namespace_name` (itself wrapping a single `name`
638
+ // token, even for a single-segment clause) — never a `qualified_name`, since the shared prefix
639
+ // already carries every segment before it. Verified against a real parse, not guessed.
640
+ const nameNode = namedChildren(clause).find(child => child.type === 'namespace_name');
641
+ // The grammar always binds one per `namespace_use_group_clause`; the undefined case only
642
+ // satisfies `Array.prototype.find`'s general return type.
643
+ /* v8 ignore next */
644
+ const segment = nameNode?.text ?? '';
645
+ const alias = namedChildren(clause).find(child => child.type === 'namespace_aliasing_clause');
646
+ // The grammar's `namespace_aliasing_clause` rule always wraps exactly one `name`; the fallback
647
+ // only satisfies `namedChildren`'s general array-access return type.
648
+ /* v8 ignore next */
649
+ const localName = alias === undefined ? segment : (namedChildren(alias)[0]?.text ?? segment);
650
+ return { localName, importedName: segment, specifier: `${prefixText}\\${segment}` };
651
+ });
652
+ }
653
+ return namedChildren(node)
654
+ .filter(child => child.type === 'namespace_use_clause')
655
+ .map(clause => phpUseClauseBinding(clause));
656
+ }
657
+ /** A path's final `::`-separated segment, or the whole text when it has none. */
658
+ function rustLastSegment(path) {
659
+ const index = path.lastIndexOf('::');
660
+ return index === -1 ? path : path.slice(index + 2);
661
+ }
662
+ /** `prefix::segment`, or just `segment` when there is no enclosing prefix yet. */
663
+ function combineRustPrefix(prefix, segment) {
664
+ return prefix === '' ? segment : `${prefix}::${segment}`;
665
+ }
666
+ /**
667
+ * One Rust `use` clause target, resolved to zero or more import bindings — recursive because a
668
+ * `scoped_use_list`/`use_list` can nest arbitrarily (`use std::{fmt::{self, Display}, io};`).
669
+ * @param node - a `_use_clause` node: `identifier`, `self`, `scoped_identifier`, `use_wildcard`,
670
+ * `use_as_clause`, `scoped_use_list`, or `use_list`. `crate`/`super` never reach this function on their
671
+ * own — the grammar only ever produces them as a `scoped_identifier`'s `path` field, never as a
672
+ * standalone use target.
673
+ * @param prefix - the module path text accumulated from enclosing `scoped_use_list` wrappers, or `''`
674
+ * at the top of a `use_declaration`.
675
+ * @returns every import binding this clause (and any it nests) introduces.
676
+ */
677
+ function rustUseTarget(node, prefix) {
678
+ if (node.type === 'identifier') {
679
+ return [{ localName: node.text, importedName: node.text, specifier: combineRustPrefix(prefix, node.text) }];
680
+ }
681
+ if (node.type === 'self') {
682
+ // `self` inside a `use_list` (`use std::fmt::{self, Display};`) imports the enclosing module path
683
+ // itself, bound to its own last segment rather than a member of it. Verified against a real parse.
684
+ const name = rustLastSegment(prefix);
685
+ return [{ localName: name, importedName: name, specifier: prefix }];
686
+ }
687
+ if (node.type === 'scoped_identifier') {
688
+ // `name` is required by the grammar; the fallback only satisfies `childForFieldName`'s general
689
+ // return type.
690
+ const name = node.childForFieldName('name');
691
+ /* v8 ignore next */
692
+ const localName = name?.text ?? node.text;
693
+ return [{ localName, importedName: localName, specifier: combineRustPrefix(prefix, node.text) }];
694
+ }
695
+ if (node.type === 'use_wildcard') {
696
+ // The grammar's `use_wildcard` rule always wraps exactly one path node before the `*`; the
697
+ // undefined case only satisfies `namedChildren`'s general array-access return type.
698
+ const path = namedChildren(node)[0];
699
+ /* v8 ignore next */
700
+ const specifier = path === undefined ? prefix : combineRustPrefix(prefix, path.text);
701
+ // A glob import binds no individual symbol this package tracks by name, matching Go's
702
+ // whole-package-import convention.
703
+ return [{ localName: '', importedName: '*', specifier }];
704
+ }
705
+ if (node.type === 'use_as_clause') {
706
+ const path = node.childForFieldName('path');
707
+ const alias = node.childForFieldName('alias');
708
+ // Both fields are required by the grammar's `use_as_clause` rule; the empty-array case only
709
+ // satisfies `childForFieldName`'s general return type.
710
+ /* v8 ignore next */
711
+ if (path === null || alias === null)
712
+ return [];
713
+ const [binding] = rustUseTarget(path, prefix);
714
+ // `rustUseTarget` always returns exactly one binding for the node types `path` can hold here
715
+ // (`identifier`, `self`, `scoped_identifier`); the undefined case only satisfies the general
716
+ // array-destructuring return type.
717
+ /* v8 ignore next */
718
+ return binding === undefined ? [] : [{ ...binding, localName: alias.text }];
719
+ }
720
+ if (node.type === 'scoped_use_list') {
721
+ const path = node.childForFieldName('path');
722
+ const list = node.childForFieldName('list');
723
+ // `list` is required by the grammar's `scoped_use_list` rule; the empty-array case only satisfies
724
+ // `childForFieldName`'s general return type.
725
+ /* v8 ignore next */
726
+ if (list === null)
727
+ return [];
728
+ const newPrefix = path === null ? prefix : combineRustPrefix(prefix, path.text);
729
+ return namedChildren(list).flatMap(child => rustUseTarget(child, newPrefix));
730
+ }
731
+ if (node.type === 'use_list') {
732
+ return namedChildren(node).flatMap(child => rustUseTarget(child, prefix));
733
+ }
734
+ // No other node type is a valid `_use_clause` alternative per the grammar.
735
+ /* v8 ignore next */
736
+ return [];
737
+ }
738
+ /** Rust `use_declaration` extraction, dispatching to {@link rustUseTarget} on its `argument` field. */
739
+ function rustImports(node) {
740
+ const argument = node.childForFieldName('argument');
741
+ // `argument` is required by the grammar's `use_declaration` rule; the empty-array case only satisfies
742
+ // `childForFieldName`'s general return type.
743
+ /* v8 ignore next */
744
+ if (argument === null)
745
+ return [];
746
+ return rustUseTarget(argument, '');
747
+ }
748
+ /** Whether `node` is a bare name this package resolves heritage references against — a member
749
+ * expression (`ns.Base`) or a call (a mixin, `f(Base)`) is not, matching the "don't guess" precedent
750
+ * `calleeName`/`ecmascriptImports` already follow for shapes they do not fully resolve. */
751
+ function isHeritageName(node) {
752
+ return node.type === 'identifier' || node.type === 'type_identifier' || node.type === 'name';
753
+ }
754
+ /**
755
+ * ECMAScript-family `extends`/`implements` extraction from a `class_declaration`'s `class_heritage`
756
+ * child. Plain JavaScript's `class_heritage` wraps the extended expression directly (`extends Base` has
757
+ * no further wrapper — JavaScript has no `implements`); TypeScript's wraps an `extends_clause` and an
758
+ * optional `implements_clause` instead, verified against a real parse, not guessed.
759
+ * @param node - the `class_declaration` node.
760
+ * @param sourceKey - the declaring class's own {@link RawDefinition.key}.
761
+ * @returns every heritage reference the class declares.
762
+ */
763
+ function ecmascriptClassHeritage(node, sourceKey) {
764
+ const heritage = namedChildren(node).find(child => child.type === 'class_heritage');
765
+ if (heritage === undefined)
766
+ return [];
767
+ const refs = [];
768
+ for (const child of namedChildren(heritage)) {
769
+ if (child.type === 'extends_clause') {
770
+ const target = namedChildren(child)[0];
771
+ if (target !== undefined && isHeritageName(target))
772
+ refs.push({ sourceKey, targetName: target.text, relation: 'extends' });
773
+ continue;
774
+ }
775
+ if (child.type === 'implements_clause') {
776
+ for (const impl of namedChildren(child)) {
777
+ if (isHeritageName(impl))
778
+ refs.push({ sourceKey, targetName: impl.text, relation: 'implements' });
779
+ }
780
+ continue;
781
+ }
782
+ // Plain JavaScript: `child` is the extended expression itself.
783
+ if (isHeritageName(child))
784
+ refs.push({ sourceKey, targetName: child.text, relation: 'extends' });
785
+ }
786
+ return refs;
787
+ }
788
+ /**
789
+ * TypeScript `interface_declaration` `extends` extraction from its `extends_type_clause` child — an
790
+ * interface can extend more than one other interface (`interface C extends A, B {}`).
791
+ * @param node - the `interface_declaration` node.
792
+ * @param sourceKey - the declaring interface's own {@link RawDefinition.key}.
793
+ * @returns every heritage reference the interface declares.
794
+ */
795
+ function tsInterfaceHeritage(node, sourceKey) {
796
+ const clause = namedChildren(node).find(child => child.type === 'extends_type_clause');
797
+ if (clause === undefined)
798
+ return [];
799
+ return namedChildren(clause)
800
+ .filter(isHeritageName)
801
+ .map(target => ({ sourceKey, targetName: target.text, relation: 'extends' }));
802
+ }
803
+ /**
804
+ * Python `class_definition` base-class extraction from its `argument_list` child — shared with call
805
+ * argument syntax, so a `keyword_argument` (`metaclass=Meta`) is filtered out rather than treated as a
806
+ * base; Python draws no distinction between a base class and an interface, so every entry is `extends`.
807
+ * @param node - the `class_definition` node.
808
+ * @param sourceKey - the declaring class's own {@link RawDefinition.key}.
809
+ * @returns every heritage reference the class declares.
810
+ */
811
+ function pythonClassHeritage(node, sourceKey) {
812
+ const args = namedChildren(node).find(child => child.type === 'argument_list');
813
+ if (args === undefined)
814
+ return [];
815
+ return namedChildren(args)
816
+ .filter(isHeritageName)
817
+ .map(target => ({ sourceKey, targetName: target.text, relation: 'extends' }));
818
+ }
819
+ /**
820
+ * Java `class_declaration`/`record_declaration` heritage extraction from their `superclass` (a record
821
+ * has none — a record can never extend another class, so the field is simply absent) and `interfaces`
822
+ * fields — both hold the clause node (`superclass`/`super_interfaces`) directly, one level above the
823
+ * `type_identifier`(s) themselves, verified against a real parse, not guessed.
824
+ * @param node - the `class_declaration`/`record_declaration` node.
825
+ * @param sourceKey - the declaring type's own {@link RawDefinition.key}.
826
+ * @returns every heritage reference the type declares.
827
+ */
828
+ function javaClassHeritage(node, sourceKey) {
829
+ const refs = [];
830
+ const superclass = node.childForFieldName('superclass');
831
+ const target = superclass === null ? undefined : namedChildren(superclass)[0];
832
+ if (target !== undefined && isHeritageName(target))
833
+ refs.push({ sourceKey, targetName: target.text, relation: 'extends' });
834
+ const interfaces = node.childForFieldName('interfaces');
835
+ const typeList = interfaces === null ? undefined : namedChildren(interfaces)[0];
836
+ if (typeList !== undefined) {
837
+ for (const impl of namedChildren(typeList)) {
838
+ if (isHeritageName(impl))
839
+ refs.push({ sourceKey, targetName: impl.text, relation: 'implements' });
840
+ }
841
+ }
842
+ return refs;
843
+ }
844
+ /**
845
+ * Java `interface_declaration` `extends` extraction — an interface can extend more than one other
846
+ * interface (`interface C extends A, B {}`), from its `extends_interfaces` child; unlike
847
+ * `class_declaration`'s `superclass`/`interfaces` fields, `interface_declaration` binds no field name
848
+ * of its own to this clause, so it is found by node type instead, matching `ecmascriptClassHeritage`'s
849
+ * same fallback for plain JavaScript. Verified against a real parse, not guessed.
850
+ * @param node - the `interface_declaration` node.
851
+ * @param sourceKey - the declaring interface's own {@link RawDefinition.key}.
852
+ * @returns every heritage reference the interface declares.
853
+ */
854
+ function javaInterfaceHeritage(node, sourceKey) {
855
+ const clause = namedChildren(node).find(child => child.type === 'extends_interfaces');
856
+ if (clause === undefined)
857
+ return [];
858
+ const typeList = namedChildren(clause)[0];
859
+ // `extends_interfaces` always wraps exactly one `type_list` per the grammar (an interface can never
860
+ // write a bare `extends` with nothing after it); the undefined case only satisfies the general
861
+ // array-access return type.
862
+ /* v8 ignore next */
863
+ if (typeList === undefined)
864
+ return [];
865
+ return namedChildren(typeList)
866
+ .filter(isHeritageName)
867
+ .map(target => ({ sourceKey, targetName: target.text, relation: 'extends' }));
868
+ }
869
+ /**
870
+ * C++/C# base-list heritage extraction, shared by both: neither C++'s `class_specifier`/`struct_specifier`
871
+ * nor C#'s `class_declaration`/`struct_declaration`/`record_declaration`/`interface_declaration` bind
872
+ * their base list to a dedicated field name — both are found by node type instead, matching
873
+ * `ecmascriptClassHeritage`'s same fallback for plain JavaScript. Neither grammar syntactically
874
+ * distinguishes an extended base class from an implemented interface in this list — the same ambiguity
875
+ * `pythonClassHeritage` already documents for Python's `argument_list` bases — so every entry reports
876
+ * `extends`, including when the declaring node is itself an interface extending another interface (C#'s
877
+ * `interface IBar : IFoo`), matching `javaInterfaceHeritage`/`tsInterfaceHeritage`'s existing convention
878
+ * for that shape. Verified against a real parse, not guessed.
879
+ * @param node - the declaring class/struct/interface/record node.
880
+ * @param sourceKey - the declaring definition's own {@link RawDefinition.key}.
881
+ * @param clauseType - the base-list node's own type (`base_class_clause` for C++, `base_list` for C#).
882
+ * @returns every heritage reference the definition declares.
883
+ */
884
+ function baseListHeritage(node, sourceKey, clauseType) {
885
+ const clause = namedChildren(node).find(child => child.type === clauseType);
886
+ if (clause === undefined)
887
+ return [];
888
+ return namedChildren(clause)
889
+ .filter(isHeritageName)
890
+ .map(target => ({ sourceKey, targetName: target.text, relation: 'extends' }));
891
+ }
892
+ /**
893
+ * PHP `class_declaration`/`interface_declaration`/`enum_declaration` heritage extraction. A class's
894
+ * single `extends` base sits in its own `base_clause` (PHP classes have no multiple inheritance); an
895
+ * interface's possibly-multiple `extends` bases reuse that same `base_clause` node type; a class's or
896
+ * enum's `implements` list sits in a `class_interface_clause` — neither clause binds to a field of its
897
+ * own, found by node type instead, matching `ecmascriptClassHeritage`'s same fallback for plain
898
+ * JavaScript. One function covers all three kinds since neither clause is exclusive to one of them (an
899
+ * interface never has a `class_interface_clause`, an enum never has a `base_clause`), so there is no
900
+ * ambiguity in reporting the relation `base_clause` → `extends` and `class_interface_clause` →
901
+ * `implements` regardless of which kind is declaring. A trait's `use` of another trait is not
902
+ * `extends`/`implements` shaped and is not reported here — see `extractHeritage`'s call site. Verified
903
+ * against a real parse, not guessed.
904
+ * @param node - the declaring class/interface/enum node.
905
+ * @param sourceKey - the declaring definition's own {@link RawDefinition.key}.
906
+ * @returns every heritage reference the definition declares.
907
+ */
908
+ function phpHeritage(node, sourceKey) {
909
+ const refs = [];
910
+ const base = namedChildren(node).find(child => child.type === 'base_clause');
911
+ if (base !== undefined) {
912
+ for (const target of namedChildren(base)) {
913
+ if (isHeritageName(target))
914
+ refs.push({ sourceKey, targetName: target.text, relation: 'extends' });
915
+ }
916
+ }
917
+ const iface = namedChildren(node).find(child => child.type === 'class_interface_clause');
918
+ if (iface !== undefined) {
919
+ for (const target of namedChildren(iface)) {
920
+ if (isHeritageName(target))
921
+ refs.push({ sourceKey, targetName: target.text, relation: 'implements' });
922
+ }
923
+ }
924
+ return refs;
925
+ }
926
+ /**
927
+ * Rust `trait_item` supertrait extraction from its `bounds` field (`trait Shape: Debug + Display {}`).
928
+ * Every supertrait requirement is reported as `extends`, matching `tsInterfaceHeritage`'s and
929
+ * `javaInterfaceHeritage`'s convention for a multi-base interface declaration; a `impl Trait for Type`
930
+ * block's own trait relationship is not extracted here or anywhere else in this package — unlike a
931
+ * class/struct/trait declaration, an `impl` block is never itself captured as a `RawDefinition` (it
932
+ * introduces no name of its own the seam's `container`/qualified-name scheme could attach to), so it has
933
+ * no {@link RawDefinition.key} to source a heritage reference from. Verified against a real parse, not
934
+ * guessed.
935
+ * @param node - the `trait_item` node.
936
+ * @param sourceKey - the declaring trait's own {@link RawDefinition.key}.
937
+ * @returns every heritage reference the trait declares.
938
+ */
939
+ function rustTraitHeritage(node, sourceKey) {
940
+ const bounds = node.childForFieldName('bounds');
941
+ if (bounds === null)
942
+ return [];
943
+ return namedChildren(bounds)
944
+ .filter(isHeritageName)
945
+ .map(target => ({ sourceKey, targetName: target.text, relation: 'extends' }));
946
+ }
947
+ /**
948
+ * Dispatch heritage extraction to the language family that owns a captured class, struct, or interface
949
+ * node's syntax. Go is absent: its interfaces are satisfied structurally, never declared at the
950
+ * implementing type, so there is no static reference here to extract.
951
+ * @param node - the captured `class`-, `struct`-, `interface`-, or `trait`-kind definition node.
952
+ * @param kind - the captured definition's seam kind (`'class'`, `'struct'`, `'interface'`, or `'trait'`).
953
+ * @param sourceKey - the declaring definition's own {@link RawDefinition.key}.
954
+ * @param language - the seam language label the file was parsed as.
955
+ * @returns every heritage reference the definition declares.
956
+ */
957
+ function extractHeritage(node, kind, sourceKey, language) {
958
+ // `kind === 'trait'` only ever comes from Rust's `trait_item` rule — no other language in
959
+ // LANGUAGE_TABLE produces it.
960
+ if (kind === 'trait')
961
+ return rustTraitHeritage(node, sourceKey);
962
+ // `kind === 'interface'` only ever comes from TYPESCRIPT_DEFINITIONS's, Java's, or C#'s
963
+ // `interface_declaration` rule — no other language in LANGUAGE_TABLE produces it.
964
+ if (kind === 'interface') {
965
+ if (language === 'java')
966
+ return javaInterfaceHeritage(node, sourceKey);
967
+ if (language === 'csharp')
968
+ return baseListHeritage(node, sourceKey, 'base_list');
969
+ if (language === 'php')
970
+ return phpHeritage(node, sourceKey);
971
+ return tsInterfaceHeritage(node, sourceKey);
972
+ }
973
+ // `kind === 'struct'` only ever comes from C/C++'s `struct_specifier`/`union_specifier` or C#'s
974
+ // `struct_declaration` rule — a plain C struct/union has no base-list syntax at all, so
975
+ // `baseListHeritage` simply finds nothing to report for it.
976
+ if (kind === 'struct')
977
+ return baseListHeritage(node, sourceKey, language === 'csharp' ? 'base_list' : 'base_class_clause');
978
+ // `kind === 'enum'` only ever comes from PHP's `enum_declaration` rule today — no other language's
979
+ // enum rule reaches this dispatch (TypeScript's/Java's/C#'s enum kinds carry no heritage syntax of
980
+ // their own this package extracts elsewhere), so every other language reports nothing here.
981
+ if (kind === 'enum')
982
+ return language === 'php' ? phpHeritage(node, sourceKey) : [];
983
+ if (kind !== 'class')
984
+ return [];
985
+ if (language === 'python')
986
+ return pythonClassHeritage(node, sourceKey);
987
+ // `kind === 'class'` from Java's `class_declaration`/`record_declaration` rules — see `javaClassHeritage`.
988
+ if (language === 'java')
989
+ return javaClassHeritage(node, sourceKey);
990
+ if (language === 'cpp')
991
+ return baseListHeritage(node, sourceKey, 'base_class_clause');
992
+ if (language === 'csharp')
993
+ return baseListHeritage(node, sourceKey, 'base_list');
994
+ if (language === 'php')
995
+ return phpHeritage(node, sourceKey);
996
+ // Otherwise only comes from ECMASCRIPT_DEFINITIONS's `class_declaration` rule — Go has no class
997
+ // concept, so this is never reached with `language === 'go'`.
998
+ return ecmascriptClassHeritage(node, sourceKey);
999
+ }
177
1000
  /**
178
1001
  * Extract every definition, call, and import from one parsed file.
179
1002
  * @param tree - the file's parsed syntax tree.
@@ -184,12 +1007,30 @@ export function extractFile(tree, spec) {
184
1007
  const definitions = [];
185
1008
  const calls = [];
186
1009
  const imports = [];
1010
+ const heritage = [];
187
1011
  const containerNames = [];
188
1012
  const containerKeys = [null];
1013
+ const commonJsExports = ECMASCRIPT_LANGUAGES.has(spec.language) ? commonJsExportedNames(tree.rootNode) : EMPTY_NAME_SET;
1014
+ // Tracks whether the node currently being visited sits at module top level, directly inside a class
1015
+ // body, or inside a function/method body — see `DefinitionRule.scopeRestricted` and
1016
+ // `LanguageSpec.bareFunctionScopeTypes`.
1017
+ const scopeKinds = ['module'];
189
1018
  function visit(node) {
190
1019
  const rule = matchDefinition(node, spec.definitions);
191
- if (rule !== undefined) {
192
- const nameNode = node.childForFieldName(rule.nameField);
1020
+ // A `scopeRestricted` rule matched inside a function/method body is treated as no match at all —
1021
+ // the node still gets visited below, just without becoming a definition or a container.
1022
+ const captured = rule !== undefined
1023
+ && (rule.scopeRestricted !== true || scopeKinds[scopeKinds.length - 1] !== 'other');
1024
+ if (captured) {
1025
+ // `matchDefinition` already confirmed `declaratorName`/`firstChildName` return non-`undefined` for
1026
+ // this same node when `rule` matched; the assertions below only satisfy the ternary's
1027
+ // `SyntaxNode | null` type, mirroring `node.childForFieldName`'s own return type — not a runtime
1028
+ // branch, so nothing here needs a test of its own.
1029
+ const nameNode = rule.nameField === SELF_NAME_FIELD ? node
1030
+ : rule.nameField === DECLARATOR_NAME_FIELD ? declaratorName(node)
1031
+ : rule.nameField === FIRST_CHILD_NAME_FIELD ? firstChildName(node)
1032
+ : rule.nameField === PHP_ELEMENT_NAME_FIELD ? phpElementName(node)
1033
+ : node.childForFieldName(rule.nameField);
193
1034
  // A matched rule's node type is always the NAMED-declaration form the grammar mandates a name
194
1035
  // for; the anonymous form (`function_expression`, `class` as an expression, both produced by an
195
1036
  // anonymous default export) parses as a different node type this rule never matches.
@@ -207,14 +1048,30 @@ export function extractFile(tree, spec) {
207
1048
  endLine: node.endPosition.row + 1,
208
1049
  startColumn: node.startPosition.column,
209
1050
  endColumn: node.endPosition.column,
210
- isExported: isExported(node, spec.language, nameNode.text),
1051
+ isExported: isExported(node, spec.language, nameNode.text, commonJsExports),
211
1052
  isAsync: hasKeywordChild(node, 'async'),
212
- isStatic: hasKeywordChild(node, 'static'),
1053
+ // Java nests every modifier keyword one level down inside a `modifiers` node rather than as
1054
+ // a direct child of the declaration itself — see `javaHasModifier`. C/C++ wrap `static` in a
1055
+ // `storage_class_specifier` — see `cHasStorageClassKeyword`. C# gives each modifier its own
1056
+ // flat `modifier` node — see `csharpHasModifier`.
1057
+ isStatic: spec.language === 'java' ? javaHasModifier(node, 'static')
1058
+ : spec.language === 'c' || spec.language === 'cpp' ? cHasStorageClassKeyword(node, 'static')
1059
+ : spec.language === 'csharp' ? csharpHasModifier(node, 'static')
1060
+ : spec.language === 'php' ? phpHasStaticModifier(node)
1061
+ : hasKeywordChild(node, 'static'),
1062
+ decorators: pythonDecorators(node, spec.language),
213
1063
  });
1064
+ heritage.push(...extractHeritage(node, rule.kind, key, spec.language));
214
1065
  containerNames.push(nameNode.text);
215
1066
  containerKeys.push(key);
1067
+ // A C/C++/C# `struct` is scoped exactly like a `class` for this purpose — a `scopeRestricted`
1068
+ // field rule must fire directly inside either body, not just a `class` one. Rust's `mod`
1069
+ // (`namespace`) is scoped like the module top level it nests, not like a function body — a
1070
+ // `scopeRestricted` `const`/`static` rule must still fire directly inside one.
1071
+ scopeKinds.push(rule.kind === 'class' || rule.kind === 'struct' ? 'class' : rule.kind === 'namespace' ? 'module' : 'other');
216
1072
  for (const child of namedChildren(node))
217
1073
  visit(child);
1074
+ scopeKinds.pop();
218
1075
  containerKeys.pop();
219
1076
  containerNames.pop();
220
1077
  return;
@@ -223,30 +1080,55 @@ export function extractFile(tree, spec) {
223
1080
  if (spec.callTypes.includes(node.type)) {
224
1081
  // Every call-shaped node type in every language table entry requires its callee field; the null
225
1082
  // case only satisfies `childForFieldName`'s general return type.
226
- const callee = node.childForFieldName(spec.callFunctionField);
1083
+ const calleeField = spec.callFunctionFieldByType?.[node.type] ?? spec.callFunctionField;
1084
+ const callee = node.childForFieldName(calleeField);
227
1085
  /* v8 ignore next */
228
1086
  const name = callee === null ? undefined : calleeName(callee);
229
1087
  if (name !== undefined) {
1088
+ // `callee` is non-null whenever `name` is: the optional chaining only satisfies the type
1089
+ // system's view of the field lookup above, not a real possibility here.
1090
+ /* v8 ignore next */
1091
+ const isBareCallee = callee?.type === 'identifier' || callee?.type === 'name';
230
1092
  calls.push({
231
1093
  callerKey: containerKeys[containerKeys.length - 1] ?? null,
232
1094
  calleeName: name,
233
1095
  line: node.startPosition.row + 1,
234
1096
  column: node.startPosition.column,
235
- // `callee` is non-null whenever `name` is: the optional chaining only satisfies the type
236
- // system's view of the field lookup above, not a real possibility here.
237
- /* v8 ignore next */
238
- isMemberCall: callee?.type !== 'identifier',
1097
+ // The `object` check covers Java's `method_invocation` and PHP's `member_call_expression`,
1098
+ // whose `name` field is always a bare identifier — the receiver, if any, sits in a separate
1099
+ // sibling field instead of wrapping the callee the way every other grammar's member expression
1100
+ // does. The `scope` check covers PHP's `scoped_call_expression` (`Class::method()`), whose
1101
+ // receiver sits in a sibling `scope` field instead of `object`; no other call-type node in
1102
+ // LANGUAGE_TABLE binds either field, so both are a no-op for every other language. Without
1103
+ // `isBareCallee` also accepting PHP's `'name'` type alongside `'identifier'`, every PHP call —
1104
+ // including an ordinary global `function_call_expression` — would be misclassified as
1105
+ // member-like, since PHP's bare-name node type is `name`, never `identifier`. Verified against
1106
+ // a real parse, not guessed.
1107
+ isMemberCall: !isBareCallee || node.childForFieldName('object') !== null || node.childForFieldName('scope') !== null,
239
1108
  });
240
1109
  }
241
1110
  }
242
1111
  if (spec.importTypes.includes(node.type)) {
243
1112
  imports.push(...extractImports(node, spec.language));
244
1113
  }
1114
+ if (node.type === 'call_expression' && ECMASCRIPT_LANGUAGES.has(spec.language)) {
1115
+ const requireImport = commonJsRequireImport(node);
1116
+ if (requireImport !== undefined)
1117
+ imports.push(requireImport);
1118
+ }
1119
+ // A callback or IIFE's function value is never itself captured as a definition (only a *named*
1120
+ // declaration, or one assigned through a captured `variable_declarator`, is) — but a `const`/`var`
1121
+ // in its body is still function-local, so its scope must flip to `'other'` here regardless.
1122
+ const entersBareFunctionScope = spec.bareFunctionScopeTypes.includes(node.type);
1123
+ if (entersBareFunctionScope)
1124
+ scopeKinds.push('other');
245
1125
  for (const child of namedChildren(node))
246
1126
  visit(child);
1127
+ if (entersBareFunctionScope)
1128
+ scopeKinds.pop();
247
1129
  }
248
1130
  visit(tree.rootNode);
249
- return { definitions, calls, imports };
1131
+ return { definitions, calls, imports, heritage };
250
1132
  }
251
1133
  /** Dispatch import extraction to the language family that owns `node`'s syntax. */
252
1134
  function extractImports(node, language) {
@@ -260,27 +1142,127 @@ function extractImports(node, language) {
260
1142
  return pythonImports(node);
261
1143
  case 'go':
262
1144
  return goImports(node);
1145
+ case 'java':
1146
+ return javaImports(node);
1147
+ case 'c':
1148
+ case 'cpp':
1149
+ return cIncludeImports(node);
1150
+ case 'csharp':
1151
+ return csharpUsingImports(node);
1152
+ case 'php':
1153
+ return phpImports(node);
1154
+ case 'rust':
1155
+ return rustImports(node);
263
1156
  /* v8 ignore next 2 -- exhaustive over LANGUAGE_TABLE's current language labels; unreachable. */
264
1157
  default:
265
1158
  return [];
266
1159
  }
267
1160
  }
1161
+ /** Whether `node` is the two-level `module.exports` member expression itself (not `module.exports.x`). */
1162
+ function isModuleExportsExpression(node) {
1163
+ return node?.type === 'member_expression'
1164
+ && node.childForFieldName('object')?.type === 'identifier'
1165
+ && node.childForFieldName('object')?.text === 'module'
1166
+ && node.childForFieldName('property')?.text === 'exports';
1167
+ }
1168
+ /**
1169
+ * Every name a top-level CommonJS export assignment marks exported: `module.exports.NAME = ...` /
1170
+ * `exports.NAME = ...` (named export), and `module.exports = NAME` (whole-module reassignment to a
1171
+ * single local declaration). `module.exports = { a, b }` (object-literal reassignment) is not handled
1172
+ * — distinguishing a shorthand property from a computed or renamed one adds a second layer of "don't
1173
+ * guess" cases this pass does not need yet; only the two unambiguous forms above are recognized.
1174
+ * Restricted to true top-level statements, matching this file's existing module/class-only scope
1175
+ * restriction for a `scopeRestricted` `DefinitionRule` — a conditional or function-body export
1176
+ * assignment is not a module's public surface in the same unconditional sense.
1177
+ * @param root - the file's parsed root (`program`) node.
1178
+ * @returns every name a CommonJS export assignment binds.
1179
+ */
1180
+ function commonJsExportedNames(root) {
1181
+ const names = new Set();
1182
+ for (const statement of namedChildren(root)) {
1183
+ if (statement.type !== 'expression_statement')
1184
+ continue;
1185
+ const expr = namedChildren(statement)[0];
1186
+ if (expr?.type !== 'assignment_expression')
1187
+ continue;
1188
+ const left = expr.childForFieldName('left');
1189
+ if (left?.type !== 'member_expression')
1190
+ continue;
1191
+ const object = left.childForFieldName('object');
1192
+ const property = left.childForFieldName('property');
1193
+ if (isModuleExportsExpression(object) || (object?.type === 'identifier' && object.text === 'exports')) {
1194
+ // `property` is required by the grammar's `member_expression` rule; the null case only
1195
+ // satisfies `childForFieldName`'s general return type.
1196
+ /* v8 ignore next */
1197
+ if (property !== null)
1198
+ names.add(property.text);
1199
+ continue;
1200
+ }
1201
+ if (isModuleExportsExpression(left)) {
1202
+ const right = expr.childForFieldName('right');
1203
+ if (right?.type === 'identifier')
1204
+ names.add(right.text);
1205
+ }
1206
+ }
1207
+ return names;
1208
+ }
268
1209
  /**
269
1210
  * Whether a declaration is exported from its module, by the export construct its own language
270
- * defines: ECMAScript wraps an exported statement in `export_statement`; Go's spec defines an
271
- * exported identifier as one starting with an uppercase letter, with no separate keyword; Python
272
- * defines no export construct at all, so every Python declaration reports `false` rather than guess
273
- * one from a naming convention or an `__all__` list the extractor does not read.
1211
+ * defines: ECMAScript wraps an exported statement in `export_statement`, or — for CommonJS code, still
1212
+ * common outside pure-ESM projects — is named by a top-level `module.exports`/`exports` assignment (see
1213
+ * {@link commonJsExportedNames}); Go's spec defines an exported identifier as one starting with an
1214
+ * uppercase letter, with no separate keyword; Java marks a declaration exported by an explicit
1215
+ * `public` modifier (see {@link javaHasModifier}) — an interface member's implicit `public` with no
1216
+ * keyword at all is not detected, matching this function's existing refusal to infer visibility from
1217
+ * anything but an explicit language construct; C's is external vs. internal *linkage* — a top-level
1218
+ * function or variable without `static` has external linkage (visible to other translation units), the
1219
+ * same real-rule precedent Go's capitalization check follows rather than a guess; C++ inherits that same
1220
+ * rule for its own free (non-member) functions and variables, but reports `false` for a class/struct
1221
+ * member — a method or field has no comparable linkage concept of its own, and neither does any other
1222
+ * kind (`struct`, `enum`, `type_alias`); C# marks a declaration exported by an explicit `public` modifier
1223
+ * (see {@link csharpHasModifier}), mirroring Java; Python defines no export construct at all, so every
1224
+ * Python declaration reports `false` rather than guess one from a naming convention or an `__all__` list
1225
+ * the extractor does not read; Rust marks a declaration exported by an explicit bare `pub`
1226
+ * `visibility_modifier` (see {@link rustIsPublic}) — a restricted `pub(crate)`/`pub(super)`/`pub(self)`
1227
+ * does not count, mirroring Java's/C#'s explicit-`public`-only convention.
274
1228
  * @param node - the definition node.
275
1229
  * @param language - the seam language label the file was parsed as.
276
1230
  * @param name - the declaration's simple name.
1231
+ * @param commonJsExports - every name a CommonJS export assignment in this file binds.
277
1232
  * @returns whether the language's own export rule marks this declaration exported.
278
1233
  */
279
- function isExported(node, language, name) {
1234
+ function isExported(node, language, name, commonJsExports) {
280
1235
  if (language === 'go')
281
1236
  return /^\p{Lu}/u.test(name);
1237
+ if (language === 'java')
1238
+ return javaHasModifier(node, 'public');
1239
+ if (language === 'csharp')
1240
+ return csharpHasModifier(node, 'public');
1241
+ if (language === 'c' || language === 'cpp') {
1242
+ // A C++ method is a `function_definition` directly inside a class/struct's `field_declaration_list`
1243
+ // — the same node type a free function uses, but with no linkage concept of its own to report.
1244
+ if (node.type === 'function_definition')
1245
+ return node.parent?.type !== 'field_declaration_list' && !cHasStorageClassKeyword(node, 'static');
1246
+ // A top-level `declaration` (kind `variable`) is always module-scope — `scopeRestricted` already
1247
+ // excludes the function-local case, so no further scope check is needed here.
1248
+ if (node.type === 'declaration')
1249
+ return !cHasStorageClassKeyword(node, 'static');
1250
+ return false;
1251
+ }
282
1252
  if (language === 'python')
283
1253
  return false;
1254
+ // A PHP top-level function/class/interface/trait/enum carries no visibility keyword of its own — the
1255
+ // language has no export construct for them, matching Python's precedent, and this reports `false`
1256
+ // for all of them since `phpHasVisibility` finds no `visibility_modifier` to check. A class/enum
1257
+ // member (`const`/property/method) does carry one, mirroring Java's/C#'s explicit-`public`-only
1258
+ // convention: an interface member's implicit `public` with no keyword at all is not detected either,
1259
+ // the same refusal to infer visibility from anything but an explicit language construct.
1260
+ if (language === 'php')
1261
+ return phpHasVisibility(node, 'public');
1262
+ if (language === 'rust')
1263
+ return rustIsPublic(node);
1264
+ if (commonJsExports.has(name))
1265
+ return true;
284
1266
  let current = node.parent;
285
1267
  while (current !== null) {
286
1268
  if (current.type === 'export_statement')