graph-indexer 3.0.0 → 3.1.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 +456 -0
- package/README.md +72 -15
- package/SECURITY.md +21 -4
- package/bin/graph-indexer.mjs +46 -5
- package/integrations/claude-code/.claude-plugin/plugin.json +3 -2
- package/integrations/claude-code/agents/code-structure.md +0 -3
- package/integrations/claude-code/skills/setup/SKILL.md +26 -0
- package/integrations/claude-code-local/.claude-plugin/plugin.json +9 -0
- package/integrations/{claude-code → claude-code-local}/.mcp.json +1 -1
- package/integrations/claude-code-local/README.md +10 -0
- package/integrations/claude-code-local/agents/code-structure.md +22 -0
- package/integrations/{claude-code → claude-code-local}/hooks/gi-hook.mjs +12 -6
- package/integrations/opencode/graph-indexer.js +13 -8
- package/package.json +2 -2
- package/src/cli/helper.mjs +15 -1
- package/src/cli/hook.mjs +140 -48
- package/src/cli/init.mjs +49 -17
- package/src/cli/resident.mjs +15 -5
- package/src/index/discover.mjs +3 -5
- package/src/index/indexer.mjs +77 -41
- package/src/index/modules.mjs +82 -7
- package/src/index/parse-worker.mjs +12 -0
- package/src/index/parsepool.mjs +73 -0
- package/src/index/resolver.mjs +371 -25
- package/src/mcp/server.mjs +16 -12
- package/src/mcp/tools.mjs +419 -26
- package/src/parse/arity.mjs +41 -3
- package/src/parse/extract.mjs +277 -46
- package/src/parse/lang/ecma.mjs +2 -0
- package/src/parse/lang/go.mjs +2 -0
- package/src/parse/lang/jvm.mjs +33 -0
- package/src/parse/lang/markdown.mjs +92 -0
- package/src/parse/lang/python.mjs +43 -0
- package/src/parse/lang/rust.mjs +215 -15
- package/src/parse/lang/sfc.mjs +125 -0
- package/src/parse/languages.mjs +9 -2
- package/src/query/check.mjs +55 -17
- package/src/query/facts.mjs +3 -2
- package/src/query/injection-spring.mjs +405 -0
- package/src/query/injection.mjs +173 -0
- package/src/query/intel.mjs +87 -22
- package/src/query/issue.mjs +156 -0
- package/src/query/orm.mjs +286 -0
- package/src/query/read.mjs +6 -1
- package/src/query/report.mjs +53 -0
- package/src/query/routes.mjs +142 -0
- package/src/query/showcase.mjs +59 -0
- package/src/query/testcmd.mjs +25 -14
- package/src/query/textsearch.mjs +29 -11
- package/src/search/search.mjs +85 -17
- package/src/search/semantic.mjs +201 -0
- package/src/search/vectors.mjs +122 -0
- package/src/store/db.mjs +22 -4
- package/src/util/git.mjs +30 -0
- package/src/util/paths.mjs +40 -1
- package/src/util/regex.mjs +2 -0
- package/vendor/grammars/README.md +47 -0
- package/vendor/grammars/kotlin.wasm.gz +0 -0
- package/vendor/grammars/manifest.json +4 -4
- /package/integrations/{claude-code → claude-code-local}/hooks/hooks.json +0 -0
package/src/index/resolver.mjs
CHANGED
|
@@ -15,15 +15,23 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import { normalizeType } from '../parse/extract.mjs';
|
|
18
|
-
import { fits, sigArity } from '../parse/arity.mjs';
|
|
18
|
+
import { fits, sigArity, sigParamTypes, simpleParamType } from '../parse/arity.mjs';
|
|
19
|
+
import { escapeRegExp } from '../util/regex.mjs';
|
|
19
20
|
|
|
20
21
|
const TYPE_KINDS = new Set(['class', 'interface', 'struct', 'enum', 'trait', 'type', 'object', 'module', 'impl']);
|
|
22
|
+
// generic wrappers whose members are those of their argument (`InstanceType<typeof Repl>`): a call on
|
|
23
|
+
// one is not on a type the index lacks, so it keeps the by-name fallback
|
|
24
|
+
const TYPE_WRAPPERS = new Set(['ReturnType', 'InstanceType', 'Awaited', 'Partial', 'Required', 'Readonly', 'NonNullable', 'Pick', 'Omit', 'Mocked', 'MockedObject', 'Type']);
|
|
25
|
+
// element accesses one descriptor may stack (`xs[i][j]`…): a field assigned an element of itself
|
|
26
|
+
// (`self.impl = self.impl[0]`) would add one on every lookup, with no end
|
|
27
|
+
const MAX_ELEM = 8;
|
|
21
28
|
const VALUE_TYPED_KINDS = new Set(['variable', 'constant', 'field', 'property']);
|
|
22
29
|
const CALLABLE_KINDS = new Set(['function', 'method', 'constructor', 'macro']);
|
|
23
30
|
const MEMBER_HOLDERS = new Set(['class', 'interface', 'struct', 'enum', 'trait', 'object', 'impl', 'module']);
|
|
24
31
|
const MEMBER_KINDS = new Set(['field', 'property', 'method', 'constructor']);
|
|
25
32
|
const PACKAGE_LANGS = new Set(['java', 'kotlin', 'scala', 'csharp', 'php']);
|
|
26
33
|
const DIR_PACKAGE_LANGS = new Set(['go']);
|
|
34
|
+
const SINGLE_TYPE_IMPORTS = new Set(['java', 'kotlin', 'scala']);
|
|
27
35
|
const MAX_GLOBAL_CANDIDATES = 12;
|
|
28
36
|
|
|
29
37
|
/**
|
|
@@ -100,7 +108,7 @@ function paramType(param) {
|
|
|
100
108
|
*/
|
|
101
109
|
export function callbackParamTypeText(sig, name, i, j) {
|
|
102
110
|
if (!sig) return null;
|
|
103
|
-
const m = new RegExp(`(?:^|[^\\w$])${name
|
|
111
|
+
const m = new RegExp(`(?:^|[^\\w$])${escapeRegExp(name)}\\s*(?:<[^()]*>)?\\s*\\(`).exec(sig);
|
|
104
112
|
const list = m ? bracketed(sig, m.index + m[0].length - 1) : null;
|
|
105
113
|
let type = list ? paramType(splitTopLevel(list.inner)[i] ?? '') : null;
|
|
106
114
|
if (!type) return null;
|
|
@@ -216,11 +224,15 @@ export class SymbolTable {
|
|
|
216
224
|
file(id) { return this.files.get(id); }
|
|
217
225
|
}
|
|
218
226
|
|
|
227
|
+
/** A resolved type as membersOf takes it: {id, name, fileId}, and `allOf` for a value of several traits. */
|
|
228
|
+
const asType = (t) => (t.allOf ? t : { id: t.id ?? null, name: t.name, fileId: t.fileId ?? null });
|
|
229
|
+
|
|
219
230
|
export class Resolver {
|
|
220
|
-
constructor(table, specsById, { sigOf = null } = {}) {
|
|
231
|
+
constructor(table, specsById, { sigOf = null, modules = null } = {}) {
|
|
221
232
|
this.t = table;
|
|
222
233
|
this.specs = specsById; // lang id -> spec (implicitThis etc.)
|
|
223
234
|
this.sigOf = sigOf; // symbol id -> signature text (read on demand: callback parameter types)
|
|
235
|
+
this.modules = modules; // module resolver (Rust paths: `crate::a::B`, `super::f`, `other_crate::x`)
|
|
224
236
|
this.memo = new Map();
|
|
225
237
|
}
|
|
226
238
|
|
|
@@ -261,22 +273,39 @@ export class Resolver {
|
|
|
261
273
|
/** Members named `name` of a type (by id and/or name), including inherited ones. */
|
|
262
274
|
membersOf(type, name, depth = 0, seen = new Set()) {
|
|
263
275
|
if (!type || depth > 6) return [];
|
|
276
|
+
// a value bound by several traits (`W: AsyncWrite + AsyncSeek`): the member of each that has it
|
|
277
|
+
if (type.allOf) return [...new Set(type.allOf.flatMap(t => this.membersOf(t, name, depth + 1, new Set(seen))))];
|
|
264
278
|
const key = (type.id ?? '') + ':' + type.name;
|
|
265
279
|
if (seen.has(key)) return [];
|
|
266
280
|
seen.add(key);
|
|
267
281
|
const out = [];
|
|
268
282
|
if (type.id != null) for (const id of this.t.byParent.get(type.id)?.get(name) ?? []) out.push(id);
|
|
283
|
+
const ownSym = type.id != null ? this.t.sym(type.id) : null;
|
|
269
284
|
for (const id of this.t.byOwner.get(type.name) ?? []) {
|
|
270
285
|
const s = this.t.sym(id);
|
|
271
286
|
if (s.name !== name) continue;
|
|
287
|
+
// a blanket impl's method (`impl<M: Matcher> Matcher for &M`) is the trait's own, called through its declaration
|
|
288
|
+
if (ownSym?.kind === 'trait' && s.bases?.includes(type.name) && out.length) continue;
|
|
272
289
|
// owner-declared members must live near the type (same package/dir/file family)
|
|
273
290
|
if (type.fileId != null) {
|
|
274
291
|
const a = this.t.file(s.fileId), b = this.t.file(type.fileId);
|
|
275
|
-
if (a && b && a.lang ===
|
|
292
|
+
if (a && b && a.lang === 'rust' && b.lang === 'rust') { if (this.#implFor(s, type, a, b)) out.push(id); }
|
|
293
|
+
else if (a && b && a.lang === b.lang && (a.dir === b.dir || a.package === b.package || a.lang === 'cpp' || a.lang === 'c' || a.lang === 'javascript' || a.lang === 'typescript')) out.push(id);
|
|
276
294
|
} else out.push(id);
|
|
277
295
|
}
|
|
278
|
-
if (out.length) return out;
|
|
279
296
|
const typeSym = type.id != null ? this.t.sym(type.id) : null;
|
|
297
|
+
if (out.length) {
|
|
298
|
+
// Python: `self.x = …` hoisted as a field of a class whose base declares `x` as a property
|
|
299
|
+
// runs the base's setter — the attribute is the base's property, not a new field
|
|
300
|
+
if (typeSym && out.every(id => this.t.sym(id)?.kind === 'field') && this.t.file(typeSym.fileId)?.lang === 'python') {
|
|
301
|
+
for (const base of typeSym.bases ?? []) {
|
|
302
|
+
const bt = this.resolveTypeName(base, typeSym.fileId);
|
|
303
|
+
const inherited = bt ? this.membersOf({ id: bt.id, name: bt.name, fileId: bt.fileId }, name, depth + 1, new Set(seen)) : [];
|
|
304
|
+
if (inherited.some(id => this.t.sym(id)?.kind === 'method')) return inherited;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
280
309
|
for (const base of typeSym?.bases ?? []) {
|
|
281
310
|
const bt = this.resolveTypeName(base, typeSym.fileId);
|
|
282
311
|
if (bt) {
|
|
@@ -290,6 +319,20 @@ export class Resolver {
|
|
|
290
319
|
return out;
|
|
291
320
|
}
|
|
292
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Is a Rust member declared in an `impl Owner` block a member of this type? Several crates of a
|
|
324
|
+
* workspace may each have a type of that name (ripgrep has four `RegexMatcher`s): the impl's
|
|
325
|
+
* owner is the type its own file sees under that name, or, when that file cannot tell, a type of
|
|
326
|
+
* the same Cargo package (an inherent impl lives in the type's crate).
|
|
327
|
+
*/
|
|
328
|
+
#implFor(s, type, a, b) {
|
|
329
|
+
if (type.id == null || s.fileId === type.fileId) return true;
|
|
330
|
+
const seen = this.resolveTypeName(type.name, s.fileId);
|
|
331
|
+
if (seen) return seen.id === type.id;
|
|
332
|
+
const pa = this.modules?.rustPackageOf?.(a.path), pb = this.modules?.rustPackageOf?.(b.path);
|
|
333
|
+
return pa == null || pb == null || pa === pb;
|
|
334
|
+
}
|
|
335
|
+
|
|
293
336
|
/**
|
|
294
337
|
* Method resolution order of a type: C3 linearization over its resolved bases (Python's MRO; for
|
|
295
338
|
* single inheritance it is the chain of supertypes). Unresolved bases are left out; an inconsistent
|
|
@@ -376,8 +419,13 @@ export class Resolver {
|
|
|
376
419
|
*/
|
|
377
420
|
typeFromString(str, fileId, elem = 0) {
|
|
378
421
|
if (!str) return null;
|
|
379
|
-
if (str.startsWith('call:') || str.startsWith('cb:') || str.includes('#'))
|
|
422
|
+
if (str.startsWith('call:') || str.startsWith('cb:') || str.includes('#')) {
|
|
423
|
+
const desc = str + '[]'.repeat(elem);
|
|
424
|
+
return desc.split('[]').length - 1 > MAX_ELEM ? null : this.resolveRecvType(desc, fileId, null);
|
|
425
|
+
}
|
|
380
426
|
let s = str;
|
|
427
|
+
// a tuple (`(A,B)`, Rust): its elements are reached by position (`#0`), see resolveRecvType
|
|
428
|
+
if (s.startsWith('(') && s.endsWith(')')) return elem ? null : { tupleOf: s.slice(1, -1).split(','), fileId };
|
|
381
429
|
for (let i = 0; i < elem; i++) {
|
|
382
430
|
if (!s.endsWith('[]') && !s.endsWith('{}')) {
|
|
383
431
|
// an element of a collection class (`class Registry extends Map<string, Module>`)
|
|
@@ -387,6 +435,13 @@ export class Resolver {
|
|
|
387
435
|
s = s.slice(0, -2);
|
|
388
436
|
}
|
|
389
437
|
if (s === '!') return EXTERNAL;
|
|
438
|
+
if (s.includes('+') && !s.endsWith('[]') && !s.endsWith('{}')) {
|
|
439
|
+
// `A+B` (Rust bounds, see normalizeType): the traits of the repository among them
|
|
440
|
+
const ts = s.split('+').map(x => this.typeFromString(x, fileId)).filter(Boolean);
|
|
441
|
+
const own = ts.filter(x => x !== EXTERNAL && x.id != null);
|
|
442
|
+
if (!own.length) return ts.length ? EXTERNAL : null;
|
|
443
|
+
return own.length === 1 ? own[0] : { id: null, name: s, fileId, allOf: own };
|
|
444
|
+
}
|
|
390
445
|
if (s.endsWith('[]')) return { arrayOf: s.slice(0, -2), fileId }; // a collection value
|
|
391
446
|
if (s.endsWith('{}')) return { mapOf: s.slice(0, -2), fileId }; // a map value
|
|
392
447
|
if (this.#qualifiedType(s, fileId) === EXTERNAL || this.#nestedType(s, fileId) === EXTERNAL) return EXTERNAL;
|
|
@@ -425,7 +480,7 @@ export class Resolver {
|
|
|
425
480
|
if (TYPE_KINDS.has(s.kind)) cur = head.elem ? null : s; // constructor call
|
|
426
481
|
else if (s.type) cur = this.typeFromString(s.type, s.fileId, head.elem);
|
|
427
482
|
}
|
|
428
|
-
if ((cur?.arrayOf != null || cur?.mapOf != null) && parts.length === 1) cur = EXTERNAL;
|
|
483
|
+
if ((cur?.arrayOf != null || cur?.mapOf != null || cur?.tupleOf != null) && parts.length === 1) cur = EXTERNAL;
|
|
429
484
|
} else if (head.name.startsWith('cb:')) {
|
|
430
485
|
cur = this.#callbackParam(head.name, fileId, srcId, head.elem);
|
|
431
486
|
} else if (!head.elem) {
|
|
@@ -449,6 +504,20 @@ export class Resolver {
|
|
|
449
504
|
for (let i = 1; i < parts.length && cur && cur !== EXTERNAL; i++) {
|
|
450
505
|
const { name, elem } = parts[i];
|
|
451
506
|
if (!name) { cur = elem ? this.#elementOf(cur, elem) : cur; continue; } // `T#[]`: an element of T itself
|
|
507
|
+
// `~m`: member m of the type if it has one, else the same value (`x.clone()`, `opt.unwrap()`)
|
|
508
|
+
if (name.startsWith('~')) {
|
|
509
|
+
const real = name.slice(1);
|
|
510
|
+
const own = cur.arrayOf == null && cur.mapOf == null && cur.tupleOf == null ? this.membersOf(asType(cur), real) : [];
|
|
511
|
+
if (!own.length) { cur = elem ? this.#elementOf(cur, elem) : cur; continue; }
|
|
512
|
+
parts[i] = { name: real, elem };
|
|
513
|
+
i--;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (cur.tupleOf != null) {
|
|
517
|
+
const el = /^\d+$/.test(name) ? cur.tupleOf[Number(name)] : null;
|
|
518
|
+
cur = el && el !== '?' ? this.typeFromString(el, cur.fileId, elem) : EXTERNAL;
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
452
521
|
if (cur.arrayOf != null) {
|
|
453
522
|
// `list.get(0)` / `xs.first()` reach the element; other members are the language's
|
|
454
523
|
cur = spec?.elementMethods?.has(name) ? this.typeFromString(cur.arrayOf, cur.fileId, elem) : EXTERNAL;
|
|
@@ -460,7 +529,7 @@ export class Resolver {
|
|
|
460
529
|
: spec?.mapValueMethods?.has(name) ? this.typeFromString(cur.mapOf + '[]', cur.fileId, elem) : EXTERNAL;
|
|
461
530
|
continue;
|
|
462
531
|
}
|
|
463
|
-
const mem = this.membersOf(
|
|
532
|
+
const mem = this.membersOf(asType(cur), name);
|
|
464
533
|
if (!mem.length && cur.id != null && spec?.qualifiedTypes) {
|
|
465
534
|
// Go: an embedded field is named after its type (`m.MatchRegexp` in a struct embedding MatchRegexp)
|
|
466
535
|
const os = this.t.sym(cur.id);
|
|
@@ -477,6 +546,8 @@ export class Resolver {
|
|
|
477
546
|
let next = null;
|
|
478
547
|
for (const id of mem) {
|
|
479
548
|
const m = this.t.sym(id);
|
|
549
|
+
// a nested type reached through its outer one (`Token.TokenType.valueOf(…)`)
|
|
550
|
+
if (TYPE_KINDS.has(m.kind) && !elem) { next = { id: m.id, name: m.name, fileId: m.fileId }; break; }
|
|
480
551
|
if (m.type) { next = this.typeFromString(m.type, m.fileId, elem); if (next) break; }
|
|
481
552
|
}
|
|
482
553
|
if (!next) {
|
|
@@ -492,7 +563,7 @@ export class Resolver {
|
|
|
492
563
|
}
|
|
493
564
|
cur = next;
|
|
494
565
|
}
|
|
495
|
-
const res = cur === EXTERNAL || cur?.arrayOf != null || cur?.mapOf != null
|
|
566
|
+
const res = cur === EXTERNAL || cur?.arrayOf != null || cur?.mapOf != null || cur?.tupleOf != null ? EXTERNAL : cur ? asType(cur) : null;
|
|
496
567
|
this.memo.set(k, res);
|
|
497
568
|
return res;
|
|
498
569
|
}
|
|
@@ -555,9 +626,11 @@ export class Resolver {
|
|
|
555
626
|
if (targetFileId == null || depth > 6 || seen.has(targetFileId)) return [];
|
|
556
627
|
seen.add(targetFileId);
|
|
557
628
|
const out = [];
|
|
629
|
+
// a Rust or Go method (declared in an `impl` block or with a receiver) is no item of its module
|
|
630
|
+
const lang = this.t.file(targetFileId)?.lang, typedOwners = lang === 'rust' || lang === 'go';
|
|
558
631
|
for (const id of this.t.fileSyms.get(targetFileId) ?? []) {
|
|
559
632
|
const s = this.t.sym(id);
|
|
560
|
-
if (s.name === name && s.parentId == null) out.push(id);
|
|
633
|
+
if (s.name === name && s.parentId == null && !(typedOwners && s.owner && (MEMBER_KINDS.has(s.kind) || s.kind === 'type'))) out.push(id);
|
|
561
634
|
}
|
|
562
635
|
if (out.length) return out;
|
|
563
636
|
// in Python a later import rebinds the name (`from a import *` then `from b import *`)
|
|
@@ -567,12 +640,41 @@ export class Resolver {
|
|
|
567
640
|
const passes = imp.reexport === name || imp.reexport === '*' || imp.local === name || (imp.wildcard && imp.imported === '*');
|
|
568
641
|
if (!passes || imp.targetFileId == null) continue;
|
|
569
642
|
const inner = imp.reexport === name && imp.imported && imp.imported !== '*' ? imp.imported : (imp.local === name && imp.imported && imp.imported !== '*' ? imp.imported : name);
|
|
570
|
-
|
|
643
|
+
// `export { default as X } from './x'`: x's default export, whatever it is called there
|
|
644
|
+
const r = inner === 'default' && name !== 'default' ? this.#defaultExport(imp.targetFileId, name) : this.exportedFrom(imp.targetFileId, inner, depth + 1, seen);
|
|
571
645
|
if (r.length) return r;
|
|
572
646
|
}
|
|
573
647
|
return out;
|
|
574
648
|
}
|
|
575
649
|
|
|
650
|
+
/** The module a Python package or module file exposes as `seg`: a submodule file, or a module it imports. */
|
|
651
|
+
#submodule(fileId, seg) {
|
|
652
|
+
const f = this.t.file(fileId);
|
|
653
|
+
if (f?.path.endsWith('/__init__.py')) {
|
|
654
|
+
const dir = f.path.slice(0, -'/__init__.py'.length);
|
|
655
|
+
const sub = this.t.fileByPath.get(`${dir}/${seg}.py`) ?? this.t.fileByPath.get(`${dir}/${seg}/__init__.py`);
|
|
656
|
+
if (sub) return sub.id;
|
|
657
|
+
}
|
|
658
|
+
return this.#moduleFileOf(fileId, seg);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/** Whether a module binds `name` through an import of a module outside the repository, following re-exports. */
|
|
662
|
+
#reexportsExternal(targetFileId, name, depth = 0) {
|
|
663
|
+
if (targetFileId == null || depth > 6) return false;
|
|
664
|
+
for (const imp of this.t.imports.get(targetFileId) ?? []) {
|
|
665
|
+
// `export * from './icons'` where ./icons re-exports `name` from a package
|
|
666
|
+
if (imp.reexport === '*' && imp.targetFileId != null && imp.local == null) {
|
|
667
|
+
if (this.#reexportsExternal(imp.targetFileId, name, depth + 1)) return true;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if (imp.wildcard || !(imp.local === name || imp.reexport === name)) continue;
|
|
671
|
+
if (imp.targetFileId == null && !imp.targetDir) return true;
|
|
672
|
+
const inner = imp.imported && imp.imported !== '*' && imp.imported !== 'default' ? imp.imported : name;
|
|
673
|
+
if (imp.targetFileId != null && !this.exportedFrom(imp.targetFileId, inner).length) return this.#reexportsExternal(imp.targetFileId, inner, depth + 1);
|
|
674
|
+
}
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
|
|
576
678
|
#symbolsInDir(dir, name, fileId) {
|
|
577
679
|
const out = [];
|
|
578
680
|
for (const fid of this.t.dirFiles.get(dir) ?? []) {
|
|
@@ -584,6 +686,24 @@ export class Resolver {
|
|
|
584
686
|
return out;
|
|
585
687
|
}
|
|
586
688
|
|
|
689
|
+
/**
|
|
690
|
+
* `import java.io.UncheckedIOException;`: a single-type import (Java, Kotlin, Scala) from a
|
|
691
|
+
* package no file of the repository is in, nor an enclosing one — the type is the library's,
|
|
692
|
+
* not a same-name class of the repository.
|
|
693
|
+
*/
|
|
694
|
+
#importsLibraryType(file, imp) {
|
|
695
|
+
if (!SINGLE_TYPE_IMPORTS.has(file.lang) || imp.wildcard || !imp.imported || imp.imported === '*' || !imp.source) return false;
|
|
696
|
+
const segs = imp.source.split('.');
|
|
697
|
+
for (let n = segs.length; n > 0; n--) if (this.t.packageFiles.has(segs.slice(0, n).join('.'))) return false;
|
|
698
|
+
// nor a package that encloses some of the repository's (`import org.acme.X` with `org.acme.core` here)
|
|
699
|
+
if (this.pkgPrefixes?.size !== this.t.packageFiles.size) {
|
|
700
|
+
const set = new Set();
|
|
701
|
+
for (const p of this.t.packageFiles.keys()) { const s = p.split('.'); for (let n = 1; n < s.length; n++) set.add(s.slice(0, n).join('.')); }
|
|
702
|
+
this.pkgPrefixes = { size: this.t.packageFiles.size, set };
|
|
703
|
+
}
|
|
704
|
+
return !this.pkgPrefixes.set.has(imp.source);
|
|
705
|
+
}
|
|
706
|
+
|
|
587
707
|
#symbolsInPackage(pkg, name) {
|
|
588
708
|
const out = [];
|
|
589
709
|
for (const fid of this.t.packageFiles.get(pkg) ?? []) {
|
|
@@ -604,12 +724,19 @@ export class Resolver {
|
|
|
604
724
|
*/
|
|
605
725
|
resolve(ref) {
|
|
606
726
|
this.argc = ref.argc ?? null;
|
|
727
|
+
try { this.argTypes = ref.arg_types ? JSON.parse(ref.arg_types) : (ref.argTypes ?? null); } catch { this.argTypes = null; }
|
|
607
728
|
const fileId = ref.file_id;
|
|
608
729
|
const srcId = ref.src_id ?? null;
|
|
609
730
|
const recv = ref.recv || '';
|
|
610
731
|
const kind = ref.kind;
|
|
611
732
|
const name = ref.name;
|
|
612
|
-
if (recv)
|
|
733
|
+
if (recv) {
|
|
734
|
+
const r = this.#resolveMember(name, kind, recv, ref.recv_type, fileId, srcId);
|
|
735
|
+
// `f32::to_bits(self)` inside the `to_bits` of `impl Float for f32` is the primitive's own
|
|
736
|
+
// method (inherent methods come first), never a call of itself
|
|
737
|
+
if (r?.id != null && r.id === srcId && /^[a-z]\w*$/.test(recv) && this.specs[this.t.file(fileId)?.lang]?.isPrimitiveType?.(recv)) return { id: null, conf: 0, ncand: 0, external: true };
|
|
738
|
+
return r;
|
|
739
|
+
}
|
|
613
740
|
return this.#resolveName(name, kind, fileId, srcId) ?? { id: null, conf: 0, ncand: 0 };
|
|
614
741
|
}
|
|
615
742
|
|
|
@@ -635,6 +762,32 @@ export class Resolver {
|
|
|
635
762
|
return fits({ n: Math.max(0, this.argc), open: this.argc < 0 }, sigArity(this.sigOf(s.id), s.name));
|
|
636
763
|
}
|
|
637
764
|
|
|
765
|
+
/**
|
|
766
|
+
* Overloads whose parameter types the call's argument types fit, best fits first kept: an
|
|
767
|
+
* exact type beats a widening (`int` → `long`), boxing or a type parameter; a primitive or
|
|
768
|
+
* String argument never fits a different primitive or String parameter (`'-'` is no String).
|
|
769
|
+
*/
|
|
770
|
+
#byArgTypes(cands) {
|
|
771
|
+
const scored = [];
|
|
772
|
+
for (const s of cands) {
|
|
773
|
+
const params = sigParamTypes(this.sigOf(s.id), s.name);
|
|
774
|
+
if (!params) return cands;
|
|
775
|
+
let score = 0, ok = true;
|
|
776
|
+
this.argTypes.forEach((a, i) => {
|
|
777
|
+
if (!ok || a === '?') return;
|
|
778
|
+
let p = params[Math.min(i, params.length - 1)];
|
|
779
|
+
if (p == null) { ok = false; return; }
|
|
780
|
+
if (/\.\.\.$/.test(p)) p = i >= params.length - 1 && !(this.argTypes.length === params.length && a.endsWith('[]')) ? p.slice(0, -3) : p.slice(0, -3) + '[]';
|
|
781
|
+
const fit = argFits(a, simpleParamType(p));
|
|
782
|
+
if (fit < 0) ok = false; else score += fit;
|
|
783
|
+
});
|
|
784
|
+
if (ok) scored.push({ s, score });
|
|
785
|
+
}
|
|
786
|
+
if (!scored.length) return cands;
|
|
787
|
+
const best = Math.max(...scored.map(x => x.score));
|
|
788
|
+
return scored.filter(x => x.score === best).map(x => x.s);
|
|
789
|
+
}
|
|
790
|
+
|
|
638
791
|
#compatible(s, kind) {
|
|
639
792
|
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
793
|
if (kind === 'type' || kind === 'inherit') return TYPE_KINDS.has(s.kind);
|
|
@@ -646,8 +799,11 @@ export class Resolver {
|
|
|
646
799
|
}
|
|
647
800
|
|
|
648
801
|
#pick(ids, kind, conf, fileId) {
|
|
649
|
-
|
|
802
|
+
let cands = ids.map(id => this.t.sym(id)).filter(s => s && this.#compatible(s, kind));
|
|
650
803
|
if (!cands.length) return null;
|
|
804
|
+
// Rust: a method of a trait implemented for the type and for references to it (`impl
|
|
805
|
+
// IntoIterator for Map` and `for &'a Map`): a call on the value reaches the value's impl
|
|
806
|
+
if (cands.length > 1 && cands.some(s => s.byRef) && cands.some(s => !s.byRef) && cands.every(s => s.qname === cands[0].qname)) cands = cands.filter(s => !s.byRef);
|
|
651
807
|
if (cands.length === 1) return { id: cands[0].id, conf, ncand: 1 };
|
|
652
808
|
// overloads / redeclarations in one type or file: prefer the definition with a body (larger span)
|
|
653
809
|
const sameQ = cands.every(s => s.qname === cands[0].qname);
|
|
@@ -659,6 +815,12 @@ export class Resolver {
|
|
|
659
815
|
const fixed = fit.filter(s => sigArity(this.sigOf(s.id), s.name)?.max !== Infinity);
|
|
660
816
|
if (fixed.length && fixed.length < fit.length) fit = fixed;
|
|
661
817
|
if (fit.length === 1) return { id: fit[0].id, conf, ncand: 1 };
|
|
818
|
+
// several of that count: the one the arguments' types fit (`append('-')` is append(char))
|
|
819
|
+
if (fit.length > 1 && this.argTypes) {
|
|
820
|
+
const typed = this.#byArgTypes(fit);
|
|
821
|
+
if (typed.length === 1) return { id: typed[0].id, conf, ncand: 1 };
|
|
822
|
+
if (typed.length && typed.length < fit.length) fit = typed;
|
|
823
|
+
}
|
|
662
824
|
if (fit.length > 1 && fit.length < cands.length) return this.#pick(fit.map(s => s.id), kind, conf, fileId);
|
|
663
825
|
}
|
|
664
826
|
const scored = cands.map(s => ({ s, score: this.#proximity(s, fileId) + (s.endLine - s.startLine) / 1e4 }));
|
|
@@ -700,7 +862,8 @@ export class Resolver {
|
|
|
700
862
|
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
863
|
}
|
|
702
864
|
// 2. implicit this: members of the enclosing type (Java/Kotlin/C#/Scala/C++/Ruby)
|
|
703
|
-
|
|
865
|
+
// (`new AVLNode<>(…)` inside AVLNode names the class, not one of its constructors)
|
|
866
|
+
if (spec?.implicitThis && srcId != null && kind !== 'type' && kind !== 'inherit' && kind !== 'new') {
|
|
704
867
|
const type = this.enclosingType(srcId);
|
|
705
868
|
if (type) {
|
|
706
869
|
const r = this.#pick(this.#overloadsReached(type, this.membersOf(type, name), name), kind, 0.9, fileId);
|
|
@@ -708,7 +871,9 @@ export class Resolver {
|
|
|
708
871
|
}
|
|
709
872
|
}
|
|
710
873
|
// 3. same file, top-level
|
|
711
|
-
|
|
874
|
+
// a member declared outside its type's body (a Rust `impl` block, a Go receiver, a C++
|
|
875
|
+
// `T::m`) is reached through a receiver or `self`, never by its bare name
|
|
876
|
+
const local = (this.t.fileSyms.get(fileId) ?? []).filter(id => { const s = this.t.sym(id); return s.name === name && this.#fileLevel(s); });
|
|
712
877
|
if (local.length) {
|
|
713
878
|
const r = this.#pick(local, kind, 0.9, fileId);
|
|
714
879
|
if (r) return r;
|
|
@@ -719,8 +884,14 @@ export class Resolver {
|
|
|
719
884
|
const target = imp.imported && imp.imported !== '*' && imp.imported !== 'default' ? imp.imported : name;
|
|
720
885
|
let ids = [];
|
|
721
886
|
if (imp.targetFileId != null) {
|
|
722
|
-
|
|
723
|
-
|
|
887
|
+
if (imp.imported === 'default') {
|
|
888
|
+
// a default import names the module's default export, whatever the importer calls it:
|
|
889
|
+
// a definition of that name in the module itself, else its default export; only then
|
|
890
|
+
// what the module imports under that name (it may import another `name` of its own)
|
|
891
|
+
ids = (this.t.fileSyms.get(imp.targetFileId) ?? []).filter(id => { const s = this.t.sym(id); return s.name === name && s.parentId == null; });
|
|
892
|
+
if (!ids.length) ids = this.#defaultExport(imp.targetFileId, name);
|
|
893
|
+
if (!ids.length) ids = this.exportedFrom(imp.targetFileId, target);
|
|
894
|
+
} else ids = this.exportedFrom(imp.targetFileId, target);
|
|
724
895
|
}
|
|
725
896
|
if (!ids.length && imp.targetDir) ids = this.#symbolsInDir(imp.targetDir, target, fileId);
|
|
726
897
|
if (!ids.length && imp.source && PACKAGE_LANGS.has(file.lang)) ids = this.#symbolsInPackage(imp.source, target);
|
|
@@ -728,7 +899,10 @@ export class Resolver {
|
|
|
728
899
|
const r = this.#pick(ids, kind, 0.95, fileId);
|
|
729
900
|
if (r) return r;
|
|
730
901
|
}
|
|
731
|
-
if (imp.targetFileId == null && !imp.targetDir && !PACKAGE_LANGS.has(file.lang)) return { id: null, conf: 0, ncand: 0, external: true };
|
|
902
|
+
if (imp.targetFileId == null && !imp.targetDir && (!PACKAGE_LANGS.has(file.lang) || this.#importsLibraryType(file, imp))) return { id: null, conf: 0, ncand: 0, external: true };
|
|
903
|
+
// the module re-exports the name from a package outside the repository (`fastapi/__init__.py`:
|
|
904
|
+
// `from starlette.responses import Response`): not a same-name definition found globally
|
|
905
|
+
if (!ids.length && imp.targetFileId != null && this.#reexportsExternal(imp.targetFileId, target)) return { id: null, conf: 0, ncand: 0, external: true };
|
|
732
906
|
}
|
|
733
907
|
// 5. same package / directory
|
|
734
908
|
if (PACKAGE_LANGS.has(file.lang) && file.package) {
|
|
@@ -765,22 +939,51 @@ export class Resolver {
|
|
|
765
939
|
const r = ids.length ? this.#pick(ids, kind, 0.8, fileId) : null;
|
|
766
940
|
if (r) return r;
|
|
767
941
|
}
|
|
768
|
-
// 8. global by name
|
|
942
|
+
// 8. global by name — not for a name Python code evaluates (a call, a value): one its module
|
|
943
|
+
// neither defines nor imports is a NameError, unless a star import of a module outside the
|
|
944
|
+
// index brought it. Types are still looked up (an annotation's name may be imported only
|
|
945
|
+
// under TYPE_CHECKING, and a type recorded for a receiver has lost its module: `routing.APIRouter`)
|
|
946
|
+
if (file.lang === 'python' && kind !== 'type' && kind !== 'inherit' && !(this.t.imports.get(fileId) ?? []).some(w => w.wildcard && w.targetFileId == null)) return null;
|
|
769
947
|
return this.#global(name, kind, fileId);
|
|
770
948
|
}
|
|
771
949
|
|
|
950
|
+
/**
|
|
951
|
+
* A definition its file's other code names directly: a top-level one, or a type nested in a type
|
|
952
|
+
* (Java's inner classes). Not a member declared outside its type's body (a Rust `impl` block, a Go
|
|
953
|
+
* receiver, a C++ `T::m`: reached through a receiver or `self`), not a type local to a function
|
|
954
|
+
* (seen from inside it, step 1), and not an associated type (`impl Visitor for V { type Value = …; }`).
|
|
955
|
+
*/
|
|
956
|
+
#fileLevel(s) {
|
|
957
|
+
if (s.owner && (MEMBER_KINDS.has(s.kind) || (s.kind === 'type' && s.parentId == null))) return false;
|
|
958
|
+
if (s.parentId == null) return true;
|
|
959
|
+
if (!TYPE_KINDS.has(s.kind)) return false;
|
|
960
|
+
for (let p = this.t.sym(s.parentId), g = 0; p && g < 16; p = p.parentId != null ? this.t.sym(p.parentId) : null, g++) if (CALLABLE_KINDS.has(p.kind)) return false;
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
|
|
772
964
|
#defaultExport(targetFileId, localName) {
|
|
773
965
|
// `export default class Foo` / `module.exports = Foo` → the file's main exported symbol
|
|
774
966
|
const ids = (this.t.fileSyms.get(targetFileId) ?? []).filter(id => { const s = this.t.sym(id); return s.parentId == null && s.exported; });
|
|
775
967
|
const same = ids.filter(id => this.t.sym(id).name.toLowerCase() === localName.toLowerCase());
|
|
776
968
|
if (same.length) return same;
|
|
969
|
+
// a Vue or Svelte file's default export is the component, named after the file whatever the importer calls it
|
|
970
|
+
const file = this.t.file(targetFileId);
|
|
971
|
+
if (file && (file.lang === 'vue' || file.lang === 'svelte')) {
|
|
972
|
+
const base = file.path.split('/').pop().replace(/\.[^.]+$/, '');
|
|
973
|
+
const stem = (base === 'index' ? file.path.split('/').slice(-2, -1)[0] ?? base : base).replace(/(^|[-_.])(\w)/g, (_, __, c) => c.toUpperCase());
|
|
974
|
+
const comp = ids.filter(id => this.t.sym(id).name === stem || this.t.sym(id).name === `${stem}Component`);
|
|
975
|
+
if (comp.length) return comp;
|
|
976
|
+
}
|
|
777
977
|
return ids.length === 1 ? ids : [];
|
|
778
978
|
}
|
|
779
979
|
|
|
780
980
|
#global(name, kind, fileId) {
|
|
781
981
|
const all = (this.t.byName.get(name) ?? []).filter(id => {
|
|
782
982
|
const s = this.t.sym(id);
|
|
783
|
-
|
|
983
|
+
if (!s || !this.#compatible(s, kind) || !(this.#fileLevel(s) || kind === 'call')) return false;
|
|
984
|
+
// a function nested in a function is visible only inside it (found in step 1 there)
|
|
985
|
+
const parent = s.parentId != null ? this.t.sym(s.parentId) : null;
|
|
986
|
+
return !(parent && CALLABLE_KINDS.has(parent.kind) && CALLABLE_KINDS.has(s.kind));
|
|
784
987
|
});
|
|
785
988
|
const file = this.t.file(fileId);
|
|
786
989
|
// never bind across unrelated languages (a TS call never resolves to a Python def)
|
|
@@ -788,10 +991,15 @@ export class Resolver {
|
|
|
788
991
|
const f = this.t.file(this.t.sym(id).fileId);
|
|
789
992
|
return f && (f.lang === file.lang || familyOf(f.lang) === familyOf(file.lang));
|
|
790
993
|
});
|
|
994
|
+
// an ES module sees another module's top-level names only through an import (resolved
|
|
995
|
+
// above): a value read that reached here is a local the extractor did not scope (a lambda
|
|
996
|
+
// parameter, a block-scoped const), not that name. Calls are left alone (CommonJS patterns).
|
|
997
|
+
const isolated = (kind === 'value' || kind === 'read') && familyOf(file.lang) === 'ecma' && this.#isModule(fileId);
|
|
791
998
|
// unqualified calls cannot target methods of unrelated classes in most languages
|
|
792
999
|
const cands = sameFamily.filter(id => {
|
|
793
1000
|
const s = this.t.sym(id);
|
|
794
1001
|
if (s.kind === 'method' || s.kind === 'field' || s.kind === 'property') return false;
|
|
1002
|
+
if (isolated && s.fileId !== fileId && s.parentId == null && this.#isModule(s.fileId)) return false;
|
|
795
1003
|
return true;
|
|
796
1004
|
});
|
|
797
1005
|
if (!cands.length) return null;
|
|
@@ -800,6 +1008,12 @@ export class Resolver {
|
|
|
800
1008
|
return this.#pick(cands, kind, base, fileId);
|
|
801
1009
|
}
|
|
802
1010
|
|
|
1011
|
+
/** A JS/TS file that imports or exports something: its top-level names are its own. */
|
|
1012
|
+
#isModule(fileId) {
|
|
1013
|
+
if ((this.t.imports.get(fileId) ?? []).length) return true;
|
|
1014
|
+
return (this.t.fileSyms.get(fileId) ?? []).some(id => { const s = this.t.sym(id); return s?.parentId == null && s?.exported; });
|
|
1015
|
+
}
|
|
1016
|
+
|
|
803
1017
|
#resolveMember(name, kind, recv, recvType, fileId, srcId) {
|
|
804
1018
|
const file = this.t.file(fileId);
|
|
805
1019
|
const root = recv.split('.')[0];
|
|
@@ -813,13 +1027,23 @@ export class Resolver {
|
|
|
813
1027
|
const ts = type.id != null ? this.t.sym(type.id) : null;
|
|
814
1028
|
ids = [];
|
|
815
1029
|
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);
|
|
1030
|
+
} else ids = this.#sameImpl(this.membersOf(type, name), srcId, fileId);
|
|
817
1031
|
const r = this.#pick(this.#byStatic(ids, false), kind, 0.95, fileId);
|
|
818
1032
|
if (r) return r;
|
|
819
1033
|
return { id: null, conf: 0, ncand: 0 };
|
|
820
1034
|
}
|
|
821
1035
|
}
|
|
822
1036
|
}
|
|
1037
|
+
// a Rust trait-path call: `SerializeSeq::serialize_element(self, v)`, `Debug::fmt(n, f)`
|
|
1038
|
+
if (file.lang === 'rust' && kind === 'call' && this.argTypes?.length === 1) {
|
|
1039
|
+
const r = this.#traitPathCall(name, recv, fileId);
|
|
1040
|
+
if (r) return r;
|
|
1041
|
+
}
|
|
1042
|
+
// a Rust path: `crate::a::f`, `super::B::new`, `other_crate::m::T`, `alias::sub::f`
|
|
1043
|
+
if (file.lang === 'rust' && this.modules) {
|
|
1044
|
+
const r = this.#rustPath(name, kind, recv, fileId);
|
|
1045
|
+
if (r) return r;
|
|
1046
|
+
}
|
|
823
1047
|
// inferred receiver type (locals, params, fields, factories; chains across files)
|
|
824
1048
|
if (recvType) {
|
|
825
1049
|
const type = this.resolveRecvType(recvType, fileId, srcId);
|
|
@@ -831,6 +1055,13 @@ export class Resolver {
|
|
|
831
1055
|
// interface / abstract receiver: the member may be declared only on implementations
|
|
832
1056
|
return { id: null, conf: 0, ncand: 0 };
|
|
833
1057
|
}
|
|
1058
|
+
if (this.#unknownDeclaredType(recvType)) {
|
|
1059
|
+
// the receiver is declared as a type the index does not hold (a library type, or a
|
|
1060
|
+
// declaration the parser could not read): another type's member of that name is not
|
|
1061
|
+
// a guess, so the call stays unbound and is counted with the same-name candidates
|
|
1062
|
+
const guess = kind === 'read' ? null : this.#anyMember(name, kind, fileId);
|
|
1063
|
+
return { id: null, conf: 0, ncand: guess ? Math.max(1, guess.ncand) : 0 };
|
|
1064
|
+
}
|
|
834
1065
|
}
|
|
835
1066
|
// receiver is an imported module / namespace / package alias
|
|
836
1067
|
const imp = this.#importFor(fileId, root) ?? this.#importFor(fileId, recv);
|
|
@@ -844,8 +1075,11 @@ export class Resolver {
|
|
|
844
1075
|
if (!owners.length && PACKAGE_LANGS.has(file.lang)) owners = this.#symbolsInPackage(imp.source, importedName);
|
|
845
1076
|
for (const oid of owners) ids.push(...this.#membersVia(this.t.sym(oid), name));
|
|
846
1077
|
if (!ids.length && imp.targetFileId != null && owners.length === 0) {
|
|
847
|
-
// `from pkg import submodule` then submodule.fn()
|
|
848
|
-
|
|
1078
|
+
// `from pkg import submodule` then submodule.fn(): the submodule's own name first —
|
|
1079
|
+
// pkg/__init__.py may re-export something else under it (`from fastapi import
|
|
1080
|
+
// params; params.File` is the class in params.py, not the File function __init__ exports)
|
|
1081
|
+
const mod = file.lang === 'python' ? this.#moduleFileOf(fileId, imp.local) : null;
|
|
1082
|
+
ids = this.exportedFrom(mod ?? imp.targetFileId, name);
|
|
849
1083
|
}
|
|
850
1084
|
} else if (rest.length === 0) {
|
|
851
1085
|
if (imp.targetFileId != null) ids = this.exportedFrom(imp.targetFileId, name);
|
|
@@ -860,6 +1094,12 @@ export class Resolver {
|
|
|
860
1094
|
const mod = this.#moduleFileOf(fileId, imp.local);
|
|
861
1095
|
if (mod != null && rest.length === 0) ids = this.exportedFrom(mod, name);
|
|
862
1096
|
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));
|
|
1097
|
+
// `import flask; flask.json.dumps(…)`: the rest of the receiver names submodules
|
|
1098
|
+
if (!ids.length && mod != null && rest.length) {
|
|
1099
|
+
let cur = mod;
|
|
1100
|
+
for (const seg of rest) { cur = cur != null ? this.#submodule(cur, seg) : null; }
|
|
1101
|
+
if (cur != null) ids = this.exportedFrom(cur, name);
|
|
1102
|
+
}
|
|
863
1103
|
}
|
|
864
1104
|
if (ids.length) {
|
|
865
1105
|
const r = this.#pick(ids, kind, 0.95, fileId);
|
|
@@ -895,6 +1135,82 @@ export class Resolver {
|
|
|
895
1135
|
return this.#anyMember(name, kind, fileId) ?? { id: null, conf: 0, ncand: 0 };
|
|
896
1136
|
}
|
|
897
1137
|
|
|
1138
|
+
/**
|
|
1139
|
+
* `self.m()` in a Rust impl where several impls of the type declare `m` (tokio's `impl AsyncWrite`
|
|
1140
|
+
* for `Cursor<&mut [u8]>`, `Cursor<Vec<u8>>`…): the one in the caller's own impl block, the
|
|
1141
|
+
* nearest in the file among those implementing the caller's trait.
|
|
1142
|
+
*/
|
|
1143
|
+
#sameImpl(ids, srcId, fileId) {
|
|
1144
|
+
if (ids.length < 2 || srcId == null || this.t.file(fileId)?.lang !== 'rust') return ids;
|
|
1145
|
+
let src = this.t.sym(srcId);
|
|
1146
|
+
while (src && !CALLABLE_KINDS.has(src.kind) && src.parentId != null) src = this.t.sym(src.parentId);
|
|
1147
|
+
if (!src) return ids;
|
|
1148
|
+
const key = (s) => JSON.stringify(s.bases ?? []);
|
|
1149
|
+
const same = ids.map(id => this.t.sym(id)).filter(s => s && s.fileId === fileId && key(s) === key(src));
|
|
1150
|
+
if (same.length < 2 || same.length < ids.length) return ids;
|
|
1151
|
+
same.sort((a, b) => Math.abs(a.startLine - src.startLine) - Math.abs(b.startLine - src.startLine));
|
|
1152
|
+
return [same[0].id];
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* `Trait::m(x)` / `<T as Trait>::m(x)` (Rust): the method `m` of the impl of `Trait` for the
|
|
1157
|
+
* type of `x` (or `T`), so a type with several traits declaring `m` (`Debug::fmt` and
|
|
1158
|
+
* `Display::fmt` on one type) is told apart. Null when that type has no such impl in the
|
|
1159
|
+
* repository: the path names a type, or the trait's own declaration is what is called.
|
|
1160
|
+
*/
|
|
1161
|
+
#traitPathCall(name, recv, fileId) {
|
|
1162
|
+
const trait = recv.split('.').pop();
|
|
1163
|
+
if (!/^[A-Z]/.test(trait ?? '')) return null;
|
|
1164
|
+
const self = this.typeFromString(this.argTypes[0], fileId);
|
|
1165
|
+
if (!self || self === EXTERNAL || (self.id == null && !self.allOf)) return null;
|
|
1166
|
+
const ids = this.membersOf(self, name).filter(id => { const m = this.t.sym(id); return m && CALLABLE_KINDS.has(m.kind) && m.bases?.includes(trait); });
|
|
1167
|
+
return ids.length ? this.#pick(ids, 'call', 0.9, fileId) : null;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* A Rust path receiver, walked module by module from where it starts — `crate`, `self`,
|
|
1172
|
+
* `super`, a crate of the repository, or an imported module — to the module (and at most one
|
|
1173
|
+
* type in it) that declares `name`. Null when the path does not start at a module the index
|
|
1174
|
+
* knows; then the other rules apply.
|
|
1175
|
+
*/
|
|
1176
|
+
#rustPath(name, kind, recv, fileId) {
|
|
1177
|
+
const segs = recv.split('.');
|
|
1178
|
+
const from = this.t.file(fileId)?.path;
|
|
1179
|
+
const isModule = (p, seg) => p && (seg === 'crate' || seg === 'self' || seg === 'super' || this.modules.rustCrates?.get(seg) === p || new RegExp(`(^|/)${seg}(\\.rs|/mod\\.rs)$`).test(p));
|
|
1180
|
+
let cur = null, rest = segs;
|
|
1181
|
+
if (segs[0] === 'crate' || segs[0] === 'self' || segs[0] === 'super' || this.modules.rustCrates?.has(segs[0])) {
|
|
1182
|
+
let k = 1;
|
|
1183
|
+
while (segs[k] === 'super') k++;
|
|
1184
|
+
const t = this.modules.resolve('rust', from, segs.slice(0, k).join('::'));
|
|
1185
|
+
if (!t?.file || !isModule(t.file, segs[k - 1])) return null;
|
|
1186
|
+
cur = t.file; rest = segs.slice(k);
|
|
1187
|
+
} else {
|
|
1188
|
+
const imp = this.#importFor(fileId, segs[0]);
|
|
1189
|
+
const mod = imp?.imported === 'self' ? imp.source?.split('::').pop() : imp?.imported;
|
|
1190
|
+
let target = imp && (imp.targetPath ?? this.t.file(imp.targetFileId)?.path);
|
|
1191
|
+
if (!imp) {
|
|
1192
|
+
// a child module of this one (`mod chan;` then `chan::channel()`)
|
|
1193
|
+
target = this.modules.resolve('rust', from, `self::${segs[0]}`)?.file;
|
|
1194
|
+
if (target === from) target = null;
|
|
1195
|
+
}
|
|
1196
|
+
else if (!isModule(target, mod)) return null;
|
|
1197
|
+
if (!target || !isModule(target, imp ? mod : segs[0])) return null;
|
|
1198
|
+
cur = target; rest = segs.slice(1);
|
|
1199
|
+
}
|
|
1200
|
+
// submodules, while a segment names one
|
|
1201
|
+
while (rest.length) {
|
|
1202
|
+
const t = this.modules.resolve('rust', cur, `self::${rest[0]}`);
|
|
1203
|
+
if (!t?.file || t.file === cur || !isModule(t.file, rest[0])) break;
|
|
1204
|
+
cur = t.file; rest = rest.slice(1);
|
|
1205
|
+
}
|
|
1206
|
+
const fid = this.t.fileByPath.get(cur)?.id;
|
|
1207
|
+
if (fid == null || rest.length > 1) return null;
|
|
1208
|
+
let ids = [];
|
|
1209
|
+
if (!rest.length) ids = this.exportedFrom(fid, name);
|
|
1210
|
+
else for (const oid of this.exportedFrom(fid, rest[0])) ids.push(...this.#membersVia(this.t.sym(oid), name));
|
|
1211
|
+
return this.#pick(ids, kind, 0.95, fileId);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
898
1214
|
/**
|
|
899
1215
|
* Members named `name` reached through an imported/package symbol: a type's static members, a
|
|
900
1216
|
* typed value's instance members (`export const api = new Api()`), or members declared on the
|
|
@@ -904,7 +1220,7 @@ export class Resolver {
|
|
|
904
1220
|
if (!o) return [];
|
|
905
1221
|
if (!TYPE_KINDS.has(o.kind) && o.type) {
|
|
906
1222
|
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);
|
|
1223
|
+
if (t && t !== EXTERNAL && t.arrayOf == null && t.mapOf == null && t.tupleOf == null) return this.#byStatic(this.membersOf(t, name), false);
|
|
908
1224
|
if (t) return [];
|
|
909
1225
|
}
|
|
910
1226
|
const ids = this.membersOf({ id: o.id, name: o.name, fileId: o.fileId }, name);
|
|
@@ -918,6 +1234,17 @@ export class Resolver {
|
|
|
918
1234
|
return same.length ? same : ids;
|
|
919
1235
|
}
|
|
920
1236
|
|
|
1237
|
+
/**
|
|
1238
|
+
* A receiver type written as a plain type name that no type in the index has. Short names and
|
|
1239
|
+
* `TName` are left out: they are usually type parameters (`T`, `K`, `TItem`), whose calls reach
|
|
1240
|
+
* the members of their bound.
|
|
1241
|
+
*/
|
|
1242
|
+
#unknownDeclaredType(desc) {
|
|
1243
|
+
if (!/^[A-Z][\w$]*$/.test(desc) || /^[A-Z][A-Z0-9]?$|^T[A-Z]\w*$/.test(desc) || TYPE_WRAPPERS.has(desc)) return false;
|
|
1244
|
+
// a descriptor may name a value rather than a type (Go `s := ByCid`, a package-level constant)
|
|
1245
|
+
return !(this.t.byName.get(desc) ?? []).some(id => { const k = this.t.sym(id)?.kind; return TYPE_KINDS.has(k) || VALUE_TYPED_KINDS.has(k); });
|
|
1246
|
+
}
|
|
1247
|
+
|
|
921
1248
|
/**
|
|
922
1249
|
* Unknown receiver: bind by name only when the repository has exactly one member with that name
|
|
923
1250
|
* and the name is not also a standard-library method of the language (`get`, `set`, `apply`…).
|
|
@@ -939,8 +1266,27 @@ export class Resolver {
|
|
|
939
1266
|
}
|
|
940
1267
|
}
|
|
941
1268
|
|
|
1269
|
+
const WIDENING = { byte: ['short', 'int', 'long', 'float', 'double'], short: ['int', 'long', 'float', 'double'], char: ['int', 'long', 'float', 'double'],
|
|
1270
|
+
int: ['long', 'float', 'double'], long: ['float', 'double'], float: ['double'] };
|
|
1271
|
+
const BOXED = { int: 'Integer', char: 'Character', long: 'Long', double: 'Double', float: 'Float', boolean: 'Boolean', byte: 'Byte', short: 'Short' };
|
|
1272
|
+
const CLOSED = new Set([...Object.keys(BOXED), 'String']);
|
|
1273
|
+
|
|
1274
|
+
/** How well an argument type fits a parameter type: 3 exact, 2 widening/boxing, 1 maybe (a class, a type parameter), -1 never. */
|
|
1275
|
+
function argFits(a, p) {
|
|
1276
|
+
if (!p) return 1;
|
|
1277
|
+
if (a === p) return 3;
|
|
1278
|
+
if (a === '*') return CLOSED.has(p) && p !== 'String' ? -1 : 1; // null: any reference type
|
|
1279
|
+
if (WIDENING[a]?.includes(p) || BOXED[a] === p || BOXED[p] === a) return 2;
|
|
1280
|
+
if (p === 'Object' || /^[A-Z]\w?$/.test(p)) return 1; // anything, or a type parameter
|
|
1281
|
+
if (CLOSED.has(a) && CLOSED.has(p)) return -1;
|
|
1282
|
+
if (BOXED[a]) return ['Number', 'Comparable', 'Serializable'].includes(p) ? 1 : -1; // a primitive
|
|
1283
|
+
if (BOXED[p]) return -1; // a primitive parameter, an object argument
|
|
1284
|
+
if (a.endsWith('[]') !== p.endsWith('[]')) return -1;
|
|
1285
|
+
return 1;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
942
1288
|
export function familyOf(lang) {
|
|
943
|
-
if (lang === 'javascript' || lang === 'typescript' || lang === 'tsx') return 'ecma';
|
|
1289
|
+
if (lang === 'javascript' || lang === 'typescript' || lang === 'tsx' || lang === 'vue' || lang === 'svelte') return 'ecma';
|
|
944
1290
|
if (lang === 'c' || lang === 'cpp') return 'c';
|
|
945
1291
|
if (lang === 'java' || lang === 'kotlin' || lang === 'scala') return 'jvm';
|
|
946
1292
|
return lang;
|