clast-ai 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +14 -0
- package/.claude-plugin/plugin.json +11 -9
- package/README.md +468 -398
- package/bench/ab/onair-concept.json +22 -0
- package/bench/ab/onair-deadcode.json +7 -0
- package/bench/ab/onair-hard.json +32 -0
- package/bench/ab/onair-impact.json +17 -0
- package/bench/ab/onair-refactors.json +18 -0
- package/bench/ab/onair-survey.json +17 -0
- package/bench/ab/onair-tasks.json +20 -0
- package/bench/ab/onair.json +52 -0
- package/bench/ab/vscode-concept.json +22 -0
- package/bench/ab/vscode-hard.json +32 -0
- package/bench/ab/vscode-impact.json +17 -0
- package/bench/ab/vscode-refactors.json +18 -0
- package/bench/ab/vscode-tasks.json +20 -0
- package/bench/ab/vscode.json +52 -0
- package/bench/ab.mjs +213 -0
- package/bench/bench.mjs +98 -0
- package/bench/resolution.mjs +75 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +266 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/defaults.d.ts +12 -2
- package/dist/config/defaults.js +38 -31
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +26 -14
- package/dist/config/index.js.map +1 -1
- package/dist/db/index.d.ts +73 -13
- package/dist/db/index.js +481 -39
- package/dist/db/index.js.map +1 -1
- package/dist/engine.d.ts +15 -0
- package/dist/engine.js +28 -0
- package/dist/engine.js.map +1 -0
- package/dist/indexer/imports.d.ts +6 -0
- package/dist/indexer/imports.js +254 -0
- package/dist/indexer/imports.js.map +1 -0
- package/dist/indexer/index.d.ts +63 -0
- package/dist/indexer/index.js +360 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/process.d.ts +22 -0
- package/dist/indexer/process.js +42 -0
- package/dist/indexer/process.js.map +1 -0
- package/dist/indexer/scan.d.ts +43 -0
- package/dist/indexer/scan.js +223 -0
- package/dist/indexer/scan.js.map +1 -0
- package/dist/indexer/worker.d.ts +1 -0
- package/dist/indexer/worker.js +23 -0
- package/dist/indexer/worker.js.map +1 -0
- package/dist/llm/comment-generator.d.ts +17 -19
- package/dist/llm/comment-generator.js +46 -80
- package/dist/llm/comment-generator.js.map +1 -1
- package/dist/llm/index.d.ts +0 -1
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/prompts.d.ts +0 -1
- package/dist/llm/prompts.js +16 -16
- package/dist/migrate.d.ts +35 -0
- package/dist/migrate.js +194 -0
- package/dist/migrate.js.map +1 -0
- package/dist/parser/extractor.d.ts +21 -16
- package/dist/parser/extractor.js +700 -228
- package/dist/parser/extractor.js.map +1 -1
- package/dist/parser/index.d.ts +17 -21
- package/dist/parser/index.js +115 -171
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/langs/c.d.ts +3 -0
- package/dist/parser/langs/c.js +879 -0
- package/dist/parser/langs/c.js.map +1 -0
- package/dist/parser/langs/csharp.d.ts +2 -0
- package/dist/parser/langs/csharp.js +548 -0
- package/dist/parser/langs/csharp.js.map +1 -0
- package/dist/parser/langs/dart.d.ts +2 -0
- package/dist/parser/langs/dart.js +893 -0
- package/dist/parser/langs/dart.js.map +1 -0
- package/dist/parser/langs/go.d.ts +2 -0
- package/dist/parser/langs/go.js +455 -0
- package/dist/parser/langs/go.js.map +1 -0
- package/dist/parser/langs/java.d.ts +2 -0
- package/dist/parser/langs/java.js +390 -0
- package/dist/parser/langs/java.js.map +1 -0
- package/dist/parser/langs/php.d.ts +2 -0
- package/dist/parser/langs/php.js +486 -0
- package/dist/parser/langs/php.js.map +1 -0
- package/dist/parser/langs/python.d.ts +2 -0
- package/dist/parser/langs/python.js +216 -0
- package/dist/parser/langs/python.js.map +1 -0
- package/dist/parser/langs/ruby.d.ts +2 -0
- package/dist/parser/langs/ruby.js +602 -0
- package/dist/parser/langs/ruby.js.map +1 -0
- package/dist/parser/langs/rust.d.ts +2 -0
- package/dist/parser/langs/rust.js +931 -0
- package/dist/parser/langs/rust.js.map +1 -0
- package/dist/parser/langs/swift.d.ts +2 -0
- package/dist/parser/langs/swift.js +616 -0
- package/dist/parser/langs/swift.js.map +1 -0
- package/dist/parser/langs/typescript.d.ts +4 -0
- package/dist/parser/langs/typescript.js +535 -0
- package/dist/parser/langs/typescript.js.map +1 -0
- package/dist/parser/registry.d.ts +16 -0
- package/dist/parser/registry.js +87 -0
- package/dist/parser/registry.js.map +1 -0
- package/dist/parser/returns.d.ts +9 -0
- package/dist/parser/returns.js +152 -0
- package/dist/parser/returns.js.map +1 -0
- package/dist/parser/spec.d.ts +165 -0
- package/dist/parser/spec.js +188 -0
- package/dist/parser/spec.js.map +1 -0
- package/dist/query/arity.d.ts +27 -0
- package/dist/query/arity.js +187 -0
- package/dist/query/arity.js.map +1 -0
- package/dist/query/callgraph.d.ts +113 -0
- package/dist/query/callgraph.js +332 -0
- package/dist/query/callgraph.js.map +1 -0
- package/dist/query/format.d.ts +36 -0
- package/dist/query/format.js +136 -0
- package/dist/query/format.js.map +1 -0
- package/dist/query/hierarchy.d.ts +44 -0
- package/dist/query/hierarchy.js +441 -0
- package/dist/query/hierarchy.js.map +1 -0
- package/dist/query/resolve.d.ts +265 -0
- package/dist/query/resolve.js +1957 -0
- package/dist/query/resolve.js.map +1 -0
- package/dist/query/search.d.ts +35 -0
- package/dist/query/search.js +379 -0
- package/dist/query/search.js.map +1 -0
- package/dist/query/words.d.ts +30 -0
- package/dist/query/words.js +201 -0
- package/dist/query/words.js.map +1 -0
- package/dist/server/index.d.ts +1 -3
- package/dist/server/index.js +313 -73
- package/dist/server/index.js.map +1 -1
- package/dist/tools/context.d.ts +35 -0
- package/dist/tools/context.js +79 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/explore.d.ts +51 -0
- package/dist/tools/explore.js +779 -0
- package/dist/tools/explore.js.map +1 -0
- package/dist/tools/impact.d.ts +21 -0
- package/dist/tools/impact.js +743 -0
- package/dist/tools/impact.js.map +1 -0
- package/dist/tools/search.d.ts +59 -0
- package/dist/tools/search.js +843 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/stats.d.ts +26 -0
- package/dist/tools/stats.js +604 -0
- package/dist/tools/stats.js.map +1 -0
- package/dist/types.d.ts +95 -40
- package/dist/types.js +9 -1
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/dist/watcher/index.d.ts +18 -16
- package/dist/watcher/index.js +94 -77
- package/dist/watcher/index.js.map +1 -1
- package/package.json +21 -24
- package/plugin/server.mjs +72 -0
- package/skills/audit/SKILL.md +37 -0
- package/skills/deadcode/SKILL.md +33 -0
- package/skills/explore/SKILL.md +54 -0
- package/skills/impact/SKILL.md +24 -0
- package/skills/status/SKILL.md +11 -0
- package/src/cli.ts +265 -0
- package/src/config/defaults.ts +50 -32
- package/src/config/index.ts +23 -14
- package/src/db/index.ts +540 -43
- package/src/engine.ts +30 -0
- package/src/indexer/imports.ts +241 -0
- package/src/indexer/index.ts +369 -0
- package/src/indexer/process.ts +63 -0
- package/src/indexer/scan.ts +226 -0
- package/src/indexer/worker.ts +24 -0
- package/src/llm/comment-generator.ts +52 -103
- package/src/llm/index.ts +80 -80
- package/src/llm/prompts.ts +95 -95
- package/src/migrate.ts +213 -0
- package/src/parser/extractor.ts +643 -256
- package/src/parser/index.ts +105 -205
- package/src/parser/langs/c.ts +860 -0
- package/src/parser/langs/csharp.ts +519 -0
- package/src/parser/langs/dart.ts +820 -0
- package/src/parser/langs/go.ts +437 -0
- package/src/parser/langs/java.ts +361 -0
- package/src/parser/langs/php.ts +450 -0
- package/src/parser/langs/python.ts +206 -0
- package/src/parser/langs/ruby.ts +554 -0
- package/src/parser/langs/rust.ts +904 -0
- package/src/parser/langs/swift.ts +590 -0
- package/src/parser/langs/typescript.ts +499 -0
- package/src/parser/registry.ts +88 -0
- package/src/parser/returns.ts +137 -0
- package/src/parser/spec.ts +309 -0
- package/src/query/arity.ts +173 -0
- package/src/query/callgraph.ts +473 -0
- package/src/query/format.ts +136 -0
- package/src/query/hierarchy.ts +447 -0
- package/src/query/resolve.ts +1972 -0
- package/src/query/search.ts +430 -0
- package/src/query/words.ts +183 -0
- package/src/server/index.ts +390 -79
- package/src/tools/context.ts +82 -0
- package/src/tools/explore.ts +860 -0
- package/src/tools/impact.ts +748 -0
- package/src/tools/search.ts +886 -0
- package/src/tools/stats.ts +633 -0
- package/src/types.ts +113 -45
- package/src/version.ts +2 -0
- package/src/watcher/index.ts +82 -88
- package/tsconfig.json +6 -3
- package/.mcp.json +0 -8
- package/commands/clast-status.md +0 -9
- package/dist/config/defaults.d.ts.map +0 -1
- package/dist/config/index.d.ts.map +0 -1
- package/dist/db/index.d.ts.map +0 -1
- package/dist/db/queries.d.ts +0 -44
- package/dist/db/queries.d.ts.map +0 -1
- package/dist/db/queries.js +0 -251
- package/dist/db/queries.js.map +0 -1
- package/dist/db/schema.d.ts +0 -3
- package/dist/db/schema.d.ts.map +0 -1
- package/dist/db/schema.js +0 -94
- package/dist/db/schema.js.map +0 -1
- package/dist/db/types.d.ts +0 -44
- package/dist/db/types.d.ts.map +0 -1
- package/dist/db/types.js +0 -2
- package/dist/db/types.js.map +0 -1
- package/dist/llm/comment-generator.d.ts.map +0 -1
- package/dist/llm/index.d.ts.map +0 -1
- package/dist/llm/prompts.d.ts.map +0 -1
- package/dist/parser/call-resolver.d.ts +0 -15
- package/dist/parser/call-resolver.d.ts.map +0 -1
- package/dist/parser/call-resolver.js +0 -16
- package/dist/parser/call-resolver.js.map +0 -1
- package/dist/parser/extractor.d.ts.map +0 -1
- package/dist/parser/index.d.ts.map +0 -1
- package/dist/parser/languages.d.ts +0 -11
- package/dist/parser/languages.d.ts.map +0 -1
- package/dist/parser/languages.js +0 -74
- package/dist/parser/languages.js.map +0 -1
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/register-tools.d.ts +0 -7
- package/dist/server/register-tools.d.ts.map +0 -1
- package/dist/server/register-tools.js +0 -17
- package/dist/server/register-tools.js.map +0 -1
- package/dist/server/tools/clast-call-graph.d.ts +0 -4
- package/dist/server/tools/clast-call-graph.d.ts.map +0 -1
- package/dist/server/tools/clast-call-graph.js +0 -70
- package/dist/server/tools/clast-call-graph.js.map +0 -1
- package/dist/server/tools/clast-comment.d.ts +0 -5
- package/dist/server/tools/clast-comment.d.ts.map +0 -1
- package/dist/server/tools/clast-comment.js +0 -64
- package/dist/server/tools/clast-comment.js.map +0 -1
- package/dist/server/tools/clast-file-summary.d.ts +0 -4
- package/dist/server/tools/clast-file-summary.d.ts.map +0 -1
- package/dist/server/tools/clast-file-summary.js +0 -42
- package/dist/server/tools/clast-file-summary.js.map +0 -1
- package/dist/server/tools/clast-get-context.d.ts +0 -4
- package/dist/server/tools/clast-get-context.d.ts.map +0 -1
- package/dist/server/tools/clast-get-context.js +0 -80
- package/dist/server/tools/clast-get-context.js.map +0 -1
- package/dist/server/tools/clast-reindex.d.ts +0 -5
- package/dist/server/tools/clast-reindex.d.ts.map +0 -1
- package/dist/server/tools/clast-reindex.js +0 -36
- package/dist/server/tools/clast-reindex.js.map +0 -1
- package/dist/server/tools/clast-search.d.ts +0 -4
- package/dist/server/tools/clast-search.d.ts.map +0 -1
- package/dist/server/tools/clast-search.js +0 -29
- package/dist/server/tools/clast-search.js.map +0 -1
- package/dist/server/tools/clast-status.d.ts +0 -5
- package/dist/server/tools/clast-status.d.ts.map +0 -1
- package/dist/server/tools/clast-status.js +0 -23
- package/dist/server/tools/clast-status.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/watcher/gitignore.d.ts +0 -9
- package/dist/watcher/gitignore.d.ts.map +0 -1
- package/dist/watcher/gitignore.js +0 -38
- package/dist/watcher/gitignore.js.map +0 -1
- package/dist/watcher/index.d.ts.map +0 -1
- package/skills/clast-explore/SKILL.md +0 -41
- package/src/db/queries.ts +0 -332
- package/src/db/schema.ts +0 -106
- package/src/db/types.ts +0 -47
- package/src/parser/call-resolver.ts +0 -17
- package/src/parser/languages.ts +0 -86
- package/src/server/register-tools.ts +0 -30
- package/src/server/tools/clast-call-graph.ts +0 -82
- package/src/server/tools/clast-comment.ts +0 -76
- package/src/server/tools/clast-file-summary.ts +0 -59
- package/src/server/tools/clast-get-context.ts +0 -97
- package/src/server/tools/clast-reindex.ts +0 -52
- package/src/server/tools/clast-search.ts +0 -38
- package/src/server/tools/clast-status.ts +0 -37
- package/src/watcher/gitignore.ts +0 -42
|
@@ -0,0 +1,1972 @@
|
|
|
1
|
+
import type { ClastDb } from '../db/index.js';
|
|
2
|
+
import { SYMBOL_COLUMNS, SYMBOL_FROM } from '../db/index.js';
|
|
3
|
+
import { CONTAINER_KINDS, type SymbolKind, type SymbolRow } from '../types.js';
|
|
4
|
+
import { ARITY_LANGS, arityFits, paramRange, type ParamRange } from './arity.js';
|
|
5
|
+
import { isTestPath, isVendoredPath } from './format.js';
|
|
6
|
+
import { splitIdentifier } from './words.js';
|
|
7
|
+
|
|
8
|
+
const LANG_FAMILY: Record<string, string> = {
|
|
9
|
+
typescript: 'js', tsx: 'js', javascript: 'js', vue: 'js',
|
|
10
|
+
c: 'c', cpp: 'c', objc: 'c',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function langFamily(lang: string): string {
|
|
14
|
+
return LANG_FAMILY[lang] ?? lang;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const KIND_WEIGHT: Record<SymbolKind, number> = {
|
|
18
|
+
class: 7, interface: 6, struct: 6, trait: 6, enum: 5,
|
|
19
|
+
function: 6, method: 5, type: 4, namespace: 3, macro: 3,
|
|
20
|
+
constant: 2, variable: 1, property: 0, field: 0, declaration: -3,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export interface SymbolRef {
|
|
24
|
+
name: string;
|
|
25
|
+
/** Container segments before the name (`Foo.Bar.baz` -> [Foo, Bar]). */
|
|
26
|
+
container: string[];
|
|
27
|
+
path?: string;
|
|
28
|
+
line?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse the ways an agent may refer to a symbol:
|
|
33
|
+
* `name`, `Class.method`, `Class::method`, `Class#method`,
|
|
34
|
+
* `path/file.ts:name`, `path/file.ts:123` (symbol at line).
|
|
35
|
+
*/
|
|
36
|
+
export function parseSymbolRef(ref: string): SymbolRef {
|
|
37
|
+
let text = ref.trim().replace(/\(.*\)$/s, '').replace(/!$/, '');
|
|
38
|
+
let path: string | undefined;
|
|
39
|
+
let line: number | undefined;
|
|
40
|
+
const m = /^(.*[\\/.][^:]*):([^:\\/]+)$/.exec(text);
|
|
41
|
+
if (m && !m[1]!.endsWith(':')) {
|
|
42
|
+
path = m[1]!.replace(/\\/g, '/');
|
|
43
|
+
if (/^\d+$/.test(m[2]!)) {
|
|
44
|
+
line = Number(m[2]);
|
|
45
|
+
return { name: '', container: [], path, line };
|
|
46
|
+
}
|
|
47
|
+
text = m[2]!;
|
|
48
|
+
}
|
|
49
|
+
const segs = text.split(/::|#|\.|->|\\/).map(s => s.trim()).filter(Boolean);
|
|
50
|
+
const name = segs.pop() ?? text;
|
|
51
|
+
return { name, container: segs, path, line };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function containerMatches(qname: string, container: string[], exact: boolean): boolean {
|
|
55
|
+
if (container.length === 0) return true;
|
|
56
|
+
const parts = qname.split('.');
|
|
57
|
+
parts.pop();
|
|
58
|
+
if (parts.length < container.length) return false;
|
|
59
|
+
for (let i = 1; i <= container.length; i++) {
|
|
60
|
+
const a = parts[parts.length - i]!;
|
|
61
|
+
const b = container[container.length - i]!;
|
|
62
|
+
if (exact ? a !== b : a.toLowerCase() !== b.toLowerCase()) return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function pathMatches(path: string, filter: string): boolean {
|
|
68
|
+
const f = filter.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
69
|
+
if (path === f) return true;
|
|
70
|
+
if (path.endsWith('/' + f) || path.startsWith(f.endsWith('/') ? f : f + '/')) return true;
|
|
71
|
+
return path.toLowerCase().includes(f.toLowerCase());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Static desirability of a symbol as "the" definition of its name. */
|
|
75
|
+
export function definitionScore(s: SymbolRow): number {
|
|
76
|
+
let score = KIND_WEIGHT[s.kind] ?? 0;
|
|
77
|
+
if (s.exported) score += 2;
|
|
78
|
+
if (isTestPath(s.path)) score -= 6;
|
|
79
|
+
if (isVendoredPath(s.path)) score -= 4;
|
|
80
|
+
// Longer bodies are more likely the real implementation than a stub/overload.
|
|
81
|
+
score += Math.min(3, Math.log2(1 + s.end_line - s.start_line) / 2);
|
|
82
|
+
return score;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolve a symbol reference to definitions, best first. Falls back to
|
|
87
|
+
* case-insensitive matching when nothing matches exactly.
|
|
88
|
+
*/
|
|
89
|
+
export function findSymbols(
|
|
90
|
+
db: ClastDb,
|
|
91
|
+
ref: string | SymbolRef,
|
|
92
|
+
opts: { path?: string; kind?: string; exact?: boolean } = {}
|
|
93
|
+
): SymbolRow[] {
|
|
94
|
+
let r = typeof ref === 'string' ? parseSymbolRef(ref) : ref;
|
|
95
|
+
// Ruby `loaded!`: the parser drops a trailing `!` (Rust macro syntax), keep it when such a name exists.
|
|
96
|
+
if (typeof ref === 'string' && /!\s*$/.test(ref) && r.name && db.symbolsByName(r.name + '!').length) r = { ...r, name: r.name + '!' };
|
|
97
|
+
const pathFilter = opts.path ?? r.path;
|
|
98
|
+
|
|
99
|
+
if (r.line !== undefined && r.path) {
|
|
100
|
+
const file = findFiles(db, r.path)[0];
|
|
101
|
+
if (!file) return [];
|
|
102
|
+
const s = db.enclosingSymbol(file.id, r.line);
|
|
103
|
+
return s ? [s] : [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const filter = (rows: SymbolRow[], exact: boolean): SymbolRow[] =>
|
|
107
|
+
rows.filter(s =>
|
|
108
|
+
containerMatches(s.qname, r.container, exact) &&
|
|
109
|
+
(!pathFilter || pathMatches(s.path, pathFilter)) &&
|
|
110
|
+
(!opts.kind || s.kind === opts.kind)
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const byName = db.symbolsByName(r.name);
|
|
114
|
+
let rows = filter(byName, true);
|
|
115
|
+
// Case-insensitive matching is a fallback for a misspelt name, not for a name that exists
|
|
116
|
+
// but was filtered out by path / kind (that would silently swap in an unrelated symbol).
|
|
117
|
+
if (rows.length === 0 && byName.length === 0 && !opts.exact) {
|
|
118
|
+
rows = filter(
|
|
119
|
+
db.all<SymbolRow>(`SELECT ${SYMBOL_COLUMNS} FROM ${SYMBOL_FROM} WHERE s.name_lc = ?`, r.name.toLowerCase()),
|
|
120
|
+
false
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const accept = (s: SymbolRow): boolean =>
|
|
124
|
+
(!pathFilter || pathMatches(s.path, pathFilter)) && (!opts.kind || s.kind === opts.kind);
|
|
125
|
+
|
|
126
|
+
// Sub.inheritedMember: the member is defined on a supertype of the named container.
|
|
127
|
+
if (rows.length === 0 && r.container.length && byName.length) {
|
|
128
|
+
const ancestors = ancestorNames(db, r.container[r.container.length - 1]!);
|
|
129
|
+
if (ancestors.size) {
|
|
130
|
+
rows = byName.filter(s => {
|
|
131
|
+
const parts = s.qname.split('.');
|
|
132
|
+
return parts.length >= 2 && ancestors.has(parts[parts.length - 2]!) && accept(s);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Leading segments that name packages / modules / namespaces rather than
|
|
138
|
+
// classes: django.shortcuts.render, page.Clear, Illuminate\Pipeline\Pipeline,
|
|
139
|
+
// runtime::Handle::current. Match them against the file path instead.
|
|
140
|
+
if (rows.length === 0 && r.container.length && byName.length) {
|
|
141
|
+
for (let k = r.container.length - 1; k >= 0 && rows.length === 0; k--) {
|
|
142
|
+
const containerPart = r.container.slice(r.container.length - k);
|
|
143
|
+
const pathPart = r.container.slice(0, r.container.length - k).map(p => p.toLowerCase());
|
|
144
|
+
rows = byName.filter(s => {
|
|
145
|
+
if (!containerMatches(s.qname, containerPart, false) || !accept(s)) return false;
|
|
146
|
+
const segs = s.path.toLowerCase().replace(/\.[a-z0-9]+$/, '').split('/');
|
|
147
|
+
let at = 0;
|
|
148
|
+
for (const seg of segs) {
|
|
149
|
+
if (at < pathPart.length && seg === pathPart[at]) at++;
|
|
150
|
+
}
|
|
151
|
+
return at === pathPart.length;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// path:name where the file only re-exports the symbol (__init__.py, index.ts barrels).
|
|
157
|
+
if (rows.length === 0 && pathFilter && byName.length) {
|
|
158
|
+
const reexporters = findFiles(db, pathFilter, 4);
|
|
159
|
+
const targets = new Set<number>();
|
|
160
|
+
for (const f of reexporters) {
|
|
161
|
+
for (const hop1 of db.all<{ resolved_id: number }>(
|
|
162
|
+
`SELECT resolved_id FROM imports WHERE file_id = ? AND resolved_id IS NOT NULL`, f.id
|
|
163
|
+
)) {
|
|
164
|
+
targets.add(hop1.resolved_id);
|
|
165
|
+
for (const hop2 of db.all<{ resolved_id: number }>(
|
|
166
|
+
`SELECT resolved_id FROM imports WHERE file_id = ? AND resolved_id IS NOT NULL`, hop1.resolved_id
|
|
167
|
+
)) targets.add(hop2.resolved_id);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
rows = byName.filter(
|
|
171
|
+
s => targets.has(s.file_id) && containerMatches(s.qname, r.container, false) && (!opts.kind || s.kind === opts.kind)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const importers = new Map<number, number>();
|
|
175
|
+
const importerBoost = (fileId: number): number => {
|
|
176
|
+
let n = importers.get(fileId);
|
|
177
|
+
if (n === undefined) {
|
|
178
|
+
n = db.get<{ n: number }>(`SELECT COUNT(*) AS n FROM imports WHERE resolved_id = ?`, fileId)?.n ?? 0;
|
|
179
|
+
importers.set(fileId, n);
|
|
180
|
+
}
|
|
181
|
+
// Among copies of the same code, the one the rest of the project imports is the real one.
|
|
182
|
+
return Math.min(4, Math.log2(1 + n) / 2);
|
|
183
|
+
};
|
|
184
|
+
const multi = rows.length > 1;
|
|
185
|
+
return rows
|
|
186
|
+
.map(s => ({ s, score: definitionScore(s) + (multi ? importerBoost(s.file_id) : 0) }))
|
|
187
|
+
.sort((a, b) => b.score - a.score || a.s.path.length - b.s.path.length || a.s.path.localeCompare(b.s.path) || a.s.start_line - b.s.start_line)
|
|
188
|
+
.map(x => x.s);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Drop overloads / re-declarations of `chosen` (same qualified name in the same file). */
|
|
192
|
+
export function otherDefinitions(
|
|
193
|
+
chosen: SymbolRow,
|
|
194
|
+
all: SymbolRow[],
|
|
195
|
+
family?: Set<number>
|
|
196
|
+
): { others: SymbolRow[]; overloads: number } {
|
|
197
|
+
const others: SymbolRow[] = [];
|
|
198
|
+
let overloads = 0;
|
|
199
|
+
for (const s of all) {
|
|
200
|
+
if (s.id === chosen.id) continue;
|
|
201
|
+
// Interface declarations, overrides and implementations of the chosen member are not rivals.
|
|
202
|
+
if (family && family.has(s.id)) continue;
|
|
203
|
+
if (s.file_id === chosen.file_id && s.qname === chosen.qname) overloads++;
|
|
204
|
+
// The parts of a C# partial class are one class.
|
|
205
|
+
else if (
|
|
206
|
+
s.qname === chosen.qname && s.kind === chosen.kind && s.lang === 'csharp' &&
|
|
207
|
+
/\bpartial\b/.test(s.signature) && /\bpartial\b/.test(chosen.signature)
|
|
208
|
+
) continue;
|
|
209
|
+
// A C / C++ prototype and its definition are one function, not two candidates.
|
|
210
|
+
else if (cTwins(s, chosen)) continue;
|
|
211
|
+
else others.push(s);
|
|
212
|
+
}
|
|
213
|
+
return { others, overloads };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Names of all supertypes (transitive, bounded) of the containers called `name`. */
|
|
217
|
+
export function ancestorNames(db: ClastDb, name: string): Set<string> {
|
|
218
|
+
const out = new Set<string>();
|
|
219
|
+
let frontier = [name];
|
|
220
|
+
for (let depth = 0; depth < 5 && frontier.length; depth++) {
|
|
221
|
+
const next: string[] = [];
|
|
222
|
+
for (const n of frontier) {
|
|
223
|
+
for (const row of db.all<{ name: string }>(
|
|
224
|
+
`SELECT h.name FROM heritage h JOIN symbols s ON s.id = h.symbol_id WHERE s.name = ?`, n
|
|
225
|
+
)) {
|
|
226
|
+
if (row.name !== name && !out.has(row.name)) {
|
|
227
|
+
out.add(row.name);
|
|
228
|
+
next.push(row.name);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
frontier = next.slice(0, 40);
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface FileHit {
|
|
238
|
+
id: number;
|
|
239
|
+
path: string;
|
|
240
|
+
lang: string;
|
|
241
|
+
line_count: number;
|
|
242
|
+
symbol_count: number;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Resolve a (possibly partial) file path: exact, suffix, then substring. */
|
|
246
|
+
export function findFiles(db: ClastDb, query: string, limit = 20): FileHit[] {
|
|
247
|
+
const q = query.replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+/, '');
|
|
248
|
+
const cols = 'id, path, lang, line_count, symbol_count';
|
|
249
|
+
const exact = db.get<FileHit>(`SELECT ${cols} FROM files WHERE path = ?`, q);
|
|
250
|
+
if (exact) return [exact];
|
|
251
|
+
const like = (s: string): string => s.replace(/[\\%_]/g, c => '\\' + c);
|
|
252
|
+
const suffix = db.all<FileHit>(
|
|
253
|
+
`SELECT ${cols} FROM files WHERE path LIKE ? ESCAPE '\\' ORDER BY length(path) LIMIT ?`,
|
|
254
|
+
'%/' + like(q), limit
|
|
255
|
+
);
|
|
256
|
+
if (suffix.length) return suffix;
|
|
257
|
+
return db.all<FileHit>(
|
|
258
|
+
`SELECT ${cols} FROM files WHERE path LIKE ? ESCAPE '\\' ORDER BY length(path) LIMIT ?`,
|
|
259
|
+
'%' + like(q) + '%', limit
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// Call resolution
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Everything that can be the target of a call expression. Fields, properties
|
|
269
|
+
* and variables are included because they often hold callables
|
|
270
|
+
* (`register_lookup = class_or_instance_method(...)`, `onClick = () => {}`).
|
|
271
|
+
*/
|
|
272
|
+
const CALL_TARGET_KINDS = new Set<SymbolKind>([
|
|
273
|
+
'function', 'method', 'class', 'struct', 'macro', 'declaration', 'type', 'interface', 'trait', 'enum',
|
|
274
|
+
'field', 'property', 'variable', 'constant',
|
|
275
|
+
]);
|
|
276
|
+
const VALUE_KINDS = new Set<SymbolKind>(['field', 'property', 'variable', 'constant']);
|
|
277
|
+
const TYPE_KINDS = new Set<SymbolKind>(['class', 'struct', 'interface', 'trait', 'enum', 'type']);
|
|
278
|
+
|
|
279
|
+
/** In C nothing but functions and macros can be called. */
|
|
280
|
+
const C_CALLABLE = new Set<SymbolKind>(['function', 'declaration', 'macro']);
|
|
281
|
+
|
|
282
|
+
/** Families where a bare name must be defined in the file or imported into it. */
|
|
283
|
+
const STRICT_IMPORTS = new Set(['js', 'python']);
|
|
284
|
+
/** Languages where every file in a directory shares one namespace (no import needed). */
|
|
285
|
+
const SAME_DIR_PACKAGE = new Set(['java', 'go', 'kotlin', 'scala', 'csharp', 'php']);
|
|
286
|
+
const CALLABLE = new Set<SymbolKind>(['function', 'method', 'declaration', 'macro']);
|
|
287
|
+
/** Families where an unresolved import of a name proves the name is external. */
|
|
288
|
+
const EXTERNAL_IF_UNRESOLVED = new Set(['js', 'python', 'java']);
|
|
289
|
+
/** Languages where extension methods / open classes let unrelated code add methods to a type. */
|
|
290
|
+
const OPEN_TYPES = new Set(['csharp', 'rust', 'ruby', 'swift', 'kotlin']);
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Method names that exist on built-in collections, strings, promises, streams
|
|
294
|
+
* and loggers in most languages. A call to one of these on a receiver of
|
|
295
|
+
* unknown type is far more likely to be the builtin than a project method
|
|
296
|
+
* with the same name, so it needs real evidence to be linked.
|
|
297
|
+
*/
|
|
298
|
+
const COMMON_MEMBER_NAMES = new Set([
|
|
299
|
+
'get', 'set', 'add', 'put', 'push', 'pop', 'shift', 'unshift', 'append', 'prepend', 'extend', 'insert', 'remove',
|
|
300
|
+
'delete', 'clear', 'update', 'join', 'split', 'count', 'len', 'length', 'size', 'keys', 'values', 'items',
|
|
301
|
+
'entries', 'index', 'find', 'filter', 'map', 'reduce', 'each', 'foreach', 'sort', 'reverse', 'slice', 'splice',
|
|
302
|
+
'concat', 'includes', 'contains', 'has', 'indexof', 'strip', 'trim', 'replace', 'format', 'encode', 'decode',
|
|
303
|
+
'read', 'write', 'close', 'open', 'flush', 'send', 'receive', 'recv', 'next', 'then', 'catch', 'finally',
|
|
304
|
+
'tostring', 'string', 'str', 'equals', 'hashcode', 'compare', 'compareto', 'clone', 'copy', 'isempty', 'empty',
|
|
305
|
+
'first', 'last', 'any', 'all', 'sum', 'min', 'max', 'call', 'apply', 'bind', 'exec', 'test', 'match', 'search',
|
|
306
|
+
'emit', 'on', 'off', 'once', 'log', 'error', 'warn', 'info', 'debug', 'trace', 'print', 'println', 'parse',
|
|
307
|
+
'stringify', 'start', 'stop', 'run', 'execute', 'init', 'reset', 'dispose', 'destroy', 'name', 'value', 'type',
|
|
308
|
+
'id', 'data', 'key', 'lock', 'unlock', 'wait', 'notify', 'await', 'unwrap', 'expect', 'ok', 'err', 'is_some',
|
|
309
|
+
'is_none', 'iter', 'into_iter', 'collect', 'into', 'from', 'as_ref', 'as_str', 'to_string', 'to_owned', 'new',
|
|
310
|
+
'default', 'build', 'create', 'load', 'save', 'validate', 'render', 'handle', 'process', 'intersection',
|
|
311
|
+
'difference', 'union', 'isarray', 'callable', 'tolowercase', 'touppercase', 'lower', 'upper', 'startswith',
|
|
312
|
+
'endswith', 'substring', 'substr', 'charat', 'tofixed', 'getattr', 'setattr', 'super', 'end', 'begin', 'at',
|
|
313
|
+
]);
|
|
314
|
+
|
|
315
|
+
export interface CallSite {
|
|
316
|
+
fileId: number;
|
|
317
|
+
path: string;
|
|
318
|
+
lang: string;
|
|
319
|
+
/** Receiver text, COMPLEX ('?') for expression receivers, null for bare calls. */
|
|
320
|
+
receiver: string | null;
|
|
321
|
+
/** Declared type of the receiver, when the extractor could determine it. */
|
|
322
|
+
rtype?: string | null;
|
|
323
|
+
/**
|
|
324
|
+
* Row id of the call that produces this call's receiver (`a.b().c()`:
|
|
325
|
+
* for c, the call to b). The receiver type is then b's return type.
|
|
326
|
+
*/
|
|
327
|
+
via?: number | null;
|
|
328
|
+
/** Symbol the call is made from, if known. */
|
|
329
|
+
caller?: SymbolRow | null;
|
|
330
|
+
/** Go: directory of the package the receiver type is qualified with (set by the resolver). */
|
|
331
|
+
rtypeDir?: string;
|
|
332
|
+
/** Number of arguments at the call site (-1 / undefined = unknown). */
|
|
333
|
+
argc?: number | null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export interface Resolution {
|
|
337
|
+
/** Candidates, best first, with scores. */
|
|
338
|
+
candidates: Array<{ sym: SymbolRow; score: number }>;
|
|
339
|
+
/** True when the best candidate is clearly better than the rest. */
|
|
340
|
+
confident: boolean;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Languages where a bare field name inside a method means this.field. */
|
|
344
|
+
const IMPLICIT_THIS = new Set(['java', 'csharp', 'cpp', 'kotlin', 'swift']);
|
|
345
|
+
/** Language families where a class is written in one place: a same-named class in another file is another class. */
|
|
346
|
+
const SINGLE_FILE_CLASS = new Set(['js', 'python', 'java', 'php', 'dart']);
|
|
347
|
+
|
|
348
|
+
/** `super.m()`, `super().m()`, C# `base.M()`, PHP `parent::m()`: the inherited implementation, never the caller's own. */
|
|
349
|
+
function isSuperReceiver(recv: string | null | undefined, lang: string): boolean {
|
|
350
|
+
if (!recv) return false;
|
|
351
|
+
return recv === 'super' || (recv === 'base' && lang === 'csharp') || (recv === 'parent' && lang === 'php');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const HEADER_RE = /\.(h|hh|hpp|hxx|inl|ipp)$/i;
|
|
355
|
+
|
|
356
|
+
/** A C / C++ function with internal linkage: visible only in its own translation unit. */
|
|
357
|
+
export function isFileLocal(s: SymbolRow): boolean {
|
|
358
|
+
return /^(?:\w+\s+)*static\b/.test(s.signature) && !HEADER_RE.test(s.path) && s.qname === s.name;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* C / C++: a prototype and its definition (or two declarations of one function) are one target.
|
|
363
|
+
* Two full definitions in different source files are different functions, as is anything `static`.
|
|
364
|
+
*/
|
|
365
|
+
export function cTwins(a: SymbolRow, b: SymbolRow): boolean {
|
|
366
|
+
if (a.qname !== b.qname || langFamily(a.lang) !== 'c' || langFamily(b.lang) !== 'c') return false;
|
|
367
|
+
if (a.file_id === b.file_id) return true;
|
|
368
|
+
if (isFileLocal(a) || isFileLocal(b)) return false;
|
|
369
|
+
// Class members (Foo::bar in a header and in a .cc) pair up by qualified name.
|
|
370
|
+
if (a.qname !== a.name) return true;
|
|
371
|
+
return a.kind === 'declaration' || b.kind === 'declaration' || HEADER_RE.test(a.path) || HEADER_RE.test(b.path);
|
|
372
|
+
}
|
|
373
|
+
const SELF_RECEIVERS = new Set(['this', 'self', 'super', '$this', 'cls', 'base', 'Self', 'static', 'parent', 'me']);
|
|
374
|
+
const CONFIDENT_SCORE = 55;
|
|
375
|
+
const CONFIDENT_GAP = 12;
|
|
376
|
+
|
|
377
|
+
/** Above this many same-named definitions, candidates are pre-selected before scoring. */
|
|
378
|
+
const PRESELECT_THRESHOLD = 40;
|
|
379
|
+
|
|
380
|
+
interface NameIndex {
|
|
381
|
+
all: SymbolRow[];
|
|
382
|
+
/** Members by the name of their class / struct / trait. */
|
|
383
|
+
byContainer: Map<string, SymbolRow[]>;
|
|
384
|
+
byFile: Map<number, SymbolRow[]>;
|
|
385
|
+
/** Free functions, classes, variables: everything that is not a member. */
|
|
386
|
+
free: SymbolRow[];
|
|
387
|
+
containers: string[];
|
|
388
|
+
/** Pre-selected candidates per receiver spelling (for common method names). */
|
|
389
|
+
byReceiver: Map<string, SymbolRow[]>;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
interface ImportedNames {
|
|
393
|
+
/** Bound name -> file it is imported from (null = outside the project). */
|
|
394
|
+
names: Map<string, number | null>;
|
|
395
|
+
/** Files imported with `*`. */
|
|
396
|
+
wildcards: Set<number>;
|
|
397
|
+
/** Bound alias -> original name in the source file. */
|
|
398
|
+
aliases: Map<string, string>;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Receivers that are language / runtime globals: nothing reached through them is project code. */
|
|
402
|
+
const BUILTIN_RECEIVERS: Record<string, Set<string>> = {
|
|
403
|
+
js: new Set([
|
|
404
|
+
'console', 'Math', 'JSON', 'Object', 'Array', 'Number', 'String', 'Boolean', 'Date', 'Promise', 'Reflect',
|
|
405
|
+
'Symbol', 'process', 'window', 'document', 'navigator', 'globalThis', 'Buffer', 'Intl', 'Atomics', 'RegExp',
|
|
406
|
+
'WebAssembly', 'localStorage', 'sessionStorage', 'performance', 'crypto', 'URL', 'Error', 'Map', 'Set',
|
|
407
|
+
'BigInt', 'Proxy', 'global', 'self', 'location', 'history', 'fs', 'path', 'os', 'util',
|
|
408
|
+
]),
|
|
409
|
+
python: new Set(['os', 'sys', 're', 'json', 'math', 'logging', 'time', 'datetime', 'itertools', 'functools', 'str', 'dict', 'list']),
|
|
410
|
+
java: new Set(['System', 'Math', 'String', 'Objects', 'Arrays', 'Collections', 'Integer', 'Long', 'Thread', 'Optional']),
|
|
411
|
+
csharp: new Set([
|
|
412
|
+
'Console', 'Math', 'String', 'Convert', 'Task', 'Enumerable', 'Path', 'File', 'Guid', 'Environment',
|
|
413
|
+
// keyword types: string.IsNullOrEmpty(), int.Parse(), object.ReferenceEquals()
|
|
414
|
+
'string', 'object', 'int', 'long', 'short', 'byte', 'uint', 'ulong', 'double', 'float', 'decimal', 'bool', 'char',
|
|
415
|
+
]),
|
|
416
|
+
go: new Set(['fmt', 'os', 'strings', 'strconv', 'bytes', 'errors', 'time', 'sync', 'math', 'sort', 'io', 'context']),
|
|
417
|
+
rust: new Set(['std', 'core', 'alloc', 'Vec', 'String', 'Box', 'Arc', 'Rc', 'Option', 'Result', 'HashMap', 'mem', 'ptr', 'fmt']),
|
|
418
|
+
ruby: new Set([
|
|
419
|
+
'Kernel', 'File', 'Dir', 'Time', 'Math', 'JSON', 'String', 'Array', 'Hash', 'Integer', 'Class', 'Module', 'Struct',
|
|
420
|
+
'Object', 'Proc', 'Set', 'Regexp', 'Thread', 'Mutex', 'Process', 'IO', 'ENV', 'Float', 'Symbol', 'Comparable',
|
|
421
|
+
]),
|
|
422
|
+
php: new Set(['parent']),
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
/** Variable names that say nothing about the type of the object they hold. */
|
|
426
|
+
const GENERIC_RECEIVERS = new Set([
|
|
427
|
+
'obj', 'object', 'item', 'instance', 'inst', 'value', 'val', 'data', 'result', 'res', 'ret', 'it', 'el', 'elem',
|
|
428
|
+
'element', 'entry', 'node', 'target', 'source', 'src', 'dst', 'dest', 'other', 'that', 'tmp', 'temp', 'ref',
|
|
429
|
+
'ptr', 'arg', 'args', 'param', 'params', 'input', 'output', 'out', 'key', 'thing', 'current', 'cur', 'next',
|
|
430
|
+
'prev', 'first', 'last', 'left', 'right', 'child', 'parent', 'root', 'base', 'impl', 'delegate', 'inner',
|
|
431
|
+
'outer', 'wrapped',
|
|
432
|
+
]);
|
|
433
|
+
|
|
434
|
+
function dirOf(path: string): string {
|
|
435
|
+
const i = path.lastIndexOf('/');
|
|
436
|
+
return i === -1 ? '' : path.slice(0, i);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Resolves call sites to definitions. Holds per-request caches (imports of a
|
|
441
|
+
* file, candidate definitions of a name, class ancestry); create a new
|
|
442
|
+
* instance whenever the index changes.
|
|
443
|
+
*/
|
|
444
|
+
export class Resolver {
|
|
445
|
+
private goPackages: { dirs: Set<string>; prefixes: Set<string>; bySource: Map<string, string | null> } | null = null;
|
|
446
|
+
private goImportCache = new Map<number, Map<string, string | null>>();
|
|
447
|
+
|
|
448
|
+
/** Go: project directory an import path names, or null when it is outside the project. */
|
|
449
|
+
private goPackageDir(source: string): string | null {
|
|
450
|
+
let g = this.goPackages;
|
|
451
|
+
if (!g) {
|
|
452
|
+
g = this.goPackages = { dirs: new Set(), prefixes: new Set(), bySource: new Map() };
|
|
453
|
+
for (const f of this.db.all<{ path: string }>(`SELECT path FROM files WHERE lang = 'go'`)) g.dirs.add(dirOf(f.path));
|
|
454
|
+
// The module path is not stored: learn it as the prefix that most imports of project
|
|
455
|
+
// directories share (`github.com/gohugoio/hugo` + `/deps`).
|
|
456
|
+
const counts = new Map<string, number>();
|
|
457
|
+
const sources = this.db.all<{ source: string; n: number }>(
|
|
458
|
+
`SELECT i.source, COUNT(*) AS n FROM imports i JOIN files f ON f.id = i.file_id WHERE f.lang = 'go' GROUP BY i.source`
|
|
459
|
+
);
|
|
460
|
+
for (const s of sources) {
|
|
461
|
+
const parts = s.source.split('/');
|
|
462
|
+
for (let k = 1; k < parts.length; k++) {
|
|
463
|
+
if (g.dirs.has(parts.slice(k).join('/'))) {
|
|
464
|
+
const prefix = parts.slice(0, k).join('/');
|
|
465
|
+
counts.set(prefix, (counts.get(prefix) ?? 0) + s.n);
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
let top = 0;
|
|
471
|
+
for (const n of counts.values()) top = Math.max(top, n);
|
|
472
|
+
// (In a project with only a handful of internal imports a single one has to be enough.)
|
|
473
|
+
const floor = top >= 4 ? Math.max(2, top * 0.05) : 1;
|
|
474
|
+
for (const [prefix, n] of counts) if (n >= floor) g.prefixes.add(prefix);
|
|
475
|
+
}
|
|
476
|
+
let hit = g.bySource.get(source);
|
|
477
|
+
if (hit !== undefined) return hit;
|
|
478
|
+
hit = null;
|
|
479
|
+
if (g.prefixes.has(source) && g.dirs.has('')) hit = '';
|
|
480
|
+
else {
|
|
481
|
+
const parts = source.split('/');
|
|
482
|
+
for (let k = 1; k < parts.length; k++) {
|
|
483
|
+
if (g.prefixes.has(parts.slice(0, k).join('/')) && g.dirs.has(parts.slice(k).join('/'))) {
|
|
484
|
+
hit = parts.slice(k).join('/');
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
g.bySource.set(source, hit);
|
|
490
|
+
return hit;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Go: package names a file imports -> project directory (null = external package). */
|
|
494
|
+
private goImports(fileId: number): Map<string, string | null> {
|
|
495
|
+
let map = this.goImportCache.get(fileId);
|
|
496
|
+
if (!map) {
|
|
497
|
+
map = new Map();
|
|
498
|
+
for (const r of this.db.all<{ source: string; names: string }>(
|
|
499
|
+
`SELECT source, names FROM imports INDEXED BY idx_imports_file WHERE file_id = ?`, fileId
|
|
500
|
+
)) {
|
|
501
|
+
const name = r.names.split(',')[0]?.trim();
|
|
502
|
+
if (name && name !== '_' && name !== '.') map.set(name, this.goPackageDir(r.source));
|
|
503
|
+
}
|
|
504
|
+
this.goImportCache.set(fileId, map);
|
|
505
|
+
}
|
|
506
|
+
return map;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Go: a receiver type written `pkg.Type`. Returns the site with the bare type name and
|
|
511
|
+
* the package directory, or null when the package is not part of this project.
|
|
512
|
+
*/
|
|
513
|
+
private goQualified(site: CallSite): CallSite | null {
|
|
514
|
+
const t = site.rtype;
|
|
515
|
+
if (!t || (site.lang !== 'go' && site.lang !== 'rust')) return site;
|
|
516
|
+
const dot = t.indexOf('.');
|
|
517
|
+
if (dot === -1) return site;
|
|
518
|
+
const pkg = t.slice(0, dot);
|
|
519
|
+
const type = t.slice(dot + 1);
|
|
520
|
+
if (site.lang === 'rust') {
|
|
521
|
+
// `std::io::Error`, or `io::Error` after `use std::io;`: a standard-library type, whatever the
|
|
522
|
+
// project calls its own types. Any other module path keeps the old by-name behaviour.
|
|
523
|
+
if (pkg === 'std' || this.stdNames(site.fileId).has(pkg)) return null;
|
|
524
|
+
return { ...site, rtype: type };
|
|
525
|
+
}
|
|
526
|
+
const dir = this.goImports(site.fileId).get(pkg);
|
|
527
|
+
if (dir === null) return null;
|
|
528
|
+
if (dir !== undefined) return { ...site, rtype: type, rtypeDir: dir };
|
|
529
|
+
// Declared in a file with other imports (a field of a struct defined elsewhere): go by the directory name.
|
|
530
|
+
const named = this.db.symbolsByName(type).find(
|
|
531
|
+
s => (CONTAINER_KINDS.has(s.kind) || s.kind === 'type') && s.lang === 'go' && dirOf(s.path).split('/').pop() === pkg
|
|
532
|
+
);
|
|
533
|
+
return named ? { ...site, rtype: type, rtypeDir: dirOf(named.path) } : { ...site, rtype: type };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private importCache = new Map<number, Set<number>>();
|
|
537
|
+
private importNameCache = new Map<number, ImportedNames>();
|
|
538
|
+
private candidateCache = new Map<string, SymbolRow[]>();
|
|
539
|
+
private ancestorCache = new Map<number, Set<string>>();
|
|
540
|
+
private rubyAncestorCache = new Map<number, Set<string>>();
|
|
541
|
+
private relatedCache = new Map<string, { anc: Set<string>; desc: Set<string> }>();
|
|
542
|
+
private familyCache = new Map<number, Set<number>>();
|
|
543
|
+
private familyCountCache = new Map<string, number>();
|
|
544
|
+
private parentCache = new Map<number, SymbolRow | null>();
|
|
545
|
+
private mentionCache = new Map<string, boolean>();
|
|
546
|
+
private projectTypeCache = new Map<string, boolean>();
|
|
547
|
+
|
|
548
|
+
constructor(private readonly db: ClastDb) {}
|
|
549
|
+
|
|
550
|
+
/** File ids imported (directly) by a file. */
|
|
551
|
+
importsOf(fileId: number): Set<number> {
|
|
552
|
+
let set = this.importCache.get(fileId);
|
|
553
|
+
if (!set) {
|
|
554
|
+
set = new Set<number>();
|
|
555
|
+
for (const r of this.db.all<{ resolved_id: number | null }>(
|
|
556
|
+
`SELECT resolved_id FROM imports INDEXED BY idx_imports_file WHERE file_id = ?`, fileId
|
|
557
|
+
)) {
|
|
558
|
+
if (r.resolved_id !== null) set.add(r.resolved_id);
|
|
559
|
+
}
|
|
560
|
+
this.importCache.set(fileId, set);
|
|
561
|
+
}
|
|
562
|
+
return set;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* True when the name `type`, as seen from the calling file, is a class defined in that file or in
|
|
567
|
+
* the file it is imported from, and `cand` is a member of a same-named class somewhere else.
|
|
568
|
+
*/
|
|
569
|
+
private otherTypeInScope(type: string, cand: SymbolRow, site: CallSite): boolean {
|
|
570
|
+
const from = this.importedNames(site.fileId).names.get(type);
|
|
571
|
+
const home = from === undefined ? site.fileId : from;
|
|
572
|
+
if (home === null || home === cand.file_id) return false;
|
|
573
|
+
const types = this.db.symbolsByName(type).filter(s => CONTAINER_KINDS.has(s.kind) || s.kind === 'type');
|
|
574
|
+
if (types.some(s => s.file_id === home)) return true;
|
|
575
|
+
// Java: a type of the same package needs no import and wins over anything else of that name.
|
|
576
|
+
if (from === undefined && (site.lang === 'java' || site.lang === 'php')) {
|
|
577
|
+
const dir = dirOf(site.path);
|
|
578
|
+
return dirOf(cand.path) !== dir && types.some(s => s.lang === site.lang && dirOf(s.path) === dir);
|
|
579
|
+
}
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
private arityCache = new Map<number, ParamRange | null>();
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Can a call with the site's argument count be a call of `sym`? True when either side is
|
|
587
|
+
* unknown. Tells overloads apart and rejects same-named methods with another shape.
|
|
588
|
+
*/
|
|
589
|
+
arityFits(sym: SymbolRow, site: CallSite): boolean {
|
|
590
|
+
const argc = site.argc;
|
|
591
|
+
if (argc === undefined || argc === null || argc < 0) return true;
|
|
592
|
+
if (!CALLABLE.has(sym.kind) || sym.kind === 'macro' || !ARITY_LANGS.has(sym.lang)) return true;
|
|
593
|
+
let range = this.arityCache.get(sym.id);
|
|
594
|
+
if (range === undefined) {
|
|
595
|
+
range = paramRange(sym.signature, sym.name, sym.lang);
|
|
596
|
+
if (range && langFamily(sym.lang) === 'c') {
|
|
597
|
+
// C++ default arguments are written on the declaration in the header, not on the definition:
|
|
598
|
+
// `void f(int a, int b = 0);` + `void f(int a, int b) { ... }` accept the same calls.
|
|
599
|
+
for (const id of this.familyOf(sym)) {
|
|
600
|
+
const twin = id === sym.id ? undefined : this.db.symbolById(id);
|
|
601
|
+
if (!twin || twin.qname !== sym.qname || twin.file_id === sym.file_id) continue;
|
|
602
|
+
const other = paramRange(twin.signature, twin.name, twin.lang);
|
|
603
|
+
if (!other) { range = null; break; }
|
|
604
|
+
range = { min: Math.min(range.min, other.min), max: Math.max(range.max, other.max), ext: false };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
this.arityCache.set(sym.id, range);
|
|
608
|
+
}
|
|
609
|
+
if (!range) return true;
|
|
610
|
+
const recv = site.receiver;
|
|
611
|
+
const unbound = !!recv && (/^[A-Z]/.test(recv) || recv.includes('::'));
|
|
612
|
+
return arityFits(range, argc, sym.lang, unbound);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
private filePathCache = new Map<number, string | null>();
|
|
616
|
+
|
|
617
|
+
private filePath(fileId: number): string | null {
|
|
618
|
+
let p = this.filePathCache.get(fileId);
|
|
619
|
+
if (p === undefined) {
|
|
620
|
+
p = this.db.get<{ path: string }>(`SELECT path FROM files WHERE id = ?`, fileId)?.path ?? null;
|
|
621
|
+
this.filePathCache.set(fileId, p);
|
|
622
|
+
}
|
|
623
|
+
return p;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private instanceTypeCache = new Map<string, string | null>();
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Type of a module-level variable `name` defined in file `fileId`, read from its declaration:
|
|
630
|
+
* `pre_save = ModelSignal(...)`, `export const logger = new Logger()`, `export const x: Foo = ...`.
|
|
631
|
+
*/
|
|
632
|
+
private instanceType(fileId: number, name: string): string | null {
|
|
633
|
+
const key = fileId + ':' + name;
|
|
634
|
+
let t = this.instanceTypeCache.get(key);
|
|
635
|
+
if (t !== undefined) return t;
|
|
636
|
+
t = null;
|
|
637
|
+
const decl = this.db.symbolsByName(name).find(s => s.file_id === fileId && VALUE_KINDS.has(s.kind) && s.qname === s.name);
|
|
638
|
+
if (decl) {
|
|
639
|
+
const sig = decl.signature;
|
|
640
|
+
const m =
|
|
641
|
+
/:\s*([A-Z][\w$]*)\s*(?:<[^=]*>)?\s*=/.exec(sig) ??
|
|
642
|
+
/=\s*(?:new\s+)?(?:[\w$]+\.)*([A-Z][\w$]*)\s*[(<]/.exec(sig);
|
|
643
|
+
if (m && this.isProjectType(m[1]!)) t = m[1]!;
|
|
644
|
+
}
|
|
645
|
+
this.instanceTypeCache.set(key, t);
|
|
646
|
+
return t;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** Go: does the file import the package in directory `dir`? */
|
|
650
|
+
private importsGoDir(fileId: number, dir: string): boolean {
|
|
651
|
+
for (const d of this.goImports(fileId).values()) if (d === dir) return true;
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** Names a file imports, mapped to the file they come from (null = not in this project). */
|
|
656
|
+
private importedNames(fileId: number): ImportedNames {
|
|
657
|
+
let entry = this.importNameCache.get(fileId);
|
|
658
|
+
if (!entry) {
|
|
659
|
+
entry = { names: new Map(), wildcards: new Set(), aliases: new Map() };
|
|
660
|
+
for (const r of this.db.all<{ names: string; resolved_id: number | null }>(
|
|
661
|
+
`SELECT names, resolved_id FROM imports INDEXED BY idx_imports_file WHERE file_id = ?`, fileId
|
|
662
|
+
)) {
|
|
663
|
+
if (!r.names) continue;
|
|
664
|
+
for (const raw of r.names.split(',')) {
|
|
665
|
+
if (raw === '*') {
|
|
666
|
+
if (r.resolved_id !== null) entry.wildcards.add(r.resolved_id);
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
// "alias=original": the file binds `alias`, the definition is called `original`.
|
|
670
|
+
const eq = raw.indexOf('=');
|
|
671
|
+
const n = eq === -1 ? raw : raw.slice(0, eq);
|
|
672
|
+
if (eq !== -1) entry.aliases.set(n, raw.slice(eq + 1));
|
|
673
|
+
if (!entry.names.has(n) || entry.names.get(n) === null) entry.names.set(n, r.resolved_id);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
this.importNameCache.set(fileId, entry);
|
|
677
|
+
}
|
|
678
|
+
return entry;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
private defaultExportCache = new Map<number, string | null>();
|
|
682
|
+
|
|
683
|
+
/** Name of the symbol a file exports as its default / `module.exports`, if any. */
|
|
684
|
+
private defaultExportOf(fileId: number): string | null {
|
|
685
|
+
let name = this.defaultExportCache.get(fileId);
|
|
686
|
+
if (name === undefined) {
|
|
687
|
+
name = this.db.get<{ default_export: string | null }>(
|
|
688
|
+
`SELECT default_export FROM files WHERE id = ?`, fileId
|
|
689
|
+
)?.default_export ?? null;
|
|
690
|
+
this.defaultExportCache.set(fileId, name);
|
|
691
|
+
}
|
|
692
|
+
return name;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** Can code in `fileId` plausibly refer to `target`: same file, imports its file, or names its class? */
|
|
696
|
+
canSee(fileId: number, target: SymbolRow): boolean {
|
|
697
|
+
const imports = this.importsOf(fileId);
|
|
698
|
+
if (fileId === target.file_id || imports.has(target.file_id)) return true;
|
|
699
|
+
const container = this.containerName(target);
|
|
700
|
+
if (container !== null && !container.startsWith('(') && this.fileMentions(fileId, container)) return true;
|
|
701
|
+
// Through a package that re-exports it: `from django.db import models` -> models.Model in base.py.
|
|
702
|
+
if (imports.size <= 60) {
|
|
703
|
+
for (const f of imports) if (this.importsOf(f).has(target.file_id)) return true;
|
|
704
|
+
}
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* A call at `rawSite` was bound to `bound`, a declaration of the same member on a supertype of
|
|
710
|
+
* `target`'s class. Can it run `target`? Not when the receiver is statically a class unrelated to
|
|
711
|
+
* the target's (a sibling subclass), and not for `super.m()`, which names one implementation.
|
|
712
|
+
*/
|
|
713
|
+
reaches(target: SymbolRow, bound: SymbolRow, rawSite: CallSite): boolean {
|
|
714
|
+
const tc = this.containerName(target);
|
|
715
|
+
const bc = this.containerName(bound);
|
|
716
|
+
if (!tc || !bc || tc === bc) return true;
|
|
717
|
+
// Go interfaces are satisfied structurally: there is no declared relation to check against.
|
|
718
|
+
if (target.lang === 'go') return true;
|
|
719
|
+
if (isSuperReceiver(rawSite.receiver, rawSite.lang)) return false;
|
|
720
|
+
const site = this.goQualified(this.withFieldType(rawSite)) ?? rawSite;
|
|
721
|
+
const recv = site.receiver;
|
|
722
|
+
let type: string | null = null;
|
|
723
|
+
if (site.rtype && recv && !SELF_RECEIVERS.has(recv)) type = site.rtype;
|
|
724
|
+
else if (site.caller && ((recv && SELF_RECEIVERS.has(recv)) || (!recv && IMPLICIT_THIS.has(site.lang)))) {
|
|
725
|
+
type = this.enclosingContainer(site.caller)?.name ?? null;
|
|
726
|
+
}
|
|
727
|
+
if (!type || type === tc) return true;
|
|
728
|
+
const rel = this.relatedTo(tc);
|
|
729
|
+
return rel.anc.has(type) || rel.desc.has(type);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
private aliasUseCache = new Map<string, Array<{ fileId: number; alias: string }>>();
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Local names under which other files refer to `sym`: import aliases
|
|
736
|
+
* (`import { X as Y }`) and default-import / require bindings of a file
|
|
737
|
+
* whose default export is `sym`.
|
|
738
|
+
*/
|
|
739
|
+
aliasesOf(sym: SymbolRow): Array<{ fileId: number; alias: string }> {
|
|
740
|
+
const key = sym.file_id + ':' + sym.name;
|
|
741
|
+
let out = this.aliasUseCache.get(key);
|
|
742
|
+
if (out) return out;
|
|
743
|
+
out = [];
|
|
744
|
+
const isDefault = this.defaultExportOf(sym.file_id) === sym.name;
|
|
745
|
+
const sources = [sym.file_id];
|
|
746
|
+
if (isDefault) {
|
|
747
|
+
// Files that pass the default export on: `module.exports = require('./lib/express')`.
|
|
748
|
+
for (const r of this.db.all<{ id: number }>(
|
|
749
|
+
`SELECT f.id FROM files f JOIN imports i ON i.file_id = f.id
|
|
750
|
+
WHERE f.default_export = '*' AND i.resolved_id = ? LIMIT 20`,
|
|
751
|
+
sym.file_id
|
|
752
|
+
)) {
|
|
753
|
+
sources.push(r.id);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
const rows: Array<{ file_id: number; names: string }> = [];
|
|
757
|
+
for (const source of sources) {
|
|
758
|
+
rows.push(...this.db.all<{ file_id: number; names: string }>(
|
|
759
|
+
`SELECT file_id, names FROM imports INDEXED BY idx_imports_resolved WHERE resolved_id = ? AND names <> ''`,
|
|
760
|
+
source
|
|
761
|
+
));
|
|
762
|
+
}
|
|
763
|
+
for (const r of rows) {
|
|
764
|
+
const parts = r.names.split(',');
|
|
765
|
+
for (const raw of parts) {
|
|
766
|
+
const eq = raw.indexOf('=');
|
|
767
|
+
if (eq !== -1) {
|
|
768
|
+
if (raw.slice(eq + 1) === sym.name && raw.slice(0, eq) !== sym.name) out.push({ fileId: r.file_id, alias: raw.slice(0, eq) });
|
|
769
|
+
} else if (isDefault && parts.length === 1 && raw !== sym.name && raw !== '*') {
|
|
770
|
+
// `const express = require('./express')` / `import express from './express'`
|
|
771
|
+
out.push({ fileId: r.file_id, alias: raw });
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
// One hop further: files that import the alias from a file that re-exports the symbol under it.
|
|
776
|
+
const seen = new Set(out.map(o => o.fileId + ':' + o.alias));
|
|
777
|
+
for (const via of [...out]) {
|
|
778
|
+
for (const r of this.db.all<{ file_id: number; names: string }>(
|
|
779
|
+
`SELECT file_id, names FROM imports INDEXED BY idx_imports_resolved WHERE resolved_id = ? AND names <> '' LIMIT 2000`,
|
|
780
|
+
via.fileId
|
|
781
|
+
)) {
|
|
782
|
+
for (const raw of r.names.split(',')) {
|
|
783
|
+
const eq = raw.indexOf('=');
|
|
784
|
+
const imported = eq === -1 ? raw : raw.slice(eq + 1);
|
|
785
|
+
if (imported !== via.alias) continue;
|
|
786
|
+
const local = eq === -1 ? raw : raw.slice(0, eq);
|
|
787
|
+
const k = r.file_id + ':' + local;
|
|
788
|
+
if (!seen.has(k)) {
|
|
789
|
+
seen.add(k);
|
|
790
|
+
out.push({ fileId: r.file_id, alias: local });
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
this.aliasUseCache.set(key, out);
|
|
796
|
+
return out;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
candidates(name: string): SymbolRow[] {
|
|
800
|
+
let list = this.candidateCache.get(name);
|
|
801
|
+
if (!list) {
|
|
802
|
+
list = this.db.symbolsByName(name).filter(s => CALL_TARGET_KINDS.has(s.kind));
|
|
803
|
+
this.candidateCache.set(name, list);
|
|
804
|
+
}
|
|
805
|
+
return list;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
private parentOf(sym: SymbolRow): SymbolRow | null {
|
|
809
|
+
if (sym.parent_id === null) return null;
|
|
810
|
+
let p = this.parentCache.get(sym.parent_id);
|
|
811
|
+
if (p === undefined) {
|
|
812
|
+
p = this.db.symbolById(sym.parent_id) ?? null;
|
|
813
|
+
this.parentCache.set(sym.parent_id, p);
|
|
814
|
+
}
|
|
815
|
+
return p;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** Nearest enclosing class-like symbol of `sym` (lexical), or its container by qualified name. */
|
|
819
|
+
private enclosingContainer(sym: SymbolRow): { name: string; id: number | null } | null {
|
|
820
|
+
const cached = this.containerCache.get(sym.id);
|
|
821
|
+
if (cached !== undefined) return cached;
|
|
822
|
+
const found = this.findEnclosingContainer(sym);
|
|
823
|
+
this.containerCache.set(sym.id, found);
|
|
824
|
+
return found;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
private findEnclosingContainer(sym: SymbolRow): { name: string; id: number | null } | null {
|
|
828
|
+
if (CONTAINER_KINDS.has(sym.kind)) return { name: sym.name, id: sym.id };
|
|
829
|
+
let namespace: { name: string; id: number | null } | null = null;
|
|
830
|
+
let cur: SymbolRow | null = sym;
|
|
831
|
+
for (let i = 0; i < 8 && cur; i++) {
|
|
832
|
+
cur = this.parentOf(cur);
|
|
833
|
+
if (!cur || !CONTAINER_KINDS.has(cur.kind)) continue;
|
|
834
|
+
if (cur.kind !== 'namespace') return { name: cur.name, id: cur.id };
|
|
835
|
+
namespace ??= { name: cur.name, id: cur.id };
|
|
836
|
+
}
|
|
837
|
+
// Go receivers, Rust impl blocks, C++ out-of-line definitions (possibly inside a
|
|
838
|
+
// namespace block): the class is only in the qualified name.
|
|
839
|
+
const name = this.containerName(sym);
|
|
840
|
+
if (name && (!namespace || name !== namespace.name)) return { name, id: null };
|
|
841
|
+
return namespace;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
private stdNameCache = new Map<number, Set<string>>();
|
|
845
|
+
|
|
846
|
+
/** Rust: names a file imports from the standard library (`use std::task::{Context, Wake}`). */
|
|
847
|
+
private stdNames(fileId: number): Set<string> {
|
|
848
|
+
let set = this.stdNameCache.get(fileId);
|
|
849
|
+
if (!set) {
|
|
850
|
+
set = new Set<string>();
|
|
851
|
+
for (const r of this.db.all<{ source: string; names: string }>(
|
|
852
|
+
`SELECT source, names FROM imports INDEXED BY idx_imports_file WHERE file_id = ?`, fileId
|
|
853
|
+
)) {
|
|
854
|
+
if (!/^(std|core|alloc)(::|$)/.test(r.source)) continue;
|
|
855
|
+
for (const n of r.names.split(',')) {
|
|
856
|
+
const name = n.trim().split('=')[0]!.trim();
|
|
857
|
+
if (name) set.add(name);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
this.stdNameCache.set(fileId, set);
|
|
861
|
+
}
|
|
862
|
+
return set;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* A supertype written in file `fileId` that cannot be a project type even though the project
|
|
867
|
+
* defines one of that name: a Go type of an external package (`io.Closer`), a Rust trait the
|
|
868
|
+
* file imports from the standard library (`use std::task::Wake`).
|
|
869
|
+
*/
|
|
870
|
+
externalSupertype(name: string, qualifier: string | null, fileId: number, lang: string): boolean {
|
|
871
|
+
if (lang === 'go') return !!qualifier && this.goImports(fileId).get(qualifier) === null;
|
|
872
|
+
if (lang === 'rust') return !qualifier && this.stdNames(fileId).has(name);
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Ruby constants are looked up lexically: `Base` written inside `Rails::Generators::NamedBase` is
|
|
878
|
+
* `Rails::Generators::Base`, never `ActiveRecord::Base`, and `Thor::Group` is not some other
|
|
879
|
+
* `Group`. Returns every part of the (possibly reopened) class or module, or nothing when no
|
|
880
|
+
* enclosing scope defines it (a gem).
|
|
881
|
+
*/
|
|
882
|
+
private rubyConstant(from: SymbolRow, qualifier: string | null, name: string): SymbolRow[] {
|
|
883
|
+
const cands = this.db.symbolsByName(name).filter(s => CONTAINER_KINDS.has(s.kind) && s.lang === 'ruby');
|
|
884
|
+
if (cands.length === 0) return [];
|
|
885
|
+
const parts = from.qname.split('.');
|
|
886
|
+
const tail = (qualifier ? qualifier.replace(/::/g, '.').replace(/^\./, '') + '.' : '') + name;
|
|
887
|
+
for (let n = parts.length; n >= 0; n--) {
|
|
888
|
+
const want = (n ? parts.slice(0, n).join('.') + '.' : '') + tail;
|
|
889
|
+
const hits = cands.filter(s => s.qname === want && s.id !== from.id);
|
|
890
|
+
if (hits.length) return hits;
|
|
891
|
+
}
|
|
892
|
+
return [];
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Qualified names of the Ruby ancestors (superclasses and mixins) of a class or module, found
|
|
897
|
+
* by lexical constant lookup. All parts of a reopened class contribute their own heritage.
|
|
898
|
+
*/
|
|
899
|
+
rubyAncestors(containerId: number): Set<string> {
|
|
900
|
+
let set = this.rubyAncestorCache.get(containerId);
|
|
901
|
+
if (set) return set;
|
|
902
|
+
set = new Set<string>();
|
|
903
|
+
const start = this.db.symbolById(containerId);
|
|
904
|
+
let frontier: SymbolRow[] = start
|
|
905
|
+
? this.db.symbolsByName(start.name).filter(s => s.qname === start.qname && s.lang === 'ruby' && CONTAINER_KINDS.has(s.kind))
|
|
906
|
+
: [];
|
|
907
|
+
for (let depth = 0; depth < 8 && frontier.length; depth++) {
|
|
908
|
+
const next: SymbolRow[] = [];
|
|
909
|
+
for (const sym of frontier) {
|
|
910
|
+
for (const row of this.db.all<{ name: string; qualifier: string | null }>(
|
|
911
|
+
`SELECT name, qualifier FROM heritage WHERE symbol_id = ?`, sym.id
|
|
912
|
+
)) {
|
|
913
|
+
for (const parent of this.rubyConstant(sym, row.qualifier, row.name)) {
|
|
914
|
+
if (parent.qname === start?.qname) continue;
|
|
915
|
+
if (!set.has(parent.qname)) next.push(parent);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
for (const p of next) set.add(p.qname);
|
|
920
|
+
frontier = next.slice(0, 48);
|
|
921
|
+
}
|
|
922
|
+
this.rubyAncestorCache.set(containerId, set);
|
|
923
|
+
return set;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/** Names of the classes a container extends/implements, transitively (bounded). */
|
|
927
|
+
ancestorsOf(containerId: number): Set<string> {
|
|
928
|
+
let set = this.ancestorCache.get(containerId);
|
|
929
|
+
if (set) return set;
|
|
930
|
+
set = new Set<string>();
|
|
931
|
+
const self = this.db.symbolById(containerId);
|
|
932
|
+
if (self?.lang === 'ruby') {
|
|
933
|
+
// By identity, then reported as names for the callers that compare names.
|
|
934
|
+
for (const q of this.rubyAncestors(containerId)) set.add(q.slice(q.lastIndexOf('.') + 1));
|
|
935
|
+
this.ancestorCache.set(containerId, set);
|
|
936
|
+
return set;
|
|
937
|
+
}
|
|
938
|
+
let frontier = [containerId];
|
|
939
|
+
for (let depth = 0; depth < 5 && frontier.length; depth++) {
|
|
940
|
+
const next: number[] = [];
|
|
941
|
+
for (const id of frontier) {
|
|
942
|
+
for (const row of this.db.all<{ name: string; file_id: number; qualifier: string | null; lang: string }>(
|
|
943
|
+
`SELECT h.name, h.file_id, h.qualifier, f.lang FROM heritage h JOIN files f ON f.id = h.file_id WHERE h.symbol_id = ?`, id
|
|
944
|
+
)) {
|
|
945
|
+
if (this.externalSupertype(row.name, row.qualifier, row.file_id, row.lang)) continue;
|
|
946
|
+
// `class A extends Base` where Base is `import { X as Base }`: the supertype is X.
|
|
947
|
+
const h = { name: this.importedNames(row.file_id).aliases.get(row.name) ?? row.name };
|
|
948
|
+
if (set.has(h.name)) continue;
|
|
949
|
+
set.add(h.name);
|
|
950
|
+
for (const parent of this.db.symbolsByName(h.name)) {
|
|
951
|
+
if (CONTAINER_KINDS.has(parent.kind)) next.push(parent.id);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
frontier = next.slice(0, 24);
|
|
956
|
+
}
|
|
957
|
+
this.ancestorCache.set(containerId, set);
|
|
958
|
+
return set;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/** Ancestor and descendant type names of every container called `name`. */
|
|
962
|
+
relatedTo(name: string): { anc: Set<string>; desc: Set<string> } {
|
|
963
|
+
let rel = this.relatedCache.get(name);
|
|
964
|
+
if (rel) return rel;
|
|
965
|
+
rel = { anc: new Set(), desc: new Set() };
|
|
966
|
+
for (const s of this.db.symbolsByName(name)) {
|
|
967
|
+
if (!CONTAINER_KINDS.has(s.kind)) continue;
|
|
968
|
+
for (const a of this.ancestorsOf(s.id)) rel.anc.add(a);
|
|
969
|
+
// Subtypes that extend this type under an import alias.
|
|
970
|
+
for (const use of this.aliasesOf(s)) {
|
|
971
|
+
for (const row of this.db.all<{ name: string }>(
|
|
972
|
+
`SELECT s.name FROM heritage h JOIN symbols s ON s.id = h.symbol_id WHERE h.name = ? AND h.file_id = ?`,
|
|
973
|
+
use.alias, use.fileId
|
|
974
|
+
)) {
|
|
975
|
+
if (row.name !== name) rel.desc.add(row.name);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
let frontier = [name, ...rel.desc];
|
|
980
|
+
for (let depth = 0; depth < 6 && frontier.length && rel.desc.size < 4000; depth++) {
|
|
981
|
+
const next: string[] = [];
|
|
982
|
+
for (const n of frontier) {
|
|
983
|
+
for (const row of this.db.all<{ name: string; file_id: number; qualifier: string | null; lang: string }>(
|
|
984
|
+
`SELECT s.name, h.file_id, h.qualifier, f.lang FROM heritage h JOIN symbols s ON s.id = h.symbol_id
|
|
985
|
+
JOIN files f ON f.id = h.file_id WHERE h.name = ? LIMIT 1000`, n
|
|
986
|
+
)) {
|
|
987
|
+
if (this.externalSupertype(n, row.qualifier, row.file_id, row.lang)) continue;
|
|
988
|
+
if (row.name !== name && !rel.desc.has(row.name)) {
|
|
989
|
+
rel.desc.add(row.name);
|
|
990
|
+
next.push(row.name);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
frontier = next.slice(0, 400);
|
|
995
|
+
}
|
|
996
|
+
this.relatedCache.set(name, rel);
|
|
997
|
+
return rel;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
private containerName(s: SymbolRow): string | null {
|
|
1001
|
+
const i = s.qname.lastIndexOf('.');
|
|
1002
|
+
if (i === -1) return null;
|
|
1003
|
+
const j = s.qname.lastIndexOf('.', i - 1);
|
|
1004
|
+
return s.qname.slice(j + 1, i);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/** Does the file's code mention this identifier at all? */
|
|
1008
|
+
private fileMentions(fileId: number, name: string): boolean {
|
|
1009
|
+
const key = fileId + ':' + name;
|
|
1010
|
+
let hit = this.mentionCache.get(key);
|
|
1011
|
+
if (hit === undefined) {
|
|
1012
|
+
const nameId = this.db.nameId(name, false);
|
|
1013
|
+
hit =
|
|
1014
|
+
nameId !== undefined &&
|
|
1015
|
+
this.db.get(`SELECT 1 AS x FROM occ WHERE name_id = ? AND file_id = ?`, nameId, fileId) !== undefined;
|
|
1016
|
+
this.mentionCache.set(key, hit);
|
|
1017
|
+
}
|
|
1018
|
+
return hit;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/** Is `name` a class-like type defined in this project? */
|
|
1022
|
+
private isProjectType(name: string): boolean {
|
|
1023
|
+
let hit = this.projectTypeCache.get(name);
|
|
1024
|
+
if (hit === undefined) {
|
|
1025
|
+
hit = this.db.symbolsByName(name).some(s => CONTAINER_KINDS.has(s.kind) || s.kind === 'type');
|
|
1026
|
+
this.projectTypeCache.set(name, hit);
|
|
1027
|
+
}
|
|
1028
|
+
return hit;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Symbols that are "the same target" as `sym` for call-graph purposes:
|
|
1033
|
+
* overloads (same qualified name in the same file), C/C++ prototypes and
|
|
1034
|
+
* their definitions, and same-named methods in containers related by
|
|
1035
|
+
* inheritance (interface method <-> implementation, base <-> override).
|
|
1036
|
+
* A call resolved to any member counts for all.
|
|
1037
|
+
*/
|
|
1038
|
+
familyOf(sym: SymbolRow): Set<number> {
|
|
1039
|
+
let fam = this.familyCache.get(sym.id);
|
|
1040
|
+
if (fam) return fam;
|
|
1041
|
+
fam = new Set<number>([sym.id]);
|
|
1042
|
+
let same = this.candidates(sym.name);
|
|
1043
|
+
const container = this.containerName(sym);
|
|
1044
|
+
let related: Set<string> | null = null;
|
|
1045
|
+
// Go has no overriding: `Engine.Use` shadows the embedded `RouterGroup.Use`, callers are not shared.
|
|
1046
|
+
if (container && same.length > 1 && sym.lang !== 'go') {
|
|
1047
|
+
const rel = this.relatedTo(container);
|
|
1048
|
+
related = new Set([...rel.anc, ...rel.desc]);
|
|
1049
|
+
if (same.length >= 600) {
|
|
1050
|
+
// Crowded name (`run`, `get`): look only at same-named members of related types.
|
|
1051
|
+
const idx = this.nameIndex(sym.name);
|
|
1052
|
+
const narrowed: SymbolRow[] = [...(idx.byFile.get(sym.file_id) ?? [])];
|
|
1053
|
+
for (const c of idx.containers) if (related.has(c)) narrowed.push(...(idx.byContainer.get(c) ?? []));
|
|
1054
|
+
same = narrowed;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
const cFamily = langFamily(sym.lang) === 'c';
|
|
1058
|
+
for (const other of same) {
|
|
1059
|
+
if (other.id === sym.id) continue;
|
|
1060
|
+
const sameKindOfThing = CALLABLE.has(other.kind) === CALLABLE.has(sym.kind);
|
|
1061
|
+
if (
|
|
1062
|
+
other.qname === sym.qname && sameKindOfThing &&
|
|
1063
|
+
(other.file_id === sym.file_id || (cFamily && CALLABLE.has(sym.kind) && cTwins(sym, other)))
|
|
1064
|
+
) {
|
|
1065
|
+
// Overloads; in C/C++ also the header declaration of a definition.
|
|
1066
|
+
fam.add(other.id);
|
|
1067
|
+
} else if (related && (other.kind === 'method' || other.kind === sym.kind)) {
|
|
1068
|
+
const oc = this.containerName(other);
|
|
1069
|
+
if (!oc || !related.has(oc)) continue;
|
|
1070
|
+
if (oc === container && other.file_id !== sym.file_id) {
|
|
1071
|
+
// Two types with the same short name (Cache\Repository and Config\Repository, both implementing
|
|
1072
|
+
// a contract called Repository) are related only if one of them is that interface.
|
|
1073
|
+
const po = this.parentOf(other);
|
|
1074
|
+
const ps = this.parentOf(sym);
|
|
1075
|
+
// (Methods written outside the type body - Rust impl blocks, Go receivers - have no parent symbol:
|
|
1076
|
+
// the same method name on a same-named type in another file is then simply another type.)
|
|
1077
|
+
if (!po || !ps) continue;
|
|
1078
|
+
if (po.kind !== 'interface' && ps.kind !== 'interface' && po.kind !== 'trait' && ps.kind !== 'trait') continue;
|
|
1079
|
+
// ... and, where files import what they use, only if one of the two files imports the other
|
|
1080
|
+
// (`use Contracts\Events\Dispatcher as DispatcherContract`): Bus\Dispatcher is someone else's contract.
|
|
1081
|
+
if (
|
|
1082
|
+
SINGLE_FILE_CLASS.has(langFamily(sym.lang)) &&
|
|
1083
|
+
!this.importsOf(sym.file_id).has(other.file_id) && !this.importsOf(other.file_id).has(sym.file_id)
|
|
1084
|
+
) continue;
|
|
1085
|
+
}
|
|
1086
|
+
fam.add(other.id);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
this.familyCache.set(sym.id, fam);
|
|
1090
|
+
return fam;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* True when `other` overrides `target` in a subtype and `target` has a body of its own:
|
|
1095
|
+
* a call bound to the override never runs `target` (the reverse is not true: a call bound
|
|
1096
|
+
* to the base method may dispatch to the override).
|
|
1097
|
+
*/
|
|
1098
|
+
overriddenBelow(target: SymbolRow, other: SymbolRow): boolean {
|
|
1099
|
+
if (other.id === target.id || other.qname === target.qname) return false;
|
|
1100
|
+
const tc = this.containerName(target);
|
|
1101
|
+
const oc = this.containerName(other);
|
|
1102
|
+
if (!tc || !oc || tc === oc) return false;
|
|
1103
|
+
const parent = this.parentOf(target);
|
|
1104
|
+
if (!parent || (parent.kind !== 'class' && parent.kind !== 'struct')) return false;
|
|
1105
|
+
if (target.kind === 'declaration' || /\babstract\b/.test(target.signature)) return false;
|
|
1106
|
+
const rel = this.relatedTo(tc);
|
|
1107
|
+
return rel.desc.has(oc) && !rel.anc.has(oc);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/** How many genuinely different definitions (families) share this name. */
|
|
1111
|
+
private familyCount(name: string): number {
|
|
1112
|
+
let n = this.familyCountCache.get(name);
|
|
1113
|
+
if (n !== undefined) return n;
|
|
1114
|
+
const cands = this.candidates(name);
|
|
1115
|
+
if (cands.length > 60) {
|
|
1116
|
+
n = cands.length; // too many to group; certainly not distinctive
|
|
1117
|
+
} else {
|
|
1118
|
+
const seen = new Set<number>();
|
|
1119
|
+
n = 0;
|
|
1120
|
+
for (const c of cands) {
|
|
1121
|
+
if (seen.has(c.id)) continue;
|
|
1122
|
+
n++;
|
|
1123
|
+
for (const id of this.familyOf(c)) seen.add(id);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
this.familyCountCache.set(name, n);
|
|
1127
|
+
return n;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* Evidence that a member call on a receiver of unknown type targets `cand`
|
|
1132
|
+
* (a method of `container`). Zero evidence means "not linked".
|
|
1133
|
+
*/
|
|
1134
|
+
private memberEvidence(
|
|
1135
|
+
name: string, cand: SymbolRow, container: string | null, site: CallSite, recv: string | null, lenient = false
|
|
1136
|
+
): number {
|
|
1137
|
+
let e = 0;
|
|
1138
|
+
if (cand.file_id === site.fileId) e += 24;
|
|
1139
|
+
else if (this.importsOf(site.fileId).has(cand.file_id)) e += 22;
|
|
1140
|
+
else if (dirOf(cand.path) === dirOf(site.path)) e += site.lang === 'go' || site.lang === 'java' ? 15 : 8;
|
|
1141
|
+
if (container && cand.file_id !== site.fileId && this.fileMentions(site.fileId, container)) e += 18;
|
|
1142
|
+
let receiverMatches = false;
|
|
1143
|
+
if (recv && container && !GENERIC_RECEIVERS.has(recv.toLowerCase())) {
|
|
1144
|
+
if (looselyNamed(recv, container)) {
|
|
1145
|
+
e += 30;
|
|
1146
|
+
receiverMatches = true;
|
|
1147
|
+
} else if (recv.length <= 2 && container.toLowerCase().startsWith(recv.toLowerCase())) {
|
|
1148
|
+
// Go convention: `s` for *Site, `c` for *Context, `rg` ... Within a package this is good evidence.
|
|
1149
|
+
e += 8;
|
|
1150
|
+
receiverMatches = site.lang === 'go' && dirOf(cand.path) === dirOf(site.path);
|
|
1151
|
+
} else {
|
|
1152
|
+
// `@cache.fetch_multi` -> Cache::Store#fetch_multi: the receiver names the enclosing namespace.
|
|
1153
|
+
const q = cand.qname.split('.');
|
|
1154
|
+
const outer = q.length >= 3 ? q[q.length - 3]! : null;
|
|
1155
|
+
if (outer && outer.length >= 4 && looselyNamed(recv, outer)) {
|
|
1156
|
+
e += 22;
|
|
1157
|
+
receiverMatches = true;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (COMMON_MEMBER_NAMES.has(name.toLowerCase())) {
|
|
1162
|
+
// .get(), .add(), .count(): only the receiver's name can tell a project method from a builtin.
|
|
1163
|
+
return receiverMatches || lenient ? e : 0;
|
|
1164
|
+
}
|
|
1165
|
+
// `connection_handler.establish_connection`: the receiver names ConnectionHandler, so a same-named
|
|
1166
|
+
// method on some other class (even the caller's own) is not the target.
|
|
1167
|
+
if (!receiverMatches && recv && !GENERIC_RECEIVERS.has(recv.toLowerCase()) && this.receiverNamesOther(name, recv, container)) {
|
|
1168
|
+
return 0;
|
|
1169
|
+
}
|
|
1170
|
+
if (this.familyCount(name) === 1) {
|
|
1171
|
+
// Defined once in the whole project. A multi-word name (`complete_inflight`, `pollElapsed`)
|
|
1172
|
+
// is then almost certainly this method; a single word (`changed`, `close`) may be a library's.
|
|
1173
|
+
e += splitIdentifier(name).length >= 2 && name.length >= 8 ? 46 : 20;
|
|
1174
|
+
} else if (e === 0 && recv && splitIdentifier(name).length >= 2 && name.length >= 8 && this.familyCount(name) <= 3) {
|
|
1175
|
+
// A distinctive name with two or three definitions and no other clue (`@cache.fetch_multi` in a
|
|
1176
|
+
// test): still worth listing as a name-only candidate rather than hiding the call site.
|
|
1177
|
+
e += 16;
|
|
1178
|
+
}
|
|
1179
|
+
return e;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
private receiverOtherCache = new Map<string, Set<string>>();
|
|
1183
|
+
|
|
1184
|
+
/** Does the receiver's name match the class of a different definition of `name`? */
|
|
1185
|
+
private receiverNamesOther(name: string, recv: string, container: string | null): boolean {
|
|
1186
|
+
const key = name + '\0' + recv;
|
|
1187
|
+
let matches = this.receiverOtherCache.get(key);
|
|
1188
|
+
if (!matches) {
|
|
1189
|
+
matches = new Set<string>();
|
|
1190
|
+
const cands = this.candidates(name);
|
|
1191
|
+
if (cands.length <= 400) {
|
|
1192
|
+
for (const c of cands) {
|
|
1193
|
+
const cn = this.containerName(c);
|
|
1194
|
+
if (cn && looselyNamed(recv, cn)) matches.add(cn);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
this.receiverOtherCache.set(key, matches);
|
|
1198
|
+
}
|
|
1199
|
+
return matches.size > 0 && (container === null || !matches.has(container));
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/** Score how plausible it is that a call at `site` targets `cand`; null = impossible. */
|
|
1203
|
+
private score(name: string, cand: SymbolRow, site: CallSite): number | null {
|
|
1204
|
+
const fam = langFamily(site.lang);
|
|
1205
|
+
if (langFamily(cand.lang) !== fam) return null;
|
|
1206
|
+
if (site.lang === 'c' && !C_CALLABLE.has(cand.kind)) return null;
|
|
1207
|
+
|
|
1208
|
+
// Production code does not call into tests, fixtures or examples.
|
|
1209
|
+
if (isTestPath(cand.path) && !isTestPath(site.path)) return null;
|
|
1210
|
+
// ... nor into vendored / third-party trees it does not import.
|
|
1211
|
+
if (
|
|
1212
|
+
isVendoredPath(cand.path) && !isVendoredPath(site.path) && cand.file_id !== site.fileId &&
|
|
1213
|
+
!this.importsOf(site.fileId).has(cand.file_id) &&
|
|
1214
|
+
!(site.lang === 'go' && (dirOf(cand.path) === dirOf(site.path) || this.importsGoDir(site.fileId, dirOf(cand.path))))
|
|
1215
|
+
) {
|
|
1216
|
+
return null;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const sameFile = cand.file_id === site.fileId;
|
|
1220
|
+
const parent = this.parentOf(cand);
|
|
1221
|
+
|
|
1222
|
+
// Functions nested in a function are visible only inside it.
|
|
1223
|
+
if (parent && (parent.kind === 'function' || parent.kind === 'method')) {
|
|
1224
|
+
if (!sameFile || !site.caller) return null;
|
|
1225
|
+
const q = site.caller.qname;
|
|
1226
|
+
if (q !== parent.qname && !q.startsWith(parent.qname + '.')) return null;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// A type nested inside a class is visible by its bare name only within that class.
|
|
1230
|
+
if (
|
|
1231
|
+
!site.receiver && parent && TYPE_KINDS.has(cand.kind) &&
|
|
1232
|
+
CONTAINER_KINDS.has(parent.kind) && parent.kind !== 'namespace'
|
|
1233
|
+
) {
|
|
1234
|
+
const q = site.caller?.qname ?? '';
|
|
1235
|
+
if (!sameFile || !(q === parent.qname || q.startsWith(parent.qname + '.'))) return null;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const container = this.containerName(cand);
|
|
1239
|
+
const inContainer = parent !== null && CONTAINER_KINDS.has(parent.kind);
|
|
1240
|
+
// A function directly inside a C++ namespace is a free function: callable by its bare name from any
|
|
1241
|
+
// code that includes it, whatever class the caller belongs to.
|
|
1242
|
+
const nsFunction = fam === 'c' && parent !== null && parent.kind === 'namespace' && cand.kind !== 'method';
|
|
1243
|
+
const isMember =
|
|
1244
|
+
!nsFunction &&
|
|
1245
|
+
(cand.kind === 'method' ||
|
|
1246
|
+
(inContainer && (cand.kind === 'function' || cand.kind === 'declaration' || VALUE_KINDS.has(cand.kind))));
|
|
1247
|
+
// The number of arguments rules the definition out (another overload, or an unrelated method of that name).
|
|
1248
|
+
if (site.argc !== undefined && site.argc !== null && site.argc >= 0 && !this.arityFits(cand, site)) return null;
|
|
1249
|
+
// C: a static function is callable only inside its own file (or a file that includes it).
|
|
1250
|
+
if (fam === 'c' && !sameFile && isFileLocal(cand) && !this.importsOf(site.fileId).has(cand.file_id)) return null;
|
|
1251
|
+
// A module-level variable is only callable by its bare name; never through an object.
|
|
1252
|
+
if (VALUE_KINDS.has(cand.kind) && !inContainer && site.receiver) return null;
|
|
1253
|
+
// A field is only callable when it holds a function: `s.Conf.Watching()` is not the bool field `Watching`.
|
|
1254
|
+
if (VALUE_KINDS.has(cand.kind) && inContainer) {
|
|
1255
|
+
if (cand.lang === 'java') return null;
|
|
1256
|
+
if (cand.lang === 'go' && !cand.signature.includes('func')) return null;
|
|
1257
|
+
if (cand.lang === 'rust' && !/\bfn\b|\bFn(Mut|Once)?\b/.test(cand.signature)) return null;
|
|
1258
|
+
}
|
|
1259
|
+
const callerC = site.caller ? this.enclosingContainer(site.caller) : null;
|
|
1260
|
+
let tie = this.tieCache.get(cand.id);
|
|
1261
|
+
if (tie === undefined) {
|
|
1262
|
+
tie = definitionScore(cand) * 0.5;
|
|
1263
|
+
this.tieCache.set(cand.id, tie);
|
|
1264
|
+
}
|
|
1265
|
+
const recv = site.receiver;
|
|
1266
|
+
|
|
1267
|
+
// The caller's own class. Where classes cannot be split over files, a class of the same name
|
|
1268
|
+
// in another file (one `DatabaseSchemaEditor` per backend) is a different class.
|
|
1269
|
+
// Ruby: classes are open (many files) but identified by their full name. `Base` is a different
|
|
1270
|
+
// class in every namespace, so the caller's class and its ancestors are compared by qualified name.
|
|
1271
|
+
const rubyCallerQ =
|
|
1272
|
+
site.lang === 'ruby' && callerC !== null && callerC.id !== null && parent !== null && CONTAINER_KINDS.has(parent.kind)
|
|
1273
|
+
? this.db.symbolById(callerC.id)?.qname ?? null
|
|
1274
|
+
: null;
|
|
1275
|
+
const inCallerClass =
|
|
1276
|
+
rubyCallerQ !== null
|
|
1277
|
+
? isMember && parent!.qname === rubyCallerQ
|
|
1278
|
+
: isMember && callerC !== null && container === callerC.name &&
|
|
1279
|
+
(!SINGLE_FILE_CLASS.has(fam) || sameFile || callerC.id === null || parent === null ||
|
|
1280
|
+
!CONTAINER_KINDS.has(parent.kind) || parent.id === callerC.id);
|
|
1281
|
+
const inCallerAncestors = (): boolean =>
|
|
1282
|
+
rubyCallerQ !== null
|
|
1283
|
+
? isMember && this.rubyAncestors(callerC!.id!).has(parent!.qname)
|
|
1284
|
+
: isMember && callerC !== null && container !== null &&
|
|
1285
|
+
(callerC.id !== null ? this.ancestorsOf(callerC.id).has(container) : this.relatedTo(callerC.name).anc.has(container));
|
|
1286
|
+
|
|
1287
|
+
// --- typed receiver: x.foo() where x: T ------------------------------------
|
|
1288
|
+
if (site.rtype && recv && !SELF_RECEIVERS.has(recv)) {
|
|
1289
|
+
const T = site.rtype;
|
|
1290
|
+
if (isMember && container) {
|
|
1291
|
+
const vis = this.typeVisibility(T, cand, site);
|
|
1292
|
+
if (container === T) {
|
|
1293
|
+
// Go `pkg.Type`: methods live in the package that defines the type.
|
|
1294
|
+
if (site.rtypeDir !== undefined && dirOf(cand.path) !== site.rtypeDir) return null;
|
|
1295
|
+
// The calling file imports (or defines) its own type T: a same-named class elsewhere is not it.
|
|
1296
|
+
if (SINGLE_FILE_CLASS.has(fam) && !sameFile && this.otherTypeInScope(T, cand, site)) return null;
|
|
1297
|
+
return 100 + vis + tie;
|
|
1298
|
+
}
|
|
1299
|
+
const rel = this.relatedTo(T);
|
|
1300
|
+
if (rel.anc.has(container)) return 86 + vis + tie; // inherited from a supertype
|
|
1301
|
+
if (rel.desc.has(container)) {
|
|
1302
|
+
// An override in a subtype. When T itself is a library type (TextWriter, Stream) nearly all
|
|
1303
|
+
// calls on it go elsewhere: keep the project override as a possibility, never as a fact.
|
|
1304
|
+
return this.isProjectType(T) ? 72 + vis + tie : Math.min(48, 40 + vis + tie);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
// Not a member of T or its relatives. Only languages with extension
|
|
1308
|
+
// methods / open classes can still reach an unrelated definition.
|
|
1309
|
+
if (!OPEN_TYPES.has(site.lang)) return null;
|
|
1310
|
+
if (!isMember) return null;
|
|
1311
|
+
const e = this.memberEvidence(name, cand, container, site, null);
|
|
1312
|
+
// An extension method with a distinctive name that the project defines once (`app.UseRouting()`).
|
|
1313
|
+
const unique = e >= 46 && splitIdentifier(name).length >= 2 && name.length >= 8 && this.familyCount(name) === 1;
|
|
1314
|
+
return e >= 30 ? Math.min(unique ? 60 : 50, 10 + e) + tie : null;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// --- bare call: foo() ---------------------------------------------------
|
|
1318
|
+
if (!recv) {
|
|
1319
|
+
if (isMember) {
|
|
1320
|
+
// Python / JS: a bare name never reaches a method (it needs self. / this.).
|
|
1321
|
+
if (STRICT_IMPORTS.has(fam)) return null;
|
|
1322
|
+
if (inCallerClass) return (sameFile ? 88 : 72) + tie; // implicit this / same (partial) class
|
|
1323
|
+
if (inCallerAncestors()) return 68 + tie;
|
|
1324
|
+
if (site.lang === 'java' && container !== name) {
|
|
1325
|
+
// `import static pkg.Utils.mkEntry;` / `import static pkg.Utils.*;`
|
|
1326
|
+
// (Not constructors: `import pkg.TopicPartition` + `new TopicPartition()` names the class.)
|
|
1327
|
+
const imp = this.importedNames(site.fileId);
|
|
1328
|
+
if (imp.names.get(name) === cand.file_id) return 94 + tie;
|
|
1329
|
+
if (imp.wildcards.has(cand.file_id)) return 58 + tie;
|
|
1330
|
+
}
|
|
1331
|
+
if (site.lang === 'go' || site.lang === 'rust') return null;
|
|
1332
|
+
if (site.lang === 'ruby' || site.lang === 'php') {
|
|
1333
|
+
const e = this.memberEvidence(name, cand, container, site, null);
|
|
1334
|
+
return e >= 20 ? Math.min(48, 10 + e) + tie : null;
|
|
1335
|
+
}
|
|
1336
|
+
// Java / C# / C++: an outer class's method or a static import.
|
|
1337
|
+
return sameFile ? 40 + tie : null;
|
|
1338
|
+
}
|
|
1339
|
+
const imported = this.importedNames(site.fileId);
|
|
1340
|
+
const from = imported.names.get(name);
|
|
1341
|
+
if (sameFile) return 82 + tie;
|
|
1342
|
+
if (from !== undefined && from !== null) {
|
|
1343
|
+
if (from === cand.file_id) return 96 + tie;
|
|
1344
|
+
if (this.importsOf(from).has(cand.file_id)) return 62 + tie; // re-exported one hop away
|
|
1345
|
+
return null; // the name is imported from somewhere else
|
|
1346
|
+
}
|
|
1347
|
+
if (STRICT_IMPORTS.has(fam)) {
|
|
1348
|
+
if (imported.wildcards.has(cand.file_id)) return 58 + tie;
|
|
1349
|
+
if (this.importsOf(site.fileId).has(cand.file_id)) return 46 + tie; // default / destructured import
|
|
1350
|
+
// Classic script files share one global scope.
|
|
1351
|
+
if (site.lang === 'javascript' && this.importsOf(site.fileId).size === 0 && imported.names.size === 0) {
|
|
1352
|
+
return dirOf(cand.path) === dirOf(site.path) ? 44 + tie : null;
|
|
1353
|
+
}
|
|
1354
|
+
return null;
|
|
1355
|
+
}
|
|
1356
|
+
if (site.lang === 'go') return dirOf(cand.path) === dirOf(site.path) ? 78 + tie : null;
|
|
1357
|
+
// Same enclosing namespace / module as the caller (Ruby modules, C++ / C# namespaces, Rust mods).
|
|
1358
|
+
const ns = site.caller ? namespaceAffinity(site.caller.qname, cand.qname) : 0;
|
|
1359
|
+
if (this.importsOf(site.fileId).has(cand.file_id)) return 64 + ns + tie;
|
|
1360
|
+
if (fam === 'c') {
|
|
1361
|
+
// Headers include headers: one transitive hop still counts as "included".
|
|
1362
|
+
for (const f of this.importsOf(site.fileId)) {
|
|
1363
|
+
if (this.importsOf(f).has(cand.file_id)) return 56 + tie;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
const a = cand.path.split('/');
|
|
1367
|
+
const b = site.path.split('/');
|
|
1368
|
+
let common = 0;
|
|
1369
|
+
while (common < a.length - 1 && common < b.length - 1 && a[common] === b[common]) common++;
|
|
1370
|
+
if (common === a.length - 1 && common === b.length - 1) {
|
|
1371
|
+
// Same directory. In package-per-directory languages that is the same package: no import needed.
|
|
1372
|
+
return (SAME_DIR_PACKAGE.has(site.lang) ? 66 : 46) + ns + tie;
|
|
1373
|
+
}
|
|
1374
|
+
return 18 + Math.min(12, common * 3) + ns * 2 + tie;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// --- super.foo() / base.Foo() / parent::foo() ----------------------------------
|
|
1378
|
+
if (isSuperReceiver(recv, site.lang)) {
|
|
1379
|
+
if (!isMember || (callerC !== null && container === callerC.name)) return null;
|
|
1380
|
+
// (Which class of that name: the one the subclass's file imports, when there are copies.)
|
|
1381
|
+
if (inCallerAncestors()) return 92 + (container ? this.typeVisibility(container, cand, site) : 0) + tie;
|
|
1382
|
+
// A mixin calls up the MRO of whatever class it is mixed into (`super().dispatch()` in a
|
|
1383
|
+
// Django mixin): the parent is not declared here, so any other definition is a candidate.
|
|
1384
|
+
// (Only where a method resolution order exists: in JS / PHP / Java `super` is the declared parent.)
|
|
1385
|
+
if (fam !== 'python' && fam !== 'ruby') return null;
|
|
1386
|
+
if (callerC && container && this.relatedTo(callerC.name).desc.has(container)) return null;
|
|
1387
|
+
return 34 + tie;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// --- this.foo() / self.foo() ---------------------------------------------
|
|
1391
|
+
if (SELF_RECEIVERS.has(recv)) {
|
|
1392
|
+
if (!isMember) {
|
|
1393
|
+
// JS object-literal style: `this.helper()` where helper is a function in the same file.
|
|
1394
|
+
return fam === 'js' && sameFile && cand.kind === 'function' ? 36 + tie : null;
|
|
1395
|
+
}
|
|
1396
|
+
if (inCallerClass) return (sameFile ? 96 : 80) + tie;
|
|
1397
|
+
if (inCallerAncestors()) return 76 + (container ? this.typeVisibility(container, cand, site) : 0) + tie;
|
|
1398
|
+
// `function View() { this.lookup() }` with `View.prototype.lookup = ...` in the same file.
|
|
1399
|
+
if (!callerC && site.caller && container === site.caller.name && sameFile) return 90 + tie;
|
|
1400
|
+
if (callerC && container && this.relatedTo(callerC.name).desc.has(container)) return 56 + tie; // abstract hook
|
|
1401
|
+
// Inside a trait / mixin, `$this->save()` is a method of whatever class uses it: even a
|
|
1402
|
+
// common name is then worth a (name-only) candidate.
|
|
1403
|
+
const host = callerC && callerC.id !== null ? this.db.symbolById(callerC.id) : undefined;
|
|
1404
|
+
const mixin = !!host && (host.kind === 'trait' || (host.lang === 'ruby' && host.kind === 'namespace') || /Mixin$/.test(host.name));
|
|
1405
|
+
const e = this.memberEvidence(name, cand, container, site, null, mixin);
|
|
1406
|
+
return e >= (mixin ? 8 : 20) ? Math.min(46, 10 + e) + tie : null; // mixin-style
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// --- expression receiver: a.b().foo(), "x".foo() --------------------------
|
|
1410
|
+
if (recv === '?') {
|
|
1411
|
+
if (!isMember) return null;
|
|
1412
|
+
const e = this.memberEvidence(name, cand, container, site, null);
|
|
1413
|
+
// Nothing is known about the receiver: with several definitions of the name, being in the same
|
|
1414
|
+
// file (or imported) makes one of them likely, not certain.
|
|
1415
|
+
const cap = this.familyCount(name) === 1 ? 90 : 50;
|
|
1416
|
+
return e >= 15 ? Math.min(cap, 10 + e) + tie : null;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// --- named receiver: obj.foo(), Foo.bar(), pkg.Func(), ns::func() ---------
|
|
1420
|
+
const bare = recv.replace(/^[$@&*]+/, '');
|
|
1421
|
+
const last = bare.split(/\.|::|->|\\/).pop() ?? bare;
|
|
1422
|
+
const first = bare.split(/\.|::|->|\\/)[0] ?? bare;
|
|
1423
|
+
// console.log(), Math.max(), os.path.join(): runtime globals and external modules.
|
|
1424
|
+
if (BUILTIN_RECEIVERS[fam]?.has(first) && !this.isProjectType(first)) return null;
|
|
1425
|
+
const names = this.importedNames(site.fileId).names;
|
|
1426
|
+
if (EXTERNAL_IF_UNRESOLVED.has(fam) && names.get(first) === null) return null;
|
|
1427
|
+
if (site.lang === 'go') {
|
|
1428
|
+
// `pkg.Func()`, `pkg.Type{}`: the receiver is an imported package, which is a directory.
|
|
1429
|
+
const dir = this.goImports(site.fileId).get(first);
|
|
1430
|
+
if (dir === null) return null; // a package outside the project
|
|
1431
|
+
if (dir !== undefined) {
|
|
1432
|
+
if (dirOf(cand.path) !== dir) return null;
|
|
1433
|
+
if (bare === first) return isMember ? null : 94 + tie;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
if (container && (last === container || bare === container)) {
|
|
1437
|
+
// `Assert.Equal()` written without an outer class does not mean a class `Assert` nested inside
|
|
1438
|
+
// some other class (`Http2Utilities.Assert`), unless the call is made from inside that class.
|
|
1439
|
+
if (bare === container && parent) {
|
|
1440
|
+
const outer = this.parentOf(parent);
|
|
1441
|
+
if (outer && CONTAINER_KINDS.has(outer.kind) && outer.kind !== 'namespace') {
|
|
1442
|
+
const q = site.caller?.qname ?? '';
|
|
1443
|
+
// Inside the outer class the nested one is the nearest of that name: it shadows any other.
|
|
1444
|
+
if (q === outer.qname || q.startsWith(outer.qname + '.')) return 106 + tie;
|
|
1445
|
+
// (`import pkg.Outer.Inner;` makes the nested class nameable on its own.)
|
|
1446
|
+
if (!sameFile && names.get(container) !== cand.file_id) return null;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
return 92 + this.typeVisibility(container, cand, site) + tie; // static / qualified call
|
|
1450
|
+
}
|
|
1451
|
+
if (isMember) {
|
|
1452
|
+
// Static / class-level call through a subclass: Sub.inheritedMethod().
|
|
1453
|
+
if (container && /^[A-Z]/.test(last) && this.isProjectType(last) && this.relatedTo(last).anc.has(container)) {
|
|
1454
|
+
return 84 + tie;
|
|
1455
|
+
}
|
|
1456
|
+
const e = this.memberEvidence(name, cand, container, site, last);
|
|
1457
|
+
return e >= 15 ? 10 + e + tie : null;
|
|
1458
|
+
}
|
|
1459
|
+
// Rust macros are addressed through a crate / module path: tokio::select!, crate::cfg_rt!.
|
|
1460
|
+
if (cand.kind === 'macro' && site.lang === 'rust') return 70 + tie;
|
|
1461
|
+
const from = names.get(last);
|
|
1462
|
+
if (from !== undefined && from !== null) {
|
|
1463
|
+
if (from === cand.file_id) return 94 + tie; // import * as ns / import module
|
|
1464
|
+
// Python `from django.utils import text`: the name is a submodule of the imported package.
|
|
1465
|
+
if (fam === 'python') {
|
|
1466
|
+
const pkg = this.filePath(from);
|
|
1467
|
+
if (pkg && /(^|\/)__init__\.py$/.test(pkg) && cand.path === dirOf(pkg) + '/' + last + '.py') return 92 + tie;
|
|
1468
|
+
}
|
|
1469
|
+
// The namespace is a package whose __init__ / index re-exports the definition.
|
|
1470
|
+
return this.importsOf(from).has(cand.file_id) ? 72 + tie : null;
|
|
1471
|
+
}
|
|
1472
|
+
if (moduleLike(last, cand.path)) return (this.importsOf(site.fileId).has(cand.file_id) ? 86 : 74) + tie;
|
|
1473
|
+
return null;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
/**
|
|
1477
|
+
* Small bonus for the definition of type `typeName` that the calling file
|
|
1478
|
+
* can actually see (same file, imported by name, or nearest in the tree).
|
|
1479
|
+
* Decides between identical copies of a class (vendored forks, monorepo
|
|
1480
|
+
* packages).
|
|
1481
|
+
*/
|
|
1482
|
+
private typeVisibility(typeName: string, cand: SymbolRow, site: CallSite): number {
|
|
1483
|
+
if (cand.file_id === site.fileId) return 8;
|
|
1484
|
+
const from = this.importedNames(site.fileId).names.get(typeName);
|
|
1485
|
+
if (from === cand.file_id) return 8;
|
|
1486
|
+
if (this.importsOf(site.fileId).has(cand.file_id)) return 6;
|
|
1487
|
+
if (from !== undefined && from !== null && this.importsOf(from).has(cand.file_id)) return 5;
|
|
1488
|
+
const a = cand.path.split('/');
|
|
1489
|
+
const b = site.path.split('/');
|
|
1490
|
+
let common = 0;
|
|
1491
|
+
while (common < a.length - 1 && common < b.length - 1 && a[common] === b[common]) common++;
|
|
1492
|
+
return Math.min(4, common);
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
private fieldTypeCache = new Map<string, string | null>();
|
|
1496
|
+
|
|
1497
|
+
/** Declared type of `container.field` (also looking at supertypes), or null if unknown / ambiguous. */
|
|
1498
|
+
private fieldType(container: string, field: string): string | null {
|
|
1499
|
+
// Go field types may be written `pkg.Type`.
|
|
1500
|
+
const dot = container.lastIndexOf('.');
|
|
1501
|
+
if (dot !== -1) container = container.slice(dot + 1);
|
|
1502
|
+
const key = container + '.' + field;
|
|
1503
|
+
let t = this.fieldTypeCache.get(key);
|
|
1504
|
+
if (t !== undefined) return t;
|
|
1505
|
+
t = null;
|
|
1506
|
+
const owners = [container, ...[...this.relatedTo(container).anc].slice(0, 8)];
|
|
1507
|
+
for (const owner of owners) {
|
|
1508
|
+
const rows = this.db.all<{ type: string }>(
|
|
1509
|
+
`SELECT DISTINCT type FROM field_types WHERE container = ? AND field = ? LIMIT 3`, owner, field
|
|
1510
|
+
);
|
|
1511
|
+
if (rows.length === 1) { t = rows[0]!.type; break; }
|
|
1512
|
+
if (rows.length > 1) break; // same class name with different field types: do not guess
|
|
1513
|
+
}
|
|
1514
|
+
if (t === null) {
|
|
1515
|
+
// A getter / property with a declared type reads like a field: `get grid(): Grid`, `@property def conn(self) -> Connection`.
|
|
1516
|
+
for (const owner of owners) {
|
|
1517
|
+
const getters = this.db.symbolsByName(field).filter(
|
|
1518
|
+
s =>
|
|
1519
|
+
s.ret && this.containerName(s) === owner &&
|
|
1520
|
+
(s.kind === 'property' || /^(?:[\w@.()]+\s+)*get\s/.test(s.signature) || /^@(?:cached_)?property\b/.test(s.signature))
|
|
1521
|
+
);
|
|
1522
|
+
if (getters.length === 0) continue;
|
|
1523
|
+
const types = new Set(getters.map(g => g.ret));
|
|
1524
|
+
if (types.size === 1) t = getters[0]!.ret;
|
|
1525
|
+
break;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
this.fieldTypeCache.set(key, t);
|
|
1529
|
+
return t;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
/**
|
|
1533
|
+
* Fill in the receiver type from class field declarations when the
|
|
1534
|
+
* extractor could not (the class is declared in another file: C++ headers,
|
|
1535
|
+
* partial classes, Go structs elsewhere in the package).
|
|
1536
|
+
*/
|
|
1537
|
+
private withFieldType(site: CallSite, depth = 0): CallSite {
|
|
1538
|
+
if (site.rtype && site.rtype.includes('#')) {
|
|
1539
|
+
// Deferred by the extractor: the declared type of field `f` of type `T` ("T#f"), defined in another file.
|
|
1540
|
+
const cut = site.rtype.indexOf('#');
|
|
1541
|
+
const t = this.fieldType(site.rtype.slice(0, cut), site.rtype.slice(cut + 1));
|
|
1542
|
+
site = { ...site, rtype: t };
|
|
1543
|
+
}
|
|
1544
|
+
if (site.rtype) {
|
|
1545
|
+
// The declared type may be written with an import alias (`const q: JobQueue`).
|
|
1546
|
+
const original = this.importedNames(site.fileId).aliases.get(site.rtype);
|
|
1547
|
+
return original ? { ...site, rtype: original } : site;
|
|
1548
|
+
}
|
|
1549
|
+
const r = site.receiver;
|
|
1550
|
+
if (r && site.via) {
|
|
1551
|
+
// The receiver is produced by another call (a chain link, or a variable initialised from a call).
|
|
1552
|
+
const t = this.chainType(site, depth);
|
|
1553
|
+
if (t) return { ...site, rtype: t };
|
|
1554
|
+
if (r === '?') return site;
|
|
1555
|
+
}
|
|
1556
|
+
// An imported module-level instance: `from signals import pre_save` ... `pre_save.send()`.
|
|
1557
|
+
if (r && /^[A-Za-z_$][\w$]*$/.test(r) && !SELF_RECEIVERS.has(r)) {
|
|
1558
|
+
const from = this.importedNames(site.fileId).names.get(r);
|
|
1559
|
+
if (from !== undefined && from !== null) {
|
|
1560
|
+
const original = this.importedNames(site.fileId).aliases.get(r) ?? r;
|
|
1561
|
+
const t = this.instanceType(from, original);
|
|
1562
|
+
if (t) return { ...site, rtype: t };
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
if (!r || r === '?' || !site.caller) return site;
|
|
1566
|
+
let field: string | null = null;
|
|
1567
|
+
const m = /^(?:this|self|\$this|base|Self)(?:\.|->|::)([A-Za-z_]\w*)$/.exec(r) ?? /^@([A-Za-z_]\w*)$/.exec(r);
|
|
1568
|
+
if (m) field = m[1]!;
|
|
1569
|
+
else {
|
|
1570
|
+
// self.shared.inject.push(): follow the declared type of each field in turn.
|
|
1571
|
+
const deep = /^(?:this|self|\$this)((?:(?:\.|->)[A-Za-z_]\w*){2,4})$/.exec(r);
|
|
1572
|
+
if (deep) {
|
|
1573
|
+
const container = this.enclosingContainer(site.caller);
|
|
1574
|
+
let type: string | null = container ? container.name : null;
|
|
1575
|
+
for (const f of deep[1]!.split(/\.|->/).filter(Boolean)) {
|
|
1576
|
+
type = type ? this.fieldType(type, f) : null;
|
|
1577
|
+
if (!type) break;
|
|
1578
|
+
}
|
|
1579
|
+
return type ? { ...site, rtype: type } : site;
|
|
1580
|
+
}
|
|
1581
|
+
if (IMPLICIT_THIS.has(site.lang) && /^[A-Za-z_]\w*$/.test(r) && !SELF_RECEIVERS.has(r)) field = r;
|
|
1582
|
+
}
|
|
1583
|
+
if (!field) return site;
|
|
1584
|
+
const container = this.enclosingContainer(site.caller);
|
|
1585
|
+
if (!container) return site;
|
|
1586
|
+
const t = this.fieldType(container.name, field);
|
|
1587
|
+
return t ? { ...site, rtype: t } : site;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
private nameIndexCache = new Map<string, NameIndex>();
|
|
1591
|
+
private tieCache = new Map<number, number>();
|
|
1592
|
+
private containerCache = new Map<number, { name: string; id: number | null } | null>();
|
|
1593
|
+
|
|
1594
|
+
/** Candidates of a name grouped for fast pre-selection (only built for crowded names). */
|
|
1595
|
+
private nameIndex(name: string): NameIndex {
|
|
1596
|
+
let idx = this.nameIndexCache.get(name);
|
|
1597
|
+
if (idx) return idx;
|
|
1598
|
+
const all = this.candidates(name);
|
|
1599
|
+
idx = { all, byContainer: new Map(), byFile: new Map(), free: [], containers: [], byReceiver: new Map() };
|
|
1600
|
+
for (const c of all) {
|
|
1601
|
+
const f = idx.byFile.get(c.file_id);
|
|
1602
|
+
if (f) f.push(c);
|
|
1603
|
+
else idx.byFile.set(c.file_id, [c]);
|
|
1604
|
+
const parent = this.parentOf(c);
|
|
1605
|
+
const inContainer = parent !== null && CONTAINER_KINDS.has(parent.kind);
|
|
1606
|
+
const nsFunction = langFamily(c.lang) === 'c' && parent !== null && parent.kind === 'namespace' && c.kind !== 'method';
|
|
1607
|
+
const isMember = !nsFunction && (c.kind === 'method' || (inContainer && c.kind !== 'class' && c.kind !== 'struct' && c.kind !== 'interface'));
|
|
1608
|
+
const container = isMember ? this.containerName(c) : null;
|
|
1609
|
+
if (container) {
|
|
1610
|
+
const list = idx.byContainer.get(container);
|
|
1611
|
+
if (list) list.push(c);
|
|
1612
|
+
else idx.byContainer.set(container, [c]);
|
|
1613
|
+
} else {
|
|
1614
|
+
idx.free.push(c);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
idx.containers = [...idx.byContainer.keys()];
|
|
1618
|
+
this.nameIndexCache.set(name, idx);
|
|
1619
|
+
return idx;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/**
|
|
1623
|
+
* Candidates that can possibly score for this call site. Purely an
|
|
1624
|
+
* optimisation for names with hundreds of definitions (`run`, `add`,
|
|
1625
|
+
* `get`): it must return a superset of what score() accepts.
|
|
1626
|
+
*/
|
|
1627
|
+
private preselect(name: string, site: CallSite): SymbolRow[] {
|
|
1628
|
+
const idx = this.nameIndex(name);
|
|
1629
|
+
const recv = site.receiver;
|
|
1630
|
+
const fam = langFamily(site.lang);
|
|
1631
|
+
const common = COMMON_MEMBER_NAMES.has(name.toLowerCase());
|
|
1632
|
+
const out = new Set<SymbolRow>();
|
|
1633
|
+
const add = (list: SymbolRow[] | undefined): void => {
|
|
1634
|
+
if (list) for (const s of list) out.add(s);
|
|
1635
|
+
};
|
|
1636
|
+
const addRelated = (type: string): void => {
|
|
1637
|
+
add(idx.byContainer.get(type));
|
|
1638
|
+
const rel = this.relatedTo(type);
|
|
1639
|
+
for (const a of rel.anc) add(idx.byContainer.get(a));
|
|
1640
|
+
// The type (or a supertype) declares the member: overrides further down are the same
|
|
1641
|
+
// family and always score lower, so a hub interface (`IDisposable.dispose` with thousands
|
|
1642
|
+
// of implementers) does not need them scored at every call site.
|
|
1643
|
+
if (out.size > 0 && rel.desc.size > 50) return;
|
|
1644
|
+
if (rel.desc.size < idx.containers.length) {
|
|
1645
|
+
for (const d of rel.desc) add(idx.byContainer.get(d));
|
|
1646
|
+
} else {
|
|
1647
|
+
for (const c of idx.containers) if (rel.desc.has(c)) add(idx.byContainer.get(c));
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
const callerC = site.caller ? this.enclosingContainer(site.caller) : null;
|
|
1651
|
+
|
|
1652
|
+
if (site.rtype && recv && !SELF_RECEIVERS.has(recv)) {
|
|
1653
|
+
addRelated(site.rtype);
|
|
1654
|
+
return out.size === 0 && OPEN_TYPES.has(site.lang) ? idx.all : [...out];
|
|
1655
|
+
}
|
|
1656
|
+
if (!recv) {
|
|
1657
|
+
if (callerC) addRelated(callerC.name);
|
|
1658
|
+
add(idx.byFile.get(site.fileId));
|
|
1659
|
+
const imported = this.importedNames(site.fileId);
|
|
1660
|
+
const from = imported.names.get(name);
|
|
1661
|
+
if (from !== undefined && from !== null) {
|
|
1662
|
+
add(idx.byFile.get(from));
|
|
1663
|
+
for (const f of this.importsOf(from)) add(idx.byFile.get(f));
|
|
1664
|
+
}
|
|
1665
|
+
if (STRICT_IMPORTS.has(fam)) {
|
|
1666
|
+
const imports = this.importsOf(site.fileId);
|
|
1667
|
+
if (site.lang === 'javascript' && imports.size === 0 && imported.names.size === 0) return idx.all;
|
|
1668
|
+
for (const f of imported.wildcards) add(idx.byFile.get(f));
|
|
1669
|
+
for (const f of imports) add(idx.byFile.get(f));
|
|
1670
|
+
return [...out];
|
|
1671
|
+
}
|
|
1672
|
+
if (site.lang === 'go') {
|
|
1673
|
+
const dir = dirOf(site.path);
|
|
1674
|
+
for (const s of idx.free) if (dirOf(s.path) === dir) out.add(s);
|
|
1675
|
+
return [...out];
|
|
1676
|
+
}
|
|
1677
|
+
if (!common && (site.lang === 'ruby' || site.lang === 'php')) return idx.all;
|
|
1678
|
+
add(idx.free);
|
|
1679
|
+
return [...out];
|
|
1680
|
+
}
|
|
1681
|
+
if (SELF_RECEIVERS.has(recv)) {
|
|
1682
|
+
if (!common) return idx.all;
|
|
1683
|
+
if (callerC) addRelated(callerC.name);
|
|
1684
|
+
add(idx.byFile.get(site.fileId));
|
|
1685
|
+
return [...out];
|
|
1686
|
+
}
|
|
1687
|
+
if (recv === '?') return common ? [] : idx.all;
|
|
1688
|
+
if (!common) return idx.all;
|
|
1689
|
+
|
|
1690
|
+
// Named receiver + common method name: only the receiver's name can link it.
|
|
1691
|
+
const bare = recv.replace(/^[$@&*]+/, '');
|
|
1692
|
+
const last = bare.split(/\.|::|->|\\/).pop() ?? bare;
|
|
1693
|
+
const lastLc = last.toLowerCase();
|
|
1694
|
+
const generic = GENERIC_RECEIVERS.has(lastLc);
|
|
1695
|
+
const typeAncestors = /^[A-Z]/.test(last) && this.isProjectType(last) ? this.relatedTo(last).anc : null;
|
|
1696
|
+
// The same receiver names recur at thousands of sites: match them against the containers once.
|
|
1697
|
+
const key = bare + '\0' + last;
|
|
1698
|
+
let matched = idx.byReceiver.get(key);
|
|
1699
|
+
if (!matched) {
|
|
1700
|
+
matched = [];
|
|
1701
|
+
for (const c of idx.containers) {
|
|
1702
|
+
if (
|
|
1703
|
+
c === last || bare === c ||
|
|
1704
|
+
(typeAncestors !== null && typeAncestors.has(c)) ||
|
|
1705
|
+
(!generic && (looselyNamed(last, c) || (last.length <= 2 && c.toLowerCase().startsWith(lastLc))))
|
|
1706
|
+
) {
|
|
1707
|
+
matched.push(...(idx.byContainer.get(c) ?? []));
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
matched.push(...idx.free);
|
|
1711
|
+
idx.byReceiver.set(key, matched);
|
|
1712
|
+
}
|
|
1713
|
+
return matched;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/**
|
|
1717
|
+
* True when a member call's receiver gives no usable type information: an
|
|
1718
|
+
* expression (`a.b().foo()`) or an untyped variable that is neither
|
|
1719
|
+
* this/self, a runtime global, nor an external module. Such call sites
|
|
1720
|
+
* cannot be attributed to any one definition of a common method name.
|
|
1721
|
+
*/
|
|
1722
|
+
isOpaqueReceiver(rawSite: CallSite): boolean {
|
|
1723
|
+
const site = this.withFieldType(rawSite);
|
|
1724
|
+
const recv = site.receiver;
|
|
1725
|
+
if (!recv || site.rtype || SELF_RECEIVERS.has(recv)) return false;
|
|
1726
|
+
if (recv === '?') return true;
|
|
1727
|
+
const fam = langFamily(site.lang);
|
|
1728
|
+
const first = recv.replace(/^[$@&*]+/, '').split(/\.|::|->|\\/)[0] ?? recv;
|
|
1729
|
+
if (BUILTIN_RECEIVERS[fam]?.has(first) && !this.isProjectType(first)) return false;
|
|
1730
|
+
if (EXTERNAL_IF_UNRESOLVED.has(fam) && this.importedNames(site.fileId).names.get(first) === null) return false;
|
|
1731
|
+
// A capitalised receiver is a type (static call): a project type is handled by the
|
|
1732
|
+
// resolver, anything else is an external class. Neither is "unknown".
|
|
1733
|
+
if (/^[A-Z]/.test(first) && !first.includes('_')) return false;
|
|
1734
|
+
return true;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
private chainTypeCache = new Map<number, string | null>();
|
|
1738
|
+
|
|
1739
|
+
/**
|
|
1740
|
+
* Type of an expression receiver that is itself a call: the return type of
|
|
1741
|
+
* that call's target (or the class, for constructor calls). Follows up to
|
|
1742
|
+
* four links of a chain.
|
|
1743
|
+
*/
|
|
1744
|
+
private chainType(site: CallSite, depth: number): string | null {
|
|
1745
|
+
const via = site.via;
|
|
1746
|
+
if (!via || depth > 4) return null;
|
|
1747
|
+
const cached = this.chainTypeCache.get(via);
|
|
1748
|
+
if (cached !== undefined) return cached;
|
|
1749
|
+
this.chainTypeCache.set(via, null); // guards against cycles
|
|
1750
|
+
const row = this.db.get<{ name: string; receiver: string | null; rtype: string | null; via: number | null }>(
|
|
1751
|
+
`SELECT n.name, c.receiver, c.rtype, c.via FROM calls c JOIN names n ON n.id = c.name_id WHERE c.rowid = ?`, via
|
|
1752
|
+
);
|
|
1753
|
+
let type: string | null = null;
|
|
1754
|
+
if (row) {
|
|
1755
|
+
const res = this.resolve(
|
|
1756
|
+
row.name,
|
|
1757
|
+
{ fileId: site.fileId, path: site.path, lang: site.lang, receiver: row.receiver, rtype: row.rtype, via: row.via, caller: site.caller },
|
|
1758
|
+
depth + 1
|
|
1759
|
+
);
|
|
1760
|
+
const top = res.candidates[0];
|
|
1761
|
+
if (top) {
|
|
1762
|
+
if (TYPE_KINDS.has(top.sym.kind)) type = top.sym.name; // Foo(...) / new Foo(...)
|
|
1763
|
+
else if (top.sym.kind === 'method' && this.containerName(top.sym) === top.sym.name) type = top.sym.name; // constructor
|
|
1764
|
+
else {
|
|
1765
|
+
// Prefer a declared return type from anywhere in the target's family
|
|
1766
|
+
// (the interface may declare it while the implementation infers it).
|
|
1767
|
+
type = top.sym.ret;
|
|
1768
|
+
if (!type) {
|
|
1769
|
+
for (const c of res.candidates) {
|
|
1770
|
+
if (c.sym.ret && this.familyOf(top.sym).has(c.sym.id)) { type = c.sym.ret; break; }
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
this.chainTypeCache.set(via, type);
|
|
1777
|
+
return type;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
/** Rank the definitions a call to `name` at `site` may refer to. */
|
|
1781
|
+
resolve(name: string, rawSite: CallSite, depth = 0, typesOnly = false): Resolution {
|
|
1782
|
+
// The calling file may know the definition under another name:
|
|
1783
|
+
// `import { X as Y }`, `import express from './express'`, `const express = require('./express')`.
|
|
1784
|
+
if (!rawSite.receiver) {
|
|
1785
|
+
const imported = this.importedNames(rawSite.fileId);
|
|
1786
|
+
const from = imported.names.get(name);
|
|
1787
|
+
if (from !== undefined && from !== null) {
|
|
1788
|
+
const original = imported.aliases.get(name);
|
|
1789
|
+
// ... or the file it is imported from re-exports it under this name (`export { parseId as getId } from`).
|
|
1790
|
+
let real: string | null = original ?? this.importedNames(from).aliases.get(name) ?? null;
|
|
1791
|
+
if (!real && !this.candidates(name).some(c => c.file_id === from)) {
|
|
1792
|
+
real = this.defaultExportOf(from);
|
|
1793
|
+
if (real === '*') {
|
|
1794
|
+
// The imported file re-exports another module's default export.
|
|
1795
|
+
real = null;
|
|
1796
|
+
for (const f of this.importsOf(from)) {
|
|
1797
|
+
const d = this.defaultExportOf(f);
|
|
1798
|
+
if (d && d !== '*') { real = d; break; }
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
if (real && real !== name) {
|
|
1803
|
+
const hits = this.candidates(real).filter(c => c.file_id === from || this.importsOf(from).has(c.file_id));
|
|
1804
|
+
if (hits.length) {
|
|
1805
|
+
const scored = hits
|
|
1806
|
+
.map(sym => ({ sym, score: (sym.file_id === from ? 96 : 62) + definitionScore(sym) * 0.5 }))
|
|
1807
|
+
.sort((a, b) => b.score - a.score);
|
|
1808
|
+
return { candidates: scored, confident: true };
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
let all = this.candidates(name);
|
|
1815
|
+
// Resolving a type name (a supertype in `extends X`): constructors and same-named members are not answers.
|
|
1816
|
+
// (Ruby modules are mixins: `include Foo` names a module.)
|
|
1817
|
+
if (typesOnly) all = all.filter(c => TYPE_KINDS.has(c.kind) || (c.kind === 'namespace' && c.lang === 'ruby'));
|
|
1818
|
+
if (all.length === 0) return { candidates: [], confident: false };
|
|
1819
|
+
const typed = this.goQualified(this.withFieldType(rawSite, depth));
|
|
1820
|
+
if (!typed) return { candidates: [], confident: false }; // receiver of a type from an external Go package
|
|
1821
|
+
const site = typed;
|
|
1822
|
+
let cands = all.length > PRESELECT_THRESHOLD && !typesOnly ? this.preselect(name, site) : all;
|
|
1823
|
+
if (cands.length === 0) return { candidates: [], confident: false };
|
|
1824
|
+
|
|
1825
|
+
// A bare name imported from outside the project cannot be a project symbol.
|
|
1826
|
+
if (!site.receiver && EXTERNAL_IF_UNRESOLVED.has(langFamily(site.lang))) {
|
|
1827
|
+
const from = this.importedNames(site.fileId).names.get(name);
|
|
1828
|
+
if (from === null && !all.some(c => c.file_id === site.fileId)) return { candidates: [], confident: false };
|
|
1829
|
+
}
|
|
1830
|
+
// A receiver whose declared type is not defined in this project is external too
|
|
1831
|
+
// (except where extension methods can add project methods to foreign types).
|
|
1832
|
+
if (
|
|
1833
|
+
site.rtype && site.receiver && !SELF_RECEIVERS.has(site.receiver) &&
|
|
1834
|
+
!OPEN_TYPES.has(site.lang) && !this.isProjectType(site.rtype)
|
|
1835
|
+
) {
|
|
1836
|
+
return { candidates: [], confident: false };
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
const scored: Array<{ sym: SymbolRow; score: number }> = [];
|
|
1840
|
+
for (const sym of cands) {
|
|
1841
|
+
const score = this.score(name, sym, site);
|
|
1842
|
+
if (score !== null) scored.push({ sym, score });
|
|
1843
|
+
}
|
|
1844
|
+
if (scored.length === 0) return { candidates: [], confident: false };
|
|
1845
|
+
if (scored.length > 1) {
|
|
1846
|
+
// `record.topic()` with both a `topic` field and a `topic()` accessor: the call means the method.
|
|
1847
|
+
const callables = new Set<string>();
|
|
1848
|
+
for (const c of scored) if (!VALUE_KINDS.has(c.sym.kind)) callables.add(c.sym.file_id + ':' + c.sym.qname);
|
|
1849
|
+
if (callables.size) {
|
|
1850
|
+
for (let i = scored.length - 1; i >= 0; i--) {
|
|
1851
|
+
const s = scored[i]!.sym;
|
|
1852
|
+
if (VALUE_KINDS.has(s.kind) && callables.has(s.file_id + ':' + s.qname)) scored.splice(i, 1);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1857
|
+
const top = scored[0]!;
|
|
1858
|
+
// The runner-up that matters is the best candidate that is a genuinely
|
|
1859
|
+
// different target (not an overload / interface twin of the winner).
|
|
1860
|
+
const family = scored.length > 1 ? this.familyOf(top.sym) : null;
|
|
1861
|
+
const second = scored.find(
|
|
1862
|
+
(c, i) =>
|
|
1863
|
+
i > 0 && !family!.has(c.sym.id) &&
|
|
1864
|
+
// Copies of one definition are the same answer: build variants (#[cfg], foo_linux.go /
|
|
1865
|
+
// foo_windows.go, #ifdef) score alike, and for vendored forks the winner is simply the copy
|
|
1866
|
+
// this file can see. Neither makes the call ambiguous.
|
|
1867
|
+
// (Only those: two crates that each define `Error::kind(&self) -> &ErrorKind` are different targets.)
|
|
1868
|
+
!(
|
|
1869
|
+
c.sym.qname === top.sym.qname && c.sym.kind === top.sym.kind && c.sym.signature === top.sym.signature &&
|
|
1870
|
+
(dirOf(c.sym.path) === dirOf(top.sym.path) || isVendoredPath(c.sym.path) !== isVendoredPath(top.sym.path))
|
|
1871
|
+
)
|
|
1872
|
+
);
|
|
1873
|
+
// Two same-named members of same-named types (a forked `URI.toString`, a re-declared `ILogger.warn`)
|
|
1874
|
+
// differ only in which type the calling file can see: its import / proximity bonus decides.
|
|
1875
|
+
const sameNamed = second !== undefined && second.sym.qname === top.sym.qname && second.sym.kind === top.sym.kind;
|
|
1876
|
+
let confident = top.score >= CONFIDENT_SCORE && (!second || top.score - second.score >= (sameNamed ? 4 : CONFIDENT_GAP));
|
|
1877
|
+
// An untyped receiver whose name fits two unrelated classes equally (`user.set_password()` with both
|
|
1878
|
+
// AbstractBaseUser and AnonymousUser defining it): the name decides nothing, so neither is a fact.
|
|
1879
|
+
if (
|
|
1880
|
+
confident && second && site.receiver && !site.rtype && site.receiver !== '?' &&
|
|
1881
|
+
!SELF_RECEIVERS.has(site.receiver)
|
|
1882
|
+
) {
|
|
1883
|
+
const last = site.receiver.replace(/^[$@&*]+/, '').split(/\.|::|->|\\/).pop() ?? '';
|
|
1884
|
+
const tc = this.containerName(top.sym);
|
|
1885
|
+
const sc = this.containerName(second.sym);
|
|
1886
|
+
if (last && tc && sc && tc !== sc && tc !== last && looselyNamed(last, tc) && looselyNamed(last, sc)) confident = false;
|
|
1887
|
+
}
|
|
1888
|
+
// A bare call to a distinctively named function or class that the project defines exactly once
|
|
1889
|
+
// (`new AntiforgeryTokenSet()`, `lookupKeyReadOrReply()`): proximity is irrelevant, there is nothing else it can be.
|
|
1890
|
+
if (
|
|
1891
|
+
!confident && !second && !site.receiver && top.score >= 18 &&
|
|
1892
|
+
!STRICT_IMPORTS.has(langFamily(site.lang)) &&
|
|
1893
|
+
(splitIdentifier(name).length >= 2 || name.length >= 10) && this.familyCount(name) === 1
|
|
1894
|
+
) {
|
|
1895
|
+
confident = true;
|
|
1896
|
+
}
|
|
1897
|
+
// Swift / Dart / Kotlin construct by calling the type: `Session(...)`. With a single type of that
|
|
1898
|
+
// name in the project there is nothing else it can be.
|
|
1899
|
+
if (
|
|
1900
|
+
!confident && !second && !site.receiver && TYPE_KINDS.has(top.sym.kind) && /^[A-Z]/.test(name) &&
|
|
1901
|
+
(site.lang === 'swift' || site.lang === 'dart' || site.lang === 'kotlin')
|
|
1902
|
+
) {
|
|
1903
|
+
confident = true;
|
|
1904
|
+
}
|
|
1905
|
+
return { candidates: scored, confident };
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
interface LooseName {
|
|
1910
|
+
/** Lowercase, without leading / trailing underscores. */
|
|
1911
|
+
lc: string;
|
|
1912
|
+
words: string[];
|
|
1913
|
+
initials: string;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
const looseNames = new Map<string, LooseName>();
|
|
1917
|
+
|
|
1918
|
+
/** Per-name preparation for looselyNamed (the pair test itself runs millions of times on crowded names). */
|
|
1919
|
+
function looseName(name: string): LooseName {
|
|
1920
|
+
let info = looseNames.get(name);
|
|
1921
|
+
if (!info) {
|
|
1922
|
+
const words = splitIdentifier(name);
|
|
1923
|
+
info = { lc: name.toLowerCase().replace(/^_+|_+$/g, ''), words, initials: words.map(w => w[0]).join('') };
|
|
1924
|
+
if (looseNames.size > 300_000) looseNames.clear();
|
|
1925
|
+
looseNames.set(name, info);
|
|
1926
|
+
}
|
|
1927
|
+
return info;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
/** `db` ~ `Database`, `userRepo` ~ `UserRepository`, `parser` ~ `SourceParser`. */
|
|
1931
|
+
function looselyNamed(receiver: string, container: string): boolean {
|
|
1932
|
+
const ri = looseName(receiver);
|
|
1933
|
+
const r = ri.lc;
|
|
1934
|
+
if (r.length < 2) return false;
|
|
1935
|
+
const ci = looseName(container);
|
|
1936
|
+
const c = container.length === ci.lc.length ? ci.lc : container.toLowerCase();
|
|
1937
|
+
if (r === c) return true;
|
|
1938
|
+
if (r.length >= 3 && (c.includes(r) || r.includes(c))) return true;
|
|
1939
|
+
const rw = ri.words;
|
|
1940
|
+
const cw = ci.words;
|
|
1941
|
+
if (rw.length === 0 || cw.length === 0) return false;
|
|
1942
|
+
// Every receiver word is a prefix of some container word (repo -> repository).
|
|
1943
|
+
let all = true;
|
|
1944
|
+
for (const w of rw) {
|
|
1945
|
+
if (w.length < 2) { all = false; break; }
|
|
1946
|
+
let found = false;
|
|
1947
|
+
for (const x of cw) if (x.startsWith(w)) { found = true; break; }
|
|
1948
|
+
if (!found) { all = false; break; }
|
|
1949
|
+
}
|
|
1950
|
+
if (all) return true;
|
|
1951
|
+
// Initialism: `sp` -> SourceParser.
|
|
1952
|
+
return r === ci.initials;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
/** Points for sharing leading namespace segments: `A.B.x` and `A.B.y` -> 12, `A.x` and `A.y` -> 6. */
|
|
1956
|
+
function namespaceAffinity(callerQname: string, candQname: string): number {
|
|
1957
|
+
const a = callerQname.split('.');
|
|
1958
|
+
const b = candQname.split('.');
|
|
1959
|
+
let common = 0;
|
|
1960
|
+
while (common < a.length - 1 && common < b.length - 1 && a[common] === b[common]) common++;
|
|
1961
|
+
return Math.min(3, common) * 6;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
/** Receiver names the module/package a function lives in. */
|
|
1965
|
+
function moduleLike(receiver: string, path: string): boolean {
|
|
1966
|
+
const r = receiver.toLowerCase();
|
|
1967
|
+
const parts = path.toLowerCase().split('/');
|
|
1968
|
+
const file = parts.pop()!.replace(/\.[a-z0-9]+$/, '');
|
|
1969
|
+
if (file === r || file.replace(/[-_]/g, '') === r.replace(/[-_]/g, '')) return true;
|
|
1970
|
+
const dir = parts.pop();
|
|
1971
|
+
return dir !== undefined && (dir === r || dir.replace(/[-_]/g, '') === r.replace(/[-_]/g, ''));
|
|
1972
|
+
}
|