graph-indexer 2.1.0 → 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 +31 -105
- 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,954 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference resolution: binds every reference occurrence to the definition it most likely
|
|
3
|
+
* denotes, with an explicit confidence, using only what a parser can see:
|
|
4
|
+
*
|
|
5
|
+
* receiver `this`/`self`/`super` → members of the enclosing type (+ inherited)
|
|
6
|
+
* receiver with an inferred type → members of that type (+ inherited), across files
|
|
7
|
+
* receiver that is an import alias → exported symbol of that module / package
|
|
8
|
+
* bare name → nested scope → enclosing type (implicit-this languages)
|
|
9
|
+
* → same file → explicit import → same package/dir
|
|
10
|
+
* → wildcard import → unique global name → ambiguous
|
|
11
|
+
*
|
|
12
|
+
* Confidence scale: ≥0.9 exact (scope/type/import proven) · ≥0.7 high · ≥0.4 likely · <0.4 possible.
|
|
13
|
+
* Ambiguous references stay attached to the best candidate with a low confidence and a
|
|
14
|
+
* candidate count, so tools can say "3 possible targets" instead of guessing silently.
|
|
15
|
+
*/
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { normalizeType } from '../parse/extract.mjs';
|
|
18
|
+
import { fits, sigArity } from '../parse/arity.mjs';
|
|
19
|
+
|
|
20
|
+
const TYPE_KINDS = new Set(['class', 'interface', 'struct', 'enum', 'trait', 'type', 'object', 'module', 'impl']);
|
|
21
|
+
const VALUE_TYPED_KINDS = new Set(['variable', 'constant', 'field', 'property']);
|
|
22
|
+
const CALLABLE_KINDS = new Set(['function', 'method', 'constructor', 'macro']);
|
|
23
|
+
const MEMBER_HOLDERS = new Set(['class', 'interface', 'struct', 'enum', 'trait', 'object', 'impl', 'module']);
|
|
24
|
+
const MEMBER_KINDS = new Set(['field', 'property', 'method', 'constructor']);
|
|
25
|
+
const PACKAGE_LANGS = new Set(['java', 'kotlin', 'scala', 'csharp', 'php']);
|
|
26
|
+
const DIR_PACKAGE_LANGS = new Set(['go']);
|
|
27
|
+
const MAX_GLOBAL_CANDIDATES = 12;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Method names that standard-library types of each language family also define (`map.get(k)`,
|
|
31
|
+
* `promise.then()`, `w.Header().Get()`, `list.append()`). A call with an unknown receiver and one
|
|
32
|
+
* of these names is never bound by name alone: it is far more often the library's method than a
|
|
33
|
+
* same-named method in the repository.
|
|
34
|
+
*/
|
|
35
|
+
const COMMON_MEMBERS = {
|
|
36
|
+
ecma: 'get set has delete add clear forEach map filter reduce reduceRight find findIndex findLast findLastIndex some every includes indexOf lastIndexOf join split slice splice concat push pop shift unshift sort reverse keys values entries fill flat flatMap at apply call bind then catch finally toString valueOf toJSON emit on once off addListener removeListener removeAllListeners listeners send write end close open read pipe resolve reject next return throw test exec match matchAll replace replaceAll trim trimStart trimEnd startsWith endsWith padStart padEnd charAt charCodeAt codePointAt substring substr toLowerCase toUpperCase localeCompare normalize repeat assign create freeze defineProperty hasOwnProperty isArray from of parse stringify log error warn info debug trace json status sendStatus header set setHeader getHeader removeHeader writeHead redirect render use listen subscribe unsubscribe complete pipe lift toPromise getTime toISOString setTimeout clone copy dispose destroy init start stop run update create remove insert append',
|
|
37
|
+
python: 'get set items keys values update pop popitem append extend insert remove clear copy sort reverse index count join split rsplit splitlines strip lstrip rstrip replace format startswith endswith lower upper title encode decode read write close open readline readlines seek tell flush send recv add discard union intersection difference issubset setdefault fromkeys sleep info debug warning warn error exception critical log search match fullmatch findall finditer sub compile group groups groupdict dumps loads dump load exists mkdir makedirs is_file is_dir resolve cast run start stop wait acquire release put get_nowait put_nowait result done cancel',
|
|
38
|
+
go: 'Get Set Add Del Delete Has Len Less Swap String Error Write Read Close Next Value Done Err Lock Unlock RLock RUnlock Wait Load Store LoadOrStore Header WriteHeader Printf Println Print Sprintf Errorf Fatal Fatalf Log Logf Run Cleanup Helper Skip Parse Format Unix Now Since Sub Before After Equal Scan Query QueryRow Exec Begin Commit Rollback Marshal Unmarshal Encode Decode New Copy Reset Bytes Seek Flush Cancel Deadline WithValue Context Body Cookie Query',
|
|
39
|
+
jvm: 'get set add addAll put putAll remove contains containsKey containsValue size isEmpty clear iterator stream map filter collect forEach equals hashCode toString valueOf of apply accept test length charAt substring append build close write read flush println print printf format getName getClass orElse orElseThrow isPresent ifPresent join compareTo keySet values entrySet getKey getValue sort',
|
|
40
|
+
csharp: 'Add AddRange Remove Contains ContainsKey Get Set ToString Equals GetHashCode Count Any All Select Where First FirstOrDefault Last ToList ToArray Dispose Write WriteLine Read ReadLine Close Invoke GetValue SetValue TryGetValue Append Clear Insert IndexOf Join Split Trim Replace Format',
|
|
41
|
+
rust: 'get get_mut set insert remove push pop len is_empty iter iter_mut into_iter map filter collect unwrap expect clone to_string to_owned as_str as_ref as_mut borrow borrow_mut lock read write send recv next fmt eq cmp partial_cmp hash from into new default ok err is_some is_none and_then unwrap_or unwrap_or_else map_err contains extend join split trim',
|
|
42
|
+
ruby: 'each map select reject find detect include? push pop shift unshift each_with_index each_with_object to_s to_i to_a to_h to_sym keys values fetch merge join split strip call new puts print send respond_to? nil? empty? any? all? first last count size length',
|
|
43
|
+
php: 'get set has add remove count toArray all first last map filter each push pop merge keys values',
|
|
44
|
+
c: 'push_back pop_back size begin end find insert erase at c_str get reset clear empty front back emplace emplace_back data swap',
|
|
45
|
+
};
|
|
46
|
+
const COMMON_BY_FAMILY = Object.fromEntries(Object.entries(COMMON_MEMBERS).map(([k, v]) => [k, new Set(v.split(/\s+/))]));
|
|
47
|
+
|
|
48
|
+
function push(map, key, val) {
|
|
49
|
+
const a = map.get(key);
|
|
50
|
+
if (a) a.push(val); else map.set(key, [val]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Receiver type whose members never live in the repository (primitives, builtins, collections). */
|
|
54
|
+
export const EXTERNAL = Object.freeze({ external: true });
|
|
55
|
+
|
|
56
|
+
/** Split a list at top-level commas (brackets, generics and nested function types kept whole). */
|
|
57
|
+
function splitTopLevel(text, sep = ',') {
|
|
58
|
+
const out = [];
|
|
59
|
+
let depth = 0, cur = '';
|
|
60
|
+
for (let k = 0; k < text.length; k++) {
|
|
61
|
+
const ch = text[k];
|
|
62
|
+
if ('([{<'.includes(ch)) depth++;
|
|
63
|
+
else if (')]}'.includes(ch) || (ch === '>' && text[k - 1] !== '=')) depth--;
|
|
64
|
+
if (ch === sep && depth === 0) { out.push(cur.trim()); cur = ''; } else cur += ch;
|
|
65
|
+
}
|
|
66
|
+
if (cur.trim()) out.push(cur.trim());
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The text between the bracket at `open` and its match, or null when it is not closed. */
|
|
71
|
+
function bracketed(text, open) {
|
|
72
|
+
let depth = 0;
|
|
73
|
+
for (let k = open; k < text.length; k++) {
|
|
74
|
+
if (text[k] === '(') depth++;
|
|
75
|
+
else if (text[k] === ')' && --depth === 0) return { inner: text.slice(open + 1, k), end: k };
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Declared type of a parameter (`name?: T = x` → `T`), or null when it has none. */
|
|
81
|
+
function paramType(param) {
|
|
82
|
+
if (/^\.\.\./.test(param)) return null; // rest parameter
|
|
83
|
+
const colon = splitTopLevel(param, ':');
|
|
84
|
+
if (colon.length < 2) return null;
|
|
85
|
+
const type = colon.slice(1).join(':');
|
|
86
|
+
// a default value starts at a top-level `=` that is not part of `=>`, `==`, `<=`, `>=`, `!=`
|
|
87
|
+
let depth = 0;
|
|
88
|
+
for (let k = 0; k < type.length; k++) {
|
|
89
|
+
const ch = type[k];
|
|
90
|
+
if ('([{<'.includes(ch)) depth++;
|
|
91
|
+
else if (')]}'.includes(ch) || (ch === '>' && type[k - 1] !== '=')) depth--;
|
|
92
|
+
else if (ch === '=' && depth === 0 && !'=>'.includes(type[k + 1] ?? '') && !'=!<>'.includes(type[k - 1] ?? '')) return type.slice(0, k).trim() || null;
|
|
93
|
+
}
|
|
94
|
+
return type.trim() || null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* `cb` in `on(event: string, cb: (socket: JsonSocket, n: number) => void)`: the declared type of
|
|
99
|
+
* parameter `j` of the function type that parameter `i` of `name` declares, from its signature.
|
|
100
|
+
*/
|
|
101
|
+
export function callbackParamTypeText(sig, name, i, j) {
|
|
102
|
+
if (!sig) return null;
|
|
103
|
+
const m = new RegExp(`(?:^|[^\\w$])${name.replace(/[$]/g, '\\$')}\\s*(?:<[^()]*>)?\\s*\\(`).exec(sig);
|
|
104
|
+
const list = m ? bracketed(sig, m.index + m[0].length - 1) : null;
|
|
105
|
+
let type = list ? paramType(splitTopLevel(list.inner)[i] ?? '') : null;
|
|
106
|
+
if (!type) return null;
|
|
107
|
+
// `cb?: ((s: T) => void) | undefined`: drop null/undefined, unwrap parentheses
|
|
108
|
+
const alts = splitTopLevel(type, '|').filter(t => t !== 'undefined' && t !== 'null');
|
|
109
|
+
if (alts.length !== 1) return null;
|
|
110
|
+
type = alts[0];
|
|
111
|
+
for (let g = 0; g < 3 && type.startsWith('('); g++) {
|
|
112
|
+
const b = bracketed(type, 0);
|
|
113
|
+
if (!b || b.end !== type.length - 1) break;
|
|
114
|
+
type = b.inner.trim();
|
|
115
|
+
}
|
|
116
|
+
const fn = type.startsWith('(') ? bracketed(type, 0) : null;
|
|
117
|
+
if (!fn || !/^\s*=>/.test(type.slice(fn.end + 1))) return null;
|
|
118
|
+
return paramType(splitTopLevel(fn.inner)[j] ?? '');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** `name[][]` → { name, elem: 2 } (element access applied to a descriptor part). */
|
|
122
|
+
function splitElem(part) {
|
|
123
|
+
let elem = 0;
|
|
124
|
+
while (part.endsWith('[]')) { part = part.slice(0, -2); elem++; }
|
|
125
|
+
return { name: part, elem };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export class SymbolTable {
|
|
129
|
+
constructor() {
|
|
130
|
+
this.syms = new Map(); // id -> sym
|
|
131
|
+
this.byName = new Map(); // name -> [id]
|
|
132
|
+
this.byQname = new Map(); // qname -> [id]
|
|
133
|
+
this.byParent = new Map(); // parentId -> Map(name -> [id])
|
|
134
|
+
this.byOwner = new Map(); // owner name -> [id] (members declared outside their type: Go/Rust/C++/JS prototype)
|
|
135
|
+
this.fileSyms = new Map(); // fileId -> [id]
|
|
136
|
+
this.files = new Map(); // fileId -> { id, path, lang, package, dir, isTest }
|
|
137
|
+
this.fileByPath = new Map();
|
|
138
|
+
this.packageFiles = new Map(); // package -> [fileId]
|
|
139
|
+
this.dirFiles = new Map(); // dir -> [fileId]
|
|
140
|
+
this.imports = new Map(); // fileId -> [imp]
|
|
141
|
+
this.importedBy = new Map(); // fileId -> Set(fileId)
|
|
142
|
+
this.fieldTypes = new Map(); // ownerQname -> Map(name -> type)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
addFile(f) {
|
|
146
|
+
const file = { ...f, dir: path.posix.dirname(f.path) };
|
|
147
|
+
this.files.set(f.id, file);
|
|
148
|
+
this.fileByPath.set(f.path, file);
|
|
149
|
+
if (f.package) push(this.packageFiles, f.package, f.id);
|
|
150
|
+
push(this.dirFiles, file.dir, f.id);
|
|
151
|
+
this.fileSyms.set(f.id, []);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
removeFile(fileId) {
|
|
155
|
+
const f = this.files.get(fileId);
|
|
156
|
+
if (!f) return;
|
|
157
|
+
for (const id of this.fileSyms.get(fileId) ?? []) this.#removeSym(id);
|
|
158
|
+
this.fileSyms.delete(fileId);
|
|
159
|
+
this.files.delete(fileId);
|
|
160
|
+
this.fileByPath.delete(f.path);
|
|
161
|
+
const rm = (map, key) => { const a = map.get(key); if (a) { const i = a.indexOf(fileId); if (i >= 0) a.splice(i, 1); } };
|
|
162
|
+
if (f.package) rm(this.packageFiles, f.package);
|
|
163
|
+
rm(this.dirFiles, f.dir);
|
|
164
|
+
this.setImports(fileId, []);
|
|
165
|
+
for (const [q, m] of this.fieldTypes) if (m.fileId === fileId) this.fieldTypes.delete(q);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
addSym(s) {
|
|
169
|
+
this.syms.set(s.id, s);
|
|
170
|
+
push(this.byName, s.name, s.id);
|
|
171
|
+
push(this.byQname, s.qname, s.id);
|
|
172
|
+
if (s.parentId != null) {
|
|
173
|
+
let m = this.byParent.get(s.parentId);
|
|
174
|
+
if (!m) this.byParent.set(s.parentId, (m = new Map()));
|
|
175
|
+
push(m, s.name, s.id);
|
|
176
|
+
}
|
|
177
|
+
if (s.owner && (s.parentId == null || this.syms.get(s.parentId)?.name !== s.owner)) push(this.byOwner, s.owner, s.id);
|
|
178
|
+
push(this.fileSyms, s.fileId, s.id);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#removeSym(id) {
|
|
182
|
+
const s = this.syms.get(id);
|
|
183
|
+
if (!s) return;
|
|
184
|
+
const rm = (map, key) => { const a = map.get(key); if (a) { const i = a.indexOf(id); if (i >= 0) a.splice(i, 1); if (!a.length) map.delete(key); } };
|
|
185
|
+
rm(this.byName, s.name);
|
|
186
|
+
rm(this.byQname, s.qname);
|
|
187
|
+
if (s.parentId != null) { const m = this.byParent.get(s.parentId); if (m) rm(m, s.name); }
|
|
188
|
+
if (s.owner) rm(this.byOwner, s.owner);
|
|
189
|
+
this.byParent.delete(id);
|
|
190
|
+
this.syms.delete(id);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
setImports(fileId, imps) {
|
|
194
|
+
for (const old of this.imports.get(fileId) ?? []) {
|
|
195
|
+
if (old.targetFileId != null) this.importedBy.get(old.targetFileId)?.delete(fileId);
|
|
196
|
+
}
|
|
197
|
+
this.imports.set(fileId, imps);
|
|
198
|
+
for (const imp of imps) {
|
|
199
|
+
if (imp.targetFileId != null) {
|
|
200
|
+
let s = this.importedBy.get(imp.targetFileId);
|
|
201
|
+
if (!s) this.importedBy.set(imp.targetFileId, (s = new Set()));
|
|
202
|
+
s.add(fileId);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
setFieldTypes(fileId, rows) {
|
|
208
|
+
for (const r of rows) {
|
|
209
|
+
let m = this.fieldTypes.get(r.owner_qname ?? r.owner);
|
|
210
|
+
if (!m) { m = new Map(); m.fileId = fileId; this.fieldTypes.set(r.owner_qname ?? r.owner, m); }
|
|
211
|
+
if (!m.has(r.name)) m.set(r.name, r.type);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
sym(id) { return this.syms.get(id); }
|
|
216
|
+
file(id) { return this.files.get(id); }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export class Resolver {
|
|
220
|
+
constructor(table, specsById, { sigOf = null } = {}) {
|
|
221
|
+
this.t = table;
|
|
222
|
+
this.specs = specsById; // lang id -> spec (implicitThis etc.)
|
|
223
|
+
this.sigOf = sigOf; // symbol id -> signature text (read on demand: callback parameter types)
|
|
224
|
+
this.memo = new Map();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
resetMemo() { this.memo.clear(); }
|
|
228
|
+
|
|
229
|
+
// ── type & member helpers ─────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
/** Enclosing type of a symbol: {id?, name} or null. */
|
|
232
|
+
enclosingType(symId) {
|
|
233
|
+
for (let s = this.t.sym(symId), guard = 0; s && guard < 32; s = s.parentId != null ? this.t.sym(s.parentId) : null, guard++) {
|
|
234
|
+
if (MEMBER_HOLDERS.has(s.kind) && s.kind !== 'module') return { id: s.id, name: s.name, fileId: s.fileId };
|
|
235
|
+
// a member declared in its type's body: the type itself, not one found by name (`<anonymous>` repeats)
|
|
236
|
+
const parent = s.parentId != null ? this.t.sym(s.parentId) : null;
|
|
237
|
+
if (parent && MEMBER_HOLDERS.has(parent.kind) && parent.kind !== 'module' && (!s.owner || parent.name === s.owner)) continue;
|
|
238
|
+
if (s.owner && (CALLABLE_KINDS.has(s.kind) || s.kind === 'field' || s.kind === 'property')) {
|
|
239
|
+
const typeSym = this.#typeByNameNear(s.owner, s.fileId);
|
|
240
|
+
return typeSym ? { id: typeSym.id, name: typeSym.name, fileId: typeSym.fileId } : { id: null, name: s.owner, fileId: s.fileId };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
#typeByNameNear(name, fileId) {
|
|
247
|
+
const ids = this.t.byName.get(name);
|
|
248
|
+
if (!ids) return null;
|
|
249
|
+
let best = null, bestScore = -1;
|
|
250
|
+
const f = this.t.file(fileId);
|
|
251
|
+
for (const id of ids) {
|
|
252
|
+
const s = this.t.sym(id);
|
|
253
|
+
if (!TYPE_KINDS.has(s.kind)) continue;
|
|
254
|
+
const sf = this.t.file(s.fileId);
|
|
255
|
+
const score = (s.fileId === fileId ? 4 : 0) + (sf?.dir === f?.dir ? 2 : 0) + (sf?.package && sf.package === f?.package ? 2 : 0);
|
|
256
|
+
if (score > bestScore) { best = s; bestScore = score; }
|
|
257
|
+
}
|
|
258
|
+
return best;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Members named `name` of a type (by id and/or name), including inherited ones. */
|
|
262
|
+
membersOf(type, name, depth = 0, seen = new Set()) {
|
|
263
|
+
if (!type || depth > 6) return [];
|
|
264
|
+
const key = (type.id ?? '') + ':' + type.name;
|
|
265
|
+
if (seen.has(key)) return [];
|
|
266
|
+
seen.add(key);
|
|
267
|
+
const out = [];
|
|
268
|
+
if (type.id != null) for (const id of this.t.byParent.get(type.id)?.get(name) ?? []) out.push(id);
|
|
269
|
+
for (const id of this.t.byOwner.get(type.name) ?? []) {
|
|
270
|
+
const s = this.t.sym(id);
|
|
271
|
+
if (s.name !== name) continue;
|
|
272
|
+
// owner-declared members must live near the type (same package/dir/file family)
|
|
273
|
+
if (type.fileId != null) {
|
|
274
|
+
const a = this.t.file(s.fileId), b = this.t.file(type.fileId);
|
|
275
|
+
if (a && b && a.lang === b.lang && (a.dir === b.dir || a.package === b.package || a.lang === 'cpp' || a.lang === 'c' || a.lang === 'rust' || a.lang === 'javascript' || a.lang === 'typescript')) out.push(id);
|
|
276
|
+
} else out.push(id);
|
|
277
|
+
}
|
|
278
|
+
if (out.length) return out;
|
|
279
|
+
const typeSym = type.id != null ? this.t.sym(type.id) : null;
|
|
280
|
+
for (const base of typeSym?.bases ?? []) {
|
|
281
|
+
const bt = this.resolveTypeName(base, typeSym.fileId);
|
|
282
|
+
if (bt) {
|
|
283
|
+
const r = this.membersOf({ id: bt.id, name: bt.name, fileId: bt.fileId }, name, depth + 1, seen);
|
|
284
|
+
if (r.length) return r;
|
|
285
|
+
} else if (base !== type.name) {
|
|
286
|
+
const r = this.membersOf({ id: null, name: base, fileId: null }, name, depth + 1, seen);
|
|
287
|
+
if (r.length) return r;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Method resolution order of a type: C3 linearization over its resolved bases (Python's MRO; for
|
|
295
|
+
* single inheritance it is the chain of supertypes). Unresolved bases are left out; an inconsistent
|
|
296
|
+
* hierarchy falls back to depth-first order. Ids, the type first.
|
|
297
|
+
*/
|
|
298
|
+
mro(typeId) {
|
|
299
|
+
const k = 'mro|' + typeId;
|
|
300
|
+
if (this.memo.has(k)) return this.memo.get(k);
|
|
301
|
+
this.memo.set(k, [typeId]); // cycle guard
|
|
302
|
+
const ts = this.t.sym(typeId);
|
|
303
|
+
const bases = [];
|
|
304
|
+
for (const b of ts?.bases ?? []) {
|
|
305
|
+
const bt = this.resolveTypeName(b, ts.fileId);
|
|
306
|
+
if (bt && bt.id !== typeId && !bases.includes(bt.id)) bases.push(bt.id);
|
|
307
|
+
}
|
|
308
|
+
const seqs = [...bases.map(b => [...this.mro(b)]), [...bases]];
|
|
309
|
+
const out = [typeId];
|
|
310
|
+
for (;;) {
|
|
311
|
+
const rest = seqs.filter(s => s.length);
|
|
312
|
+
if (!rest.length) break;
|
|
313
|
+
const head = rest.map(s => s[0]).find(h => !rest.some(s => s.indexOf(h) > 0));
|
|
314
|
+
if (head === undefined) { for (const s of rest) for (const x of s) if (!out.includes(x)) out.push(x); break; }
|
|
315
|
+
out.push(head);
|
|
316
|
+
for (const s of rest) if (s[0] === head) s.shift();
|
|
317
|
+
}
|
|
318
|
+
this.memo.set(k, out);
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Resolve a type name visible from a file to a type symbol. */
|
|
323
|
+
resolveTypeName(name, fileId) {
|
|
324
|
+
if (!name || name.endsWith('[]') || name === '!') return null;
|
|
325
|
+
const k = 't|' + fileId + '|' + name;
|
|
326
|
+
if (this.memo.has(k)) return this.memo.get(k);
|
|
327
|
+
this.memo.set(k, null); // cycle guard
|
|
328
|
+
const q = this.#qualifiedType(name, fileId) ?? this.#nestedType(name, fileId);
|
|
329
|
+
const r = q !== undefined ? null : this.#resolveName(name, 'type', fileId, null);
|
|
330
|
+
const s = q !== undefined ? (q === EXTERNAL ? null : q) : r && r.id != null ? this.t.sym(r.id) : null;
|
|
331
|
+
const res = s && TYPE_KINDS.has(s.kind) ? s : null;
|
|
332
|
+
this.memo.set(k, res);
|
|
333
|
+
return res;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* `pkg.T` in a language whose types keep their package (Go): the type in the imported package's
|
|
338
|
+
* directory, EXTERNAL when the package is not in the repository, null when the repository's
|
|
339
|
+
* package has no such type; undefined when the name is not package-qualified.
|
|
340
|
+
*/
|
|
341
|
+
#qualifiedType(name, fileId) {
|
|
342
|
+
const dot = name.indexOf('.');
|
|
343
|
+
if (dot <= 0 || name.indexOf('.', dot + 1) >= 0) return undefined;
|
|
344
|
+
const lang = this.t.file(fileId)?.lang;
|
|
345
|
+
if (!this.specs[lang]?.qualifiedTypes) return undefined;
|
|
346
|
+
const imp = this.#importFor(fileId, name.slice(0, dot));
|
|
347
|
+
if (!imp) return undefined;
|
|
348
|
+
if (imp.targetFileId == null && !imp.targetDir) return EXTERNAL;
|
|
349
|
+
const ids = imp.targetDir ? this.#symbolsInDir(imp.targetDir, name.slice(dot + 1), fileId) : this.exportedFrom(imp.targetFileId, name.slice(dot + 1));
|
|
350
|
+
return ids.map(id => this.t.sym(id)).find(s => s && TYPE_KINDS.has(s.kind)) ?? null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* `Outer.Inner` in a language whose stored types keep their enclosing types (Java): the type
|
|
355
|
+
* nested in the one the first name resolves to; EXTERNAL when that one is the language's
|
|
356
|
+
* (`Map.Entry`); undefined when the name is not nested or its outer type is unknown.
|
|
357
|
+
*/
|
|
358
|
+
#nestedType(name, fileId) {
|
|
359
|
+
const spec = this.specs[this.t.file(fileId)?.lang];
|
|
360
|
+
if (!spec?.nestedTypes || !name.includes('.')) return undefined;
|
|
361
|
+
const [head, ...rest] = name.split('.');
|
|
362
|
+
let cur = this.resolveTypeName(head, fileId);
|
|
363
|
+
if (!cur) return spec.isPrimitiveType?.(head) ? EXTERNAL : undefined;
|
|
364
|
+
for (const seg of rest) {
|
|
365
|
+
const ids = (this.t.byParent.get(cur.id)?.get(seg) ?? []).map(id => this.t.sym(id)).filter(x => x && TYPE_KINDS.has(x.kind));
|
|
366
|
+
if (!ids.length) return null;
|
|
367
|
+
cur = ids[0];
|
|
368
|
+
}
|
|
369
|
+
return cur;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* A stored type string (see extract.mjs) → type {id,name,fileId}, EXTERNAL (members never live
|
|
374
|
+
* in the repository: primitives, builtins, collections) or null (unknown). `elem` applies
|
|
375
|
+
* element access that many times (`xs[i]`, loop variables).
|
|
376
|
+
*/
|
|
377
|
+
typeFromString(str, fileId, elem = 0) {
|
|
378
|
+
if (!str) return null;
|
|
379
|
+
if (str.startsWith('call:') || str.startsWith('cb:') || str.includes('#')) return this.resolveRecvType(str + '[]'.repeat(elem), fileId, null);
|
|
380
|
+
let s = str;
|
|
381
|
+
for (let i = 0; i < elem; i++) {
|
|
382
|
+
if (!s.endsWith('[]') && !s.endsWith('{}')) {
|
|
383
|
+
// an element of a collection class (`class Registry extends Map<string, Module>`)
|
|
384
|
+
const t = s !== '!' ? this.resolveTypeName(s, fileId) : null;
|
|
385
|
+
return t ? this.#elementOf(t, elem - i) : null;
|
|
386
|
+
}
|
|
387
|
+
s = s.slice(0, -2);
|
|
388
|
+
}
|
|
389
|
+
if (s === '!') return EXTERNAL;
|
|
390
|
+
if (s.endsWith('[]')) return { arrayOf: s.slice(0, -2), fileId }; // a collection value
|
|
391
|
+
if (s.endsWith('{}')) return { mapOf: s.slice(0, -2), fileId }; // a map value
|
|
392
|
+
if (this.#qualifiedType(s, fileId) === EXTERNAL || this.#nestedType(s, fileId) === EXTERNAL) return EXTERNAL;
|
|
393
|
+
const t = this.resolveTypeName(s, fileId);
|
|
394
|
+
if (t) return t;
|
|
395
|
+
const spec = this.specs[this.t.file(fileId)?.lang];
|
|
396
|
+
return spec?.isPrimitiveType?.(s) ? EXTERNAL : null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** An element of a type, `elem` levels down: of a collection value, or of a collection class. */
|
|
400
|
+
#elementOf(t, elem) {
|
|
401
|
+
if (t.arrayOf != null) return this.typeFromString(t.arrayOf, t.fileId, elem - 1);
|
|
402
|
+
if (t.mapOf != null) return this.typeFromString(t.mapOf, t.fileId, elem - 1); // a map's value
|
|
403
|
+
const s = t.id != null ? this.t.sym(t.id) : null;
|
|
404
|
+
return s?.type && /(\[\]|\{\})$/.test(s.type) ? this.typeFromString(s.type, s.fileId, elem) : null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Resolve an extraction-time receiver type descriptor to a type {id,name,fileId} or EXTERNAL. */
|
|
408
|
+
resolveRecvType(desc, fileId, srcId) {
|
|
409
|
+
if (!desc) return null;
|
|
410
|
+
if (desc === '!') return EXTERNAL;
|
|
411
|
+
const k = 'r|' + fileId + '|' + desc;
|
|
412
|
+
if (this.memo.has(k)) return this.memo.get(k);
|
|
413
|
+
this.memo.set(k, null);
|
|
414
|
+
const parts = desc.split('#').map(splitElem);
|
|
415
|
+
const head = parts[0];
|
|
416
|
+
let cur = null;
|
|
417
|
+
if (head.name.startsWith('call:')) {
|
|
418
|
+
const callee = head.name.slice(5);
|
|
419
|
+
const dot = callee.lastIndexOf('.');
|
|
420
|
+
// `call:pkg.New`: the function is reached through a module / import alias
|
|
421
|
+
const r = dot > 0 ? this.#resolveMember(callee.slice(dot + 1), 'call', callee.slice(0, dot), null, fileId, srcId)
|
|
422
|
+
: this.#resolveName(callee, 'call', fileId, srcId);
|
|
423
|
+
const s = r?.id != null ? this.t.sym(r.id) : null;
|
|
424
|
+
if (s) {
|
|
425
|
+
if (TYPE_KINDS.has(s.kind)) cur = head.elem ? null : s; // constructor call
|
|
426
|
+
else if (s.type) cur = this.typeFromString(s.type, s.fileId, head.elem);
|
|
427
|
+
}
|
|
428
|
+
if ((cur?.arrayOf != null || cur?.mapOf != null) && parts.length === 1) cur = EXTERNAL;
|
|
429
|
+
} else if (head.name.startsWith('cb:')) {
|
|
430
|
+
cur = this.#callbackParam(head.name, fileId, srcId, head.elem);
|
|
431
|
+
} else if (!head.elem) {
|
|
432
|
+
// same-file qualified name first (this.x → Class#x uses the class qname)
|
|
433
|
+
const q = (this.t.byQname.get(head.name) ?? []).map(id => this.t.sym(id)).find(s => s.fileId === fileId && TYPE_KINDS.has(s.kind));
|
|
434
|
+
const qt = q ? undefined : this.#qualifiedType(head.name, fileId) ?? this.#nestedType(head.name, fileId);
|
|
435
|
+
cur = q ?? (qt !== undefined ? qt : this.typeFromString(head.name.split('.').pop(), fileId));
|
|
436
|
+
if (!cur && !head.name.includes('.')) {
|
|
437
|
+
// a capitalised root can be a value rather than a type: `export const NestFactory =
|
|
438
|
+
// new NestFactoryStatic()` makes `NestFactory.create()` a member of NestFactoryStatic
|
|
439
|
+
const r = this.#resolveName(head.name, 'value', fileId, srcId);
|
|
440
|
+
const v = r?.id != null ? this.t.sym(r.id) : null;
|
|
441
|
+
if (v && VALUE_TYPED_KINDS.has(v.kind) && v.type && v.type !== head.name) cur = this.typeFromString(v.type, v.fileId);
|
|
442
|
+
}
|
|
443
|
+
if (!cur && head.name.includes('.')) {
|
|
444
|
+
// enclosing class that is not a symbol itself (e.g. JS prototype owner)
|
|
445
|
+
cur = { id: null, name: head.name.split('.').pop(), fileId };
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const spec = this.specs[this.t.file(fileId)?.lang];
|
|
449
|
+
for (let i = 1; i < parts.length && cur && cur !== EXTERNAL; i++) {
|
|
450
|
+
const { name, elem } = parts[i];
|
|
451
|
+
if (!name) { cur = elem ? this.#elementOf(cur, elem) : cur; continue; } // `T#[]`: an element of T itself
|
|
452
|
+
if (cur.arrayOf != null) {
|
|
453
|
+
// `list.get(0)` / `xs.first()` reach the element; other members are the language's
|
|
454
|
+
cur = spec?.elementMethods?.has(name) ? this.typeFromString(cur.arrayOf, cur.fileId, elem) : EXTERNAL;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (cur.mapOf != null) {
|
|
458
|
+
// `m.get(k)` reaches a value, `m.values()` the collection of values
|
|
459
|
+
cur = spec?.mapMethods?.has(name) ? this.typeFromString(cur.mapOf, cur.fileId, elem)
|
|
460
|
+
: spec?.mapValueMethods?.has(name) ? this.typeFromString(cur.mapOf + '[]', cur.fileId, elem) : EXTERNAL;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const mem = this.membersOf({ id: cur.id ?? null, name: cur.name, fileId: cur.fileId }, name);
|
|
464
|
+
if (!mem.length && cur.id != null && spec?.qualifiedTypes) {
|
|
465
|
+
// Go: an embedded field is named after its type (`m.MatchRegexp` in a struct embedding MatchRegexp)
|
|
466
|
+
const os = this.t.sym(cur.id);
|
|
467
|
+
const emb = (os?.declaredBases ?? os?.bases ?? []).find(b => b === name || b.endsWith('.' + name));
|
|
468
|
+
const et = emb ? this.resolveTypeName(emb, os.fileId) : null;
|
|
469
|
+
if (et) { cur = elem ? this.#elementOf(et, elem) : et; continue; }
|
|
470
|
+
}
|
|
471
|
+
if (!mem.length && cur.id != null) {
|
|
472
|
+
// a class that is a collection (`class Registry extends Map<string, Module>`): `get` & co.
|
|
473
|
+
const ct = this.t.sym(cur.id)?.type;
|
|
474
|
+
const c = ct ? this.typeFromString(ct, cur.fileId) : null;
|
|
475
|
+
if (c && (c.arrayOf != null || c.mapOf != null)) { cur = c; i--; continue; }
|
|
476
|
+
}
|
|
477
|
+
let next = null;
|
|
478
|
+
for (const id of mem) {
|
|
479
|
+
const m = this.t.sym(id);
|
|
480
|
+
if (m.type) { next = this.typeFromString(m.type, m.fileId, elem); if (next) break; }
|
|
481
|
+
}
|
|
482
|
+
if (!next) {
|
|
483
|
+
// inferred field types (self.x = X() / this.x = new X() / this.x = param), also inherited
|
|
484
|
+
let owner = cur;
|
|
485
|
+
for (let d = 0; owner && !next && d < 5; d++) {
|
|
486
|
+
const ft = this.t.fieldTypes.get(owner.qname ?? owner.name)?.get(name);
|
|
487
|
+
if (ft) { next = this.typeFromString(ft, owner.fileId ?? fileId, elem); break; }
|
|
488
|
+
const os = owner.id != null ? this.t.sym(owner.id) : null;
|
|
489
|
+
const base = os?.bases?.[0];
|
|
490
|
+
owner = base ? this.resolveTypeName(base, os.fileId) : null;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
cur = next;
|
|
494
|
+
}
|
|
495
|
+
const res = cur === EXTERNAL || cur?.arrayOf != null || cur?.mapOf != null ? EXTERNAL : cur ? { id: cur.id ?? null, name: cur.name, fileId: cur.fileId ?? null } : null;
|
|
496
|
+
this.memo.set(k, res);
|
|
497
|
+
return res;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Type of a callback parameter (`cb:i:j:callee`, see extract.mjs) from the callee's signature. */
|
|
501
|
+
#callbackParam(desc, fileId, srcId, elem) {
|
|
502
|
+
const m = /^cb:(\d+):(\d+):(.+)$/.exec(desc);
|
|
503
|
+
if (!m || !this.sigOf) return null;
|
|
504
|
+
const target = m[3];
|
|
505
|
+
let callee = null;
|
|
506
|
+
const sep = target.lastIndexOf('::');
|
|
507
|
+
if (sep > 0) {
|
|
508
|
+
// a method of a typed receiver (`this`, a field, a local)
|
|
509
|
+
const type = this.resolveRecvType(target.slice(0, sep).replace(/~/g, '#'), fileId, srcId);
|
|
510
|
+
if (type && type !== EXTERNAL) callee = this.membersOf(type, target.slice(sep + 2)).map(id => this.t.sym(id)).find(s => s && CALLABLE_KINDS.has(s.kind)) ?? null;
|
|
511
|
+
} else {
|
|
512
|
+
// `@helpers.create`: a function reached through a module, namespace or class; else a name in scope
|
|
513
|
+
const dot = target.lastIndexOf('.');
|
|
514
|
+
const r = target.startsWith('@') && dot > 0 ? this.#resolveMember(target.slice(dot + 1), 'call', target.slice(1, dot), null, fileId, srcId)
|
|
515
|
+
: this.#resolveName(target, 'call', fileId, srcId);
|
|
516
|
+
callee = r?.id != null ? this.t.sym(r.id) : null;
|
|
517
|
+
}
|
|
518
|
+
if (!callee || !CALLABLE_KINDS.has(callee.kind)) return null;
|
|
519
|
+
const text = callbackParamTypeText(this.sigOf(callee.id), callee.name, Number(m[1]), Number(m[2]));
|
|
520
|
+
const spec = this.specs[this.t.file(callee.fileId)?.lang];
|
|
521
|
+
const t = text && spec ? normalizeType(text, spec) : null;
|
|
522
|
+
return t ? this.typeFromString(t, callee.fileId, elem) : null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ── import helpers ───────────────────────────────────────────────────────────
|
|
526
|
+
|
|
527
|
+
#importFor(fileId, local) {
|
|
528
|
+
for (const imp of this.t.imports.get(fileId) ?? []) if (imp.local === local) return imp;
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* The module file an imported Python name stands for, when that name is a module: `import a.b as x`,
|
|
534
|
+
* `from pkg import sub` (a submodule), or a name the imported module itself imports as a module —
|
|
535
|
+
* `from sqlglot import exp`, where sqlglot/__init__.py does `from sqlglot import expressions as exp`.
|
|
536
|
+
*/
|
|
537
|
+
#moduleFileOf(fileId, local, depth = 0) {
|
|
538
|
+
if (depth > 4 || local == null) return null;
|
|
539
|
+
const imp = this.#importFor(fileId, local);
|
|
540
|
+
if (!imp) return null;
|
|
541
|
+
if (imp.imported === '*' && !imp.wildcard) return imp.targetFileId ?? null;
|
|
542
|
+
const n = imp.imported && imp.imported !== '*' && imp.imported !== 'default' ? imp.imported : null;
|
|
543
|
+
if (!n) return null;
|
|
544
|
+
if (imp.targetDir) {
|
|
545
|
+
const sub = `${imp.targetDir}/${n.replace(/\./g, '/')}`;
|
|
546
|
+
const f = this.t.fileByPath.get(sub + '.py') ?? this.t.fileByPath.get(sub + '/__init__.py');
|
|
547
|
+
if (f) return f.id;
|
|
548
|
+
}
|
|
549
|
+
if (imp.targetFileId != null && imp.targetFileId !== fileId) return this.#moduleFileOf(imp.targetFileId, n, depth + 1);
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Top-level symbols named `name` exported by a module file, following re-exports. */
|
|
554
|
+
exportedFrom(targetFileId, name, depth = 0, seen = new Set()) {
|
|
555
|
+
if (targetFileId == null || depth > 6 || seen.has(targetFileId)) return [];
|
|
556
|
+
seen.add(targetFileId);
|
|
557
|
+
const out = [];
|
|
558
|
+
for (const id of this.t.fileSyms.get(targetFileId) ?? []) {
|
|
559
|
+
const s = this.t.sym(id);
|
|
560
|
+
if (s.name === name && s.parentId == null) out.push(id);
|
|
561
|
+
}
|
|
562
|
+
if (out.length) return out;
|
|
563
|
+
// in Python a later import rebinds the name (`from a import *` then `from b import *`)
|
|
564
|
+
const imports = this.t.imports.get(targetFileId) ?? [];
|
|
565
|
+
for (const imp of this.t.file(targetFileId)?.lang === 'python' ? [...imports].reverse() : imports) {
|
|
566
|
+
// `export { X } from './x'`, `export * from './x'`, Python `from .x import X` in __init__
|
|
567
|
+
const passes = imp.reexport === name || imp.reexport === '*' || imp.local === name || (imp.wildcard && imp.imported === '*');
|
|
568
|
+
if (!passes || imp.targetFileId == null) continue;
|
|
569
|
+
const inner = imp.reexport === name && imp.imported && imp.imported !== '*' ? imp.imported : (imp.local === name && imp.imported && imp.imported !== '*' ? imp.imported : name);
|
|
570
|
+
const r = this.exportedFrom(imp.targetFileId, inner, depth + 1, seen);
|
|
571
|
+
if (r.length) return r;
|
|
572
|
+
}
|
|
573
|
+
return out;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
#symbolsInDir(dir, name, fileId) {
|
|
577
|
+
const out = [];
|
|
578
|
+
for (const fid of this.t.dirFiles.get(dir) ?? []) {
|
|
579
|
+
for (const id of this.t.fileSyms.get(fid) ?? []) {
|
|
580
|
+
const s = this.t.sym(id);
|
|
581
|
+
if (s.name === name && (s.parentId == null) && !(s.kind === 'method' && s.owner)) out.push(id);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return out;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
#symbolsInPackage(pkg, name) {
|
|
588
|
+
const out = [];
|
|
589
|
+
for (const fid of this.t.packageFiles.get(pkg) ?? []) {
|
|
590
|
+
for (const id of this.t.fileSyms.get(fid) ?? []) {
|
|
591
|
+
const s = this.t.sym(id);
|
|
592
|
+
if (s.name === name && s.parentId == null) out.push(id);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return out;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ── main entry ───────────────────────────────────────────────────────────────
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Resolve one reference.
|
|
602
|
+
* @param {{ name, kind, recv, recv_type, file_id, src_id }} ref
|
|
603
|
+
* @returns {{ id: number|null, conf: number, ncand: number }}
|
|
604
|
+
*/
|
|
605
|
+
resolve(ref) {
|
|
606
|
+
this.argc = ref.argc ?? null;
|
|
607
|
+
const fileId = ref.file_id;
|
|
608
|
+
const srcId = ref.src_id ?? null;
|
|
609
|
+
const recv = ref.recv || '';
|
|
610
|
+
const kind = ref.kind;
|
|
611
|
+
const name = ref.name;
|
|
612
|
+
if (recv) return this.#resolveMember(name, kind, recv, ref.recv_type, fileId, srcId);
|
|
613
|
+
return this.#resolveName(name, kind, fileId, srcId) ?? { id: null, conf: 0, ncand: 0 };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Where overloads are methods of their own, a call none of a type's own overloads can take
|
|
618
|
+
* reaches an inherited one (`element.attr("x")` runs Node.attr(key) when Element declares only
|
|
619
|
+
* attr(key, value)): the nearest supertype declaring an overload the arguments fit.
|
|
620
|
+
*/
|
|
621
|
+
#overloadsReached(type, ids, name) {
|
|
622
|
+
if (this.argc == null || !ids.length || type?.id == null || ids.some(id => this.#reaches(this.t.sym(id)))) return ids;
|
|
623
|
+
if (!ids.every(id => CALLABLE_KINDS.has(this.t.sym(id)?.kind))) return ids;
|
|
624
|
+
for (const tid of this.mro(type.id).slice(1)) {
|
|
625
|
+
const ts = this.t.sym(tid);
|
|
626
|
+
const own = ts ? this.membersOf({ id: ts.id, name: ts.name, fileId: ts.fileId }, name).filter(id => this.#reaches(this.t.sym(id))) : [];
|
|
627
|
+
if (own.length) return own;
|
|
628
|
+
}
|
|
629
|
+
return ids;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** Can the call being resolved reach this callable (its arguments fit, where overloads are methods of their own)? */
|
|
633
|
+
#reaches(s) {
|
|
634
|
+
if (this.argc == null || !this.sigOf || !this.specs[this.t.file(s.fileId)?.lang]?.distinctOverloads) return true;
|
|
635
|
+
return fits({ n: Math.max(0, this.argc), open: this.argc < 0 }, sigArity(this.sigOf(s.id), s.name));
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
#compatible(s, kind) {
|
|
639
|
+
if (kind === 'value' && CALLABLE_KINDS.has(s.kind) && this.specs[this.t.file(s.fileId)?.lang]?.localValues) return false; // Java: methods are no values
|
|
640
|
+
if (kind === 'type' || kind === 'inherit') return TYPE_KINDS.has(s.kind);
|
|
641
|
+
if (kind === 'new') return TYPE_KINDS.has(s.kind) || s.kind === 'function' || s.kind === 'constructor';
|
|
642
|
+
if (kind === 'call') return CALLABLE_KINDS.has(s.kind) || TYPE_KINDS.has(s.kind) || s.kind === 'variable' || s.kind === 'field' || s.kind === 'property' || s.kind === 'constant';
|
|
643
|
+
if (kind === 'decorator') return CALLABLE_KINDS.has(s.kind) || TYPE_KINDS.has(s.kind) || s.kind === 'variable';
|
|
644
|
+
if (kind === 'read') return s.kind === 'field' || s.kind === 'property' || s.kind === 'method' || s.kind === 'constant' || s.kind === 'variable' || TYPE_KINDS.has(s.kind);
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
#pick(ids, kind, conf, fileId) {
|
|
649
|
+
const cands = ids.map(id => this.t.sym(id)).filter(s => s && this.#compatible(s, kind));
|
|
650
|
+
if (!cands.length) return null;
|
|
651
|
+
if (cands.length === 1) return { id: cands[0].id, conf, ncand: 1 };
|
|
652
|
+
// overloads / redeclarations in one type or file: prefer the definition with a body (larger span)
|
|
653
|
+
const sameQ = cands.every(s => s.qname === cands[0].qname);
|
|
654
|
+
// where overloads are methods of their own, the call reaches the one its arguments fit
|
|
655
|
+
if (sameQ && this.argc != null && this.sigOf && this.specs[this.t.file(cands[0].fileId)?.lang]?.distinctOverloads) {
|
|
656
|
+
const a = { n: Math.max(0, this.argc), open: this.argc < 0 };
|
|
657
|
+
let fit = cands.filter(s => fits(a, sigArity(this.sigOf(s.id), s.name)));
|
|
658
|
+
// an overload that takes the arguments as they are wins over a variadic one (javac's phases)
|
|
659
|
+
const fixed = fit.filter(s => sigArity(this.sigOf(s.id), s.name)?.max !== Infinity);
|
|
660
|
+
if (fixed.length && fixed.length < fit.length) fit = fixed;
|
|
661
|
+
if (fit.length === 1) return { id: fit[0].id, conf, ncand: 1 };
|
|
662
|
+
if (fit.length > 1 && fit.length < cands.length) return this.#pick(fit.map(s => s.id), kind, conf, fileId);
|
|
663
|
+
}
|
|
664
|
+
const scored = cands.map(s => ({ s, score: this.#proximity(s, fileId) + (s.endLine - s.startLine) / 1e4 }));
|
|
665
|
+
scored.sort((a, b) => b.score - a.score || a.s.id - b.s.id);
|
|
666
|
+
return { id: scored[0].s.id, conf: sameQ ? conf : conf * 0.75, ncand: cands.length };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
#proximity(s, fileId) {
|
|
670
|
+
const a = this.t.file(s.fileId), b = this.t.file(fileId);
|
|
671
|
+
if (!a || !b) return 0;
|
|
672
|
+
let score = 0;
|
|
673
|
+
if (s.fileId === fileId) score += 4;
|
|
674
|
+
if (a.dir === b.dir) score += 2;
|
|
675
|
+
if (a.package && a.package === b.package) score += 1.5;
|
|
676
|
+
if (this.t.importedBy.get(s.fileId)?.has(fileId)) score += 3;
|
|
677
|
+
const pa = a.dir.split('/'), pb = b.dir.split('/');
|
|
678
|
+
let common = 0;
|
|
679
|
+
while (common < pa.length && common < pb.length && pa[common] === pb[common]) common++;
|
|
680
|
+
score += common * 0.15;
|
|
681
|
+
if (a.isTest && !b.isTest) score -= 3;
|
|
682
|
+
if (a.lang !== b.lang) score -= 2;
|
|
683
|
+
if (s.exported) score += 0.3;
|
|
684
|
+
return score;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
#resolveName(name, kind, fileId, srcId) {
|
|
688
|
+
const file = this.t.file(fileId);
|
|
689
|
+
if (!file) return null;
|
|
690
|
+
const spec = this.specs[file.lang];
|
|
691
|
+
// 1. lexical scope: symbols nested in enclosing definitions (closures, inner functions)
|
|
692
|
+
for (let s = srcId != null ? this.t.sym(srcId) : null, g = 0; s && g < 32; s = s.parentId != null ? this.t.sym(s.parentId) : null, g++) {
|
|
693
|
+
// members of an object literal or a type literal are not names in scope
|
|
694
|
+
const ids = this.t.byParent.get(s.id)?.get(name)?.filter(id => !MEMBER_KINDS.has(this.t.sym(id)?.kind));
|
|
695
|
+
if (ids?.length && !MEMBER_HOLDERS.has(s.kind)) {
|
|
696
|
+
const r = this.#pick(ids, kind, 0.95, fileId);
|
|
697
|
+
if (r) return r;
|
|
698
|
+
}
|
|
699
|
+
// recursion; a method calls itself by a bare name only where members are in scope (implicit this)
|
|
700
|
+
if (s.name === name && kind === 'call' && CALLABLE_KINDS.has(s.kind) && (s.kind !== 'method' || spec?.implicitThis) && this.#reaches(s)) return { id: s.id, conf: 0.9, ncand: 1 };
|
|
701
|
+
}
|
|
702
|
+
// 2. implicit this: members of the enclosing type (Java/Kotlin/C#/Scala/C++/Ruby)
|
|
703
|
+
if (spec?.implicitThis && srcId != null && kind !== 'type' && kind !== 'inherit') {
|
|
704
|
+
const type = this.enclosingType(srcId);
|
|
705
|
+
if (type) {
|
|
706
|
+
const r = this.#pick(this.#overloadsReached(type, this.membersOf(type, name), name), kind, 0.9, fileId);
|
|
707
|
+
if (r) return r;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
// 3. same file, top-level
|
|
711
|
+
const local = (this.t.fileSyms.get(fileId) ?? []).filter(id => { const s = this.t.sym(id); return s.name === name && (s.parentId == null || TYPE_KINDS.has(s.kind)); });
|
|
712
|
+
if (local.length) {
|
|
713
|
+
const r = this.#pick(local, kind, 0.9, fileId);
|
|
714
|
+
if (r) return r;
|
|
715
|
+
}
|
|
716
|
+
// 4. explicit import binding
|
|
717
|
+
const imp = this.#importFor(fileId, name);
|
|
718
|
+
if (imp) {
|
|
719
|
+
const target = imp.imported && imp.imported !== '*' && imp.imported !== 'default' ? imp.imported : name;
|
|
720
|
+
let ids = [];
|
|
721
|
+
if (imp.targetFileId != null) {
|
|
722
|
+
ids = this.exportedFrom(imp.targetFileId, target);
|
|
723
|
+
if (!ids.length && imp.imported === 'default') ids = this.#defaultExport(imp.targetFileId, name);
|
|
724
|
+
}
|
|
725
|
+
if (!ids.length && imp.targetDir) ids = this.#symbolsInDir(imp.targetDir, target, fileId);
|
|
726
|
+
if (!ids.length && imp.source && PACKAGE_LANGS.has(file.lang)) ids = this.#symbolsInPackage(imp.source, target);
|
|
727
|
+
if (ids.length) {
|
|
728
|
+
const r = this.#pick(ids, kind, 0.95, fileId);
|
|
729
|
+
if (r) return r;
|
|
730
|
+
}
|
|
731
|
+
if (imp.targetFileId == null && !imp.targetDir && !PACKAGE_LANGS.has(file.lang)) return { id: null, conf: 0, ncand: 0, external: true };
|
|
732
|
+
}
|
|
733
|
+
// 5. same package / directory
|
|
734
|
+
if (PACKAGE_LANGS.has(file.lang) && file.package) {
|
|
735
|
+
const r = this.#pick(this.#symbolsInPackage(file.package, name), kind, 0.85, fileId);
|
|
736
|
+
if (r) return r;
|
|
737
|
+
}
|
|
738
|
+
if (DIR_PACKAGE_LANGS.has(file.lang)) {
|
|
739
|
+
const r = this.#pick(this.#symbolsInDir(file.dir, name, fileId), kind, 0.85, fileId);
|
|
740
|
+
if (r) return r;
|
|
741
|
+
}
|
|
742
|
+
// 6. wildcard imports (import pkg.*, from x import *, using Ns;, use x::*)
|
|
743
|
+
for (const w of this.t.imports.get(fileId) ?? []) {
|
|
744
|
+
if (!w.wildcard) continue;
|
|
745
|
+
let ids = [];
|
|
746
|
+
if (w.targetFileId != null) ids = this.exportedFrom(w.targetFileId, name);
|
|
747
|
+
else if (w.targetDir) ids = this.#symbolsInDir(w.targetDir, name, fileId);
|
|
748
|
+
else if (w.source && PACKAGE_LANGS.has(file.lang)) ids = this.#symbolsInPackage(w.source, name);
|
|
749
|
+
const r = ids.length ? this.#pick(ids, kind, 0.85, fileId) : null;
|
|
750
|
+
if (r) return r;
|
|
751
|
+
}
|
|
752
|
+
// 7. C/C++: included headers and their implementation files
|
|
753
|
+
if (file.lang === 'c' || file.lang === 'cpp') {
|
|
754
|
+
const ids = [];
|
|
755
|
+
for (const inc of this.t.imports.get(fileId) ?? []) {
|
|
756
|
+
if (inc.targetFileId == null) continue;
|
|
757
|
+
const hdr = this.t.file(inc.targetFileId);
|
|
758
|
+
ids.push(...this.exportedFrom(inc.targetFileId, name));
|
|
759
|
+
const stem = hdr.path.replace(/\.(h|hh|hpp|hxx)$/, '');
|
|
760
|
+
for (const ext of ['.c', '.cc', '.cpp', '.cxx']) {
|
|
761
|
+
const impl = this.t.fileByPath.get(stem + ext);
|
|
762
|
+
if (impl) ids.push(...this.exportedFrom(impl.id, name));
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const r = ids.length ? this.#pick(ids, kind, 0.8, fileId) : null;
|
|
766
|
+
if (r) return r;
|
|
767
|
+
}
|
|
768
|
+
// 8. global by name
|
|
769
|
+
return this.#global(name, kind, fileId);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
#defaultExport(targetFileId, localName) {
|
|
773
|
+
// `export default class Foo` / `module.exports = Foo` → the file's main exported symbol
|
|
774
|
+
const ids = (this.t.fileSyms.get(targetFileId) ?? []).filter(id => { const s = this.t.sym(id); return s.parentId == null && s.exported; });
|
|
775
|
+
const same = ids.filter(id => this.t.sym(id).name.toLowerCase() === localName.toLowerCase());
|
|
776
|
+
if (same.length) return same;
|
|
777
|
+
return ids.length === 1 ? ids : [];
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
#global(name, kind, fileId) {
|
|
781
|
+
const all = (this.t.byName.get(name) ?? []).filter(id => {
|
|
782
|
+
const s = this.t.sym(id);
|
|
783
|
+
return s && this.#compatible(s, kind) && (s.parentId == null || TYPE_KINDS.has(s.kind) || kind === 'call');
|
|
784
|
+
});
|
|
785
|
+
const file = this.t.file(fileId);
|
|
786
|
+
// never bind across unrelated languages (a TS call never resolves to a Python def)
|
|
787
|
+
const sameFamily = all.filter(id => {
|
|
788
|
+
const f = this.t.file(this.t.sym(id).fileId);
|
|
789
|
+
return f && (f.lang === file.lang || familyOf(f.lang) === familyOf(file.lang));
|
|
790
|
+
});
|
|
791
|
+
// unqualified calls cannot target methods of unrelated classes in most languages
|
|
792
|
+
const cands = sameFamily.filter(id => {
|
|
793
|
+
const s = this.t.sym(id);
|
|
794
|
+
if (s.kind === 'method' || s.kind === 'field' || s.kind === 'property') return false;
|
|
795
|
+
return true;
|
|
796
|
+
});
|
|
797
|
+
if (!cands.length) return null;
|
|
798
|
+
if (cands.length > MAX_GLOBAL_CANDIDATES) return { id: null, conf: 0, ncand: cands.length };
|
|
799
|
+
const base = cands.length === 1 ? 0.6 : 0.35;
|
|
800
|
+
return this.#pick(cands, kind, base, fileId);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
#resolveMember(name, kind, recv, recvType, fileId, srcId) {
|
|
804
|
+
const file = this.t.file(fileId);
|
|
805
|
+
const root = recv.split('.')[0];
|
|
806
|
+
// this / self / super
|
|
807
|
+
if (root === 'this' || recv === 'super') {
|
|
808
|
+
const type = srcId != null ? this.enclosingType(srcId) : null;
|
|
809
|
+
if (type) {
|
|
810
|
+
if (recv === 'this' || recv === 'super') {
|
|
811
|
+
let ids;
|
|
812
|
+
if (recv === 'super') {
|
|
813
|
+
const ts = type.id != null ? this.t.sym(type.id) : null;
|
|
814
|
+
ids = [];
|
|
815
|
+
for (const b of ts?.bases ?? []) { const bt = this.resolveTypeName(b, ts.fileId); if (bt) ids.push(...this.membersOf({ id: bt.id, name: bt.name, fileId: bt.fileId }, name)); }
|
|
816
|
+
} else ids = this.membersOf(type, name);
|
|
817
|
+
const r = this.#pick(this.#byStatic(ids, false), kind, 0.95, fileId);
|
|
818
|
+
if (r) return r;
|
|
819
|
+
return { id: null, conf: 0, ncand: 0 };
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
// inferred receiver type (locals, params, fields, factories; chains across files)
|
|
824
|
+
if (recvType) {
|
|
825
|
+
const type = this.resolveRecvType(recvType, fileId, srcId);
|
|
826
|
+
if (type === EXTERNAL) return { id: null, conf: 0, ncand: 0, external: true };
|
|
827
|
+
if (type) {
|
|
828
|
+
const ids = this.#byStatic(this.#overloadsReached(type, this.membersOf(type, name), name), false);
|
|
829
|
+
const r = this.#pick(ids, kind, 0.9, fileId);
|
|
830
|
+
if (r) return r;
|
|
831
|
+
// interface / abstract receiver: the member may be declared only on implementations
|
|
832
|
+
return { id: null, conf: 0, ncand: 0 };
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
// receiver is an imported module / namespace / package alias
|
|
836
|
+
const imp = this.#importFor(fileId, root) ?? this.#importFor(fileId, recv);
|
|
837
|
+
if (imp) {
|
|
838
|
+
const rest = recv === imp.local ? [] : recv.slice(imp.local.length + 1).split('.').filter(Boolean);
|
|
839
|
+
let ids = [];
|
|
840
|
+
const importedName = imp.imported && imp.imported !== '*' && imp.imported !== 'default' ? imp.imported : null;
|
|
841
|
+
if (importedName && rest.length === 0) {
|
|
842
|
+
// `import { Foo } from './foo'; Foo.bar()` → static member of Foo, or module object's member
|
|
843
|
+
let owners = imp.targetFileId != null ? this.exportedFrom(imp.targetFileId, importedName) : [];
|
|
844
|
+
if (!owners.length && PACKAGE_LANGS.has(file.lang)) owners = this.#symbolsInPackage(imp.source, importedName);
|
|
845
|
+
for (const oid of owners) ids.push(...this.#membersVia(this.t.sym(oid), name));
|
|
846
|
+
if (!ids.length && imp.targetFileId != null && owners.length === 0) {
|
|
847
|
+
// `from pkg import submodule` then submodule.fn()
|
|
848
|
+
ids = this.exportedFrom(imp.targetFileId, name);
|
|
849
|
+
}
|
|
850
|
+
} else if (rest.length === 0) {
|
|
851
|
+
if (imp.targetFileId != null) ids = this.exportedFrom(imp.targetFileId, name);
|
|
852
|
+
if (!ids.length && imp.targetDir) ids = this.#symbolsInDir(imp.targetDir, name, fileId);
|
|
853
|
+
} else if (imp.targetFileId != null || imp.targetDir) {
|
|
854
|
+
// ns.Class.method / pkg.sub.fn / pkg.DefaultClient.Do
|
|
855
|
+
let owners = imp.targetFileId != null ? this.exportedFrom(imp.targetFileId, rest[0]) : this.#symbolsInDir(imp.targetDir, rest[0], fileId);
|
|
856
|
+
if (rest.length === 1) for (const oid of owners) ids.push(...this.#membersVia(this.t.sym(oid), name));
|
|
857
|
+
}
|
|
858
|
+
if (!ids.length && file.lang === 'python') {
|
|
859
|
+
// the receiver is a module reached through a package (`exp.Column`, `exp.Literal.string`)
|
|
860
|
+
const mod = this.#moduleFileOf(fileId, imp.local);
|
|
861
|
+
if (mod != null && rest.length === 0) ids = this.exportedFrom(mod, name);
|
|
862
|
+
else if (mod != null && rest.length === 1) for (const oid of this.exportedFrom(mod, rest[0])) ids.push(...this.#membersVia(this.t.sym(oid), name));
|
|
863
|
+
}
|
|
864
|
+
if (ids.length) {
|
|
865
|
+
const r = this.#pick(ids, kind, 0.95, fileId);
|
|
866
|
+
if (r) return r;
|
|
867
|
+
}
|
|
868
|
+
if (imp.targetFileId == null && !imp.targetDir && !PACKAGE_LANGS.has(file.lang)) return { id: null, conf: 0, ncand: 0, external: true };
|
|
869
|
+
}
|
|
870
|
+
// receiver is a type name (static call / namespace-like object)
|
|
871
|
+
if (/^[A-Za-z_$][\w$]*$/.test(root) && !recv.includes('.')) {
|
|
872
|
+
const r0 = this.#resolveName(root, 'type', fileId, srcId);
|
|
873
|
+
let typeSym = r0?.id != null ? this.t.sym(r0.id) : null;
|
|
874
|
+
if (!typeSym) {
|
|
875
|
+
// a typed package-level value (`DefaultClient.Do()`, a module singleton in the same package)
|
|
876
|
+
const rv = this.#resolveName(root, 'read', fileId, srcId);
|
|
877
|
+
const v = rv?.id != null ? this.t.sym(rv.id) : null;
|
|
878
|
+
if (v && v.type && (v.kind === 'variable' || v.kind === 'constant')) {
|
|
879
|
+
const r = this.#pick(this.#membersVia(v, name), kind, Math.min(0.9, rv.conf), fileId);
|
|
880
|
+
if (r) return r;
|
|
881
|
+
}
|
|
882
|
+
// JS object namespaces (`proto.handle`, `utils.merge`): same-file owner
|
|
883
|
+
const owned = (this.t.byOwner.get(root) ?? []).filter(id => this.t.sym(id).name === name);
|
|
884
|
+
const r = this.#pick(owned, kind, owned.some(id => this.t.sym(id).fileId === fileId) ? 0.8 : 0.6, fileId);
|
|
885
|
+
if (r) return r;
|
|
886
|
+
} else {
|
|
887
|
+
const ids = this.#byStatic(this.membersOf({ id: typeSym.id, name: typeSym.name, fileId: typeSym.fileId }, name), true);
|
|
888
|
+
const r = this.#pick(ids, kind, Math.min(0.9, r0.conf), fileId);
|
|
889
|
+
if (r) return r;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
// unknown receiver: any member with that name (dynamic dispatch by name). Property reads are
|
|
893
|
+
// too common to guess (`x.length`, `opts.name`), so they stay unbound without type evidence.
|
|
894
|
+
if (kind === 'read') return { id: null, conf: 0, ncand: 0 };
|
|
895
|
+
return this.#anyMember(name, kind, fileId) ?? { id: null, conf: 0, ncand: 0 };
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Members named `name` reached through an imported/package symbol: a type's static members, a
|
|
900
|
+
* typed value's instance members (`export const api = new Api()`), or members declared on the
|
|
901
|
+
* symbol itself (JS object namespaces).
|
|
902
|
+
*/
|
|
903
|
+
#membersVia(o, name) {
|
|
904
|
+
if (!o) return [];
|
|
905
|
+
if (!TYPE_KINDS.has(o.kind) && o.type) {
|
|
906
|
+
const t = this.typeFromString(o.type, o.fileId);
|
|
907
|
+
if (t && t !== EXTERNAL && t.arrayOf == null && t.mapOf == null) return this.#byStatic(this.membersOf(t, name), false);
|
|
908
|
+
if (t) return [];
|
|
909
|
+
}
|
|
910
|
+
const ids = this.membersOf({ id: o.id, name: o.name, fileId: o.fileId }, name);
|
|
911
|
+
return TYPE_KINDS.has(o.kind) ? this.#byStatic(ids, true) : ids;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/** Keep members of the wanted staticness when both kinds share the name (`Logger.error` vs `logger.error`). */
|
|
915
|
+
#byStatic(ids, wantStatic) {
|
|
916
|
+
if (ids.length < 2) return ids;
|
|
917
|
+
const same = ids.filter(id => !!this.t.sym(id)?.isStatic === wantStatic);
|
|
918
|
+
return same.length ? same : ids;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Unknown receiver: bind by name only when the repository has exactly one member with that name
|
|
923
|
+
* and the name is not also a standard-library method of the language (`get`, `set`, `apply`…).
|
|
924
|
+
* Everything else stays unbound with its candidate count — a guessed edge that is wrong most of
|
|
925
|
+
* the time costs an agent more than an honest "unknown".
|
|
926
|
+
*/
|
|
927
|
+
#anyMember(name, kind, fileId) {
|
|
928
|
+
const file = this.t.file(fileId);
|
|
929
|
+
const fam = familyOf(file.lang);
|
|
930
|
+
const ids = (this.t.byName.get(name) ?? []).filter(id => {
|
|
931
|
+
const s = this.t.sym(id);
|
|
932
|
+
if (!s || !(s.kind === 'method' || s.kind === 'field' || s.kind === 'property' || s.kind === 'function' && s.owner)) return false;
|
|
933
|
+
const f = this.t.file(s.fileId);
|
|
934
|
+
return f && familyOf(f.lang) === fam;
|
|
935
|
+
});
|
|
936
|
+
if (!ids.length) return null;
|
|
937
|
+
if (ids.length > 1 || COMMON_BY_FAMILY[fam]?.has(name)) return { id: null, conf: 0, ncand: ids.length };
|
|
938
|
+
return this.#pick(ids, kind, 0.5, fileId);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
export function familyOf(lang) {
|
|
943
|
+
if (lang === 'javascript' || lang === 'typescript' || lang === 'tsx') return 'ecma';
|
|
944
|
+
if (lang === 'c' || lang === 'cpp') return 'c';
|
|
945
|
+
if (lang === 'java' || lang === 'kotlin' || lang === 'scala') return 'jvm';
|
|
946
|
+
return lang;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
export function confidenceLabel(conf) {
|
|
950
|
+
if (conf >= 0.9) return 'exact';
|
|
951
|
+
if (conf >= 0.7) return 'high';
|
|
952
|
+
if (conf >= 0.4) return 'likely';
|
|
953
|
+
return 'possible';
|
|
954
|
+
}
|