graph-indexer 2.1.1 → 3.0.1

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.
Files changed (131) hide show
  1. package/CHANGELOG.md +312 -1
  2. package/README.md +236 -312
  3. package/SECURITY.md +53 -58
  4. package/bin/graph-indexer.mjs +251 -0
  5. package/integrations/claude-code/.claude-plugin/plugin.json +9 -0
  6. package/integrations/claude-code/.mcp.json +8 -0
  7. package/integrations/claude-code/agents/code-structure.md +22 -0
  8. package/integrations/claude-code/hooks/gi-hook.mjs +103 -0
  9. package/integrations/claude-code/hooks/hooks.json +16 -0
  10. package/integrations/opencode/graph-indexer.js +129 -0
  11. package/package.json +32 -109
  12. package/src/cli/helper.mjs +67 -0
  13. package/src/cli/hook.mjs +366 -0
  14. package/src/cli/init.mjs +272 -0
  15. package/src/cli/resident.mjs +185 -0
  16. package/src/index/discover.mjs +128 -0
  17. package/src/index/fingerprint.mjs +30 -0
  18. package/src/index/graph.mjs +61 -0
  19. package/src/index/indexer.mjs +476 -0
  20. package/src/index/modules.mjs +332 -0
  21. package/src/index/resolver.mjs +961 -0
  22. package/src/mcp/render.mjs +60 -0
  23. package/src/mcp/server.mjs +147 -0
  24. package/src/mcp/tools.mjs +974 -0
  25. package/src/parse/arity.mjs +153 -0
  26. package/src/parse/extract.mjs +1038 -0
  27. package/src/parse/lang/csharp.mjs +106 -0
  28. package/src/parse/lang/ecma.mjs +438 -0
  29. package/src/parse/lang/go.mjs +194 -0
  30. package/src/parse/lang/jvm.mjs +118 -0
  31. package/src/parse/lang/kotlin.mjs +97 -0
  32. package/src/parse/lang/misc.mjs +115 -0
  33. package/src/parse/lang/native.mjs +126 -0
  34. package/src/parse/lang/python.mjs +153 -0
  35. package/src/parse/lang/rust.mjs +157 -0
  36. package/src/parse/lang/scripting.mjs +219 -0
  37. package/src/parse/languages.mjs +48 -0
  38. package/src/parse/runtime.mjs +56 -0
  39. package/src/query/check.mjs +345 -0
  40. package/src/query/facts.mjs +323 -0
  41. package/src/query/intel.mjs +683 -0
  42. package/src/query/read.mjs +81 -0
  43. package/src/query/testcmd.mjs +130 -0
  44. package/src/query/textsearch.mjs +178 -0
  45. package/src/search/search.mjs +210 -0
  46. package/src/search/tokenize.mjs +188 -0
  47. package/src/store/db.mjs +169 -0
  48. package/src/util/git.mjs +30 -0
  49. package/src/util/paths.mjs +63 -0
  50. package/src/util/regex.mjs +2 -0
  51. package/vendor/grammars/bash.wasm.gz +0 -0
  52. package/vendor/grammars/c.wasm.gz +0 -0
  53. package/vendor/grammars/c_sharp.wasm.gz +0 -0
  54. package/vendor/grammars/cpp.wasm.gz +0 -0
  55. package/vendor/grammars/css.wasm.gz +0 -0
  56. package/vendor/grammars/go.wasm.gz +0 -0
  57. package/vendor/grammars/java.wasm.gz +0 -0
  58. package/vendor/grammars/javascript.wasm.gz +0 -0
  59. package/vendor/grammars/kotlin.wasm.gz +0 -0
  60. package/vendor/grammars/manifest.json +148 -0
  61. package/vendor/grammars/php.wasm.gz +0 -0
  62. package/vendor/grammars/python.wasm.gz +0 -0
  63. package/vendor/grammars/ruby.wasm.gz +0 -0
  64. package/vendor/grammars/rust.wasm.gz +0 -0
  65. package/vendor/grammars/scala.wasm.gz +0 -0
  66. package/vendor/grammars/tsx.wasm.gz +0 -0
  67. package/vendor/grammars/typescript.wasm.gz +0 -0
  68. package/vendor/web-tree-sitter/web-tree-sitter.js +3980 -0
  69. package/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
  70. package/cli-ui.mjs +0 -78
  71. package/colbert.mjs +0 -168
  72. package/config.mjs +0 -494
  73. package/daemon-ctl.mjs +0 -175
  74. package/daemon-lock.mjs +0 -69
  75. package/embedders/python/mlx_embed_server.py +0 -61
  76. package/embedders/python/requirements-mlx.txt +0 -5
  77. package/embedders/setup-mlx.mjs +0 -110
  78. package/embeddings.mjs +0 -571
  79. package/engine/binary.mjs +0 -437
  80. package/engine/memory.mjs +0 -1003
  81. package/engine/sqlite.mjs +0 -1136
  82. package/enrichment.mjs +0 -521
  83. package/git-signals.mjs +0 -248
  84. package/indexer.mjs +0 -546
  85. package/init.mjs +0 -1809
  86. package/layout.mjs +0 -123
  87. package/mcp/centrality.mjs +0 -117
  88. package/mcp/format.mjs +0 -61
  89. package/mcp/resolver.mjs +0 -116
  90. package/mcp/symbolgraph.mjs +0 -137
  91. package/mcp/taint.mjs +0 -225
  92. package/mcp/tools.mjs +0 -1582
  93. package/mcp/topology.mjs +0 -676
  94. package/mcp-server.mjs +0 -197
  95. package/parse/extractor.mjs +0 -628
  96. package/parse/imports.mjs +0 -365
  97. package/parse/interprocedural.mjs +0 -134
  98. package/parse/languages.mjs +0 -253
  99. package/parse/metadata.mjs +0 -701
  100. package/parse/routes.mjs +0 -327
  101. package/parse/scip.mjs +0 -391
  102. package/parse/taint-patterns.mjs +0 -119
  103. package/prompts/CORE.md +0 -91
  104. package/prompts/DOMAIN_TEMPLATE.md +0 -46
  105. package/prompts/INTEGRATION.md +0 -123
  106. package/prompts/frameworks/ANDROID.md +0 -17
  107. package/prompts/frameworks/ASPNET_CORE.md +0 -19
  108. package/prompts/frameworks/FASTAPI_DJANGO.md +0 -20
  109. package/prompts/frameworks/LARAVEL_SYMFONY.md +0 -22
  110. package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +0 -19
  111. package/prompts/frameworks/RAILS.md +0 -20
  112. package/prompts/frameworks/REACT.md +0 -20
  113. package/prompts/frameworks/SPRING_BOOT.md +0 -20
  114. package/prompts/languages/BASH.md +0 -22
  115. package/prompts/languages/C.md +0 -21
  116. package/prompts/languages/CSHARP.md +0 -24
  117. package/prompts/languages/CSS_SCSS.md +0 -22
  118. package/prompts/languages/GO.md +0 -24
  119. package/prompts/languages/JAVA.md +0 -23
  120. package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +0 -24
  121. package/prompts/languages/KOTLIN.md +0 -24
  122. package/prompts/languages/PHP.md +0 -23
  123. package/prompts/languages/PYTHON.md +0 -23
  124. package/prompts/languages/RUBY.md +0 -23
  125. package/prompts/languages/RUST.md +0 -21
  126. package/prompts/languages/SWIFT.md +0 -21
  127. package/seal.mjs +0 -400
  128. package/search-core.mjs +0 -777
  129. package/search-sparse.mjs +0 -183
  130. package/storage.mjs +0 -73
  131. package/watch-daemon.mjs +0 -289
@@ -0,0 +1,1038 @@
1
+ /**
2
+ * Language-agnostic extraction engine. One tree-sitter query per grammar (compiled once)
3
+ * yields definitions, references, imports and type evidence; this module turns the raw
4
+ * captures into:
5
+ *
6
+ * symbols — every definition (functions, methods, classes, fields, ...), nested with a
7
+ * qualified name (Class.method), signature, doc, exported/visibility flags;
8
+ * refs — every reference occurrence with its position, enclosing symbol, receiver
9
+ * descriptor and (when inferable intra-file) the receiver's static type;
10
+ * imports — the file's import bindings (resolved against the repo by the resolver).
11
+ *
12
+ * Only captured nodes cross the WASM boundary, which keeps extraction fast.
13
+ */
14
+ import { compileQuery, createParser, loadGrammar } from './runtime.mjs';
15
+ import { callArgc } from './arity.mjs';
16
+
17
+ const MAX_SIG = 240;
18
+ const MEMBER_PARENT = new Set(['class', 'interface', 'struct', 'trait', 'impl', 'enum', 'object', 'module']);
19
+ const REF_PRIORITY = { decorator: 6, inherit: 5, new: 4, call: 3, import: 2, type: 1, value: 0, read: -1 };
20
+ const MAX_DOC = 600;
21
+
22
+ const compiled = new Map(); // spec.id -> { language, query, parser }
23
+
24
+ export async function prepareLanguage(spec) {
25
+ let c = compiled.get(spec.id);
26
+ if (!c) {
27
+ const language = await loadGrammar(spec.grammar);
28
+ const query = compileQuery(language, spec.query, spec.id);
29
+ c = { language, query, parser: createParser(language) };
30
+ compiled.set(spec.id, c);
31
+ }
32
+ return c;
33
+ }
34
+
35
+ function collapse(s) { return s.replace(/\s+/g, ' ').trim(); }
36
+
37
+ /** Signature = definition text up to its body, whitespace-collapsed. */
38
+ function signatureOf(defNode, spec, source) {
39
+ // `const f = (a, b) => {…}` / `f = lambda …`: the function is the value, its body ends the signature
40
+ const body = (spec.bodyField ? defNode.childForFieldName(spec.bodyField) : null)
41
+ ?? defNode.childForFieldName('value')?.childForFieldName('body') ?? null;
42
+ let end = body ? body.startIndex : defNode.endIndex;
43
+ let text = source.slice(defNode.startIndex, end);
44
+ if (!body) {
45
+ const nl = text.indexOf('\n');
46
+ if (nl > 0) text = text.slice(0, nl);
47
+ }
48
+ text = collapse(text).replace(/[{:=\s]+$/, '');
49
+ if (spec.signatureCleanup) text = spec.signatureCleanup(text, defNode);
50
+ return text.length > MAX_SIG ? text.slice(0, MAX_SIG - 1) + '…' : text;
51
+ }
52
+
53
+ function cleanComment(text) {
54
+ return text
55
+ .replace(/^\s*(\/\*\*?|\*\/|\/\/\/?!?|#+|--)/gm, '')
56
+ .replace(/\*\/\s*$/gm, '')
57
+ .replace(/^\s*\*\s?/gm, '')
58
+ .split('\n').map(l => l.trim()).filter(Boolean).join(' ');
59
+ }
60
+
61
+ /** Doc = contiguous comments immediately above the definition (or its decorators/export wrapper). */
62
+ function docOf(defNode, spec, source) {
63
+ if (spec.docOf) {
64
+ const d = spec.docOf(defNode, source);
65
+ if (d != null) return d.length > MAX_DOC ? d.slice(0, MAX_DOC - 1) + '…' : d;
66
+ }
67
+ let anchor = defNode;
68
+ // climb through wrappers that sit between comment and definition
69
+ while (anchor.parent && spec.docWrappers?.has(anchor.parent.type)) anchor = anchor.parent;
70
+ if (anchor.type === 'variable_declarator') anchor = anchor.parent;
71
+ if (anchor.parent?.type === 'export_statement') anchor = anchor.parent;
72
+ const parts = [];
73
+ let prev = anchor.previousNamedSibling ?? anchor.previousSibling;
74
+ let expectRow = anchor.startPosition.row;
75
+ // attributes/annotations written between the doc comment and the definition (#[inline], [Attr])
76
+ while (prev && spec.skipBeforeDoc?.has(prev.type)) { expectRow = prev.startPosition.row; prev = prev.previousNamedSibling ?? prev.previousSibling; }
77
+ while (prev && spec.commentTypes.includes(prev.type) && expectRow - prev.endPosition.row <= 2) {
78
+ parts.unshift(prev.text);
79
+ expectRow = prev.startPosition.row;
80
+ prev = prev.previousNamedSibling ?? prev.previousSibling;
81
+ }
82
+ if (!parts.length) return '';
83
+ const doc = cleanComment(parts.join('\n'));
84
+ return doc.length > MAX_DOC ? doc.slice(0, MAX_DOC - 1) + '…' : doc;
85
+ }
86
+
87
+ // Expression wrappers that do not change the static type of what they wrap.
88
+ const PASS_THROUGH = new Set(['parenthesized_expression', 'non_null_expression', 'await_expression', 'await', 'try_expression', 'reference_expression']);
89
+ // Literal constructions: `Foo{…}` (Go), `Foo { … }` (Rust), `new Foo(…)` (Java/C#/PHP)
90
+ const CONSTRUCTIONS = { composite_literal: 'type', struct_expression: 'name', object_creation_expression: 'type', new_expression: 'constructor' };
91
+ // Casts / assertions: the asserted type is the static type.
92
+ const CASTS = new Set(['as_expression', 'satisfies_expression', 'cast_expression', 'type_assertion_expression']);
93
+ // Literal values: their members are the language's (`'x'.trim()`); null-ish ones carry no type.
94
+ const LITERAL_NODES = new Set(['string', 'template_string', 'number', 'true', 'false', 'string_literal', 'integer', 'float', 'integer_literal',
95
+ 'decimal_integer_literal', 'decimal_floating_point_literal', 'interpreted_string_literal', 'raw_string_literal', 'int_literal', 'float_literal',
96
+ 'boolean', 'boolean_literal', 'char_literal', 'character_literal', 'real_literal', 'concatenated_string', 'rune_literal', 'regex']);
97
+ const NULL_NODES = new Set(['null', 'undefined', 'none', 'nil', 'null_literal']);
98
+ // Collection methods whose callback's first parameter is an element (typed by the collection).
99
+ const ELEMENT_WISE = new Set(['forEach', 'map', 'filter', 'find', 'findLast', 'findIndex', 'findLastIndex', 'some', 'every', 'flatMap']);
100
+ // Parameters declared inside a type — `cb: (app: App) => void`, interface method signatures —
101
+ // name no variable in any scope; binding them would shadow or contradict real locals.
102
+ const TYPE_CONTEXTS = new Set(['function_type', 'constructor_type', 'method_signature', 'abstract_method_signature', 'call_signature', 'construct_signature', 'function_signature', 'index_signature', 'type_annotation']);
103
+ const FUNCTION_NODES = new Set(['function_declaration', 'function_expression', 'arrow_function', 'method_definition', 'generator_function', 'generator_function_declaration']);
104
+ function inTypeContext(node) {
105
+ for (let p = node.parent, g = 0; p && g < 12; p = p.parent, g++) {
106
+ if (TYPE_CONTEXTS.has(p.type)) return true;
107
+ if (FUNCTION_NODES.has(p.type)) return false;
108
+ }
109
+ return false;
110
+ }
111
+ const CHAIN_RE = /^[A-Za-z_$@][\w$]*(?:\(\)|\[\])*(?:\.[A-Za-z_$][\w$]*(?:\(\)|\[\])*){0,7}$/;
112
+
113
+ /**
114
+ * `a.b(x).c[i]` → `a.b().c[]`: argument lists and subscripts collapsed, operators normalised
115
+ * (`?.` `!.` `->` `::` `!!`), string literals neutralised. Null when not a plain access chain.
116
+ */
117
+ function simplifyChain(text) {
118
+ if (text.length > 4000) return null;
119
+ let s = text.replace(/\bawait\s+/g, '').replace(/^new\s+/, '');
120
+ if (/["'`]/.test(s)) s = s.replace(/"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|`(?:[^`\\]|\\.)*`/g, '0');
121
+ // `(await a.b()).c()`: a leading parenthesised group followed by a member access is its chain
122
+ for (let g = 0; g < 3 && s.startsWith('('); g++) {
123
+ let depth = 0, k = -1;
124
+ for (let i = 0; i < s.length && k < 0; i++) {
125
+ if (s[i] === '(') depth++;
126
+ else if (s[i] === ')' && --depth === 0) k = i;
127
+ }
128
+ if (k < 0 || !/^\s*[?!]*\s*\./.test(s.slice(k + 1))) break;
129
+ const inner = s.slice(1, k).trim();
130
+ if (/\s(as|satisfies)\s/.test(inner)) return null; // a cast names a type, not a chain
131
+ s = inner.replace(/^new\s+/, '') + s.slice(k + 1);
132
+ }
133
+ // a fluent chain formatted one call per line is long mostly in whitespace: measure it without
134
+ s = s.replace(/\s+/g, '').replace(/<[^<>()]*>(?=\()/g, '');
135
+ if (s.length > 600) return null;
136
+ for (let i = 0; i < 12 && /[()[\]]/.test(s); i++) {
137
+ const next = s.replace(/\([^()[\]]*\)/g, '\u0001').replace(/\[[^()[\]]*\]/g, '\u0002');
138
+ if (next === s) return null; // unbalanced
139
+ s = next;
140
+ }
141
+ s = s.replace(/\u0001/g, '()').replace(/\u0002/g, '[]')
142
+ .replace(/!!/g, '').replace(/\?\.|!\.|->|::/g, '.').replace(/[!?]+(?=\.|$)/g, '');
143
+ return CHAIN_RE.test(s) ? s : null;
144
+ }
145
+
146
+ /** The cast at the head of a member/call chain, and where it ends (its parentheses included). */
147
+ function castHead(node) {
148
+ let n = node;
149
+ for (let k = 0; k < 16 && n && n.namedChildCount; k++) {
150
+ if (k > 0 && CASTS.has(n.type)) {
151
+ const end = n.parent?.type === 'parenthesized_expression' && n.parent.startIndex === n.startIndex - 1 ? n.parent.endIndex : n.endIndex;
152
+ return end < node.endIndex ? { node: n, end } : null;
153
+ }
154
+ const c = n.namedChildren[0];
155
+ if (!c || c.startIndex > n.startIndex + 1) return null; // not at the head of the chain
156
+ n = c;
157
+ }
158
+ return null;
159
+ }
160
+
161
+ /**
162
+ * Compact descriptor of an expression, used for receivers and for variable initialisers:
163
+ * `this` · `super` · `x` · `a.b().c[]` (access chain) · `new X` · `as T` (cast) · `?` (opaque)
164
+ */
165
+ function receiverDescriptor(node, spec) {
166
+ if (!node) return '';
167
+ for (let i = 0; i < 6 && node.namedChildCount >= 1; i++) {
168
+ if (PASS_THROUGH.has(node.type)) node = node.namedChildren[0];
169
+ else if (node.type === 'unary_expression' && node.childForFieldName('operator')?.text === '&') node = node.childForFieldName('operand') ?? node.namedChildren[0]; // Go &T{}
170
+ else break;
171
+ }
172
+ const t = node.type;
173
+ if (CONSTRUCTIONS[t]) {
174
+ const tn = node.childForFieldName(CONSTRUCTIONS[t]);
175
+ // `new Map<string, Foo>()`: the type arguments say what the collection holds
176
+ const targs = node.childForFieldName('type_arguments');
177
+ const tt = tn ? normalizeType(targs ? tn.text + targs.text : tn.text, spec) : null;
178
+ if (tt) return 'new ' + tt;
179
+ }
180
+ const text = node.text;
181
+ if (NULL_NODES.has(t)) return 'null';
182
+ if (LITERAL_NODES.has(t)) return '!';
183
+ if (spec.selfNames.includes(text) || t === 'this' || t === 'self') return 'this';
184
+ if (t === 'super' || text === 'super' || /^super\s*\([^()]*\)$/.test(text)) return 'super'; // Python `super()` / `super(Cls, self)`
185
+ if (CASTS.has(t)) {
186
+ const tn = node.childForFieldName('type') ?? node.namedChildren[node.namedChildCount - 1];
187
+ const tt = tn && tn !== node.namedChildren[0] ? normalizeType(tn.text, spec) : null;
188
+ return tt ? 'as ' + tt : '?';
189
+ }
190
+ if (spec.receiverDescriptor) {
191
+ const d = spec.receiverDescriptor(node);
192
+ if (d !== undefined) return d;
193
+ }
194
+ // a chain that starts with a cast: `val.(Module).CaddyModule().ID`, `(x as Foo).bar()` → `(T).a().b`
195
+ const cast = castHead(node);
196
+ if (cast) {
197
+ const tn = cast.node.childForFieldName('type') ?? cast.node.namedChildren[cast.node.namedChildCount - 1];
198
+ const tt = tn && tn !== cast.node.namedChildren[0] ? normalizeType(tn.text, spec) : null;
199
+ const tail = tt && !/(\[\]|\{\})$/.test(tt) ? simplifyChain('x' + text.slice(cast.end - node.startIndex)) : null;
200
+ return tail?.startsWith('x.') ? `(${tt})${tail.slice(1)}` : '?';
201
+ }
202
+ if (/^[A-Za-z_$][\w$]*$/.test(text)) return text;
203
+ const ctor = /^new\s+([A-Za-z_$][\w$.]*)\s*(?:<[^()]*>)?\s*(?:\([^]*\))?$/.exec(text);
204
+ if (ctor && !/\)\s*[.[]/.test(text)) return 'new ' + ctor[1];
205
+ const chain = simplifyChain(text);
206
+ if (!chain) return '?';
207
+ const parts = chain.split('.');
208
+ if (spec.selfNames.includes(parts[0]) || parts[0] === 'this' || parts[0] === '$this') parts[0] = 'this';
209
+ return parts.join('.');
210
+ }
211
+
212
+ /** `e.key` for a descriptor `e`, when e names a value (a variable, a chain, a cast). */
213
+ function memberOfDescriptor(d, key) {
214
+ if (!d || d === '?' || d === '!' || d === 'null' || d.startsWith('new ')) return null;
215
+ if (d.startsWith('as ')) return `(${d.slice(3)}).${key}`;
216
+ return `${d}.${key}`;
217
+ }
218
+
219
+ /** The type written for member `key` in the text of an inline object type (`{ key?: T; … }`). */
220
+ function inlineMemberType(text, key) {
221
+ for (const part of splitTop(text.slice(1, -1), ';').flatMap(x => splitTop(x, ','))) {
222
+ const m = /^\s*(?:readonly\s+)?([A-Za-z_$][\w$]*)\??\s*:([^]*)$/.exec(part);
223
+ if (m && m[1] === key) return m[2].trim();
224
+ }
225
+ return null;
226
+ }
227
+
228
+ /** The type written for member `key` in an inline object type (`{ key: T; … }`), if any. */
229
+ function memberTypeIn(typeNode, key) {
230
+ if (typeNode.type !== 'object_type') return null;
231
+ for (const m of typeNode.namedChildren) {
232
+ if (m.type !== 'property_signature' || m.childForFieldName('name')?.text !== key) continue;
233
+ return m.childForFieldName('type')?.namedChildren[0]?.text ?? null;
234
+ }
235
+ return null;
236
+ }
237
+
238
+ /** The descriptor of a collection whose elements are wanted: a copy (`Array.from(xs)`, `[...xs]`) has those of xs. */
239
+ function elementsDescriptor(node, spec) {
240
+ return receiverDescriptor(spec.collectionSource?.(node) ?? node, spec);
241
+ }
242
+
243
+ // ── type tests that narrow a variable (`x instanceof T`, `isinstance(x, T)`) ──
244
+ const EXIT_STATEMENTS = new Set(['return_statement', 'throw_statement', 'raise_statement', 'continue_statement', 'break_statement']);
245
+ const opText = (n) => n.childForFieldName('operator')?.text;
246
+ const isNegation = (n) => (n.type === 'unary_expression' && opText(n) === '!') || n.type === 'not_operator';
247
+ const isLogical = (n, ops) => (n.type === 'binary_expression' || n.type === 'boolean_operator') && ops.includes(opText(n));
248
+
249
+ /** A block (or single statement) whose last statement leaves it: return / throw / raise / continue / break. */
250
+ function alwaysExits(stmt) {
251
+ if (!stmt) return false;
252
+ if (EXIT_STATEMENTS.has(stmt.type)) return true;
253
+ if (stmt.type !== 'statement_block' && stmt.type !== 'block') return false;
254
+ const body = stmt.namedChildren.filter(c => c.type !== 'comment');
255
+ return body.length > 0 && EXIT_STATEMENTS.has(body[body.length - 1].type);
256
+ }
257
+
258
+ /**
259
+ * Source ranges where a type test proves a variable's type: `if (x instanceof T) {…}`, the rest of
260
+ * a block after `if (!(x instanceof T)) return …`, `x instanceof T && x.m()`, ternaries, Python's
261
+ * `isinstance(x, T)` in the same positions. Returns [{ subject node, type node, from, to }].
262
+ */
263
+ function typeNarrowings(root, source) {
264
+ const out = [];
265
+ for (const m of source.matchAll(/\binstanceof\b|\bisinstance\s*\(/g)) {
266
+ let test = root.descendantForIndex(m.index);
267
+ for (let i = 0; test && i < 3 && !(test.type === 'binary_expression' || test.type === 'call'); i++) test = test.parent;
268
+ if (!test) continue;
269
+ let subject, type;
270
+ if (test.type === 'binary_expression' && opText(test) === 'instanceof') {
271
+ subject = test.childForFieldName('left'); type = test.childForFieldName('right');
272
+ } else if (test.type === 'call' && test.childForFieldName('function')?.text === 'isinstance') {
273
+ const args = test.childForFieldName('arguments')?.namedChildren ?? [];
274
+ if (args.length !== 2 || args[1].type === 'tuple') continue;
275
+ [subject, type] = args;
276
+ } else continue;
277
+ if (!subject || !type) continue;
278
+ const add = (node) => { if (node) out.push({ subject, type, from: node.startIndex, to: node.endIndex }); };
279
+ // climb through the condition, tracking the truth value of `n` under which the test holds
280
+ let n = test, holdsWhen = true;
281
+ for (let p = n.parent; p; n = p, p = p.parent) {
282
+ if (p.type === 'parenthesized_expression') continue;
283
+ if (isNegation(p)) { holdsWhen = !holdsWhen; continue; }
284
+ const left = p.childForFieldName('left');
285
+ if (isLogical(p, ['&&', 'and'])) {
286
+ if (!holdsWhen) break;
287
+ if (left && left.id === n.id) add(p.childForFieldName('right'));
288
+ continue;
289
+ }
290
+ if (isLogical(p, ['||', 'or'])) {
291
+ if (holdsWhen) break;
292
+ if (left && left.id === n.id) add(p.childForFieldName('right'));
293
+ continue;
294
+ }
295
+ const cond = p.childForFieldName('condition');
296
+ if (p.type === 'if_statement' || p.type === 'elif_clause' || p.type === 'while_statement') {
297
+ if (!cond || cond.id !== n.id) break;
298
+ const cons = p.childForFieldName('consequence') ?? p.childForFieldName('body');
299
+ if (holdsWhen) { add(cons); break; }
300
+ const alts = p.childrenForFieldName?.('alternative') ?? [];
301
+ for (const a of alts) if (a.type === 'else_clause') add(a);
302
+ // `if (!(x instanceof T)) return;` — the rest of the enclosing block
303
+ if (p.type === 'if_statement' && !alts.length && alwaysExits(cons) && p.parent) out.push({ subject, type, from: p.endIndex, to: p.parent.endIndex });
304
+ break;
305
+ }
306
+ if (p.type === 'ternary_expression') {
307
+ if (!cond || cond.id !== n.id) break;
308
+ add(p.childForFieldName(holdsWhen ? 'consequence' : 'alternative'));
309
+ break;
310
+ }
311
+ if (p.type === 'conditional_expression') { // Python: a if cond else b
312
+ const [a, c, b] = p.namedChildren;
313
+ if (!c || c.id !== n.id) break;
314
+ add(holdsWhen ? a : b);
315
+ break;
316
+ }
317
+ break;
318
+ }
319
+ }
320
+ return out;
321
+ }
322
+
323
+ /**
324
+ * Extract symbols/refs/imports for one file.
325
+ * @returns {{ symbols: object[], refs: object[], imports: object[], errors: number }}
326
+ */
327
+ export async function extractFile(spec, source, relPath) {
328
+ const { query, parser } = await prepareLanguage(spec);
329
+ const tree = parser.parse(source);
330
+ try {
331
+ return extractFromTree(spec, query, tree, source, relPath);
332
+ } finally {
333
+ tree.delete();
334
+ }
335
+ }
336
+
337
+ // query time can grow super-linearly with the syntax (60,000 chained calls in a 360 KB file take
338
+ // minutes): such a file is given up after this long instead of stalling the whole index
339
+ const QUERY_BUDGET_MS = 15_000;
340
+
341
+ function extractFromTree(spec, query, tree, source, relPath) {
342
+ const root = tree.rootNode;
343
+ const deadline = Date.now() + QUERY_BUDGET_MS;
344
+ let gaveUp = false;
345
+ const matches = query.matches(root, { progressCallback: () => (gaveUp = Date.now() > deadline) });
346
+ if (gaveUp) throw new Error(`gave up after ${QUERY_BUDGET_MS / 1000} s of queries (pathological syntax)`);
347
+
348
+ const defs = []; // { node, nameNode, kind, owner }
349
+ const rawRefs = []; // { node, nameNode, kind, recvNode }
350
+ const importNodes = [];
351
+ const requireCalls = [];
352
+ const binds = []; // { node, name, type|new|call }
353
+ const fields = []; // { node, name, type|new }
354
+ const seenDef = new Map();
355
+ const seenRef = new Map();
356
+ const bindSites = new Map();
357
+ const scopeNodes = []; // anonymous functions / lambdas / comprehensions: lexical scopes
358
+ const retNodes = []; // returned expressions (return-type inference)
359
+ const cbSites = []; // functions passed as call arguments (callback parameter types)
360
+
361
+ for (const m of matches) {
362
+ let role = null, kind = null, main = null, nameNode = null, recvNode = null, ownerNode = null, typeNode = null;
363
+ let bindName = null, bindType = null, bindNew = null, bindCall = null, bindVar = null, bindExpr = null, bindElem = null, bindEach = null, srcNode = null;
364
+ let bindKey = null, bindOf = null;
365
+ let cbFn = null, cbArg = null;
366
+ for (const c of m.captures) {
367
+ const n = c.name;
368
+ if (n === 'name') nameNode = c.node;
369
+ else if (n === 'recv') recvNode = c.node;
370
+ else if (n === 'owner') ownerNode = c.node;
371
+ else if (n === 'type') typeNode = c.node;
372
+ else if (n === 'src') srcNode = c.node;
373
+ else if (n.startsWith('def.')) { role = 'def'; kind = n.slice(4); main = c.node; }
374
+ else if (n.startsWith('ref.')) { role = 'ref'; kind = n.slice(4); main = c.node; }
375
+ else if (n === 'import') { role = 'import'; main = c.node; }
376
+ else if (n === 'import.require') { role = 'require'; main = c.node; }
377
+ else if (n === 'bind') { role = 'bind'; main = c.node; }
378
+ else if (n === 'field') { role = 'field'; main = c.node; }
379
+ else if (n === 'bind.name' || n === 'field.name') bindName = c.node.text;
380
+ else if (n === 'bind.type' || n === 'field.type') bindType = c.node.text;
381
+ else if (n === 'bind.new' || n === 'field.new') bindNew = c.node.text;
382
+ else if (n === 'bind.call' || n === 'field.call') bindCall = c.node.text;
383
+ else if (n === 'field.var') bindVar = c.node.text;
384
+ else if (n === 'bind.expr' || n === 'field.expr') bindExpr = c.node;
385
+ else if (n === 'bind.elem' || n === 'field.elem') bindElem = c.node;
386
+ else if (n === 'bind.each') bindEach = c.node;
387
+ else if (n === 'bind.key') bindKey = c.node.text;
388
+ else if (n === 'bind.of') bindOf = c.node;
389
+ else if (n === 'scope') { role = 'scope'; main = c.node; }
390
+ else if (n === 'ret') { role = 'ret'; main = c.node; }
391
+ else if (n === 'cb') { role = 'cb'; main = c.node; }
392
+ else if (n === 'cb.fn') cbFn = c.node;
393
+ else if (n === 'cb.arg') cbArg = c.node;
394
+ }
395
+ if (role === 'cb') { if (cbFn && cbArg) cbSites.push({ call: main, fn: cbFn, arg: cbArg }); continue; }
396
+ if (role === 'scope') { scopeNodes.push(main); continue; }
397
+ if (role === 'ret') { retNodes.push(main); continue; }
398
+ if (role === 'def' && nameNode) {
399
+ // several patterns may match one definition (with/without return type, owner); merge them
400
+ const prev = seenDef.get(main.id);
401
+ if (prev !== undefined) {
402
+ const d = defs[prev];
403
+ if (!d.type && typeNode) d.type = typeNode.text;
404
+ if (!d.owner && ownerNode) d.owner = ownerNode.text;
405
+ continue;
406
+ }
407
+ seenDef.set(main.id, defs.length);
408
+ defs.push({ node: main, nameNode, kind, owner: ownerNode?.text ?? null, type: typeNode?.text ?? null });
409
+ } else if (role === 'ref' && nameNode) {
410
+ // one reference per name node; the most specific kind wins (a decorator's call is a
411
+ // decorator ref, a `new X()` is an instantiation, not also a plain call).
412
+ const prev = seenRef.get(nameNode.id);
413
+ if (prev !== undefined) {
414
+ if ((REF_PRIORITY[kind] ?? 0) > (REF_PRIORITY[rawRefs[prev].kind] ?? 0)) rawRefs[prev] = { node: main, nameNode, kind, recvNode: recvNode ?? rawRefs[prev].recvNode };
415
+ continue;
416
+ }
417
+ seenRef.set(nameNode.id, rawRefs.length);
418
+ rawRefs.push({ node: main, nameNode, kind, recvNode });
419
+ } else if (role === 'import') importNodes.push(main);
420
+ else if (role === 'require' && srcNode) requireCalls.push({ node: main, srcNode });
421
+ else if ((role === 'bind' || role === 'field') && bindName) {
422
+ if ((main.type === 'required_parameter' || main.type === 'optional_parameter') && inTypeContext(main)) continue;
423
+ // one record per binding site; an explicit type annotation beats an initialiser
424
+ const list = role === 'bind' ? binds : fields;
425
+ const key = role + ':' + main.id + ':' + bindName;
426
+ let expr = bindExpr ? receiverDescriptor(bindExpr, spec) : null;
427
+ // a destructured property: `const { a: x } = e` is e.a, `({ a }: T)` is T's member a
428
+ if (bindKey) {
429
+ const of = bindOf && memberTypeIn(bindOf, bindKey);
430
+ if (of) { bindType = of; expr = null; }
431
+ else if (bindOf) { const t = normalizeType(bindOf.text, spec); expr = t ? `(${t}).${bindKey}` : null; }
432
+ else expr = memberOfDescriptor(expr, bindKey);
433
+ }
434
+ const rec = { node: main, name: bindName, type: bindType, new: bindNew, call: bindCall, var: bindVar,
435
+ expr, elem: bindElem ? elementsDescriptor(bindElem, spec) : null,
436
+ each: bindEach ? elementsDescriptor(bindEach, spec) : null };
437
+ const prev = bindSites.get(key);
438
+ if (prev === undefined) { bindSites.set(key, list.length); list.push(rec); }
439
+ else { const p = list[prev]; for (const k of ['type', 'new', 'call', 'var', 'expr', 'elem', 'each']) p[k] ??= rec[k]; }
440
+ }
441
+ }
442
+
443
+ // ── definitions ────────────────────────────────────────────────────────────
444
+ const filtered = [];
445
+ for (const d of defs) {
446
+ if ((d.kind === 'variable' || d.kind === 'constant') && spec.isModuleLevel && !spec.isModuleLevel(d.node)) continue;
447
+ if (d.kind === 'field' && spec.keepField && !spec.keepField(d.node)) continue;
448
+ if (spec.filterDef && !spec.filterDef(d)) continue;
449
+ filtered.push(d);
450
+ }
451
+ filtered.sort((a, b) => a.node.startIndex - b.node.startIndex || b.node.endIndex - a.node.endIndex);
452
+
453
+ const byNodeId = new Map();
454
+ const hoisted = new Set();
455
+ const defNameIds = new Set();
456
+ const anonQnames = new Set();
457
+ const symbols = [];
458
+ for (const d of filtered) {
459
+ let name = d.nameNode.text;
460
+ if (spec.normalizeName) name = spec.normalizeName(name, d, relPath);
461
+ if (!name) continue;
462
+ // enclosing definition (container) by walking ancestors
463
+ let parentIdx = -1;
464
+ for (let p = d.node.parent, hops = 0; p && hops < 64; p = p.parent, hops++) {
465
+ const idx = byNodeId.get(p.id);
466
+ if (idx !== undefined) { parentIdx = idx; break; }
467
+ }
468
+ const lexParent = parentIdx;
469
+ // constructor parameter properties (`constructor(private repo: Repo)`) are class members
470
+ if (d.kind === 'field' && parentIdx >= 0 && CONSTRUCTOR_NAMES.has(symbols[parentIdx].name) && symbols[parentIdx].parentIdx >= 0) parentIdx = symbols[parentIdx].parentIdx;
471
+ // attributes assigned on self inside any method (Python) belong to the class, once each
472
+ else if (d.kind === 'field' && spec.hoistMemberFields && parentIdx >= 0 && CALLABLE_KINDS.has(symbols[parentIdx].kind) && symbols[parentIdx].parentIdx >= 0
473
+ && CLASS_KINDS.has(symbols[symbols[parentIdx].parentIdx].kind)) {
474
+ const cls = symbols[parentIdx].parentIdx;
475
+ if (hoisted.has(cls + ':' + name) || symbols.some(x => x.parentIdx === cls && x.name === name)) continue;
476
+ hoisted.add(cls + ':' + name);
477
+ parentIdx = cls;
478
+ }
479
+ let kind = d.kind;
480
+ const parent = parentIdx >= 0 ? symbols[parentIdx] : null;
481
+ if (kind === 'function' && parent && (parent.kind === 'class' || parent.kind === 'interface' || parent.kind === 'struct' || parent.kind === 'trait' || parent.kind === 'impl' || parent.kind === 'object')) kind = 'method';
482
+ if (spec.refineKind) kind = spec.refineKind(kind, d, parent) ?? kind;
483
+ let owner = d.owner;
484
+ if (spec.ownerOf && !owner) owner = spec.ownerOf(d.node);
485
+ const node = d.node;
486
+ // an anonymous class (`<anonymous>`) that repeats in one scope is told apart by its line
487
+ if (name === '<anonymous>' && anonQnames.has((parent ? parent.qname + '.' : '') + name)) name = `<anonymous@${node.startPosition.row + 1}>`;
488
+ let qname;
489
+ if (owner && (!parent || parent.name !== owner)) qname = (parent ? parent.qname + '.' : '') + owner + '.' + name;
490
+ else qname = parent ? parent.qname + '.' + name : name;
491
+ if (name.startsWith('<anonymous')) anonQnames.add(qname);
492
+ const sym = {
493
+ name,
494
+ qname,
495
+ kind,
496
+ parentIdx,
497
+ lexParent,
498
+ owner: owner || (parent && ['class', 'interface', 'struct', 'trait', 'impl', 'enum', 'object', 'module'].includes(parent.kind) ? parent.name : null),
499
+ startLine: node.startPosition.row + 1,
500
+ startCol: node.startPosition.column,
501
+ endLine: node.endPosition.row + 1,
502
+ endCol: node.endPosition.column,
503
+ nameLine: d.nameNode.startPosition.row + 1,
504
+ nameCol: d.nameNode.startPosition.column,
505
+ sig: signatureOf(node, spec, source),
506
+ doc: docOf(node, spec, source),
507
+ exported: false,
508
+ visibility: d.isDefault ? 'default' : (spec.visibility ? spec.visibility(node, name) : null),
509
+ decorators: spec.decoratorsOf ? spec.decoratorsOf(node) : [],
510
+ bases: spec.implicitBases?.(node) ?? [],
511
+ type: d.type ? declaredType(node, d.type, spec) : null,
512
+ isStatic: isStaticMember(node, d.nameNode, spec),
513
+ startIndex: node.startIndex,
514
+ endIndex: node.endIndex,
515
+ };
516
+ // a class's type records what collection it behaves as (`extends Map<string, Module>`)
517
+ if (MEMBER_PARENT.has(kind) && sym.type && !/(\[\]|\{\})$/.test(sym.type)) sym.type = null;
518
+ // `-> Self` / `: this` (fluent APIs) return the enclosing type
519
+ if (sym.type === 'Self' || sym.type === 'this' || sym.type === 'static') sym.type = parent && MEMBER_PARENT.has(parent.kind) ? (parent.kind === 'impl' ? parent.owner ?? parent.name : parent.name) : owner || null;
520
+ // members are part of the public surface when their container is and they are not private
521
+ if (parent && MEMBER_PARENT.has(parent.kind)) sym.exported = parent.exported && sym.visibility !== 'private';
522
+ else sym.exported = spec.isExported ? Boolean(spec.isExported(node, name)) : true;
523
+ byNodeId.set(node.id, symbols.length);
524
+ defNameIds.add(d.nameNode.id);
525
+ symbols.push(sym);
526
+ }
527
+
528
+ // innermost enclosing symbol for a byte offset (symbols sorted by start; nested ranges)
529
+ const enclosingIdx = (offset) => {
530
+ let best = -1;
531
+ // binary search last symbol with startIndex <= offset, then walk up parents
532
+ let lo = 0, hi = symbols.length - 1, cand = -1;
533
+ while (lo <= hi) {
534
+ const mid = (lo + hi) >> 1;
535
+ if (symbols[mid].startIndex <= offset) { cand = mid; lo = mid + 1; } else hi = mid - 1;
536
+ }
537
+ for (let i = cand; i >= 0; i = symbols[i].lexParent) {
538
+ if (symbols[i].endIndex >= offset) { best = i; break; }
539
+ // not containing: try previous siblings/ancestors
540
+ if (symbols[i].lexParent < 0) {
541
+ // scan backwards for a container that encloses offset
542
+ for (let j = i - 1; j >= 0; j--) if (symbols[j].startIndex <= offset && symbols[j].endIndex >= offset) { best = j; break; }
543
+ break;
544
+ }
545
+ }
546
+ return best;
547
+ };
548
+
549
+ // ── lexical scopes: callables and types, plus anonymous functions (closures, lambdas) ──
550
+ const scopes = []; // { start, end, sym (owning symbol or -1), parent }
551
+ for (let i = 0; i < symbols.length; i++) if (!VALUE_KINDS.has(symbols[i].kind)) scopes.push({ start: symbols[i].startIndex, end: symbols[i].endIndex, sym: i });
552
+ for (const n of scopeNodes) {
553
+ // `const f = () => …` / `m: function () {}`: the closure is the body of that symbol
554
+ const own = n.parent ? byNodeId.get(n.parent.id) : undefined;
555
+ scopes.push({ start: n.startIndex, end: n.endIndex, sym: own ?? -1 });
556
+ }
557
+ scopes.sort((a, b) => a.start - b.start || b.end - a.end);
558
+ {
559
+ const stack = [];
560
+ for (let k = 0; k < scopes.length; k++) {
561
+ while (stack.length && scopes[stack[stack.length - 1]].end <= scopes[k].start) stack.pop();
562
+ scopes[k].parent = stack.length ? stack[stack.length - 1] : -1;
563
+ stack.push(k);
564
+ }
565
+ }
566
+ const scopeAt = (offset) => {
567
+ let lo = 0, hi = scopes.length - 1, cand = -1;
568
+ while (lo <= hi) { const mid = (lo + hi) >> 1; if (scopes[mid].start <= offset) { cand = mid; lo = mid + 1; } else hi = mid - 1; }
569
+ for (let k = cand; k >= 0; k = scopes[k].parent) if (scopes[k].end > offset) return k;
570
+ return -1;
571
+ };
572
+ /** Where an expression is evaluated: enclosing symbol (for `this`/fields) + lexical scope (for locals). */
573
+ const ctxAt = (offset) => ({ sym: enclosingIdx(offset), scope: scopeAt(offset) });
574
+
575
+ // ── type evidence (local bindings, class fields, returns) ────────────────
576
+ //
577
+ // Types are strings the resolver understands:
578
+ // 'T' / 'T[]' (a collection of) a named type
579
+ // 'T{}' a map (dictionary) whose values are T
580
+ // 'call:f' the return type of callable f (resolved globally)
581
+ // '…#a#b' member a, then member b, of the preceding type (crosses files)
582
+ // '[]' after a call/member part: the element type of that part
583
+ // '!' a value whose members never live in the repository (primitives, arrays)
584
+ // 'cb:i:j:f' parameter j of the function passed as argument i to callable f, typed by
585
+ // f's signature (f: `name`, `@module.name`, or `<type>::name` with # as ~)
586
+ const deferred = (t) => t.startsWith('call:') || t.startsWith('cb:') || t.includes('#');
587
+ const typeText = (t) => {
588
+ const n = normalizeType(t, spec);
589
+ if (!n) return null;
590
+ return !/(\[\]|\{\})$/.test(n) && spec.isPrimitiveType?.(n) ? '!' : n;
591
+ };
592
+ // narrowed types by source range: { key (receiver descriptor), type, from, to }
593
+ const narrowed = [];
594
+ if (/\binstanceof\b|\bisinstance\s*\(/.test(source)) {
595
+ for (const r of typeNarrowings(root, source)) {
596
+ const key = receiverDescriptor(r.subject, spec), type = typeText(r.type.text);
597
+ if (type && type !== '!' && /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/.test(key)) narrowed.push({ key, type, from: r.from, to: r.to });
598
+ }
599
+ }
600
+ /** The narrowest type test that holds at `pos` for `key` (`x`, `this.x`). */
601
+ const narrowedAt = (pos, key) => {
602
+ let best = null;
603
+ for (const r of narrowed) if (r.key === key && r.from <= pos && pos < r.to && (!best || r.to - r.from < best.to - best.from)) best = r;
604
+ return best?.type ?? null;
605
+ };
606
+ const localTypes = new Map(); // `${scope}:${name}` -> binding record | { conflict }
607
+ const sameBinding = (a, b) => a.type === b.type && a.call === b.call && a.expr === b.expr && a.elem === b.elem && a.each === b.each && a.cb === b.cb && a.inline === b.inline;
608
+ // `helper((err, socket) => socket.send())`: an untyped parameter of a function passed to a call
609
+ // takes its type from the callee's signature, resolved across files (the `cb:` types above)
610
+ for (const { call, fn, arg } of cbSites) {
611
+ const args = call.childForFieldName('arguments')?.namedChildren.filter(n => n.type !== 'comment') ?? [];
612
+ const i = args.findIndex(n => n.id === arg.id);
613
+ const callee = fn.text.replace(/\s+/g, '').replace(/\?\./g, '.');
614
+ if (i < 0 || !/^(?:this\.)?[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*){0,2}$/.test(callee)) continue;
615
+ const dot = callee.lastIndexOf('.');
616
+ const recv = dot > 0 ? callee.slice(0, dot) : '', method = callee.slice(dot + 1);
617
+ if (recv && ELEMENT_WISE.has(method)) continue; // `xs.forEach(x => …)`: typed by the collection
618
+ const params = arg.childForFieldName('parameters')?.namedChildren ?? [arg.childForFieldName('parameter')].filter(Boolean);
619
+ params.filter(p => p.type !== 'comment').forEach((p, j) => {
620
+ const pat = (p.type === 'required_parameter' || p.type === 'optional_parameter') && !p.childForFieldName('type') ? p.childForFieldName('pattern') : p;
621
+ if (pat?.type === 'identifier') binds.push({ node: arg, name: pat.text, cb: { recv, method, i, j } });
622
+ });
623
+ }
624
+ const setLocal = (scope, name, val) => {
625
+ const k = scope + ':' + name;
626
+ const prev = localTypes.get(k);
627
+ if (!prev) localTypes.set(k, val);
628
+ else if (!prev.conflict && !sameBinding(prev, val)) localTypes.set(k, { conflict: true, end: Math.min(prev.end, val.end) });
629
+ else if (val.end < prev.end) prev.end = val.end;
630
+ };
631
+ for (const b of binds) {
632
+ const ctx = { ...ctxAt(b.node.startIndex), pos: b.node.startIndex };
633
+ const end = b.node.endIndex; // the local is in effect from here on
634
+ const typed = b.expr && /^(new|as) /.test(b.expr) ? b.expr.slice(3).trim() : null; // `new X(…)` / `… as X`
635
+ const type = b.type ? typeText(b.type) : (b.new ? typeText(b.new) : typed ? typeText(typed) : b.expr === '!' ? '!' : null);
636
+ if (type) setLocal(ctx.scope, b.name, { type, ctx, end });
637
+ else if (b.type && /^\{[^]*\}$/.test(b.type.trim())) setLocal(ctx.scope, b.name, { inline: b.type.trim(), ctx, end }); // `x: { a: A; … }`
638
+ else if (b.call) setLocal(ctx.scope, b.name, { call: b.call, ctx, end });
639
+ else if (b.cb) setLocal(ctx.scope, b.name, { cb: b.cb, ctx, end });
640
+ else if (b.expr && b.expr !== '?' && b.expr !== 'null' && b.expr !== b.name) setLocal(ctx.scope, b.name, { expr: b.expr, ctx, end });
641
+ else if (b.elem && b.elem !== '?') setLocal(ctx.scope, b.name, { elem: b.elem, ctx, end });
642
+ else if (b.each && b.each !== '?') setLocal(ctx.scope, b.name, { each: b.each, ctx, end });
643
+ else if (b.type || (b.expr && b.expr !== 'null')) setLocal(ctx.scope, b.name, { opaque: true, ctx, end }); // shadows outer bindings
644
+ }
645
+ const lookupLocal = (ctx, name) => {
646
+ for (let k = ctx.scope; ; k = scopes[k].parent) {
647
+ const v = localTypes.get(k + ':' + name);
648
+ if (v) return v.conflict || v.opaque ? null : v;
649
+ if (k < 0) break;
650
+ }
651
+ return null;
652
+ };
653
+ /** A local binding of `name` (typed or not) is visible at ctx: it shadows globals and builtins. */
654
+ const isDeclared = (ctx, name) => {
655
+ for (let k = ctx.scope; ; k = scopes[k].parent) {
656
+ if (localTypes.has(k + ':' + name)) return true;
657
+ if (k < 0) break;
658
+ }
659
+ return false;
660
+ };
661
+ /** Declared in a function's scope (not at the top level of the file) before `pos`. */
662
+ const isDeclaredLocally = (ctx, name, pos) => {
663
+ for (let k = ctx.scope; k >= 0; k = scopes[k].parent) if ((localTypes.get(k + ':' + name)?.end ?? Infinity) <= pos) return true;
664
+ return false;
665
+ };
666
+ const fieldTypes = new Map(); // `${classIdx}:${field}` -> type string (see above)
667
+
668
+ /** Apply `()` / `[]` operators to a type string; null when the result is unknown. */
669
+ const applyOps = (t, ops) => {
670
+ for (const op of ops) {
671
+ if (!t) return null;
672
+ if (op === '()') { if (!deferred(t) || (t.startsWith('cb:') && !t.includes('#'))) return null; continue; } // a method's type is its return type
673
+ if (deferred(t)) t += '[]';
674
+ else if (t.endsWith('[]') || t.endsWith('{}')) t = t.slice(0, -2); // element / map value
675
+ else return null; // indexing a string or an untyped object
676
+ }
677
+ return t;
678
+ };
679
+ // Iterating a map yields keys or entries except where the language hands out values (Go's
680
+ // `range`). A named type may be a collection class (`class Registry extends Map<string,
681
+ // Module>`), which only the resolver can tell: `T#[]` is an element of T itself.
682
+ const elemOf = (t) => (!t || t === '!' ? null : deferred(t) ? t + '[]' : t.endsWith('[]') ? t.slice(0, -2)
683
+ : t.endsWith('{}') ? (spec.mapIterValues ? t.slice(0, -2) : null) : t + '#[]');
684
+ // what an element-wise method hands its callback (`xs.forEach(x => …)`): an element, or a map's value
685
+ const eachOf = (t) => (!t || t === '!' ? null : deferred(t) ? t + '[]' : /(\[\]|\{\})$/.test(t) ? t.slice(0, -2) : t + '#[]');
686
+ /** The `cb:` type of a callback parameter, or null when the callee cannot be named. */
687
+ const callbackType = ({ recv, method, i, j }, ctx, depth) => {
688
+ let target = method; // a function in scope or imported
689
+ if (recv === 'this') {
690
+ const st = selfType(ctx.sym);
691
+ if (!st) return null;
692
+ target = st.name + '::' + method;
693
+ } else if (recv) {
694
+ const rt = inferType(recv, ctx, depth + 1);
695
+ if (rt && rt !== '!' && !/(\[\]|\{\})$/.test(rt)) target = rt.replace(/#/g, '~') + '::' + method;
696
+ else if (!rt && !isDeclared(ctx, recv.split('.')[0])) target = '@' + recv + '.' + method; // module, namespace or class
697
+ else return null;
698
+ }
699
+ return `cb:${i}:${j}:${target}`;
700
+ };
701
+ const bindingType = (v, depth) => {
702
+ if (v.type) return v.type;
703
+ if (v.call) return 'call:' + v.call;
704
+ if (v.cb) return depth > 5 ? null : callbackType(v.cb, v.ctx, depth);
705
+ if (v.expr) return inferType(v.expr, v.ctx, depth + 1);
706
+ if (v.elem) return elemOf(inferType(v.elem, v.ctx, depth + 1));
707
+ if (v.each) return eachOf(inferType(v.each, v.ctx, depth + 1));
708
+ return null;
709
+ };
710
+ const SEG_RE = /^([A-Za-z_$@][\w$]*)((?:\(\)|\[\])*)$/;
711
+ // the type `this`/`self` denotes inside a symbol: its class, or the receiver type of an
712
+ // out-of-body method (Rust impl blocks, Go receivers) — { idx: class symbol or -1, name }
713
+ const classByName = new Map();
714
+ const selfType = (encl) => {
715
+ const cls = classOf(encl, symbols);
716
+ if (cls >= 0) return { idx: cls, name: symbols[cls].qname };
717
+ for (let i = encl; i >= 0; i = symbols[i].parentIdx) {
718
+ const o = symbols[i].owner;
719
+ if (!o) continue;
720
+ if (!classByName.has(o)) classByName.set(o, symbols.findIndex(x => x.name === o && CLASS_KINDS.has(x.kind)));
721
+ const j = classByName.get(o);
722
+ return { idx: j, name: j >= 0 ? symbols[j].qname : o };
723
+ }
724
+ return null;
725
+ };
726
+
727
+ /** Static type of an expression descriptor evaluated in `ctx`, as far as this file tells. */
728
+ const inferType = (desc, ctx, depth = 0) => {
729
+ if (!desc || depth > 5 || desc === '?' || desc === 'super' || desc === 'null') return null;
730
+ if (desc === '!') return '!';
731
+ if (desc.startsWith('new ') || desc.startsWith('as ')) {
732
+ const tn = desc.slice(3).trim();
733
+ if (tn === 'Self' || tn === 'self' || tn === 'static' || tn === 'this') return selfType(ctx.sym)?.name ?? null;
734
+ return typeText(tn);
735
+ }
736
+ // `(T).a().b`: a chain on a cast value (see receiverDescriptor)
737
+ let cast = null;
738
+ if (desc.startsWith('(')) {
739
+ const k = desc.indexOf(')');
740
+ if (k < 0 || desc[k + 1] !== '.') return null;
741
+ cast = typeText(desc.slice(1, k));
742
+ if (!cast) return null;
743
+ desc = desc.slice(k + 2);
744
+ }
745
+ const segs = [];
746
+ for (const p of desc.split('.')) {
747
+ const m = SEG_RE.exec(p);
748
+ if (!m) return null;
749
+ segs.push({ name: m[1], ops: m[2].match(/\(\)|\[\]/g) ?? [] });
750
+ }
751
+ let t = null, i = 1;
752
+ const first = segs[0];
753
+ if (cast) { t = cast; i = 0; }
754
+ else if (first.name === 'this') {
755
+ const st = selfType(ctx.sym);
756
+ if (!st) return null;
757
+ if (segs.length === 1) return first.ops.length ? null : st.name;
758
+ const nt = ctx.pos != null && narrowed.length ? narrowedAt(ctx.pos, 'this.' + segs[1].name) : null;
759
+ const ft = nt ?? (st.idx >= 0 ? fieldTypes.get(st.idx + ':' + segs[1].name) : null);
760
+ t = applyOps(ft ?? st.name + '#' + segs[1].name, segs[1].ops);
761
+ i = 2;
762
+ } else {
763
+ const isCall = first.ops[0] === '()';
764
+ const nt = !isCall && ctx.pos != null && narrowed.length ? narrowedAt(ctx.pos, first.name) : null;
765
+ const v = isCall ? null : nt ? { type: nt } : lookupLocal(ctx, first.name);
766
+ // a value of an inline object type: its member's written type
767
+ if (v?.inline && !first.ops.length && segs.length > 1) { t = applyOps(typeText(inlineMemberType(v.inline, segs[1].name)), segs[1].ops); i = 2; }
768
+ else if (v) t = applyOps(bindingType(v, depth), first.ops);
769
+ else if (isCall) t = applyOps('call:' + first.name, first.ops.slice(1));
770
+ else if (spec.implicitThis && classOf(ctx.sym, symbols) >= 0) {
771
+ t = applyOps(fieldTypes.get(classOf(ctx.sym, symbols) + ':' + first.name) ?? null, first.ops);
772
+ // a field the class inherits (declared in a base, maybe in another file): the resolver's to find
773
+ if (!t && /^[a-z_]/.test(first.name) && !isDeclared(ctx, first.name)) t = applyOps(symbols[classOf(ctx.sym, symbols)].qname + '#' + first.name, first.ops);
774
+ }
775
+ // `Type.staticField…`: a capitalised root that is not a local names a type
776
+ if (!t && !v && segs.length > 1 && !first.ops.length && /^[A-Z]/.test(first.name)) t = first.name;
777
+ // `b := Form`: a package-level variable, maybe of another file of the package; its type is
778
+ // the resolver's to find (a Go name is never both a type and a value)
779
+ else if (!t && !v && segs.length === 1 && !first.ops.length && spec.packageValues && !isDeclared(ctx, first.name)) t = first.name;
780
+ // `pkg.New()` / `utils.makeFoo()`: a call through a module or import alias returns the callee's type
781
+ else if (!t && !v && segs.length > 1 && !first.ops.length && segs[1].ops[0] === '()') {
782
+ t = applyOps('call:' + first.name + '.' + segs[1].name, segs[1].ops.slice(1));
783
+ i = 2;
784
+ }
785
+ }
786
+ for (; i < segs.length && t; i++) {
787
+ const seg = segs[i];
788
+ const call = seg.ops[0] === '()';
789
+ if (call && spec.identityMethods?.has(seg.name)) { t = applyOps(t, seg.ops.slice(1)); continue; }
790
+ if (t === '!') return '!';
791
+ if (!deferred(t) && t.endsWith('[]')) {
792
+ // `list.get(0)`, `xs.first()`: collection accessors hand back an element
793
+ if (call && spec.elementMethods?.has(seg.name)) { t = applyOps(t.slice(0, -2), seg.ops.slice(1)); continue; }
794
+ return '!'; // any other member of a collection is the language's
795
+ }
796
+ if (!deferred(t) && t.endsWith('{}')) {
797
+ // `m.get(k)` hands back a value, `m.values()` a collection of values
798
+ if (call && spec.mapMethods?.has(seg.name)) { t = applyOps(t.slice(0, -2), seg.ops.slice(1)); continue; }
799
+ if (call && spec.mapValueMethods?.has(seg.name)) { t = applyOps(t.slice(0, -2) + '[]', seg.ops.slice(1)); continue; }
800
+ return '!';
801
+ }
802
+ t = applyOps(t + '#' + seg.name, seg.ops);
803
+ }
804
+ return t;
805
+ };
806
+
807
+ for (const f of fields) {
808
+ const ctx = ctxAt(f.node.startIndex);
809
+ const cls = classOf(ctx.sym, symbols);
810
+ if (cls < 0) continue;
811
+ let type = f.type ? typeText(f.type) : (f.new ? typeText(f.new) : null);
812
+ if (!type && f.var) { const v = lookupLocal(ctx, f.var); type = v ? bindingType(v, 0) : null; }
813
+ if (!type && f.call) type = 'call:' + f.call;
814
+ if (!type && f.expr) type = inferType(f.expr, ctx);
815
+ if (!type) continue;
816
+ if (!fieldTypes.has(cls + ':' + f.name)) fieldTypes.set(cls + ':' + f.name, type);
817
+ }
818
+ // declared field/property symbols carry their type too (struct fields, typed class attributes)
819
+ for (let i = 0; i < symbols.length; i++) {
820
+ const s = symbols[i];
821
+ if ((s.kind === 'field' || s.kind === 'property') && s.parentIdx >= 0) {
822
+ const k = s.parentIdx + ':' + s.name;
823
+ if (s.type) { if (!fieldTypes.has(k) && typeText(s.type) !== '!') fieldTypes.set(k, s.type); }
824
+ else if (fieldTypes.has(k)) s.type = fieldTypes.get(k);
825
+ }
826
+ }
827
+ // record inferred (assignment-based) field types as field symbols' type when the field itself
828
+ // is not declared in the class body (common in Python/JS: self.x = X() in __init__)
829
+ const inferredFields = [];
830
+ for (const [k, type] of fieldTypes) {
831
+ const [clsIdx, fname] = [Number(k.slice(0, k.indexOf(':'))), k.slice(k.indexOf(':') + 1)];
832
+ inferredFields.push({ owner: symbols[clsIdx].qname, name: fname, type });
833
+ }
834
+ // module-level values typed by their initialiser (`export const api = new ApiClient()`), so
835
+ // importers can reach the members of such singletons
836
+ for (let i = 0; i < symbols.length; i++) {
837
+ const s = symbols[i];
838
+ if (s.type || (s.kind !== 'variable' && s.kind !== 'constant')) continue;
839
+ const v = localTypes.get(scopeAt(s.startIndex) + ':' + s.name);
840
+ if (v && !v.conflict && !v.opaque) s.type = bindingType(v, 0);
841
+ }
842
+ // undeclared return types: all `return` expressions of a callable agree on one type
843
+ const returns = new Map(); // symIdx -> [types]
844
+ for (const n of retNodes) {
845
+ const k = scopeAt(n.startIndex);
846
+ const owner = k >= 0 ? scopes[k].sym : -1;
847
+ if (owner < 0 || symbols[owner].type || !CALLABLE_KINDS.has(symbols[owner].kind)) continue;
848
+ const desc = receiverDescriptor(n, spec);
849
+ if (desc === 'null') continue; // `return null` / `return None` keeps the other paths' type
850
+ const list = returns.get(owner) ?? returns.set(owner, []).get(owner);
851
+ list.push(inferType(desc, { sym: owner, scope: k }));
852
+ }
853
+ for (const [owner, types] of returns) {
854
+ const uniq = new Set(types);
855
+ if (uniq.size === 1 && !uniq.has(null)) symbols[owner].type = types[0];
856
+ }
857
+
858
+ const inferReceiverType = (recv, ctx) => {
859
+ if (!recv || recv === 'this') return null;
860
+ const t = inferType(recv, ctx);
861
+ return t && !deferred(t) && /(\[\]|\{\})$/.test(t) ? '!' : t; // a collection's own methods are the language's
862
+ };
863
+
864
+ // ── references ─────────────────────────────────────────────────────────────
865
+ const refs = [];
866
+ for (const r of rawRefs) {
867
+ if (defNameIds.has(r.nameNode.id)) continue; // a definition's own name
868
+ let name = r.nameNode.text;
869
+ if (spec.normalizeRefName) name = spec.normalizeRefName(name, r);
870
+ if (!name || name.length > 128) continue;
871
+ const kind = r.kind;
872
+ const recv = r.recvNode ? receiverDescriptor(r.recvNode, spec) : '';
873
+ const ctx = ctxAt(r.nameNode.startIndex);
874
+ if (kind === 'call' && !recv && spec.builtinCalls?.has(name) && !isDeclared(ctx, name)) continue;
875
+ const root = recv.split('.')[0];
876
+ // `module.x()` is the CommonJS global only when no local `module` is in scope
877
+ if (recv && spec.externalReceivers?.has(root) && !isDeclared(ctx, root)) continue;
878
+ if (kind === 'value' && spec.isNoiseValue?.(name)) continue;
879
+ // a value or a call by the name of a local (`stack := stack(3); log(stack)`, `check := func…;
880
+ // check()`) is the local, in languages where no local stands for repository code (Go: no
881
+ // imports bind locals)
882
+ if (!recv && (kind === 'value' ? spec.packageValues || spec.localValues : kind === 'call' && spec.packageValues) && isDeclaredLocally(ctx, name, r.nameNode.startIndex)) continue;
883
+ const pos = r.nameNode.startPosition;
884
+ const encl = ctx.sym;
885
+ const recvType = inferReceiverType(recv, { ...ctx, pos: r.nameNode.startIndex });
886
+ // arguments passed: tells overloads apart where each one is a method of its own
887
+ const a = spec.distinctOverloads && (kind === 'call' || kind === 'new') ? callArgc(r.node) : null;
888
+ refs.push({ name, kind, line: pos.row + 1, col: pos.column, recv, recvType, symIdx: encl, argc: a ? (a.open ? -1 : a.n) : null });
889
+ }
890
+
891
+ // inheritance: attach base names to the enclosing class symbol
892
+ for (const r of refs) {
893
+ if (r.kind !== 'inherit' || r.symIdx < 0) continue;
894
+ const s = symbols[r.symIdx];
895
+ if (['class', 'interface', 'struct', 'trait', 'enum', 'object', 'impl'].includes(s.kind) && !s.bases.includes(r.name)) s.bases.push(r.name);
896
+ }
897
+
898
+ // ── imports ────────────────────────────────────────────────────────────────
899
+ const imports = [];
900
+ for (const n of importNodes) {
901
+ for (const rec of spec.parseImport(n, source)) imports.push({ ...rec, line: n.startPosition.row + 1 });
902
+ }
903
+ for (const { node, srcNode } of requireCalls) {
904
+ for (const rec of spec.parseRequire(node, srcNode)) imports.push({ ...rec, line: node.startPosition.row + 1 });
905
+ }
906
+ const extra = spec.fileInfo ? spec.fileInfo(root, source) : null;
907
+
908
+ for (const s of symbols) { delete s.startIndex; delete s.endIndex; delete s.lexParent; }
909
+ return { symbols, refs, imports, fields: inferredFields, fileInfo: extra, errors: root.hasError ? 1 : 0 };
910
+ }
911
+
912
+ const NULLISH_TYPES = new Set(['null', 'undefined', 'None', 'void', 'nil', 'never', 'NoneType', 'Nothing', 'Unit']);
913
+ // declared-by-inference keywords in type position: the initialiser decides
914
+ const INFERRED_TYPES = new Set(['var', 'auto', 'let', 'val', 'dynamic', '_', 'implicit_type']);
915
+
916
+ /** Split at top-level occurrences of `sep` (outside <> [] () {}; `=>` is not a bracket). */
917
+ function splitTop(s, sep) {
918
+ const out = [];
919
+ let depth = 0, start = 0;
920
+ for (let i = 0; i < s.length; i++) {
921
+ const c = s[i];
922
+ if (c === '<' || c === '[' || c === '(' || c === '{') depth++;
923
+ else if ((c === '>' && s[i - 1] !== '=' && s[i - 1] !== '-') || c === ']' || c === ')' || c === '}') depth--;
924
+ else if (c === sep && depth === 0) { out.push(s.slice(start, i)); start = i + 1; }
925
+ }
926
+ out.push(s.slice(start));
927
+ return out;
928
+ }
929
+
930
+ const arrayOf = (e) => (e && !/(\[\]|\{\})$/.test(e) ? e + '[]' : null);
931
+ const mapOf = (v) => (v && !/(\[\]|\{\})$/.test(v) ? v + '{}' : null);
932
+
933
+ /**
934
+ * Static type text → what member lookup needs: `Foo`, `Foo[]` (a collection of Foo, so `x[i]`
935
+ * and loop variables are Foo), `Foo{}` (a map whose values are Foo: `m[k]`, `m.get(k)`) or null. Optional/nullable unions collapse (`Foo | undefined`,
936
+ * `Optional[Foo]`, `Foo?`), transparent wrappers unwrap per language (`Promise<Foo>`,
937
+ * `Box<Foo>`, `Task<Foo>`), pointers/references/qualifiers and namespaces are dropped.
938
+ */
939
+ export function normalizeType(t, spec, depth = 0) {
940
+ if (t == null || depth > 5) return null;
941
+ let s = String(t).trim();
942
+ if (spec?.cleanType) { s = spec.cleanType(s); if (!s) return null; }
943
+ s = s.replace(/^["']|["']$/g, '')
944
+ .replace(/^(?:(?:readonly|const|mut|dyn|impl|typeof|unique|volatile|struct|enum|class|final|ref|in|out|inout)\s+|[&*^]\s*|'[A-Za-z_]\w*\s+)+/, '')
945
+ .replace(/(?:\s*(?:[?!*&]|\.\.\.))+$/, '')
946
+ .trim();
947
+ if (!s || /^keyof\s/.test(s) || INFERRED_TYPES.has(s)) return null;
948
+ const alts = splitTop(s, '|').map(x => x.trim()).filter(Boolean);
949
+ if (alts.length > 1) {
950
+ const norm = [...new Set(alts.filter(a => !NULLISH_TYPES.has(a)).map(a => normalizeType(a, spec, depth + 1)))];
951
+ return norm.length === 1 ? norm[0] : null;
952
+ }
953
+ if (splitTop(s, '&').length > 1 || splitTop(s, ',').length > 1) return null; // intersections / tuples
954
+ let m;
955
+ if ((m = /^\((.*)\)$/s.exec(s))) return normalizeType(m[1], spec, depth + 1);
956
+ if ((m = /^(.+?)\s*\[\s*\]$/s.exec(s))) return arrayOf(normalizeType(m[1], spec, depth + 1)); // T[]
957
+ if ((m = /^([A-Za-z_$][\w$.]*)\{\}$/.exec(s))) return mapOf(normalizeType(m[1], spec, depth + 1)); // a map of T, as normalised
958
+ if ((m = /^\[\s*\]\s*(.+)$/s.exec(s))) return arrayOf(normalizeType(m[1], spec, depth + 1)); // Go []T
959
+ if ((m = /^map\s*\[[^\]]*\]\s*(.+)$/s.exec(s))) return mapOf(normalizeType(m[1], spec, depth + 1)); // Go map[K]V
960
+ if ((m = /^\[([^;\]]+?)\s*(?:;[^\]]*)?\]$/s.exec(s))) return arrayOf(normalizeType(m[1], spec, depth + 1)); // Rust [T] / [T; N]
961
+ if ((m = /^([A-Za-z_$][\w$]*(?:(?:\.|::|\\)[A-Za-z_$][\w$]*)*)\s*[<[](.*)[>\]]$/s.exec(s))) {
962
+ const head = m[1].split(/\.|::|\\/).pop();
963
+ const args = splitTop(m[2], ',').map(x => x.trim());
964
+ if (spec?.transparentTypes?.has(head)) return normalizeType(args[0], spec, depth + 1);
965
+ if (spec?.elementTypes?.has(head)) return arrayOf(normalizeType(args[0], spec, depth + 1));
966
+ if (spec?.mapTypes?.has(head)) return mapOf(normalizeType(args[args.length - 1], spec, depth + 1));
967
+ if (head === 'Union') return normalizeType(args.join('|'), spec, depth + 1);
968
+ s = m[1];
969
+ }
970
+ s = s.replace(/<.*$/s, '').replace(/\(.*$/s, '').replace(/\[.*$/s, '').trim();
971
+ const segs = s.split(/::|\.|\\/).filter(Boolean);
972
+ // `http.Request`: the package tells a type of the repository from a library's (see Resolver)
973
+ if (spec?.qualifiedTypes && segs.length === 2 && /^[A-Za-z_]\w*\.[A-Za-z_]\w*$/.test(s)) return s;
974
+ // `Token.Comment`, `org.x.Outer.Inner`: a nested type keeps its enclosing types, not its package
975
+ if (spec?.nestedTypes && segs.length >= 2 && segs.every(x => /^[A-Za-z_$][\w$]*$/.test(x))) {
976
+ let k = segs.length - 1;
977
+ while (k > 0 && /^[A-Z]/.test(segs[k - 1])) k--;
978
+ if (k < segs.length - 1) return segs.slice(k).join('.');
979
+ }
980
+ s = segs.length ? segs[segs.length - 1] : s;
981
+ return /^[A-Za-z_$][\w$]*$/.test(s) ? s : null;
982
+ }
983
+
984
+ /** @deprecated nominal head only (kept for callers that need a plain name). */
985
+ export function cleanTypeName(t, spec) {
986
+ const n = normalizeType(t, spec);
987
+ return n && n.endsWith('[]') ? n.slice(0, -2) : n;
988
+ }
989
+
990
+ const CLASS_KINDS = new Set(['class', 'struct', 'impl', 'object', 'trait', 'interface', 'enum']);
991
+ const VALUE_KINDS = new Set(['variable', 'constant', 'field', 'property']);
992
+ const CALLABLE_KINDS = new Set(['function', 'method']);
993
+ const CONSTRUCTOR_NAMES = new Set(['constructor', '__construct']);
994
+
995
+ /** `get<T extends Foo = Foo>(): T` → the declared return type `T` means `Foo` (bound or default). */
996
+ /**
997
+ * Declared type of a definition, with the definition's own type parameters replaced by their
998
+ * bound or default: `create<T extends App = App>(): Promise<T>` is typed `App` (the wrapper is
999
+ * unwrapped first, then `T` looked up).
1000
+ */
1001
+ function declaredType(defNode, typeText, spec) {
1002
+ const direct = typeParamBound(defNode, typeText);
1003
+ if (direct !== undefined) return direct === null ? null : normalizeType(direct, spec);
1004
+ const n = normalizeType(typeText, spec);
1005
+ if (!n) return null;
1006
+ const elem = n.endsWith('[]') ? '[]' : '';
1007
+ const bound = typeParamBound(defNode, elem ? n.slice(0, -2) : n);
1008
+ if (bound === undefined) return n;
1009
+ const b = bound === null ? null : normalizeType(bound, spec);
1010
+ return b ? b + elem : null;
1011
+ }
1012
+
1013
+ function typeParamBound(defNode, typeText) {
1014
+ const name = String(typeText).trim();
1015
+ if (!/^[A-Za-z_$][\w$]*$/.test(name)) return undefined;
1016
+ const tps = defNode.childForFieldName?.('type_parameters') ?? defNode.namedChildren?.find(c => c.type === 'type_parameters');
1017
+ if (!tps) return undefined;
1018
+ for (const p of tps.namedChildren) {
1019
+ const m = /^(?:(?:in|out|const|reified)\s+)*([A-Za-z_$][\w$]*)\s*(?:(?:extends|:)\s*([^=]+?))?\s*(?:=\s*(.+))?$/s.exec(p.text);
1020
+ if (m && m[1] === name) return (m[3] ?? m[2] ?? '').trim() || null;
1021
+ }
1022
+ return undefined;
1023
+ }
1024
+
1025
+ /** Static members (`static`, `@staticmethod`/`@classmethod`) never share a family with instance members. */
1026
+ function isStaticMember(node, nameNode, spec) {
1027
+ if (spec.isStatic) return spec.isStatic(node);
1028
+ const head = node.text.slice(0, Math.max(0, nameNode.startIndex - node.startIndex));
1029
+ return /(^|\s)static\s/.test(head);
1030
+ }
1031
+
1032
+ function classOf(idx, symbols) {
1033
+ for (let i = idx; i >= 0; i = symbols[i].parentIdx) {
1034
+ const k = symbols[i].kind;
1035
+ if (k === 'class' || k === 'struct' || k === 'impl' || k === 'object' || k === 'trait' || k === 'interface' || k === 'enum') return i;
1036
+ }
1037
+ return -1;
1038
+ }