graph-indexer 2.1.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +260 -1
- package/README.md +232 -312
- package/SECURITY.md +37 -59
- package/bin/graph-indexer.mjs +251 -0
- package/integrations/claude-code/.claude-plugin/plugin.json +8 -0
- package/integrations/claude-code/.mcp.json +8 -0
- package/integrations/claude-code/agents/code-structure.md +22 -0
- package/integrations/claude-code/hooks/gi-hook.mjs +99 -0
- package/integrations/claude-code/hooks/hooks.json +16 -0
- package/integrations/opencode/graph-indexer.js +124 -0
- package/package.json +32 -109
- package/src/cli/helper.mjs +67 -0
- package/src/cli/hook.mjs +356 -0
- package/src/cli/init.mjs +255 -0
- package/src/cli/resident.mjs +175 -0
- package/src/index/discover.mjs +130 -0
- package/src/index/fingerprint.mjs +30 -0
- package/src/index/graph.mjs +61 -0
- package/src/index/indexer.mjs +476 -0
- package/src/index/modules.mjs +331 -0
- package/src/index/resolver.mjs +954 -0
- package/src/mcp/render.mjs +60 -0
- package/src/mcp/server.mjs +147 -0
- package/src/mcp/tools.mjs +973 -0
- package/src/parse/arity.mjs +152 -0
- package/src/parse/extract.mjs +1031 -0
- package/src/parse/lang/csharp.mjs +106 -0
- package/src/parse/lang/ecma.mjs +438 -0
- package/src/parse/lang/go.mjs +194 -0
- package/src/parse/lang/jvm.mjs +118 -0
- package/src/parse/lang/kotlin.mjs +97 -0
- package/src/parse/lang/misc.mjs +115 -0
- package/src/parse/lang/native.mjs +126 -0
- package/src/parse/lang/python.mjs +153 -0
- package/src/parse/lang/rust.mjs +157 -0
- package/src/parse/lang/scripting.mjs +219 -0
- package/src/parse/languages.mjs +48 -0
- package/src/parse/runtime.mjs +56 -0
- package/src/query/check.mjs +343 -0
- package/src/query/facts.mjs +323 -0
- package/src/query/intel.mjs +680 -0
- package/src/query/read.mjs +81 -0
- package/src/query/testcmd.mjs +119 -0
- package/src/query/textsearch.mjs +160 -0
- package/src/search/search.mjs +210 -0
- package/src/search/tokenize.mjs +188 -0
- package/src/store/db.mjs +169 -0
- package/src/util/paths.mjs +24 -0
- package/vendor/grammars/bash.wasm.gz +0 -0
- package/vendor/grammars/c.wasm.gz +0 -0
- package/vendor/grammars/c_sharp.wasm.gz +0 -0
- package/vendor/grammars/cpp.wasm.gz +0 -0
- package/vendor/grammars/css.wasm.gz +0 -0
- package/vendor/grammars/go.wasm.gz +0 -0
- package/vendor/grammars/java.wasm.gz +0 -0
- package/vendor/grammars/javascript.wasm.gz +0 -0
- package/vendor/grammars/kotlin.wasm.gz +0 -0
- package/vendor/grammars/manifest.json +148 -0
- package/vendor/grammars/php.wasm.gz +0 -0
- package/vendor/grammars/python.wasm.gz +0 -0
- package/vendor/grammars/ruby.wasm.gz +0 -0
- package/vendor/grammars/rust.wasm.gz +0 -0
- package/vendor/grammars/scala.wasm.gz +0 -0
- package/vendor/grammars/tsx.wasm.gz +0 -0
- package/vendor/grammars/typescript.wasm.gz +0 -0
- package/vendor/web-tree-sitter/web-tree-sitter.js +3980 -0
- package/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/cli-ui.mjs +0 -78
- package/colbert.mjs +0 -168
- package/config.mjs +0 -494
- package/daemon-ctl.mjs +0 -175
- package/daemon-lock.mjs +0 -69
- package/embedders/python/mlx_embed_server.py +0 -61
- package/embedders/python/requirements-mlx.txt +0 -5
- package/embedders/setup-mlx.mjs +0 -110
- package/embeddings.mjs +0 -571
- package/engine/binary.mjs +0 -437
- package/engine/memory.mjs +0 -1003
- package/engine/sqlite.mjs +0 -1136
- package/enrichment.mjs +0 -521
- package/git-signals.mjs +0 -248
- package/indexer.mjs +0 -546
- package/init.mjs +0 -1809
- package/layout.mjs +0 -123
- package/mcp/centrality.mjs +0 -117
- package/mcp/format.mjs +0 -61
- package/mcp/resolver.mjs +0 -116
- package/mcp/symbolgraph.mjs +0 -137
- package/mcp/taint.mjs +0 -225
- package/mcp/tools.mjs +0 -1582
- package/mcp/topology.mjs +0 -676
- package/mcp-server.mjs +0 -197
- package/parse/extractor.mjs +0 -628
- package/parse/imports.mjs +0 -365
- package/parse/interprocedural.mjs +0 -134
- package/parse/languages.mjs +0 -253
- package/parse/metadata.mjs +0 -701
- package/parse/routes.mjs +0 -327
- package/parse/scip.mjs +0 -391
- package/parse/taint-patterns.mjs +0 -119
- package/prompts/CORE.md +0 -91
- package/prompts/DOMAIN_TEMPLATE.md +0 -46
- package/prompts/INTEGRATION.md +0 -123
- package/prompts/frameworks/ANDROID.md +0 -17
- package/prompts/frameworks/ASPNET_CORE.md +0 -19
- package/prompts/frameworks/FASTAPI_DJANGO.md +0 -20
- package/prompts/frameworks/LARAVEL_SYMFONY.md +0 -22
- package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +0 -19
- package/prompts/frameworks/RAILS.md +0 -20
- package/prompts/frameworks/REACT.md +0 -20
- package/prompts/frameworks/SPRING_BOOT.md +0 -20
- package/prompts/languages/BASH.md +0 -22
- package/prompts/languages/C.md +0 -21
- package/prompts/languages/CSHARP.md +0 -24
- package/prompts/languages/CSS_SCSS.md +0 -22
- package/prompts/languages/GO.md +0 -24
- package/prompts/languages/JAVA.md +0 -23
- package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +0 -24
- package/prompts/languages/KOTLIN.md +0 -24
- package/prompts/languages/PHP.md +0 -23
- package/prompts/languages/PYTHON.md +0 -23
- package/prompts/languages/RUBY.md +0 -23
- package/prompts/languages/RUST.md +0 -21
- package/prompts/languages/SWIFT.md +0 -21
- package/seal.mjs +0 -400
- package/search-core.mjs +0 -777
- package/search-sparse.mjs +0 -183
- package/storage.mjs +0 -73
- package/watch-daemon.mjs +0 -289
|
@@ -0,0 +1,1031 @@
|
|
|
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
|
+
function extractFromTree(spec, query, tree, source, relPath) {
|
|
338
|
+
const root = tree.rootNode;
|
|
339
|
+
const matches = query.matches(root);
|
|
340
|
+
|
|
341
|
+
const defs = []; // { node, nameNode, kind, owner }
|
|
342
|
+
const rawRefs = []; // { node, nameNode, kind, recvNode }
|
|
343
|
+
const importNodes = [];
|
|
344
|
+
const requireCalls = [];
|
|
345
|
+
const binds = []; // { node, name, type|new|call }
|
|
346
|
+
const fields = []; // { node, name, type|new }
|
|
347
|
+
const seenDef = new Map();
|
|
348
|
+
const seenRef = new Map();
|
|
349
|
+
const bindSites = new Map();
|
|
350
|
+
const scopeNodes = []; // anonymous functions / lambdas / comprehensions: lexical scopes
|
|
351
|
+
const retNodes = []; // returned expressions (return-type inference)
|
|
352
|
+
const cbSites = []; // functions passed as call arguments (callback parameter types)
|
|
353
|
+
|
|
354
|
+
for (const m of matches) {
|
|
355
|
+
let role = null, kind = null, main = null, nameNode = null, recvNode = null, ownerNode = null, typeNode = null;
|
|
356
|
+
let bindName = null, bindType = null, bindNew = null, bindCall = null, bindVar = null, bindExpr = null, bindElem = null, bindEach = null, srcNode = null;
|
|
357
|
+
let bindKey = null, bindOf = null;
|
|
358
|
+
let cbFn = null, cbArg = null;
|
|
359
|
+
for (const c of m.captures) {
|
|
360
|
+
const n = c.name;
|
|
361
|
+
if (n === 'name') nameNode = c.node;
|
|
362
|
+
else if (n === 'recv') recvNode = c.node;
|
|
363
|
+
else if (n === 'owner') ownerNode = c.node;
|
|
364
|
+
else if (n === 'type') typeNode = c.node;
|
|
365
|
+
else if (n === 'src') srcNode = c.node;
|
|
366
|
+
else if (n.startsWith('def.')) { role = 'def'; kind = n.slice(4); main = c.node; }
|
|
367
|
+
else if (n.startsWith('ref.')) { role = 'ref'; kind = n.slice(4); main = c.node; }
|
|
368
|
+
else if (n === 'import') { role = 'import'; main = c.node; }
|
|
369
|
+
else if (n === 'import.require') { role = 'require'; main = c.node; }
|
|
370
|
+
else if (n === 'bind') { role = 'bind'; main = c.node; }
|
|
371
|
+
else if (n === 'field') { role = 'field'; main = c.node; }
|
|
372
|
+
else if (n === 'bind.name' || n === 'field.name') bindName = c.node.text;
|
|
373
|
+
else if (n === 'bind.type' || n === 'field.type') bindType = c.node.text;
|
|
374
|
+
else if (n === 'bind.new' || n === 'field.new') bindNew = c.node.text;
|
|
375
|
+
else if (n === 'bind.call' || n === 'field.call') bindCall = c.node.text;
|
|
376
|
+
else if (n === 'field.var') bindVar = c.node.text;
|
|
377
|
+
else if (n === 'bind.expr' || n === 'field.expr') bindExpr = c.node;
|
|
378
|
+
else if (n === 'bind.elem' || n === 'field.elem') bindElem = c.node;
|
|
379
|
+
else if (n === 'bind.each') bindEach = c.node;
|
|
380
|
+
else if (n === 'bind.key') bindKey = c.node.text;
|
|
381
|
+
else if (n === 'bind.of') bindOf = c.node;
|
|
382
|
+
else if (n === 'scope') { role = 'scope'; main = c.node; }
|
|
383
|
+
else if (n === 'ret') { role = 'ret'; main = c.node; }
|
|
384
|
+
else if (n === 'cb') { role = 'cb'; main = c.node; }
|
|
385
|
+
else if (n === 'cb.fn') cbFn = c.node;
|
|
386
|
+
else if (n === 'cb.arg') cbArg = c.node;
|
|
387
|
+
}
|
|
388
|
+
if (role === 'cb') { if (cbFn && cbArg) cbSites.push({ call: main, fn: cbFn, arg: cbArg }); continue; }
|
|
389
|
+
if (role === 'scope') { scopeNodes.push(main); continue; }
|
|
390
|
+
if (role === 'ret') { retNodes.push(main); continue; }
|
|
391
|
+
if (role === 'def' && nameNode) {
|
|
392
|
+
// several patterns may match one definition (with/without return type, owner); merge them
|
|
393
|
+
const prev = seenDef.get(main.id);
|
|
394
|
+
if (prev !== undefined) {
|
|
395
|
+
const d = defs[prev];
|
|
396
|
+
if (!d.type && typeNode) d.type = typeNode.text;
|
|
397
|
+
if (!d.owner && ownerNode) d.owner = ownerNode.text;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
seenDef.set(main.id, defs.length);
|
|
401
|
+
defs.push({ node: main, nameNode, kind, owner: ownerNode?.text ?? null, type: typeNode?.text ?? null });
|
|
402
|
+
} else if (role === 'ref' && nameNode) {
|
|
403
|
+
// one reference per name node; the most specific kind wins (a decorator's call is a
|
|
404
|
+
// decorator ref, a `new X()` is an instantiation, not also a plain call).
|
|
405
|
+
const prev = seenRef.get(nameNode.id);
|
|
406
|
+
if (prev !== undefined) {
|
|
407
|
+
if ((REF_PRIORITY[kind] ?? 0) > (REF_PRIORITY[rawRefs[prev].kind] ?? 0)) rawRefs[prev] = { node: main, nameNode, kind, recvNode: recvNode ?? rawRefs[prev].recvNode };
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
seenRef.set(nameNode.id, rawRefs.length);
|
|
411
|
+
rawRefs.push({ node: main, nameNode, kind, recvNode });
|
|
412
|
+
} else if (role === 'import') importNodes.push(main);
|
|
413
|
+
else if (role === 'require' && srcNode) requireCalls.push({ node: main, srcNode });
|
|
414
|
+
else if ((role === 'bind' || role === 'field') && bindName) {
|
|
415
|
+
if ((main.type === 'required_parameter' || main.type === 'optional_parameter') && inTypeContext(main)) continue;
|
|
416
|
+
// one record per binding site; an explicit type annotation beats an initialiser
|
|
417
|
+
const list = role === 'bind' ? binds : fields;
|
|
418
|
+
const key = role + ':' + main.id + ':' + bindName;
|
|
419
|
+
let expr = bindExpr ? receiverDescriptor(bindExpr, spec) : null;
|
|
420
|
+
// a destructured property: `const { a: x } = e` is e.a, `({ a }: T)` is T's member a
|
|
421
|
+
if (bindKey) {
|
|
422
|
+
const of = bindOf && memberTypeIn(bindOf, bindKey);
|
|
423
|
+
if (of) { bindType = of; expr = null; }
|
|
424
|
+
else if (bindOf) { const t = normalizeType(bindOf.text, spec); expr = t ? `(${t}).${bindKey}` : null; }
|
|
425
|
+
else expr = memberOfDescriptor(expr, bindKey);
|
|
426
|
+
}
|
|
427
|
+
const rec = { node: main, name: bindName, type: bindType, new: bindNew, call: bindCall, var: bindVar,
|
|
428
|
+
expr, elem: bindElem ? elementsDescriptor(bindElem, spec) : null,
|
|
429
|
+
each: bindEach ? elementsDescriptor(bindEach, spec) : null };
|
|
430
|
+
const prev = bindSites.get(key);
|
|
431
|
+
if (prev === undefined) { bindSites.set(key, list.length); list.push(rec); }
|
|
432
|
+
else { const p = list[prev]; for (const k of ['type', 'new', 'call', 'var', 'expr', 'elem', 'each']) p[k] ??= rec[k]; }
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ── definitions ────────────────────────────────────────────────────────────
|
|
437
|
+
const filtered = [];
|
|
438
|
+
for (const d of defs) {
|
|
439
|
+
if ((d.kind === 'variable' || d.kind === 'constant') && spec.isModuleLevel && !spec.isModuleLevel(d.node)) continue;
|
|
440
|
+
if (d.kind === 'field' && spec.keepField && !spec.keepField(d.node)) continue;
|
|
441
|
+
if (spec.filterDef && !spec.filterDef(d)) continue;
|
|
442
|
+
filtered.push(d);
|
|
443
|
+
}
|
|
444
|
+
filtered.sort((a, b) => a.node.startIndex - b.node.startIndex || b.node.endIndex - a.node.endIndex);
|
|
445
|
+
|
|
446
|
+
const byNodeId = new Map();
|
|
447
|
+
const hoisted = new Set();
|
|
448
|
+
const defNameIds = new Set();
|
|
449
|
+
const anonQnames = new Set();
|
|
450
|
+
const symbols = [];
|
|
451
|
+
for (const d of filtered) {
|
|
452
|
+
let name = d.nameNode.text;
|
|
453
|
+
if (spec.normalizeName) name = spec.normalizeName(name, d, relPath);
|
|
454
|
+
if (!name) continue;
|
|
455
|
+
// enclosing definition (container) by walking ancestors
|
|
456
|
+
let parentIdx = -1;
|
|
457
|
+
for (let p = d.node.parent, hops = 0; p && hops < 64; p = p.parent, hops++) {
|
|
458
|
+
const idx = byNodeId.get(p.id);
|
|
459
|
+
if (idx !== undefined) { parentIdx = idx; break; }
|
|
460
|
+
}
|
|
461
|
+
const lexParent = parentIdx;
|
|
462
|
+
// constructor parameter properties (`constructor(private repo: Repo)`) are class members
|
|
463
|
+
if (d.kind === 'field' && parentIdx >= 0 && CONSTRUCTOR_NAMES.has(symbols[parentIdx].name) && symbols[parentIdx].parentIdx >= 0) parentIdx = symbols[parentIdx].parentIdx;
|
|
464
|
+
// attributes assigned on self inside any method (Python) belong to the class, once each
|
|
465
|
+
else if (d.kind === 'field' && spec.hoistMemberFields && parentIdx >= 0 && CALLABLE_KINDS.has(symbols[parentIdx].kind) && symbols[parentIdx].parentIdx >= 0
|
|
466
|
+
&& CLASS_KINDS.has(symbols[symbols[parentIdx].parentIdx].kind)) {
|
|
467
|
+
const cls = symbols[parentIdx].parentIdx;
|
|
468
|
+
if (hoisted.has(cls + ':' + name) || symbols.some(x => x.parentIdx === cls && x.name === name)) continue;
|
|
469
|
+
hoisted.add(cls + ':' + name);
|
|
470
|
+
parentIdx = cls;
|
|
471
|
+
}
|
|
472
|
+
let kind = d.kind;
|
|
473
|
+
const parent = parentIdx >= 0 ? symbols[parentIdx] : null;
|
|
474
|
+
if (kind === 'function' && parent && (parent.kind === 'class' || parent.kind === 'interface' || parent.kind === 'struct' || parent.kind === 'trait' || parent.kind === 'impl' || parent.kind === 'object')) kind = 'method';
|
|
475
|
+
if (spec.refineKind) kind = spec.refineKind(kind, d, parent) ?? kind;
|
|
476
|
+
let owner = d.owner;
|
|
477
|
+
if (spec.ownerOf && !owner) owner = spec.ownerOf(d.node);
|
|
478
|
+
const node = d.node;
|
|
479
|
+
// an anonymous class (`<anonymous>`) that repeats in one scope is told apart by its line
|
|
480
|
+
if (name === '<anonymous>' && anonQnames.has((parent ? parent.qname + '.' : '') + name)) name = `<anonymous@${node.startPosition.row + 1}>`;
|
|
481
|
+
let qname;
|
|
482
|
+
if (owner && (!parent || parent.name !== owner)) qname = (parent ? parent.qname + '.' : '') + owner + '.' + name;
|
|
483
|
+
else qname = parent ? parent.qname + '.' + name : name;
|
|
484
|
+
if (name.startsWith('<anonymous')) anonQnames.add(qname);
|
|
485
|
+
const sym = {
|
|
486
|
+
name,
|
|
487
|
+
qname,
|
|
488
|
+
kind,
|
|
489
|
+
parentIdx,
|
|
490
|
+
lexParent,
|
|
491
|
+
owner: owner || (parent && ['class', 'interface', 'struct', 'trait', 'impl', 'enum', 'object', 'module'].includes(parent.kind) ? parent.name : null),
|
|
492
|
+
startLine: node.startPosition.row + 1,
|
|
493
|
+
startCol: node.startPosition.column,
|
|
494
|
+
endLine: node.endPosition.row + 1,
|
|
495
|
+
endCol: node.endPosition.column,
|
|
496
|
+
nameLine: d.nameNode.startPosition.row + 1,
|
|
497
|
+
nameCol: d.nameNode.startPosition.column,
|
|
498
|
+
sig: signatureOf(node, spec, source),
|
|
499
|
+
doc: docOf(node, spec, source),
|
|
500
|
+
exported: false,
|
|
501
|
+
visibility: d.isDefault ? 'default' : (spec.visibility ? spec.visibility(node, name) : null),
|
|
502
|
+
decorators: spec.decoratorsOf ? spec.decoratorsOf(node) : [],
|
|
503
|
+
bases: spec.implicitBases?.(node) ?? [],
|
|
504
|
+
type: d.type ? declaredType(node, d.type, spec) : null,
|
|
505
|
+
isStatic: isStaticMember(node, d.nameNode, spec),
|
|
506
|
+
startIndex: node.startIndex,
|
|
507
|
+
endIndex: node.endIndex,
|
|
508
|
+
};
|
|
509
|
+
// a class's type records what collection it behaves as (`extends Map<string, Module>`)
|
|
510
|
+
if (MEMBER_PARENT.has(kind) && sym.type && !/(\[\]|\{\})$/.test(sym.type)) sym.type = null;
|
|
511
|
+
// `-> Self` / `: this` (fluent APIs) return the enclosing type
|
|
512
|
+
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;
|
|
513
|
+
// members are part of the public surface when their container is and they are not private
|
|
514
|
+
if (parent && MEMBER_PARENT.has(parent.kind)) sym.exported = parent.exported && sym.visibility !== 'private';
|
|
515
|
+
else sym.exported = spec.isExported ? Boolean(spec.isExported(node, name)) : true;
|
|
516
|
+
byNodeId.set(node.id, symbols.length);
|
|
517
|
+
defNameIds.add(d.nameNode.id);
|
|
518
|
+
symbols.push(sym);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// innermost enclosing symbol for a byte offset (symbols sorted by start; nested ranges)
|
|
522
|
+
const enclosingIdx = (offset) => {
|
|
523
|
+
let best = -1;
|
|
524
|
+
// binary search last symbol with startIndex <= offset, then walk up parents
|
|
525
|
+
let lo = 0, hi = symbols.length - 1, cand = -1;
|
|
526
|
+
while (lo <= hi) {
|
|
527
|
+
const mid = (lo + hi) >> 1;
|
|
528
|
+
if (symbols[mid].startIndex <= offset) { cand = mid; lo = mid + 1; } else hi = mid - 1;
|
|
529
|
+
}
|
|
530
|
+
for (let i = cand; i >= 0; i = symbols[i].lexParent) {
|
|
531
|
+
if (symbols[i].endIndex >= offset) { best = i; break; }
|
|
532
|
+
// not containing: try previous siblings/ancestors
|
|
533
|
+
if (symbols[i].lexParent < 0) {
|
|
534
|
+
// scan backwards for a container that encloses offset
|
|
535
|
+
for (let j = i - 1; j >= 0; j--) if (symbols[j].startIndex <= offset && symbols[j].endIndex >= offset) { best = j; break; }
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return best;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// ── lexical scopes: callables and types, plus anonymous functions (closures, lambdas) ──
|
|
543
|
+
const scopes = []; // { start, end, sym (owning symbol or -1), parent }
|
|
544
|
+
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 });
|
|
545
|
+
for (const n of scopeNodes) {
|
|
546
|
+
// `const f = () => …` / `m: function () {}`: the closure is the body of that symbol
|
|
547
|
+
const own = n.parent ? byNodeId.get(n.parent.id) : undefined;
|
|
548
|
+
scopes.push({ start: n.startIndex, end: n.endIndex, sym: own ?? -1 });
|
|
549
|
+
}
|
|
550
|
+
scopes.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
551
|
+
{
|
|
552
|
+
const stack = [];
|
|
553
|
+
for (let k = 0; k < scopes.length; k++) {
|
|
554
|
+
while (stack.length && scopes[stack[stack.length - 1]].end <= scopes[k].start) stack.pop();
|
|
555
|
+
scopes[k].parent = stack.length ? stack[stack.length - 1] : -1;
|
|
556
|
+
stack.push(k);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const scopeAt = (offset) => {
|
|
560
|
+
let lo = 0, hi = scopes.length - 1, cand = -1;
|
|
561
|
+
while (lo <= hi) { const mid = (lo + hi) >> 1; if (scopes[mid].start <= offset) { cand = mid; lo = mid + 1; } else hi = mid - 1; }
|
|
562
|
+
for (let k = cand; k >= 0; k = scopes[k].parent) if (scopes[k].end > offset) return k;
|
|
563
|
+
return -1;
|
|
564
|
+
};
|
|
565
|
+
/** Where an expression is evaluated: enclosing symbol (for `this`/fields) + lexical scope (for locals). */
|
|
566
|
+
const ctxAt = (offset) => ({ sym: enclosingIdx(offset), scope: scopeAt(offset) });
|
|
567
|
+
|
|
568
|
+
// ── type evidence (local bindings, class fields, returns) ────────────────
|
|
569
|
+
//
|
|
570
|
+
// Types are strings the resolver understands:
|
|
571
|
+
// 'T' / 'T[]' (a collection of) a named type
|
|
572
|
+
// 'T{}' a map (dictionary) whose values are T
|
|
573
|
+
// 'call:f' the return type of callable f (resolved globally)
|
|
574
|
+
// '…#a#b' member a, then member b, of the preceding type (crosses files)
|
|
575
|
+
// '[]' after a call/member part: the element type of that part
|
|
576
|
+
// '!' a value whose members never live in the repository (primitives, arrays)
|
|
577
|
+
// 'cb:i:j:f' parameter j of the function passed as argument i to callable f, typed by
|
|
578
|
+
// f's signature (f: `name`, `@module.name`, or `<type>::name` with # as ~)
|
|
579
|
+
const deferred = (t) => t.startsWith('call:') || t.startsWith('cb:') || t.includes('#');
|
|
580
|
+
const typeText = (t) => {
|
|
581
|
+
const n = normalizeType(t, spec);
|
|
582
|
+
if (!n) return null;
|
|
583
|
+
return !/(\[\]|\{\})$/.test(n) && spec.isPrimitiveType?.(n) ? '!' : n;
|
|
584
|
+
};
|
|
585
|
+
// narrowed types by source range: { key (receiver descriptor), type, from, to }
|
|
586
|
+
const narrowed = [];
|
|
587
|
+
if (/\binstanceof\b|\bisinstance\s*\(/.test(source)) {
|
|
588
|
+
for (const r of typeNarrowings(root, source)) {
|
|
589
|
+
const key = receiverDescriptor(r.subject, spec), type = typeText(r.type.text);
|
|
590
|
+
if (type && type !== '!' && /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/.test(key)) narrowed.push({ key, type, from: r.from, to: r.to });
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
/** The narrowest type test that holds at `pos` for `key` (`x`, `this.x`). */
|
|
594
|
+
const narrowedAt = (pos, key) => {
|
|
595
|
+
let best = null;
|
|
596
|
+
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;
|
|
597
|
+
return best?.type ?? null;
|
|
598
|
+
};
|
|
599
|
+
const localTypes = new Map(); // `${scope}:${name}` -> binding record | { conflict }
|
|
600
|
+
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;
|
|
601
|
+
// `helper((err, socket) => socket.send())`: an untyped parameter of a function passed to a call
|
|
602
|
+
// takes its type from the callee's signature, resolved across files (the `cb:` types above)
|
|
603
|
+
for (const { call, fn, arg } of cbSites) {
|
|
604
|
+
const args = call.childForFieldName('arguments')?.namedChildren.filter(n => n.type !== 'comment') ?? [];
|
|
605
|
+
const i = args.findIndex(n => n.id === arg.id);
|
|
606
|
+
const callee = fn.text.replace(/\s+/g, '').replace(/\?\./g, '.');
|
|
607
|
+
if (i < 0 || !/^(?:this\.)?[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*){0,2}$/.test(callee)) continue;
|
|
608
|
+
const dot = callee.lastIndexOf('.');
|
|
609
|
+
const recv = dot > 0 ? callee.slice(0, dot) : '', method = callee.slice(dot + 1);
|
|
610
|
+
if (recv && ELEMENT_WISE.has(method)) continue; // `xs.forEach(x => …)`: typed by the collection
|
|
611
|
+
const params = arg.childForFieldName('parameters')?.namedChildren ?? [arg.childForFieldName('parameter')].filter(Boolean);
|
|
612
|
+
params.filter(p => p.type !== 'comment').forEach((p, j) => {
|
|
613
|
+
const pat = (p.type === 'required_parameter' || p.type === 'optional_parameter') && !p.childForFieldName('type') ? p.childForFieldName('pattern') : p;
|
|
614
|
+
if (pat?.type === 'identifier') binds.push({ node: arg, name: pat.text, cb: { recv, method, i, j } });
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
const setLocal = (scope, name, val) => {
|
|
618
|
+
const k = scope + ':' + name;
|
|
619
|
+
const prev = localTypes.get(k);
|
|
620
|
+
if (!prev) localTypes.set(k, val);
|
|
621
|
+
else if (!prev.conflict && !sameBinding(prev, val)) localTypes.set(k, { conflict: true, end: Math.min(prev.end, val.end) });
|
|
622
|
+
else if (val.end < prev.end) prev.end = val.end;
|
|
623
|
+
};
|
|
624
|
+
for (const b of binds) {
|
|
625
|
+
const ctx = { ...ctxAt(b.node.startIndex), pos: b.node.startIndex };
|
|
626
|
+
const end = b.node.endIndex; // the local is in effect from here on
|
|
627
|
+
const typed = b.expr && /^(new|as) /.test(b.expr) ? b.expr.slice(3).trim() : null; // `new X(…)` / `… as X`
|
|
628
|
+
const type = b.type ? typeText(b.type) : (b.new ? typeText(b.new) : typed ? typeText(typed) : b.expr === '!' ? '!' : null);
|
|
629
|
+
if (type) setLocal(ctx.scope, b.name, { type, ctx, end });
|
|
630
|
+
else if (b.type && /^\{[^]*\}$/.test(b.type.trim())) setLocal(ctx.scope, b.name, { inline: b.type.trim(), ctx, end }); // `x: { a: A; … }`
|
|
631
|
+
else if (b.call) setLocal(ctx.scope, b.name, { call: b.call, ctx, end });
|
|
632
|
+
else if (b.cb) setLocal(ctx.scope, b.name, { cb: b.cb, ctx, end });
|
|
633
|
+
else if (b.expr && b.expr !== '?' && b.expr !== 'null' && b.expr !== b.name) setLocal(ctx.scope, b.name, { expr: b.expr, ctx, end });
|
|
634
|
+
else if (b.elem && b.elem !== '?') setLocal(ctx.scope, b.name, { elem: b.elem, ctx, end });
|
|
635
|
+
else if (b.each && b.each !== '?') setLocal(ctx.scope, b.name, { each: b.each, ctx, end });
|
|
636
|
+
else if (b.type || (b.expr && b.expr !== 'null')) setLocal(ctx.scope, b.name, { opaque: true, ctx, end }); // shadows outer bindings
|
|
637
|
+
}
|
|
638
|
+
const lookupLocal = (ctx, name) => {
|
|
639
|
+
for (let k = ctx.scope; ; k = scopes[k].parent) {
|
|
640
|
+
const v = localTypes.get(k + ':' + name);
|
|
641
|
+
if (v) return v.conflict || v.opaque ? null : v;
|
|
642
|
+
if (k < 0) break;
|
|
643
|
+
}
|
|
644
|
+
return null;
|
|
645
|
+
};
|
|
646
|
+
/** A local binding of `name` (typed or not) is visible at ctx: it shadows globals and builtins. */
|
|
647
|
+
const isDeclared = (ctx, name) => {
|
|
648
|
+
for (let k = ctx.scope; ; k = scopes[k].parent) {
|
|
649
|
+
if (localTypes.has(k + ':' + name)) return true;
|
|
650
|
+
if (k < 0) break;
|
|
651
|
+
}
|
|
652
|
+
return false;
|
|
653
|
+
};
|
|
654
|
+
/** Declared in a function's scope (not at the top level of the file) before `pos`. */
|
|
655
|
+
const isDeclaredLocally = (ctx, name, pos) => {
|
|
656
|
+
for (let k = ctx.scope; k >= 0; k = scopes[k].parent) if ((localTypes.get(k + ':' + name)?.end ?? Infinity) <= pos) return true;
|
|
657
|
+
return false;
|
|
658
|
+
};
|
|
659
|
+
const fieldTypes = new Map(); // `${classIdx}:${field}` -> type string (see above)
|
|
660
|
+
|
|
661
|
+
/** Apply `()` / `[]` operators to a type string; null when the result is unknown. */
|
|
662
|
+
const applyOps = (t, ops) => {
|
|
663
|
+
for (const op of ops) {
|
|
664
|
+
if (!t) return null;
|
|
665
|
+
if (op === '()') { if (!deferred(t) || (t.startsWith('cb:') && !t.includes('#'))) return null; continue; } // a method's type is its return type
|
|
666
|
+
if (deferred(t)) t += '[]';
|
|
667
|
+
else if (t.endsWith('[]') || t.endsWith('{}')) t = t.slice(0, -2); // element / map value
|
|
668
|
+
else return null; // indexing a string or an untyped object
|
|
669
|
+
}
|
|
670
|
+
return t;
|
|
671
|
+
};
|
|
672
|
+
// Iterating a map yields keys or entries except where the language hands out values (Go's
|
|
673
|
+
// `range`). A named type may be a collection class (`class Registry extends Map<string,
|
|
674
|
+
// Module>`), which only the resolver can tell: `T#[]` is an element of T itself.
|
|
675
|
+
const elemOf = (t) => (!t || t === '!' ? null : deferred(t) ? t + '[]' : t.endsWith('[]') ? t.slice(0, -2)
|
|
676
|
+
: t.endsWith('{}') ? (spec.mapIterValues ? t.slice(0, -2) : null) : t + '#[]');
|
|
677
|
+
// what an element-wise method hands its callback (`xs.forEach(x => …)`): an element, or a map's value
|
|
678
|
+
const eachOf = (t) => (!t || t === '!' ? null : deferred(t) ? t + '[]' : /(\[\]|\{\})$/.test(t) ? t.slice(0, -2) : t + '#[]');
|
|
679
|
+
/** The `cb:` type of a callback parameter, or null when the callee cannot be named. */
|
|
680
|
+
const callbackType = ({ recv, method, i, j }, ctx, depth) => {
|
|
681
|
+
let target = method; // a function in scope or imported
|
|
682
|
+
if (recv === 'this') {
|
|
683
|
+
const st = selfType(ctx.sym);
|
|
684
|
+
if (!st) return null;
|
|
685
|
+
target = st.name + '::' + method;
|
|
686
|
+
} else if (recv) {
|
|
687
|
+
const rt = inferType(recv, ctx, depth + 1);
|
|
688
|
+
if (rt && rt !== '!' && !/(\[\]|\{\})$/.test(rt)) target = rt.replace(/#/g, '~') + '::' + method;
|
|
689
|
+
else if (!rt && !isDeclared(ctx, recv.split('.')[0])) target = '@' + recv + '.' + method; // module, namespace or class
|
|
690
|
+
else return null;
|
|
691
|
+
}
|
|
692
|
+
return `cb:${i}:${j}:${target}`;
|
|
693
|
+
};
|
|
694
|
+
const bindingType = (v, depth) => {
|
|
695
|
+
if (v.type) return v.type;
|
|
696
|
+
if (v.call) return 'call:' + v.call;
|
|
697
|
+
if (v.cb) return depth > 5 ? null : callbackType(v.cb, v.ctx, depth);
|
|
698
|
+
if (v.expr) return inferType(v.expr, v.ctx, depth + 1);
|
|
699
|
+
if (v.elem) return elemOf(inferType(v.elem, v.ctx, depth + 1));
|
|
700
|
+
if (v.each) return eachOf(inferType(v.each, v.ctx, depth + 1));
|
|
701
|
+
return null;
|
|
702
|
+
};
|
|
703
|
+
const SEG_RE = /^([A-Za-z_$@][\w$]*)((?:\(\)|\[\])*)$/;
|
|
704
|
+
// the type `this`/`self` denotes inside a symbol: its class, or the receiver type of an
|
|
705
|
+
// out-of-body method (Rust impl blocks, Go receivers) — { idx: class symbol or -1, name }
|
|
706
|
+
const classByName = new Map();
|
|
707
|
+
const selfType = (encl) => {
|
|
708
|
+
const cls = classOf(encl, symbols);
|
|
709
|
+
if (cls >= 0) return { idx: cls, name: symbols[cls].qname };
|
|
710
|
+
for (let i = encl; i >= 0; i = symbols[i].parentIdx) {
|
|
711
|
+
const o = symbols[i].owner;
|
|
712
|
+
if (!o) continue;
|
|
713
|
+
if (!classByName.has(o)) classByName.set(o, symbols.findIndex(x => x.name === o && CLASS_KINDS.has(x.kind)));
|
|
714
|
+
const j = classByName.get(o);
|
|
715
|
+
return { idx: j, name: j >= 0 ? symbols[j].qname : o };
|
|
716
|
+
}
|
|
717
|
+
return null;
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
/** Static type of an expression descriptor evaluated in `ctx`, as far as this file tells. */
|
|
721
|
+
const inferType = (desc, ctx, depth = 0) => {
|
|
722
|
+
if (!desc || depth > 5 || desc === '?' || desc === 'super' || desc === 'null') return null;
|
|
723
|
+
if (desc === '!') return '!';
|
|
724
|
+
if (desc.startsWith('new ') || desc.startsWith('as ')) {
|
|
725
|
+
const tn = desc.slice(3).trim();
|
|
726
|
+
if (tn === 'Self' || tn === 'self' || tn === 'static' || tn === 'this') return selfType(ctx.sym)?.name ?? null;
|
|
727
|
+
return typeText(tn);
|
|
728
|
+
}
|
|
729
|
+
// `(T).a().b`: a chain on a cast value (see receiverDescriptor)
|
|
730
|
+
let cast = null;
|
|
731
|
+
if (desc.startsWith('(')) {
|
|
732
|
+
const k = desc.indexOf(')');
|
|
733
|
+
if (k < 0 || desc[k + 1] !== '.') return null;
|
|
734
|
+
cast = typeText(desc.slice(1, k));
|
|
735
|
+
if (!cast) return null;
|
|
736
|
+
desc = desc.slice(k + 2);
|
|
737
|
+
}
|
|
738
|
+
const segs = [];
|
|
739
|
+
for (const p of desc.split('.')) {
|
|
740
|
+
const m = SEG_RE.exec(p);
|
|
741
|
+
if (!m) return null;
|
|
742
|
+
segs.push({ name: m[1], ops: m[2].match(/\(\)|\[\]/g) ?? [] });
|
|
743
|
+
}
|
|
744
|
+
let t = null, i = 1;
|
|
745
|
+
const first = segs[0];
|
|
746
|
+
if (cast) { t = cast; i = 0; }
|
|
747
|
+
else if (first.name === 'this') {
|
|
748
|
+
const st = selfType(ctx.sym);
|
|
749
|
+
if (!st) return null;
|
|
750
|
+
if (segs.length === 1) return first.ops.length ? null : st.name;
|
|
751
|
+
const nt = ctx.pos != null && narrowed.length ? narrowedAt(ctx.pos, 'this.' + segs[1].name) : null;
|
|
752
|
+
const ft = nt ?? (st.idx >= 0 ? fieldTypes.get(st.idx + ':' + segs[1].name) : null);
|
|
753
|
+
t = applyOps(ft ?? st.name + '#' + segs[1].name, segs[1].ops);
|
|
754
|
+
i = 2;
|
|
755
|
+
} else {
|
|
756
|
+
const isCall = first.ops[0] === '()';
|
|
757
|
+
const nt = !isCall && ctx.pos != null && narrowed.length ? narrowedAt(ctx.pos, first.name) : null;
|
|
758
|
+
const v = isCall ? null : nt ? { type: nt } : lookupLocal(ctx, first.name);
|
|
759
|
+
// a value of an inline object type: its member's written type
|
|
760
|
+
if (v?.inline && !first.ops.length && segs.length > 1) { t = applyOps(typeText(inlineMemberType(v.inline, segs[1].name)), segs[1].ops); i = 2; }
|
|
761
|
+
else if (v) t = applyOps(bindingType(v, depth), first.ops);
|
|
762
|
+
else if (isCall) t = applyOps('call:' + first.name, first.ops.slice(1));
|
|
763
|
+
else if (spec.implicitThis && classOf(ctx.sym, symbols) >= 0) {
|
|
764
|
+
t = applyOps(fieldTypes.get(classOf(ctx.sym, symbols) + ':' + first.name) ?? null, first.ops);
|
|
765
|
+
// a field the class inherits (declared in a base, maybe in another file): the resolver's to find
|
|
766
|
+
if (!t && /^[a-z_]/.test(first.name) && !isDeclared(ctx, first.name)) t = applyOps(symbols[classOf(ctx.sym, symbols)].qname + '#' + first.name, first.ops);
|
|
767
|
+
}
|
|
768
|
+
// `Type.staticField…`: a capitalised root that is not a local names a type
|
|
769
|
+
if (!t && !v && segs.length > 1 && !first.ops.length && /^[A-Z]/.test(first.name)) t = first.name;
|
|
770
|
+
// `b := Form`: a package-level variable, maybe of another file of the package; its type is
|
|
771
|
+
// the resolver's to find (a Go name is never both a type and a value)
|
|
772
|
+
else if (!t && !v && segs.length === 1 && !first.ops.length && spec.packageValues && !isDeclared(ctx, first.name)) t = first.name;
|
|
773
|
+
// `pkg.New()` / `utils.makeFoo()`: a call through a module or import alias returns the callee's type
|
|
774
|
+
else if (!t && !v && segs.length > 1 && !first.ops.length && segs[1].ops[0] === '()') {
|
|
775
|
+
t = applyOps('call:' + first.name + '.' + segs[1].name, segs[1].ops.slice(1));
|
|
776
|
+
i = 2;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
for (; i < segs.length && t; i++) {
|
|
780
|
+
const seg = segs[i];
|
|
781
|
+
const call = seg.ops[0] === '()';
|
|
782
|
+
if (call && spec.identityMethods?.has(seg.name)) { t = applyOps(t, seg.ops.slice(1)); continue; }
|
|
783
|
+
if (t === '!') return '!';
|
|
784
|
+
if (!deferred(t) && t.endsWith('[]')) {
|
|
785
|
+
// `list.get(0)`, `xs.first()`: collection accessors hand back an element
|
|
786
|
+
if (call && spec.elementMethods?.has(seg.name)) { t = applyOps(t.slice(0, -2), seg.ops.slice(1)); continue; }
|
|
787
|
+
return '!'; // any other member of a collection is the language's
|
|
788
|
+
}
|
|
789
|
+
if (!deferred(t) && t.endsWith('{}')) {
|
|
790
|
+
// `m.get(k)` hands back a value, `m.values()` a collection of values
|
|
791
|
+
if (call && spec.mapMethods?.has(seg.name)) { t = applyOps(t.slice(0, -2), seg.ops.slice(1)); continue; }
|
|
792
|
+
if (call && spec.mapValueMethods?.has(seg.name)) { t = applyOps(t.slice(0, -2) + '[]', seg.ops.slice(1)); continue; }
|
|
793
|
+
return '!';
|
|
794
|
+
}
|
|
795
|
+
t = applyOps(t + '#' + seg.name, seg.ops);
|
|
796
|
+
}
|
|
797
|
+
return t;
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
for (const f of fields) {
|
|
801
|
+
const ctx = ctxAt(f.node.startIndex);
|
|
802
|
+
const cls = classOf(ctx.sym, symbols);
|
|
803
|
+
if (cls < 0) continue;
|
|
804
|
+
let type = f.type ? typeText(f.type) : (f.new ? typeText(f.new) : null);
|
|
805
|
+
if (!type && f.var) { const v = lookupLocal(ctx, f.var); type = v ? bindingType(v, 0) : null; }
|
|
806
|
+
if (!type && f.call) type = 'call:' + f.call;
|
|
807
|
+
if (!type && f.expr) type = inferType(f.expr, ctx);
|
|
808
|
+
if (!type) continue;
|
|
809
|
+
if (!fieldTypes.has(cls + ':' + f.name)) fieldTypes.set(cls + ':' + f.name, type);
|
|
810
|
+
}
|
|
811
|
+
// declared field/property symbols carry their type too (struct fields, typed class attributes)
|
|
812
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
813
|
+
const s = symbols[i];
|
|
814
|
+
if ((s.kind === 'field' || s.kind === 'property') && s.parentIdx >= 0) {
|
|
815
|
+
const k = s.parentIdx + ':' + s.name;
|
|
816
|
+
if (s.type) { if (!fieldTypes.has(k) && typeText(s.type) !== '!') fieldTypes.set(k, s.type); }
|
|
817
|
+
else if (fieldTypes.has(k)) s.type = fieldTypes.get(k);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
// record inferred (assignment-based) field types as field symbols' type when the field itself
|
|
821
|
+
// is not declared in the class body (common in Python/JS: self.x = X() in __init__)
|
|
822
|
+
const inferredFields = [];
|
|
823
|
+
for (const [k, type] of fieldTypes) {
|
|
824
|
+
const [clsIdx, fname] = [Number(k.slice(0, k.indexOf(':'))), k.slice(k.indexOf(':') + 1)];
|
|
825
|
+
inferredFields.push({ owner: symbols[clsIdx].qname, name: fname, type });
|
|
826
|
+
}
|
|
827
|
+
// module-level values typed by their initialiser (`export const api = new ApiClient()`), so
|
|
828
|
+
// importers can reach the members of such singletons
|
|
829
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
830
|
+
const s = symbols[i];
|
|
831
|
+
if (s.type || (s.kind !== 'variable' && s.kind !== 'constant')) continue;
|
|
832
|
+
const v = localTypes.get(scopeAt(s.startIndex) + ':' + s.name);
|
|
833
|
+
if (v && !v.conflict && !v.opaque) s.type = bindingType(v, 0);
|
|
834
|
+
}
|
|
835
|
+
// undeclared return types: all `return` expressions of a callable agree on one type
|
|
836
|
+
const returns = new Map(); // symIdx -> [types]
|
|
837
|
+
for (const n of retNodes) {
|
|
838
|
+
const k = scopeAt(n.startIndex);
|
|
839
|
+
const owner = k >= 0 ? scopes[k].sym : -1;
|
|
840
|
+
if (owner < 0 || symbols[owner].type || !CALLABLE_KINDS.has(symbols[owner].kind)) continue;
|
|
841
|
+
const desc = receiverDescriptor(n, spec);
|
|
842
|
+
if (desc === 'null') continue; // `return null` / `return None` keeps the other paths' type
|
|
843
|
+
const list = returns.get(owner) ?? returns.set(owner, []).get(owner);
|
|
844
|
+
list.push(inferType(desc, { sym: owner, scope: k }));
|
|
845
|
+
}
|
|
846
|
+
for (const [owner, types] of returns) {
|
|
847
|
+
const uniq = new Set(types);
|
|
848
|
+
if (uniq.size === 1 && !uniq.has(null)) symbols[owner].type = types[0];
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const inferReceiverType = (recv, ctx) => {
|
|
852
|
+
if (!recv || recv === 'this') return null;
|
|
853
|
+
const t = inferType(recv, ctx);
|
|
854
|
+
return t && !deferred(t) && /(\[\]|\{\})$/.test(t) ? '!' : t; // a collection's own methods are the language's
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
// ── references ─────────────────────────────────────────────────────────────
|
|
858
|
+
const refs = [];
|
|
859
|
+
for (const r of rawRefs) {
|
|
860
|
+
if (defNameIds.has(r.nameNode.id)) continue; // a definition's own name
|
|
861
|
+
let name = r.nameNode.text;
|
|
862
|
+
if (spec.normalizeRefName) name = spec.normalizeRefName(name, r);
|
|
863
|
+
if (!name || name.length > 128) continue;
|
|
864
|
+
const kind = r.kind;
|
|
865
|
+
const recv = r.recvNode ? receiverDescriptor(r.recvNode, spec) : '';
|
|
866
|
+
const ctx = ctxAt(r.nameNode.startIndex);
|
|
867
|
+
if (kind === 'call' && !recv && spec.builtinCalls?.has(name) && !isDeclared(ctx, name)) continue;
|
|
868
|
+
const root = recv.split('.')[0];
|
|
869
|
+
// `module.x()` is the CommonJS global only when no local `module` is in scope
|
|
870
|
+
if (recv && spec.externalReceivers?.has(root) && !isDeclared(ctx, root)) continue;
|
|
871
|
+
if (kind === 'value' && spec.isNoiseValue?.(name)) continue;
|
|
872
|
+
// a value or a call by the name of a local (`stack := stack(3); log(stack)`, `check := func…;
|
|
873
|
+
// check()`) is the local, in languages where no local stands for repository code (Go: no
|
|
874
|
+
// imports bind locals)
|
|
875
|
+
if (!recv && (kind === 'value' ? spec.packageValues || spec.localValues : kind === 'call' && spec.packageValues) && isDeclaredLocally(ctx, name, r.nameNode.startIndex)) continue;
|
|
876
|
+
const pos = r.nameNode.startPosition;
|
|
877
|
+
const encl = ctx.sym;
|
|
878
|
+
const recvType = inferReceiverType(recv, { ...ctx, pos: r.nameNode.startIndex });
|
|
879
|
+
// arguments passed: tells overloads apart where each one is a method of its own
|
|
880
|
+
const a = spec.distinctOverloads && (kind === 'call' || kind === 'new') ? callArgc(r.node) : null;
|
|
881
|
+
refs.push({ name, kind, line: pos.row + 1, col: pos.column, recv, recvType, symIdx: encl, argc: a ? (a.open ? -1 : a.n) : null });
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// inheritance: attach base names to the enclosing class symbol
|
|
885
|
+
for (const r of refs) {
|
|
886
|
+
if (r.kind !== 'inherit' || r.symIdx < 0) continue;
|
|
887
|
+
const s = symbols[r.symIdx];
|
|
888
|
+
if (['class', 'interface', 'struct', 'trait', 'enum', 'object', 'impl'].includes(s.kind) && !s.bases.includes(r.name)) s.bases.push(r.name);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// ── imports ────────────────────────────────────────────────────────────────
|
|
892
|
+
const imports = [];
|
|
893
|
+
for (const n of importNodes) {
|
|
894
|
+
for (const rec of spec.parseImport(n, source)) imports.push({ ...rec, line: n.startPosition.row + 1 });
|
|
895
|
+
}
|
|
896
|
+
for (const { node, srcNode } of requireCalls) {
|
|
897
|
+
for (const rec of spec.parseRequire(node, srcNode)) imports.push({ ...rec, line: node.startPosition.row + 1 });
|
|
898
|
+
}
|
|
899
|
+
const extra = spec.fileInfo ? spec.fileInfo(root, source) : null;
|
|
900
|
+
|
|
901
|
+
for (const s of symbols) { delete s.startIndex; delete s.endIndex; delete s.lexParent; }
|
|
902
|
+
return { symbols, refs, imports, fields: inferredFields, fileInfo: extra, errors: root.hasError ? 1 : 0 };
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const NULLISH_TYPES = new Set(['null', 'undefined', 'None', 'void', 'nil', 'never', 'NoneType', 'Nothing', 'Unit']);
|
|
906
|
+
// declared-by-inference keywords in type position: the initialiser decides
|
|
907
|
+
const INFERRED_TYPES = new Set(['var', 'auto', 'let', 'val', 'dynamic', '_', 'implicit_type']);
|
|
908
|
+
|
|
909
|
+
/** Split at top-level occurrences of `sep` (outside <> [] () {}; `=>` is not a bracket). */
|
|
910
|
+
function splitTop(s, sep) {
|
|
911
|
+
const out = [];
|
|
912
|
+
let depth = 0, start = 0;
|
|
913
|
+
for (let i = 0; i < s.length; i++) {
|
|
914
|
+
const c = s[i];
|
|
915
|
+
if (c === '<' || c === '[' || c === '(' || c === '{') depth++;
|
|
916
|
+
else if ((c === '>' && s[i - 1] !== '=' && s[i - 1] !== '-') || c === ']' || c === ')' || c === '}') depth--;
|
|
917
|
+
else if (c === sep && depth === 0) { out.push(s.slice(start, i)); start = i + 1; }
|
|
918
|
+
}
|
|
919
|
+
out.push(s.slice(start));
|
|
920
|
+
return out;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const arrayOf = (e) => (e && !/(\[\]|\{\})$/.test(e) ? e + '[]' : null);
|
|
924
|
+
const mapOf = (v) => (v && !/(\[\]|\{\})$/.test(v) ? v + '{}' : null);
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Static type text → what member lookup needs: `Foo`, `Foo[]` (a collection of Foo, so `x[i]`
|
|
928
|
+
* 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`,
|
|
929
|
+
* `Optional[Foo]`, `Foo?`), transparent wrappers unwrap per language (`Promise<Foo>`,
|
|
930
|
+
* `Box<Foo>`, `Task<Foo>`), pointers/references/qualifiers and namespaces are dropped.
|
|
931
|
+
*/
|
|
932
|
+
export function normalizeType(t, spec, depth = 0) {
|
|
933
|
+
if (t == null || depth > 5) return null;
|
|
934
|
+
let s = String(t).trim();
|
|
935
|
+
if (spec?.cleanType) { s = spec.cleanType(s); if (!s) return null; }
|
|
936
|
+
s = s.replace(/^["']|["']$/g, '')
|
|
937
|
+
.replace(/^(?:(?:readonly|const|mut|dyn|impl|typeof|unique|volatile|struct|enum|class|final|ref|in|out|inout)\s+|[&*^]\s*|'[A-Za-z_]\w*\s+)+/, '')
|
|
938
|
+
.replace(/(?:\s*(?:[?!*&]|\.\.\.))+$/, '')
|
|
939
|
+
.trim();
|
|
940
|
+
if (!s || /^keyof\s/.test(s) || INFERRED_TYPES.has(s)) return null;
|
|
941
|
+
const alts = splitTop(s, '|').map(x => x.trim()).filter(Boolean);
|
|
942
|
+
if (alts.length > 1) {
|
|
943
|
+
const norm = [...new Set(alts.filter(a => !NULLISH_TYPES.has(a)).map(a => normalizeType(a, spec, depth + 1)))];
|
|
944
|
+
return norm.length === 1 ? norm[0] : null;
|
|
945
|
+
}
|
|
946
|
+
if (splitTop(s, '&').length > 1 || splitTop(s, ',').length > 1) return null; // intersections / tuples
|
|
947
|
+
let m;
|
|
948
|
+
if ((m = /^\((.*)\)$/s.exec(s))) return normalizeType(m[1], spec, depth + 1);
|
|
949
|
+
if ((m = /^(.+?)\s*\[\s*\]$/s.exec(s))) return arrayOf(normalizeType(m[1], spec, depth + 1)); // T[]
|
|
950
|
+
if ((m = /^([A-Za-z_$][\w$.]*)\{\}$/.exec(s))) return mapOf(normalizeType(m[1], spec, depth + 1)); // a map of T, as normalised
|
|
951
|
+
if ((m = /^\[\s*\]\s*(.+)$/s.exec(s))) return arrayOf(normalizeType(m[1], spec, depth + 1)); // Go []T
|
|
952
|
+
if ((m = /^map\s*\[[^\]]*\]\s*(.+)$/s.exec(s))) return mapOf(normalizeType(m[1], spec, depth + 1)); // Go map[K]V
|
|
953
|
+
if ((m = /^\[([^;\]]+?)\s*(?:;[^\]]*)?\]$/s.exec(s))) return arrayOf(normalizeType(m[1], spec, depth + 1)); // Rust [T] / [T; N]
|
|
954
|
+
if ((m = /^([A-Za-z_$][\w$]*(?:(?:\.|::|\\)[A-Za-z_$][\w$]*)*)\s*[<[](.*)[>\]]$/s.exec(s))) {
|
|
955
|
+
const head = m[1].split(/\.|::|\\/).pop();
|
|
956
|
+
const args = splitTop(m[2], ',').map(x => x.trim());
|
|
957
|
+
if (spec?.transparentTypes?.has(head)) return normalizeType(args[0], spec, depth + 1);
|
|
958
|
+
if (spec?.elementTypes?.has(head)) return arrayOf(normalizeType(args[0], spec, depth + 1));
|
|
959
|
+
if (spec?.mapTypes?.has(head)) return mapOf(normalizeType(args[args.length - 1], spec, depth + 1));
|
|
960
|
+
if (head === 'Union') return normalizeType(args.join('|'), spec, depth + 1);
|
|
961
|
+
s = m[1];
|
|
962
|
+
}
|
|
963
|
+
s = s.replace(/<.*$/s, '').replace(/\(.*$/s, '').replace(/\[.*$/s, '').trim();
|
|
964
|
+
const segs = s.split(/::|\.|\\/).filter(Boolean);
|
|
965
|
+
// `http.Request`: the package tells a type of the repository from a library's (see Resolver)
|
|
966
|
+
if (spec?.qualifiedTypes && segs.length === 2 && /^[A-Za-z_]\w*\.[A-Za-z_]\w*$/.test(s)) return s;
|
|
967
|
+
// `Token.Comment`, `org.x.Outer.Inner`: a nested type keeps its enclosing types, not its package
|
|
968
|
+
if (spec?.nestedTypes && segs.length >= 2 && segs.every(x => /^[A-Za-z_$][\w$]*$/.test(x))) {
|
|
969
|
+
let k = segs.length - 1;
|
|
970
|
+
while (k > 0 && /^[A-Z]/.test(segs[k - 1])) k--;
|
|
971
|
+
if (k < segs.length - 1) return segs.slice(k).join('.');
|
|
972
|
+
}
|
|
973
|
+
s = segs.length ? segs[segs.length - 1] : s;
|
|
974
|
+
return /^[A-Za-z_$][\w$]*$/.test(s) ? s : null;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/** @deprecated nominal head only (kept for callers that need a plain name). */
|
|
978
|
+
export function cleanTypeName(t, spec) {
|
|
979
|
+
const n = normalizeType(t, spec);
|
|
980
|
+
return n && n.endsWith('[]') ? n.slice(0, -2) : n;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const CLASS_KINDS = new Set(['class', 'struct', 'impl', 'object', 'trait', 'interface', 'enum']);
|
|
984
|
+
const VALUE_KINDS = new Set(['variable', 'constant', 'field', 'property']);
|
|
985
|
+
const CALLABLE_KINDS = new Set(['function', 'method']);
|
|
986
|
+
const CONSTRUCTOR_NAMES = new Set(['constructor', '__construct']);
|
|
987
|
+
|
|
988
|
+
/** `get<T extends Foo = Foo>(): T` → the declared return type `T` means `Foo` (bound or default). */
|
|
989
|
+
/**
|
|
990
|
+
* Declared type of a definition, with the definition's own type parameters replaced by their
|
|
991
|
+
* bound or default: `create<T extends App = App>(): Promise<T>` is typed `App` (the wrapper is
|
|
992
|
+
* unwrapped first, then `T` looked up).
|
|
993
|
+
*/
|
|
994
|
+
function declaredType(defNode, typeText, spec) {
|
|
995
|
+
const direct = typeParamBound(defNode, typeText);
|
|
996
|
+
if (direct !== undefined) return direct === null ? null : normalizeType(direct, spec);
|
|
997
|
+
const n = normalizeType(typeText, spec);
|
|
998
|
+
if (!n) return null;
|
|
999
|
+
const elem = n.endsWith('[]') ? '[]' : '';
|
|
1000
|
+
const bound = typeParamBound(defNode, elem ? n.slice(0, -2) : n);
|
|
1001
|
+
if (bound === undefined) return n;
|
|
1002
|
+
const b = bound === null ? null : normalizeType(bound, spec);
|
|
1003
|
+
return b ? b + elem : null;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function typeParamBound(defNode, typeText) {
|
|
1007
|
+
const name = String(typeText).trim();
|
|
1008
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(name)) return undefined;
|
|
1009
|
+
const tps = defNode.childForFieldName?.('type_parameters') ?? defNode.namedChildren?.find(c => c.type === 'type_parameters');
|
|
1010
|
+
if (!tps) return undefined;
|
|
1011
|
+
for (const p of tps.namedChildren) {
|
|
1012
|
+
const m = /^(?:(?:in|out|const|reified)\s+)*([A-Za-z_$][\w$]*)\s*(?:(?:extends|:)\s*([^=]+?))?\s*(?:=\s*(.+))?$/s.exec(p.text);
|
|
1013
|
+
if (m && m[1] === name) return (m[3] ?? m[2] ?? '').trim() || null;
|
|
1014
|
+
}
|
|
1015
|
+
return undefined;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/** Static members (`static`, `@staticmethod`/`@classmethod`) never share a family with instance members. */
|
|
1019
|
+
function isStaticMember(node, nameNode, spec) {
|
|
1020
|
+
if (spec.isStatic) return spec.isStatic(node);
|
|
1021
|
+
const head = node.text.slice(0, Math.max(0, nameNode.startIndex - node.startIndex));
|
|
1022
|
+
return /(^|\s)static\s/.test(head);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function classOf(idx, symbols) {
|
|
1026
|
+
for (let i = idx; i >= 0; i = symbols[i].parentIdx) {
|
|
1027
|
+
const k = symbols[i].kind;
|
|
1028
|
+
if (k === 'class' || k === 'struct' || k === 'impl' || k === 'object' || k === 'trait' || k === 'interface' || k === 'enum') return i;
|
|
1029
|
+
}
|
|
1030
|
+
return -1;
|
|
1031
|
+
}
|