graph-indexer 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +442 -0
- package/README.md +54 -11
- package/SECURITY.md +17 -0
- package/colbert.mjs +168 -0
- package/config.mjs +246 -21
- package/embeddings.mjs +90 -8
- package/engine/binary.mjs +38 -0
- package/engine/memory.mjs +214 -1
- package/engine/sqlite.mjs +278 -5
- package/enrichment.mjs +95 -0
- package/indexer.mjs +254 -9
- package/init.mjs +25 -2
- package/mcp/centrality.mjs +117 -0
- package/mcp/resolver.mjs +116 -0
- package/mcp/symbolgraph.mjs +137 -0
- package/mcp/taint.mjs +225 -0
- package/mcp/tools.mjs +444 -19
- package/mcp/topology.mjs +138 -13
- package/mcp-server.mjs +26 -2
- package/package.json +29 -21
- package/parse/extractor.mjs +17 -6
- package/parse/interprocedural.mjs +134 -0
- package/parse/languages.mjs +221 -57
- package/parse/metadata.mjs +46 -0
- package/parse/scip.mjs +391 -0
- package/parse/taint-patterns.mjs +119 -0
- package/prompts/CORE.md +9 -4
- package/prompts/frameworks/FASTAPI_DJANGO.md +1 -0
- package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +1 -0
- package/prompts/frameworks/REACT.md +1 -1
- package/prompts/frameworks/SPRING_BOOT.md +1 -0
- package/prompts/languages/GO.md +1 -0
- package/prompts/languages/JAVA.md +1 -1
- package/prompts/languages/PYTHON.md +1 -1
- package/prompts/languages/RUST.md +1 -1
- package/seal.mjs +400 -0
- package/search-core.mjs +151 -1
- package/search-sparse.mjs +183 -0
- package/storage.mjs +5 -0
- package/watch-daemon.mjs +7 -2
package/parse/languages.mjs
CHANGED
|
@@ -1,86 +1,250 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file parse/languages.mjs
|
|
3
|
-
* @description Language registry:
|
|
4
|
-
* EXTENSIONS, and getParserForFile.
|
|
3
|
+
* @description Language registry: Tree-sitter grammar loading, LANGUAGE_MAP,
|
|
4
|
+
* EXTENSIONS, and getParserForFile. Grammar packages are NOT bundled
|
|
5
|
+
* dependencies of graph-indexer (they were pulled via npm's
|
|
6
|
+
* `optionalDependencies`, which installs eagerly regardless of the
|
|
7
|
+
* name — that shipped all 16 grammars, ~250 MB, to every install even
|
|
8
|
+
* when a repo only uses one or two languages). Instead, `GRAMMAR_PACKAGES`
|
|
9
|
+
* is the single-sourced registry of what each language needs, and
|
|
10
|
+
* `ensureLanguagesReady()` resolves each package: ambient node_modules
|
|
11
|
+
* first (covers this repo's own devDependencies, or a project that
|
|
12
|
+
* added a grammar directly), then a scoped `.graph-indexer/node_modules`
|
|
13
|
+
* install dir (works for ANY project — Node or not, npx-run or not,
|
|
14
|
+
* since it's keyed off `--repo`, not ambient resolution), auto-
|
|
15
|
+
* installing missing packages there on first use.
|
|
5
16
|
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
6
17
|
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
7
18
|
* @license MIT
|
|
8
19
|
*/
|
|
9
20
|
import fs from 'fs';
|
|
10
21
|
import path from 'path';
|
|
11
|
-
import
|
|
22
|
+
import { createRequire } from 'module';
|
|
23
|
+
import { pathToFileURL } from 'url';
|
|
24
|
+
import { spawnSync } from 'child_process';
|
|
12
25
|
import Parser from 'tree-sitter';
|
|
26
|
+
import { dataDir } from '../layout.mjs';
|
|
13
27
|
|
|
14
28
|
export const OLLAMA_HOST = process.env.OLLAMA_HOST || "http://localhost:11434";
|
|
15
29
|
|
|
16
|
-
// ───
|
|
30
|
+
// ─── Grammar registry (single source of truth for versions) ───────────────────
|
|
31
|
+
// Keyed by the same language keys used by init.mjs's LANGUAGES list and
|
|
32
|
+
// `.graph-indexer.json`'s `languages` array. 'css' covers two packages (CSS and
|
|
33
|
+
// SCSS share one user-facing "CSS / SCSS" selection).
|
|
34
|
+
export const GRAMMAR_PACKAGES = {
|
|
35
|
+
typescript: [{ pkg: 'tree-sitter-typescript', version: '0.21.2' }],
|
|
36
|
+
javascript: [{ pkg: 'tree-sitter-javascript', version: '0.21.4' }],
|
|
37
|
+
css: [
|
|
38
|
+
{ pkg: 'tree-sitter-css', version: '0.21.1' },
|
|
39
|
+
{ pkg: 'tree-sitter-scss', version: '1.0.0' },
|
|
40
|
+
],
|
|
41
|
+
python: [{ pkg: 'tree-sitter-python', version: '0.21.0' }],
|
|
42
|
+
rust: [{ pkg: 'tree-sitter-rust', version: '0.21.0' }],
|
|
43
|
+
go: [{ pkg: 'tree-sitter-go', version: '0.21.0' }],
|
|
44
|
+
php: [{ pkg: 'tree-sitter-php', version: '0.23.1' }],
|
|
45
|
+
java: [{ pkg: 'tree-sitter-java', version: '0.21.0' }],
|
|
46
|
+
kotlin: [{ pkg: 'tree-sitter-kotlin', version: '0.3.8' }],
|
|
47
|
+
csharp: [{ pkg: 'tree-sitter-c-sharp', version: '0.21.3' }],
|
|
48
|
+
ruby: [{ pkg: 'tree-sitter-ruby', version: '0.23.1' }],
|
|
49
|
+
c: [{ pkg: 'tree-sitter-c', version: '0.21.4' }],
|
|
50
|
+
bash: [{ pkg: 'tree-sitter-bash', version: '0.23.3' }],
|
|
51
|
+
swift: [{ pkg: 'tree-sitter-swift', version: '0.5.0' }],
|
|
52
|
+
};
|
|
17
53
|
|
|
18
|
-
function _loadProjectConfig() {
|
|
19
|
-
const configPath = path.join(process.env.MCP_PROJECT_ROOT || process.cwd(), '.graph-indexer.json');
|
|
54
|
+
function _loadProjectConfig(projectRoot) {
|
|
55
|
+
const configPath = path.join(projectRoot || process.env.MCP_PROJECT_ROOT || process.cwd(), '.graph-indexer.json');
|
|
20
56
|
try {
|
|
21
57
|
if (fs.existsSync(configPath)) return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
22
58
|
} catch { /* ignore */ }
|
|
23
59
|
return null;
|
|
24
60
|
}
|
|
25
61
|
|
|
26
|
-
|
|
27
|
-
|
|
62
|
+
// ─── Package resolution ────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/** Ambient resolution: relative to THIS module's own location (covers devDependencies
|
|
65
|
+
* in this repo's checkout, or a grammar a project installed directly). */
|
|
66
|
+
async function _tryAmbient(pkg) {
|
|
67
|
+
try { return (await import(pkg)).default; }
|
|
68
|
+
catch { return null; }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Resolve `pkg` from a scoped install dir (its own node_modules), without relying
|
|
72
|
+
* on ambient ancestor-directory resolution — required for the npx/global-install
|
|
73
|
+
* case, where graph-indexer's own location shares no node_modules ancestry with
|
|
74
|
+
* the target `--repo`. */
|
|
75
|
+
async function _importFrom(pkg, dir) {
|
|
28
76
|
try {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
77
|
+
const req = createRequire(path.join(dir, '__grammar_resolve__.js'));
|
|
78
|
+
const resolved = req.resolve(pkg);
|
|
79
|
+
return (await import(pathToFileURL(resolved).href)).default;
|
|
80
|
+
} catch { return null; }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// A platform-incompatible grammar (e.g. no Xcode toolchain for a native binding)
|
|
84
|
+
// would otherwise re-attempt its slow, doomed build on every single `idx-index`
|
|
85
|
+
// run. Once a spec fails, it's remembered here so later runs skip straight to the
|
|
86
|
+
// "not installed — skipped" warning instead of re-running node-gyp. Delete this
|
|
87
|
+
// file (or the whole node_modules dir) to retry after fixing the toolchain.
|
|
88
|
+
function _failedCachePath(dir) { return path.join(dir, '.install-failed.json'); }
|
|
89
|
+
function _readFailedCache(dir) {
|
|
90
|
+
try { return new Set(JSON.parse(fs.readFileSync(_failedCachePath(dir), 'utf-8'))); }
|
|
91
|
+
catch { return new Set(); }
|
|
92
|
+
}
|
|
93
|
+
function _writeFailedCache(dir, failedSet) {
|
|
94
|
+
try { fs.writeFileSync(_failedCachePath(dir), JSON.stringify([...failedSet])); } catch { /* best effort */ }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Installed ONE package per `npm install` call — not batched. A single native
|
|
98
|
+
// build failure (e.g. no Xcode toolchain for a grammar with a compiled binding)
|
|
99
|
+
// must only skip THAT language, not roll back the whole batch: npm treats a
|
|
100
|
+
// multi-spec `install` as one transaction, so one failing package would otherwise
|
|
101
|
+
// take every other requested language down with it (this is exactly the
|
|
102
|
+
// per-package graceful-degradation `optionalDependencies` used to give us for
|
|
103
|
+
// free). Returns the subset of `specs` that failed (including previously-known
|
|
104
|
+
// failures, which are skipped without a retry).
|
|
105
|
+
function _installGrammars(dir, specs, log) {
|
|
106
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
107
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
108
|
+
if (!fs.existsSync(pkgPath)) fs.writeFileSync(pkgPath, JSON.stringify({ name: 'graph-indexer-grammars', private: true }) + '\n');
|
|
109
|
+
const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
110
|
+
const knownBad = _readFailedCache(dir);
|
|
111
|
+
const failed = [];
|
|
112
|
+
let knownBadChanged = false;
|
|
113
|
+
for (const spec of specs) {
|
|
114
|
+
if (knownBad.has(spec)) {
|
|
115
|
+
log(`[graph-indexer] WARNING: ${spec} previously failed to install — skipping retry (delete ${path.relative(process.cwd(), dir) || dir}/.install-failed.json to retry)`);
|
|
116
|
+
failed.push(spec);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
log(`[graph-indexer] installing ${spec} (first use only, cached in ${path.relative(process.cwd(), dir) || dir}/node_modules)…`);
|
|
120
|
+
const res = spawnSync(npmBin, ['install', '--no-audit', '--no-fund', '--no-package-lock', spec], { cwd: dir, stdio: 'inherit' });
|
|
121
|
+
if (res.status !== 0) {
|
|
122
|
+
log(`[graph-indexer] WARNING: ${spec} failed to install (exit ${res.status}) — this platform may not support it; skipping`);
|
|
123
|
+
failed.push(spec);
|
|
124
|
+
knownBad.add(spec);
|
|
125
|
+
knownBadChanged = true;
|
|
126
|
+
}
|
|
33
127
|
}
|
|
128
|
+
if (knownBadChanged) _writeFailedCache(dir, knownBad);
|
|
129
|
+
return failed;
|
|
34
130
|
}
|
|
35
131
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
132
|
+
// ─── Lazy, memoized language loading ───────────────────────────────────────────
|
|
133
|
+
// LANGUAGE_MAP/EXTENSIONS are mutated IN PLACE (not reassigned) so every consumer
|
|
134
|
+
// that imported these bindings sees the same populated object/set once
|
|
135
|
+
// ensureLanguagesReady() resolves — no need to re-import or re-bind anything.
|
|
136
|
+
export const LANGUAGE_MAP = {};
|
|
137
|
+
export const EXTENSIONS = new Set();
|
|
138
|
+
|
|
139
|
+
let _readyPromise = null;
|
|
140
|
+
|
|
141
|
+
function _applyLanguageMap(modules) {
|
|
142
|
+
const primary = (key) => modules[key]?.[0]?.mod || null;
|
|
143
|
+
const byPkg = (key, pkg) => modules[key]?.find(m => m.pkg === pkg)?.mod || null;
|
|
144
|
+
|
|
145
|
+
const TypeScript = primary('typescript');
|
|
146
|
+
const JavaScript = primary('javascript');
|
|
147
|
+
const CSS = byPkg('css', 'tree-sitter-css');
|
|
148
|
+
const SCSS = byPkg('css', 'tree-sitter-scss');
|
|
149
|
+
const Python = primary('python');
|
|
150
|
+
const Rust = primary('rust');
|
|
151
|
+
const Go = primary('go');
|
|
152
|
+
const PHP = primary('php');
|
|
153
|
+
const Java = primary('java');
|
|
154
|
+
const Kotlin = primary('kotlin');
|
|
155
|
+
const CSharp = primary('csharp');
|
|
156
|
+
const Ruby = primary('ruby');
|
|
157
|
+
const C = primary('c');
|
|
158
|
+
const Bash = primary('bash');
|
|
159
|
+
const Swift = primary('swift');
|
|
160
|
+
|
|
161
|
+
const map = {
|
|
162
|
+
...(TypeScript ? { '.ts': TypeScript.typescript, '.tsx': TypeScript.tsx } : {}),
|
|
163
|
+
...(JavaScript ? { '.js': JavaScript, '.jsx': JavaScript, '.mjs': JavaScript, '.cjs': JavaScript } : {}),
|
|
164
|
+
...(CSS ? { '.css': CSS } : {}),
|
|
165
|
+
...(SCSS ? { '.scss': SCSS } : (CSS ? { '.scss': CSS } : {})),
|
|
166
|
+
...(Python ? { '.py': Python } : {}),
|
|
167
|
+
...(Rust ? { '.rs': Rust } : {}),
|
|
168
|
+
...(Go ? { '.go': Go } : {}),
|
|
169
|
+
...(PHP ? { '.php': PHP.php } : {}),
|
|
170
|
+
...(Java ? { '.java': Java } : {}),
|
|
171
|
+
...(Kotlin ? { '.kt': Kotlin, '.kts': Kotlin } : {}),
|
|
172
|
+
...(CSharp ? { '.cs': CSharp } : {}),
|
|
173
|
+
...(Ruby ? { '.rb': Ruby } : {}),
|
|
174
|
+
...(C ? { '.c': C, '.h': C } : {}),
|
|
175
|
+
// Shebang-only extensionless scripts are not keyed.
|
|
176
|
+
...(Bash ? { '.sh': Bash, '.bash': Bash } : {}),
|
|
177
|
+
...(Swift ? { '.swift': Swift } : {}),
|
|
178
|
+
};
|
|
179
|
+
for (const k of Object.keys(LANGUAGE_MAP)) delete LANGUAGE_MAP[k];
|
|
180
|
+
Object.assign(LANGUAGE_MAP, map);
|
|
181
|
+
EXTENSIONS.clear();
|
|
182
|
+
for (const k of Object.keys(map)) EXTENSIONS.add(k);
|
|
183
|
+
}
|
|
80
184
|
|
|
81
|
-
|
|
185
|
+
async function _doLoadLanguages({ projectRoot, enabledLangs, autoInstall, log }) {
|
|
186
|
+
const grammarsRoot = dataDir(projectRoot); // packages land in <grammarsRoot>/node_modules
|
|
187
|
+
const keys = enabledLangs && enabledLangs.length ? enabledLangs : Object.keys(GRAMMAR_PACKAGES);
|
|
82
188
|
|
|
189
|
+
const modules = {};
|
|
190
|
+
let missing = [];
|
|
191
|
+
for (const key of keys) {
|
|
192
|
+
const specs = GRAMMAR_PACKAGES[key];
|
|
193
|
+
if (!specs) continue;
|
|
194
|
+
modules[key] = [];
|
|
195
|
+
for (const { pkg, version } of specs) {
|
|
196
|
+
const mod = (await _tryAmbient(pkg)) || (await _importFrom(pkg, grammarsRoot));
|
|
197
|
+
if (mod) modules[key].push({ pkg, mod });
|
|
198
|
+
else missing.push({ key, pkg, version });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (missing.length && autoInstall) {
|
|
203
|
+
const specs = [...new Set(missing.map(m => `${m.pkg}@${m.version}`))];
|
|
204
|
+
const failedSpecs = new Set(_installGrammars(grammarsRoot, specs, log));
|
|
205
|
+
const stillMissing = [];
|
|
206
|
+
for (const m of missing) {
|
|
207
|
+
if (failedSpecs.has(`${m.pkg}@${m.version}`)) { stillMissing.push(m); continue; }
|
|
208
|
+
const mod = await _importFrom(m.pkg, grammarsRoot);
|
|
209
|
+
if (mod) modules[m.key].push({ pkg: m.pkg, mod });
|
|
210
|
+
else stillMissing.push(m);
|
|
211
|
+
}
|
|
212
|
+
missing = stillMissing;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (const m of missing) {
|
|
216
|
+
log(`[graph-indexer] WARNING: ${m.pkg} not installed — ${m.key} files will be skipped`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
_applyLanguageMap(modules);
|
|
220
|
+
return { missing };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Resolve (and, unless `autoInstall` is false, install) every requested language's
|
|
225
|
+
* grammar, then populate LANGUAGE_MAP/EXTENSIONS. Idempotent + memoized per process
|
|
226
|
+
* — the first call's options win; call this once, early, before any getParserForFile.
|
|
227
|
+
* `enabledLangs` defaults to `.graph-indexer.json`'s `languages` field (null = all 14).
|
|
228
|
+
*/
|
|
229
|
+
export function ensureLanguagesReady({
|
|
230
|
+
projectRoot = process.env.MCP_PROJECT_ROOT || process.cwd(),
|
|
231
|
+
enabledLangs = _loadProjectConfig(projectRoot)?.languages ?? null,
|
|
232
|
+
autoInstall = true,
|
|
233
|
+
log = (msg) => process.stderr.write(msg + '\n'),
|
|
234
|
+
} = {}) {
|
|
235
|
+
if (!_readyPromise) _readyPromise = _doLoadLanguages({ projectRoot, enabledLangs, autoInstall, log });
|
|
236
|
+
return _readyPromise;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Test-only: forces the next ensureLanguagesReady() call to reload from scratch. */
|
|
240
|
+
export function _resetLanguagesReady() { _readyPromise = null; }
|
|
241
|
+
|
|
242
|
+
let _warnedNotReady = false;
|
|
83
243
|
export function getParserForFile(ext) {
|
|
244
|
+
if (!_readyPromise && !_warnedNotReady) {
|
|
245
|
+
_warnedNotReady = true;
|
|
246
|
+
process.stderr.write('[graph-indexer] WARNING: getParserForFile() called before ensureLanguagesReady() — no grammars loaded\n');
|
|
247
|
+
}
|
|
84
248
|
const language = LANGUAGE_MAP[ext];
|
|
85
249
|
if (!language) return null;
|
|
86
250
|
const parser = new Parser();
|
package/parse/metadata.mjs
CHANGED
|
@@ -534,6 +534,52 @@ function _inferReceiverType(objNode, bindings) {
|
|
|
534
534
|
return _classifyValueNode(objNode); // inline: new Repo().save(), getStore().save()
|
|
535
535
|
}
|
|
536
536
|
|
|
537
|
+
const _FN_NODE_TYPES = new Set([
|
|
538
|
+
'function_declaration', 'function_definition', 'method_definition', 'method_declaration',
|
|
539
|
+
'arrow_function', 'function', 'function_expression', 'lambda', 'method', 'singleton_method',
|
|
540
|
+
'generator_function', 'generator_function_declaration',
|
|
541
|
+
]);
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Classify what a function chunk RETURNS, for the opt-in inter-procedural fixpoint
|
|
545
|
+
* (parse/interprocedural.mjs). Looks at this function's own return expression(s):
|
|
546
|
+
* • { type } — returns `new Repo()` (a concrete type, no annotation needed).
|
|
547
|
+
* • { viaCall } — returns a bare factory call `makeRepo()` whose type is resolved
|
|
548
|
+
* later by propagating along the call graph.
|
|
549
|
+
* • null — nothing safe (no return, mixed returns, opaque expression).
|
|
550
|
+
* Does NOT descend into nested functions/lambdas (only THIS function's returns).
|
|
551
|
+
* This is a transient signal (attached as `_return_via`) consumed at index time and
|
|
552
|
+
* stripped before serialization — it never persists in the index.
|
|
553
|
+
*/
|
|
554
|
+
export function extractReturnVia(chunkNode) {
|
|
555
|
+
const found = [];
|
|
556
|
+
const consider = (expr) => { const v = _classifyValueNode(expr); if (v) found.push(v); };
|
|
557
|
+
|
|
558
|
+
// Expression-body arrow / lambda: `() => new Repo()` has no return_statement.
|
|
559
|
+
if (chunkNode.type === 'arrow_function' || chunkNode.type === 'lambda') {
|
|
560
|
+
const body = chunkNode.childForFieldName?.('body');
|
|
561
|
+
if (body && body.type !== 'statement_block' && body.type !== 'block') consider(body);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
(function walk(node, depth) {
|
|
565
|
+
for (const child of node.children) {
|
|
566
|
+
if (depth > 0 && _FN_NODE_TYPES.has(child.type)) continue; // skip nested functions
|
|
567
|
+
if (child.type === 'return_statement') {
|
|
568
|
+
consider(child.namedChildren?.[0] || child.childForFieldName?.('argument'));
|
|
569
|
+
}
|
|
570
|
+
walk(child, depth + 1);
|
|
571
|
+
}
|
|
572
|
+
})(chunkNode, 0);
|
|
573
|
+
|
|
574
|
+
if (found.length === 0) return null;
|
|
575
|
+
const types = new Set(found.filter(f => f.type).map(f => f.type));
|
|
576
|
+
const vias = new Set(found.filter(f => f.viaCall).map(f => f.viaCall));
|
|
577
|
+
// Conservative: only a single unambiguous classification is usable.
|
|
578
|
+
if (types.size === 1 && vias.size === 0) return { type: [...types][0] };
|
|
579
|
+
if (vias.size === 1 && types.size === 0) return { viaCall: [...vias][0] };
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
|
|
537
583
|
/**
|
|
538
584
|
* Walk a subtree and collect every call site as { name, recv } (receiver hint), plus
|
|
539
585
|
* an optional `recv_type` / `recv_via_call` when the receiver's type is recoverable
|