dsh-plugin-codegraph-tree-sitter 0.1.1 → 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.
@@ -28,7 +28,80 @@ export interface DefinitionRule {
28
28
  /** Node types the field must resolve to for this rule to match. */
29
29
  readonly types: readonly string[];
30
30
  };
31
+ /**
32
+ * Present only when this rule must reject a name shape it cannot represent as a single declared
33
+ * name — a destructuring `variable_declarator` (`const {a, b} = x`) or a Python tuple/attribute
34
+ * assignment (`a, b = 1, 2`, `self.x = 1`) binds through {@link nameField} too, but to a pattern
35
+ * node this package does not attempt to name. Absent, any node type in {@link nameField} matches.
36
+ */
37
+ readonly nameNodeTypes?: readonly string[];
38
+ /**
39
+ * Present only when this rule must not match the same node type wherever it appears — a bare
40
+ * `property_identifier` names an enum member only directly inside an `enum_body`; the same node
41
+ * type also names a method, a class field, and a member-expression property everywhere else. Absent,
42
+ * any parent matches.
43
+ */
44
+ readonly parentType?: string;
45
+ /**
46
+ * True only for a rule this package extracts solely directly inside a module's top level or a class
47
+ * body — never inside a function or method body. Unlike a function or class declaration (captured at
48
+ * any depth, however deeply nested), a function-local variable is not a workspace "symbol" anyone
49
+ * would search for by name, and capturing it would flood the cross-file name index `resolve.ts` uses
50
+ * to settle calls — more names means more collisions, which means more calls silently dropped to
51
+ * `unresolved`. See `resolve.ts`'s "an ambiguous edge is worse than a missing one" rule.
52
+ *
53
+ * This is a property of the *rule*, not of {@link kind} in general: JS/TS's `field_definition` rule
54
+ * (kind `field`) sets this because a class field could in principle sit at any depth a class does —
55
+ * but Go's struct-field rule, also kind `field`, must not, because its enclosing `type_spec` is itself
56
+ * never scope-restricted (a Go type declaration is captured at any depth, like a function), so a
57
+ * scope-restricted struct-field rule could never fire at all. Absent (the common case), always
58
+ * extracted regardless of depth.
59
+ */
60
+ readonly scopeRestricted?: boolean;
61
+ /**
62
+ * Present only when a rule must distinguish two node types that share both their own type and their
63
+ * immediate parent's type — C#'s field vs. local variable both parse as a `variable_declarator`
64
+ * directly inside a `variable_declaration`, and only the *grandparent* differs
65
+ * (`field_declaration` vs. `local_declaration_statement`). Absent, any grandparent matches, the same
66
+ * as an absent {@link parentType}.
67
+ */
68
+ readonly grandparentType?: string;
31
69
  }
70
+ /**
71
+ * Sentinel {@link DefinitionRule.nameField} value for a rule whose matched node has no separate
72
+ * name-bearing child — a bare enum member (`enum_body`'s `Red` in `enum Color { Red }`) parses as a
73
+ * lone `property_identifier` token with no fields of its own; the node itself, unlike every other
74
+ * `nameField` target, *is* the name.
75
+ */
76
+ export declare const SELF_NAME_FIELD = "@self";
77
+ /**
78
+ * Sentinel {@link DefinitionRule.nameField} value for a rule whose name sits behind an arbitrarily deep
79
+ * chain of C/C++ `declarator` wrappers — `int *make(int a)`'s name is three `declarator` fields down
80
+ * (`pointer_declarator` → `function_declarator` → `identifier`), and a plain `int g` binds its
81
+ * `identifier` directly with no wrapper at all. See `declaratorName` in `extract.ts`, which this
82
+ * sentinel dispatches to instead of a flat `childForFieldName` lookup.
83
+ */
84
+ export declare const DECLARATOR_NAME_FIELD = "@declarator";
85
+ /**
86
+ * Sentinel {@link DefinitionRule.nameField} value for a rule whose name is its matched node's first
87
+ * named child with no field name of its own — C#'s `variable_declarator` binds neither its name nor its
88
+ * optional initializer to a field at all (`int Field = 5;`'s `variable_declarator` has two purely
89
+ * positional named children, `identifier` then `equals_value_clause`), unlike every other grammar this
90
+ * package extracts a name from. Distinct from {@link SELF_NAME_FIELD}: the matched node's own text
91
+ * would include that initializer (`"Field = 5"`), not just the name. See `firstChildName` in
92
+ * `extract.ts`.
93
+ */
94
+ export declare const FIRST_CHILD_NAME_FIELD = "@first-child";
95
+ /**
96
+ * Sentinel {@link DefinitionRule.nameField} value for a rule whose name is PHP-specific: a bare `name`
97
+ * node directly (`const_element`'s `VERSION` in `const VERSION = "1"`), or one nested a level deeper
98
+ * inside a `variable_name` child (`property_element`'s `$name` in `public string $name;`, and an
99
+ * `assignment_expression`'s `$fn` in `$fn = function () {}` — both bind their bare `name` behind the
100
+ * same `$`-prefixed wrapper). See `phpElementName` in `extract.ts`, which this sentinel dispatches to
101
+ * instead of a flat `childForFieldName` lookup — none of these three shapes bind their name to a field
102
+ * of its own.
103
+ */
104
+ export declare const PHP_ELEMENT_NAME_FIELD = "@php-element";
32
105
  /** One tree-sitter node type recording a relative-import binding, per language family. */
33
106
  export interface ImportRule {
34
107
  /** The tree-sitter node type anchoring one import statement, e.g. `import_statement`. */
@@ -46,10 +119,29 @@ export interface LanguageSpec {
46
119
  readonly definitions: readonly DefinitionRule[];
47
120
  /** Node types whose {@link callFunctionField} names the callee. */
48
121
  readonly callTypes: readonly string[];
49
- /** Field on a call node holding the callee expression. */
122
+ /** Field on a call node holding the callee expression, used for any {@link callTypes} member absent
123
+ * from {@link callFunctionFieldByType}. */
50
124
  readonly callFunctionField: string;
125
+ /**
126
+ * Present only when different {@link callTypes} members name their callee through different fields —
127
+ * PHP's `member_call_expression` ($obj->method()) and `scoped_call_expression` (Class::method()) both
128
+ * use `name`, while `function_call_expression` uses `function`; every other language's call types
129
+ * (today, always exactly one) share a single field, so this is absent for all of them. Overrides
130
+ * {@link callFunctionField} for the node types it lists.
131
+ */
132
+ readonly callFunctionFieldByType?: Readonly<Record<string, string>>;
51
133
  /** Node types anchoring one import statement. */
52
134
  readonly importTypes: readonly string[];
135
+ /**
136
+ * Node types that bound a new function scope even when the walk never captures them as a named
137
+ * definition — a callback (`arr.forEach(function(item) { ... })`) or an IIFE's `function_expression`
138
+ * never matches a {@link DefinitionRule} (only a *named* declaration, or one assigned through a
139
+ * captured `variable_declarator`, does), but a `const`/`var` inside its body is exactly the
140
+ * function-local binding a `scopeRestricted` {@link DefinitionRule} exists to exclude. Empty for a language with
141
+ * no anonymous-function shape that can contain a statement (Python's `lambda` body is a single
142
+ * expression, never a block).
143
+ */
144
+ readonly bareFunctionScopeTypes: readonly string[];
53
145
  }
54
146
  /** Every grammar this package loads, keyed by the seam language label it produces. */
55
147
  export declare const LANGUAGE_TABLE: readonly LanguageSpec[];
package/lib/languages.js CHANGED
@@ -8,6 +8,41 @@
8
8
  * defers until the core proves itself against real workspaces.
9
9
  * @module dsh-plugin-codegraph-tree-sitter/languages
10
10
  */
11
+ /**
12
+ * Sentinel {@link DefinitionRule.nameField} value for a rule whose matched node has no separate
13
+ * name-bearing child — a bare enum member (`enum_body`'s `Red` in `enum Color { Red }`) parses as a
14
+ * lone `property_identifier` token with no fields of its own; the node itself, unlike every other
15
+ * `nameField` target, *is* the name.
16
+ */
17
+ export const SELF_NAME_FIELD = '@self';
18
+ /**
19
+ * Sentinel {@link DefinitionRule.nameField} value for a rule whose name sits behind an arbitrarily deep
20
+ * chain of C/C++ `declarator` wrappers — `int *make(int a)`'s name is three `declarator` fields down
21
+ * (`pointer_declarator` → `function_declarator` → `identifier`), and a plain `int g` binds its
22
+ * `identifier` directly with no wrapper at all. See `declaratorName` in `extract.ts`, which this
23
+ * sentinel dispatches to instead of a flat `childForFieldName` lookup.
24
+ */
25
+ export const DECLARATOR_NAME_FIELD = '@declarator';
26
+ /**
27
+ * Sentinel {@link DefinitionRule.nameField} value for a rule whose name is its matched node's first
28
+ * named child with no field name of its own — C#'s `variable_declarator` binds neither its name nor its
29
+ * optional initializer to a field at all (`int Field = 5;`'s `variable_declarator` has two purely
30
+ * positional named children, `identifier` then `equals_value_clause`), unlike every other grammar this
31
+ * package extracts a name from. Distinct from {@link SELF_NAME_FIELD}: the matched node's own text
32
+ * would include that initializer (`"Field = 5"`), not just the name. See `firstChildName` in
33
+ * `extract.ts`.
34
+ */
35
+ export const FIRST_CHILD_NAME_FIELD = '@first-child';
36
+ /**
37
+ * Sentinel {@link DefinitionRule.nameField} value for a rule whose name is PHP-specific: a bare `name`
38
+ * node directly (`const_element`'s `VERSION` in `const VERSION = "1"`), or one nested a level deeper
39
+ * inside a `variable_name` child (`property_element`'s `$name` in `public string $name;`, and an
40
+ * `assignment_expression`'s `$fn` in `$fn = function () {}` — both bind their bare `name` behind the
41
+ * same `$`-prefixed wrapper). See `phpElementName` in `extract.ts`, which this sentinel dispatches to
42
+ * instead of a flat `childForFieldName` lookup — none of these three shapes bind their name to a field
43
+ * of its own.
44
+ */
45
+ export const PHP_ELEMENT_NAME_FIELD = '@php-element';
11
46
  /** Definitions common to every ECMAScript-family grammar (JavaScript, JSX, TypeScript, TSX). */
12
47
  const ECMASCRIPT_DEFINITIONS = [
13
48
  { nodeType: 'function_declaration', kind: 'function', nameField: 'name' },
@@ -20,6 +55,12 @@ const ECMASCRIPT_DEFINITIONS = [
20
55
  nameField: 'name',
21
56
  value: { field: 'value', types: ['arrow_function', 'function_expression', 'generator_function'] },
22
57
  },
58
+ // Falls through from the function-valued rule above: any other simply-named `const`/`let`/`var`
59
+ // binding (`lexical_declaration` and `variable_declaration` both wrap this same node type).
60
+ // `nameNodeTypes` rejects a destructuring pattern (`const {a, b} = x`) rather than name it after its
61
+ // pattern text — the same "don't guess a name" precedent `pythonBinding`/`ecmascriptImports` already
62
+ // follow for shapes this package does not resolve.
63
+ { nodeType: 'variable_declarator', kind: 'variable', nameField: 'name', nameNodeTypes: ['identifier'], scopeRestricted: true },
23
64
  ];
24
65
  /** Definitions TypeScript and TSX add on top of {@link ECMASCRIPT_DEFINITIONS}. */
25
66
  const TYPESCRIPT_DEFINITIONS = [
@@ -27,9 +68,29 @@ const TYPESCRIPT_DEFINITIONS = [
27
68
  { nodeType: 'interface_declaration', kind: 'interface', nameField: 'name' },
28
69
  { nodeType: 'type_alias_declaration', kind: 'type_alias', nameField: 'name' },
29
70
  { nodeType: 'enum_declaration', kind: 'enum', nameField: 'name' },
71
+ // `enum Color { Red, Green = 5 }` — a bare member (`Red`) parses as a lone `property_identifier`
72
+ // naming itself, tagged with `enum_body`'s own repeated `name` field; a valued member (`Green = 5`)
73
+ // wraps its own `property_identifier` in a distinct `enum_assignment` node instead. Both verified
74
+ // against a real parse, not guessed.
75
+ { nodeType: 'property_identifier', kind: 'enum_member', nameField: SELF_NAME_FIELD, parentType: 'enum_body' },
76
+ { nodeType: 'enum_assignment', kind: 'enum_member', nameField: 'name' },
77
+ // The TypeScript/TSX grammars name a class field node differently from plain JavaScript's
78
+ // `field_definition` (see JAVASCRIPT_DEFINITIONS) — verified against a real parse, not guessed.
79
+ { nodeType: 'public_field_definition', kind: 'field', nameField: 'name', scopeRestricted: true },
80
+ ];
81
+ /** Definitions JavaScript and JSX add on top of {@link ECMASCRIPT_DEFINITIONS}. */
82
+ const JAVASCRIPT_DEFINITIONS = [
83
+ ...ECMASCRIPT_DEFINITIONS,
84
+ // The plain JavaScript grammar's class field node names its field-name field `property`, not `name`
85
+ // as TypeScript's `public_field_definition` does (see TYPESCRIPT_DEFINITIONS) — verified against a
86
+ // real parse, not guessed.
87
+ { nodeType: 'field_definition', kind: 'field', nameField: 'property', scopeRestricted: true },
30
88
  ];
31
89
  const ECMASCRIPT_CALL_TYPES = ['call_expression'];
32
90
  const ECMASCRIPT_IMPORT_TYPES = ['import_statement'];
91
+ /** The ECMAScript function-value node types, matched against a real parse — see
92
+ * {@link LanguageSpec.bareFunctionScopeTypes}. */
93
+ const ECMASCRIPT_BARE_FUNCTION_SCOPE_TYPES = ['function_expression', 'arrow_function', 'generator_function'];
33
94
  /** Every grammar this package loads, keyed by the seam language label it produces. */
34
95
  export const LANGUAGE_TABLE = [
35
96
  {
@@ -40,6 +101,7 @@ export const LANGUAGE_TABLE = [
40
101
  callTypes: ECMASCRIPT_CALL_TYPES,
41
102
  callFunctionField: 'function',
42
103
  importTypes: ECMASCRIPT_IMPORT_TYPES,
104
+ bareFunctionScopeTypes: ECMASCRIPT_BARE_FUNCTION_SCOPE_TYPES,
43
105
  },
44
106
  {
45
107
  language: 'tsx',
@@ -49,15 +111,17 @@ export const LANGUAGE_TABLE = [
49
111
  callTypes: ECMASCRIPT_CALL_TYPES,
50
112
  callFunctionField: 'function',
51
113
  importTypes: ECMASCRIPT_IMPORT_TYPES,
114
+ bareFunctionScopeTypes: ECMASCRIPT_BARE_FUNCTION_SCOPE_TYPES,
52
115
  },
53
116
  {
54
117
  language: 'javascript',
55
118
  extensions: ['.js', '.mjs', '.cjs'],
56
119
  wasmFile: 'tree-sitter-javascript.wasm',
57
- definitions: ECMASCRIPT_DEFINITIONS,
120
+ definitions: JAVASCRIPT_DEFINITIONS,
58
121
  callTypes: ECMASCRIPT_CALL_TYPES,
59
122
  callFunctionField: 'function',
60
123
  importTypes: ECMASCRIPT_IMPORT_TYPES,
124
+ bareFunctionScopeTypes: ECMASCRIPT_BARE_FUNCTION_SCOPE_TYPES,
61
125
  },
62
126
  {
63
127
  language: 'jsx',
@@ -65,10 +129,11 @@ export const LANGUAGE_TABLE = [
65
129
  // The plain JavaScript grammar already parses JSX syntax; tree-sitter-wasms ships no separate
66
130
  // "jsx" binary, only distinct typescript/tsx binaries.
67
131
  wasmFile: 'tree-sitter-javascript.wasm',
68
- definitions: ECMASCRIPT_DEFINITIONS,
132
+ definitions: JAVASCRIPT_DEFINITIONS,
69
133
  callTypes: ECMASCRIPT_CALL_TYPES,
70
134
  callFunctionField: 'function',
71
135
  importTypes: ECMASCRIPT_IMPORT_TYPES,
136
+ bareFunctionScopeTypes: ECMASCRIPT_BARE_FUNCTION_SCOPE_TYPES,
72
137
  },
73
138
  {
74
139
  language: 'python',
@@ -77,10 +142,15 @@ export const LANGUAGE_TABLE = [
77
142
  definitions: [
78
143
  { nodeType: 'function_definition', kind: 'function', nameField: 'name' },
79
144
  { nodeType: 'class_definition', kind: 'class', nameField: 'name' },
145
+ // Module- and class-level `x = ...`; `nameNodeTypes` rejects a tuple assignment (`a, b = 1, 2`,
146
+ // `left` is a `pattern_list`) and an attribute target (`self.x = 1`, `left` is an `attribute`) —
147
+ // verified against a real parse, not guessed.
148
+ { nodeType: 'assignment', kind: 'variable', nameField: 'left', nameNodeTypes: ['identifier'], scopeRestricted: true },
80
149
  ],
81
150
  callTypes: ['call'],
82
151
  callFunctionField: 'function',
83
152
  importTypes: ['import_statement', 'import_from_statement'],
153
+ bareFunctionScopeTypes: [],
84
154
  },
85
155
  {
86
156
  language: 'go',
@@ -90,10 +160,269 @@ export const LANGUAGE_TABLE = [
90
160
  { nodeType: 'function_declaration', kind: 'function', nameField: 'name' },
91
161
  { nodeType: 'method_declaration', kind: 'method', nameField: 'name' },
92
162
  { nodeType: 'type_spec', kind: 'type_alias', nameField: 'name' },
163
+ // Go allows `const a, b = 1, 2` — multiple names sharing one `const_spec`/`var_spec` node, each
164
+ // tagged with the same `name` field. `matchDefinition`'s single-name-field requirement rejects
165
+ // the multi-name form entirely rather than capture only the first — a partial, silently
166
+ // misleading result would be worse than none, per this file's existing "don't guess" precedent.
167
+ { nodeType: 'const_spec', kind: 'constant', nameField: 'name', scopeRestricted: true },
168
+ { nodeType: 'var_spec', kind: 'variable', nameField: 'name', scopeRestricted: true },
169
+ // A struct field (`field_declaration`) and an interface method signature (`method_spec`) both sit
170
+ // inside a `type_spec`'s `struct_type`/`interface_type` body — `type_spec` itself (kind
171
+ // `type_alias`) is not scope-restricted (a Go type can be declared inside a function, like `type
172
+ // Local struct {...}` in `func f() {...}`), so these must not be either: a scope-restricted rule
173
+ // here could never fire, since the enclosing `type_spec` always pushes `'other'` for its children
174
+ // regardless of where it itself sits. An embedded (anonymous) field — `Nested` with no separate
175
+ // name — has no child bound to the `name` field at all, so `matchDefinition`'s name-arity check
176
+ // already excludes it without a dedicated rule; verified against a real parse, not guessed.
177
+ { nodeType: 'field_declaration', kind: 'field', nameField: 'name' },
178
+ { nodeType: 'method_spec', kind: 'method', nameField: 'name' },
93
179
  ],
94
180
  callTypes: ['call_expression'],
95
181
  callFunctionField: 'function',
96
182
  importTypes: ['import_declaration'],
183
+ // A closure literal (`f := func() { ... }`) is never itself named, so it never matches a
184
+ // `DefinitionRule` — but a `var`/`const` inside its body is still function-local.
185
+ bareFunctionScopeTypes: ['func_literal'],
186
+ },
187
+ {
188
+ language: 'java',
189
+ extensions: ['.java'],
190
+ wasmFile: 'tree-sitter-java.wasm',
191
+ definitions: [
192
+ { nodeType: 'class_declaration', kind: 'class', nameField: 'name' },
193
+ // A record has no dedicated seam kind — `class` is the closest existing fit for a concrete type
194
+ // declaration with a body, and (like a class) it can `implements` an interface; see
195
+ // `javaHeritage` in extract.ts.
196
+ { nodeType: 'record_declaration', kind: 'class', nameField: 'name' },
197
+ { nodeType: 'interface_declaration', kind: 'interface', nameField: 'name' },
198
+ { nodeType: 'enum_declaration', kind: 'enum', nameField: 'name' },
199
+ // Unlike TypeScript's bare `property_identifier` enum member (see TYPESCRIPT_DEFINITIONS),
200
+ // Java's `enum_constant` already carries its own `name` field — verified against a real parse.
201
+ { nodeType: 'enum_constant', kind: 'enum_member', nameField: 'name' },
202
+ { nodeType: 'method_declaration', kind: 'method', nameField: 'name' },
203
+ // No dedicated `constructor` seam kind exists; a constructor is a method in every way this
204
+ // package's graph cares about.
205
+ { nodeType: 'constructor_declaration', kind: 'method', nameField: 'name' },
206
+ // A field (`field_declaration`) and a local variable (`local_variable_declaration`) both wrap one
207
+ // or more `variable_declarator` children directly, each carrying its own `name` field — unlike
208
+ // Go's `const_spec`/`var_spec`, a multi-name Java declaration (`int x = 1, y = 2;`) is not one
209
+ // ambiguous shared name field but several independent `variable_declarator` nodes, each visited
210
+ // (and named) on its own, so no `nameNodeTypes`-style rejection is needed here.
211
+ { nodeType: 'variable_declarator', kind: 'field', nameField: 'name', parentType: 'field_declaration', scopeRestricted: true },
212
+ { nodeType: 'variable_declarator', kind: 'variable', nameField: 'name', parentType: 'local_variable_declaration', scopeRestricted: true },
213
+ ],
214
+ callTypes: ['method_invocation'],
215
+ // Unlike the ECMAScript/Go/Python call node's `function` field (an expression this package's
216
+ // `calleeName` unwraps down to a simple name), Java's `method_invocation` already exposes the
217
+ // callee's simple name directly through its own `name` field — the receiver, if any, is a sibling
218
+ // `object` field extract.ts checks separately to tell a member call from a bare one. Verified
219
+ // against a real parse, not guessed.
220
+ callFunctionField: 'name',
221
+ importTypes: ['import_declaration'],
222
+ // A lambda body (`() -> { int x = 1; }`) is never itself named, so it never matches a
223
+ // `DefinitionRule` — but a local variable inside its block body is still function-local, the same
224
+ // reasoning `ECMASCRIPT_BARE_FUNCTION_SCOPE_TYPES` applies to an arrow function.
225
+ bareFunctionScopeTypes: ['lambda_expression'],
226
+ },
227
+ {
228
+ language: 'c',
229
+ extensions: ['.c', '.h'],
230
+ wasmFile: 'tree-sitter-c.wasm',
231
+ definitions: [
232
+ // No dedicated `union` seam kind exists; `struct` is the closest existing fit for a
233
+ // concrete-layout aggregate type, matching Java's record→`class` precedent.
234
+ { nodeType: 'struct_specifier', kind: 'struct', nameField: 'name' },
235
+ { nodeType: 'union_specifier', kind: 'struct', nameField: 'name' },
236
+ { nodeType: 'enum_specifier', kind: 'enum', nameField: 'name' },
237
+ // `enumerator` carries its own `name` field directly, unlike TypeScript's bare enum member —
238
+ // verified against a real parse, not guessed.
239
+ { nodeType: 'enumerator', kind: 'enum_member', nameField: 'name' },
240
+ // A typedef's `declarator` names the alias directly (`typedef struct Point PointT;`'s
241
+ // `declarator` is the plain `type_identifier` "PointT", no wrapper) — unlike a function or
242
+ // variable declarator, verified against a real parse, not guessed.
243
+ { nodeType: 'type_definition', kind: 'type_alias', nameField: 'declarator' },
244
+ // `function_definition`'s name sits behind a `pointer_declarator`/`function_declarator` chain of
245
+ // arbitrary depth — see `DECLARATOR_NAME_FIELD`.
246
+ { nodeType: 'function_definition', kind: 'function', nameField: DECLARATOR_NAME_FIELD },
247
+ // A struct/union member (`field_declaration`) — same declarator-nesting rule as a top-level
248
+ // variable, minus the multi-declarator ambiguity Go's `field_declaration` has none of either.
249
+ { nodeType: 'field_declaration', kind: 'field', nameField: DECLARATOR_NAME_FIELD },
250
+ // A top-level or function-local `int g = 1;`/`int x;` — both parse as `declaration`, distinguished
251
+ // only by where the walk currently sits, the same convention JS/Python/Go's local-variable rules
252
+ // already follow.
253
+ { nodeType: 'declaration', kind: 'variable', nameField: DECLARATOR_NAME_FIELD, scopeRestricted: true },
254
+ ],
255
+ callTypes: ['call_expression'],
256
+ callFunctionField: 'function',
257
+ importTypes: ['preproc_include'],
258
+ bareFunctionScopeTypes: [],
259
+ },
260
+ {
261
+ language: 'cpp',
262
+ extensions: ['.cpp', '.cc', '.cxx', '.hpp', '.hh', '.hxx'],
263
+ wasmFile: 'tree-sitter-cpp.wasm',
264
+ definitions: [
265
+ { nodeType: 'class_specifier', kind: 'class', nameField: 'name' },
266
+ { nodeType: 'struct_specifier', kind: 'struct', nameField: 'name' },
267
+ { nodeType: 'union_specifier', kind: 'struct', nameField: 'name' },
268
+ { nodeType: 'enum_specifier', kind: 'enum', nameField: 'name' },
269
+ { nodeType: 'enumerator', kind: 'enum_member', nameField: 'name' },
270
+ { nodeType: 'type_definition', kind: 'type_alias', nameField: 'declarator' },
271
+ // A method (including a constructor/destructor) is a `function_definition` directly inside a
272
+ // class/struct's `field_declaration_list` — the same node type a free function uses, so this rule
273
+ // (tried first) must claim that shape before the unrestricted `function` rule below falls through
274
+ // to everything else. Verified against a real parse, not guessed.
275
+ { nodeType: 'function_definition', kind: 'method', nameField: DECLARATOR_NAME_FIELD, parentType: 'field_declaration_list' },
276
+ { nodeType: 'function_definition', kind: 'function', nameField: DECLARATOR_NAME_FIELD },
277
+ // `field_declaration` doubles as a pure-virtual method signature (`virtual void v() = 0;` is still
278
+ // a `field_declaration`, just with a `function_declarator` for its `declarator`) — the same
279
+ // function-valued guard `ECMASCRIPT_DEFINITIONS`'s `variable_declarator` rule uses, tried first.
280
+ { nodeType: 'field_declaration', kind: 'method', nameField: DECLARATOR_NAME_FIELD, value: { field: 'declarator', types: ['function_declarator'] } },
281
+ { nodeType: 'field_declaration', kind: 'field', nameField: DECLARATOR_NAME_FIELD },
282
+ { nodeType: 'declaration', kind: 'variable', nameField: DECLARATOR_NAME_FIELD, scopeRestricted: true },
283
+ ],
284
+ callTypes: ['call_expression'],
285
+ callFunctionField: 'function',
286
+ importTypes: ['preproc_include'],
287
+ // A lambda body (`[](){ int x = 1; }`) is never itself named, so it never matches a `DefinitionRule`
288
+ // — but a local variable inside its block body is still function-local.
289
+ bareFunctionScopeTypes: ['lambda_expression'],
290
+ },
291
+ {
292
+ language: 'csharp',
293
+ extensions: ['.cs'],
294
+ wasmFile: 'tree-sitter-c_sharp.wasm',
295
+ definitions: [
296
+ { nodeType: 'class_declaration', kind: 'class', nameField: 'name' },
297
+ { nodeType: 'struct_declaration', kind: 'struct', nameField: 'name' },
298
+ // No dedicated seam kind exists for a record; `class` is the closest existing fit, matching Java's
299
+ // record→`class` precedent.
300
+ { nodeType: 'record_declaration', kind: 'class', nameField: 'name' },
301
+ { nodeType: 'interface_declaration', kind: 'interface', nameField: 'name' },
302
+ { nodeType: 'enum_declaration', kind: 'enum', nameField: 'name' },
303
+ { nodeType: 'enum_member_declaration', kind: 'enum_member', nameField: 'name' },
304
+ { nodeType: 'method_declaration', kind: 'method', nameField: 'name' },
305
+ // No dedicated `constructor` seam kind exists; both are a method in every way this package's graph
306
+ // cares about, matching Java's constructor_declaration→`method` precedent.
307
+ { nodeType: 'constructor_declaration', kind: 'method', nameField: 'name' },
308
+ { nodeType: 'destructor_declaration', kind: 'method', nameField: 'name' },
309
+ { nodeType: 'property_declaration', kind: 'property', nameField: 'name' },
310
+ // A field (`variable_declarator` two levels under a `field_declaration`) and a local variable
311
+ // (`variable_declarator` two levels under a `local_declaration_statement`) parse identically one
312
+ // level up (`variable_declaration`) — only the grandparent tells them apart. See
313
+ // `DefinitionRule.grandparentType`. Verified against a real parse, not guessed.
314
+ { nodeType: 'variable_declarator', kind: 'field', nameField: FIRST_CHILD_NAME_FIELD, grandparentType: 'field_declaration', scopeRestricted: true },
315
+ { nodeType: 'variable_declarator', kind: 'variable', nameField: FIRST_CHILD_NAME_FIELD, grandparentType: 'local_declaration_statement', scopeRestricted: true },
316
+ ],
317
+ callTypes: ['invocation_expression'],
318
+ callFunctionField: 'function',
319
+ importTypes: ['using_directive'],
320
+ // A lambda body (`() => { int x = 1; }`) is never itself named, so it never matches a
321
+ // `DefinitionRule` — but a local variable inside its block body is still function-local.
322
+ bareFunctionScopeTypes: ['lambda_expression', 'anonymous_method_expression'],
323
+ },
324
+ {
325
+ language: 'php',
326
+ extensions: ['.php'],
327
+ wasmFile: 'tree-sitter-php.wasm',
328
+ definitions: [
329
+ { nodeType: 'function_definition', kind: 'function', nameField: 'name' },
330
+ { nodeType: 'method_declaration', kind: 'method', nameField: 'name' },
331
+ { nodeType: 'class_declaration', kind: 'class', nameField: 'name' },
332
+ { nodeType: 'interface_declaration', kind: 'interface', nameField: 'name' },
333
+ { nodeType: 'trait_declaration', kind: 'trait', nameField: 'name' },
334
+ { nodeType: 'enum_declaration', kind: 'enum', nameField: 'name' },
335
+ // `enum_case` carries its own `name` field directly, unlike TypeScript's bare enum member —
336
+ // verified against a real parse, not guessed.
337
+ { nodeType: 'enum_case', kind: 'enum_member', nameField: 'name' },
338
+ // `const_element` (`const VERSION = "1"`) and `property_element` (`public string $name;`) neither
339
+ // bind their name to a field of their own — see `PHP_ELEMENT_NAME_FIELD`. Unlike a JS/Go/Python
340
+ // local binding, neither is `scopeRestricted`: a PHP `const`/property only legitimately appears at
341
+ // namespace scope or directly inside a class/interface/trait/enum body, and every non-class body
342
+ // here (`interface`/`trait`/`enum`) pushes `'other'` scope, not `'class'` — the same "must not be
343
+ // scopeRestricted" reasoning Go's struct-field rule documents, since a restricted rule could never
344
+ // fire inside one. The one shape this misses is already invalid PHP the grammar merely tolerates
345
+ // (a `const`/property statement directly inside a function body), so capturing it there costs
346
+ // nothing in practice.
347
+ { nodeType: 'const_element', kind: 'constant', nameField: PHP_ELEMENT_NAME_FIELD },
348
+ { nodeType: 'property_element', kind: 'field', nameField: PHP_ELEMENT_NAME_FIELD },
349
+ // `$fn = function () {}` / `$fn = fn($a) => $a` — the same function-valued-binding rule
350
+ // ECMASCRIPT_DEFINITIONS's `variable_declarator` follows, captured at any depth like a named
351
+ // function declaration (not `scopeRestricted`) since a named closure is as much a workspace symbol
352
+ // as one. Unlike ECMASCRIPT_DEFINITIONS, no bare-variable fallback rule follows it: PHP draws no
353
+ // `const`/`let`-style distinction for `$x = 5;` — it is an unremarkable, ubiquitous assignment, and
354
+ // capturing every one would flood `resolve.ts`'s cross-file name index with noise no caller wants.
355
+ {
356
+ nodeType: 'assignment_expression',
357
+ kind: 'function',
358
+ nameField: PHP_ELEMENT_NAME_FIELD,
359
+ value: { field: 'right', types: ['anonymous_function_creation_expression', 'arrow_function'] },
360
+ },
361
+ ],
362
+ callTypes: ['function_call_expression', 'member_call_expression', 'scoped_call_expression'],
363
+ callFunctionField: 'function',
364
+ // `member_call_expression` ($obj->method()) and `scoped_call_expression` (Class::method()) both name
365
+ // their callee through a `name` field, not `function` — verified against a real parse, not guessed.
366
+ callFunctionFieldByType: { member_call_expression: 'name', scoped_call_expression: 'name' },
367
+ importTypes: ['namespace_use_declaration'],
368
+ // A closure's block body (`function () { ... }`) can contain a statement, so it must flip scope like
369
+ // JS's `function_expression`; an arrow function's body is always a single expression (like Python's
370
+ // `lambda`, never a block), so it is omitted, matching that same precedent.
371
+ bareFunctionScopeTypes: ['anonymous_function_creation_expression'],
372
+ },
373
+ {
374
+ language: 'rust',
375
+ extensions: ['.rs'],
376
+ wasmFile: 'tree-sitter-rust.wasm',
377
+ definitions: [
378
+ { nodeType: 'struct_item', kind: 'struct', nameField: 'name' },
379
+ // No dedicated `union` seam kind exists; `struct` is the closest existing fit, matching C/C++'s
380
+ // same union→`struct` precedent — a Rust `union_item` shares `struct_item`'s exact body shape
381
+ // (`field_declaration_list`), verified against a real parse.
382
+ { nodeType: 'union_item', kind: 'struct', nameField: 'name' },
383
+ { nodeType: 'enum_item', kind: 'enum', nameField: 'name' },
384
+ // `enum_variant` carries its own `name` field directly, unlike TypeScript's bare enum member —
385
+ // verified against a real parse, not guessed.
386
+ { nodeType: 'enum_variant', kind: 'enum_member', nameField: 'name' },
387
+ { nodeType: 'trait_item', kind: 'trait', nameField: 'name' },
388
+ { nodeType: 'mod_item', kind: 'namespace', nameField: 'name' },
389
+ { nodeType: 'type_item', kind: 'type_alias', nameField: 'name' },
390
+ // A method — including a trait's default-body method — is a `function_item` directly inside an
391
+ // `impl_item`'s or `trait_item`'s `declaration_list`, the same node type a free function uses;
392
+ // mod's `declaration_list` shares that exact node type too, so only the grandparent (not the
393
+ // immediate parent) tells a method apart from a module-level function. Tried first, before the
394
+ // unrestricted `function` rule below falls through to everything else — the same precedent C++'s
395
+ // method rule follows for its own shared `function_definition` node type. Verified against a real
396
+ // parse, not guessed.
397
+ { nodeType: 'function_item', kind: 'method', nameField: 'name', grandparentType: 'impl_item' },
398
+ { nodeType: 'function_item', kind: 'method', nameField: 'name', grandparentType: 'trait_item' },
399
+ { nodeType: 'function_item', kind: 'function', nameField: 'name' },
400
+ // A trait method declared with no body (`fn area(&self) -> f64;`) parses as a distinct node type
401
+ // from one with a body, and only ever appears directly inside a trait's `declaration_list` — an
402
+ // `impl_item` must supply a body for every method it defines. Verified against a real parse.
403
+ { nodeType: 'function_signature_item', kind: 'method', nameField: 'name', grandparentType: 'trait_item' },
404
+ // A struct/union member — verified against a real parse, not guessed; unlike Go's/C++'s field
405
+ // rule this needs no declarator-unwrapping helper, since `field_declaration` binds its name
406
+ // directly to a `name` field.
407
+ { nodeType: 'field_declaration', kind: 'field', nameField: 'name' },
408
+ // A top-level, associated (inside `impl`/`trait`), or function-local `const`/`static` all parse
409
+ // identically — `scopeRestricted` excludes only the function-local case, the same convention
410
+ // Go's `const_spec`/`var_spec` already follow. No dedicated seam kind distinguishes a `static`
411
+ // from a `const`; `static` maps to `variable` (it names a single mutable-or-shared storage
412
+ // location, unlike a `const`'s compile-time-inlined value).
413
+ { nodeType: 'const_item', kind: 'constant', nameField: 'name', scopeRestricted: true },
414
+ { nodeType: 'static_item', kind: 'variable', nameField: 'name', scopeRestricted: true },
415
+ ],
416
+ callTypes: ['call_expression'],
417
+ callFunctionField: 'function',
418
+ importTypes: ['use_declaration'],
419
+ // A closure's body can be a block containing a statement (`|x| { let y = x; y }`) — unlike an
420
+ // ECMAScript arrow function, a bare-expression closure body (`|x| x`) is indistinguishable by node
421
+ // type alone (both are plain `closure_expression`), so this is not split into two cases; a
422
+ // function-local binding inside a bare-expression body is impossible syntactically anyway (no
423
+ // statement can appear there), so flipping scope unconditionally costs nothing. Verified against a
424
+ // real parse, not guessed.
425
+ bareFunctionScopeTypes: ['closure_expression'],
97
426
  },
98
427
  ];
99
428
  /**
package/lib/resolve.d.ts CHANGED
@@ -45,6 +45,9 @@ export interface GraphNode {
45
45
  readonly isExported: boolean;
46
46
  readonly isAsync: boolean;
47
47
  readonly isStatic: boolean;
48
+ /** Decorator names applied to this declaration — see `RawDefinition.decorators`. Empty for a file
49
+ * node and for every declaration this package does not currently extract decorators from. */
50
+ readonly decorators: readonly string[];
48
51
  readonly updatedAt: number;
49
52
  }
50
53
  /** One resolved graph edge. */
package/lib/resolve.js CHANGED
@@ -99,6 +99,28 @@ function buildNameIndex(nodesByFile) {
99
99
  }
100
100
  return index;
101
101
  }
102
+ /**
103
+ * A name index restricted to `class`/`interface` nodes, for `extends`/`implements` resolution.
104
+ * `extractHeritage` only ever names a base class or interface, never a function or variable — indexing
105
+ * only those kinds keeps a same-named function or module-level `const` (`packages/tree-sitter/src/
106
+ * languages.ts` extracts those now too) from ever winning a heritage reference by coincidence, tighter
107
+ * than `buildNameIndex`'s workspace-wide, any-kind index that call resolution already accepts.
108
+ */
109
+ function buildTypeNameIndex(nodesByFile) {
110
+ const index = new Map();
111
+ for (const nodes of nodesByFile.values()) {
112
+ for (const node of nodes) {
113
+ if (node.kind !== 'class' && node.kind !== 'interface')
114
+ continue;
115
+ const existing = index.get(node.name);
116
+ if (existing === undefined)
117
+ index.set(node.name, [node]);
118
+ else
119
+ existing.push(node);
120
+ }
121
+ }
122
+ return index;
123
+ }
102
124
  /** Resolve one file's raw imports against the indexed workspace. */
103
125
  function resolveImports(path, language, imports, indexed) {
104
126
  const byLocalName = new Map();
@@ -143,6 +165,7 @@ export function resolveWorkspace(files, now) {
143
165
  isExported: false,
144
166
  isAsync: false,
145
167
  isStatic: false,
168
+ decorators: [],
146
169
  updatedAt: now,
147
170
  };
148
171
  nodes.push(fileNode);
@@ -168,6 +191,7 @@ export function resolveWorkspace(files, now) {
168
191
  isExported: def.isExported,
169
192
  isAsync: def.isAsync,
170
193
  isStatic: def.isStatic,
194
+ decorators: def.decorators,
171
195
  updatedAt: now,
172
196
  });
173
197
  }
@@ -180,6 +204,7 @@ export function resolveWorkspace(files, now) {
180
204
  }
181
205
  }
182
206
  const nameIndex = buildNameIndex(nodesByFile);
207
+ const typeNameIndex = buildTypeNameIndex(nodesByFile);
183
208
  const unresolved = [];
184
209
  for (const file of files) {
185
210
  const bindings = resolveImports(file.path, file.language, file.extraction.imports, indexed);
@@ -190,7 +215,7 @@ export function resolveWorkspace(files, now) {
190
215
  const importedLocalNames = new Set(file.extraction.imports.map(binding => binding.localName));
191
216
  for (const call of file.extraction.calls) {
192
217
  const callerId = call.callerKey === null ? mustGet(fileNodeId, file.path) : mustGet(keyToId, call.callerKey);
193
- const resolved = resolveCall(call.calleeName, bindings, nodesByFile, nameIndex);
218
+ const resolved = resolveName(call.calleeName, bindings, nodesByFile, nameIndex);
194
219
  if (resolved === undefined) {
195
220
  const likelyExternal = call.isMemberCall || importedLocalNames.has(call.calleeName);
196
221
  unresolved.push({ source: callerId, filePath: file.path, calleeName: call.calleeName, line: call.line, col: call.column, likelyExternal });
@@ -198,11 +223,27 @@ export function resolveWorkspace(files, now) {
198
223
  }
199
224
  edges.push({ source: callerId, target: resolved, kind: 'calls', line: call.line, col: call.column, provenance: 'tree-sitter' });
200
225
  }
226
+ // An unresolved `extends`/`implements` reference is dropped silently, not routed through
227
+ // `unresolved` — that array (and the `unresolvedCount`/`unresolvedLikelyInternalCount` split built
228
+ // on it) is a calibrated signal specifically about call-resolution gaps; folding in a different edge
229
+ // kind would change what that count measures.
230
+ for (const ref of file.extraction.heritage) {
231
+ const resolved = resolveName(ref.targetName, bindings, nodesByFile, typeNameIndex);
232
+ if (resolved === undefined)
233
+ continue;
234
+ edges.push({ source: mustGet(keyToId, ref.sourceKey), target: resolved, kind: ref.relation, provenance: 'tree-sitter' });
235
+ }
201
236
  }
202
237
  return { nodes, edges, unresolved };
203
238
  }
204
- /** Resolve one call's callee name to exactly one declaration, or `undefined` per rule 3. */
205
- function resolveCall(calleeName, bindings, nodesByFile, nameIndex) {
239
+ /**
240
+ * Resolve a bare name to exactly one declaration, or `undefined` per rule 3 — shared by call
241
+ * resolution (against the workspace-wide, any-kind `nameIndex`) and `extends`/`implements` resolution
242
+ * (against the `class`/`interface`-only {@link buildTypeNameIndex}); the two-tier rule itself (an
243
+ * imported binding wins if unambiguous in its target file, else a workspace-wide-unique name wins) does
244
+ * not depend on which candidate pool the caller passes in.
245
+ */
246
+ function resolveName(calleeName, bindings, nodesByFile, nameIndex) {
206
247
  const bound = bindings.byLocalName.get(calleeName);
207
248
  if (bound !== undefined) {
208
249
  const targetNodes = mustGet(nodesByFile, bound.targetFile);
package/lib/schema.js CHANGED
@@ -75,10 +75,10 @@ export async function writeGraph(databasePath, input) {
75
75
  .run(SCHEMA_VERSION, input.indexedAt, 'dsh-codegraph-tree-sitter');
76
76
  const insertNode = db.prepare(`INSERT INTO nodes
77
77
  (id, kind, name, qualified_name, file_path, language, start_line, end_line, start_column,
78
- end_column, is_exported, is_async, is_static, updated_at)
79
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
78
+ end_column, is_exported, is_async, is_static, decorators, updated_at)
79
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
80
80
  for (const node of input.nodes) {
81
- insertNode.run(node.id, node.kind, node.name, node.qualifiedName, node.filePath, node.language, node.startLine, node.endLine, node.startColumn, node.endColumn, node.isExported ? 1 : 0, node.isAsync ? 1 : 0, node.isStatic ? 1 : 0, node.updatedAt);
81
+ insertNode.run(node.id, node.kind, node.name, node.qualifiedName, node.filePath, node.language, node.startLine, node.endLine, node.startColumn, node.endColumn, node.isExported ? 1 : 0, node.isAsync ? 1 : 0, node.isStatic ? 1 : 0, JSON.stringify(node.decorators), node.updatedAt);
82
82
  }
83
83
  const insertEdge = db.prepare('INSERT INTO edges (source, target, kind, line, col, provenance) VALUES (?, ?, ?, ?, ?, ?)');
84
84
  for (const edge of input.edges) {