ucn 4.1.1 → 4.1.2
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/README.md +10 -8
- package/core/build-worker.js +21 -7
- package/core/cache.js +16 -1
- package/core/callers.js +1210 -98
- package/core/deadcode.js +59 -8
- package/core/entrypoints.js +13 -2
- package/core/imports.js +98 -6
- package/core/project.js +45 -13
- package/languages/go.js +56 -4
- package/languages/java.js +5 -1
- package/languages/javascript.js +95 -9
- package/languages/python.js +23 -1
- package/languages/rust.js +42 -8
- package/package.json +1 -1
package/core/deadcode.js
CHANGED
|
@@ -161,17 +161,52 @@ function nameOnlySelfRecursive(index, name) {
|
|
|
161
161
|
return true;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
/** Check if a position in a line is inside a string literal (quotes/backticks)
|
|
165
|
-
|
|
164
|
+
/** Check if a position in a line is inside a string literal (quotes/backticks).
|
|
165
|
+
* Language-aware (fix #259, clap-measured): a Rust apostrophe is a LIFETIME
|
|
166
|
+
* unless it closes as a char literal within a few chars — `impl<E: Send +
|
|
167
|
+
* 'static> MyTrait` read everything after 'static as "inside a string" and
|
|
168
|
+
* dropped the line's only reference to MyTrait (FALSE-DEAD on clap's
|
|
169
|
+
* autoref-specialization traits). */
|
|
170
|
+
function isInsideString(line, pos, language) {
|
|
166
171
|
let inSingle = false, inDouble = false, inBacktick = false;
|
|
172
|
+
// Template-literal interpolations are CODE (fix #267, hono-measured:
|
|
173
|
+
// `${this.activeRouter.name}` was the getter's ONLY read — masked as
|
|
174
|
+
// string interior, the symbol claimed FALSE-DEAD). JS family only: Go
|
|
175
|
+
// backtick strings are raw, no interpolation. Brace-depth tracked so
|
|
176
|
+
// `${ {a: 1} }` closes at the right brace; quotes inside interpolations
|
|
177
|
+
// are not tracked — the misjudgment direction is UNMASK (counting a
|
|
178
|
+
// usage keeps the symbol alive), never masking code (#253 rule).
|
|
179
|
+
let interpDepth = 0;
|
|
180
|
+
const jsTemplates = language === 'javascript' || language === 'typescript' ||
|
|
181
|
+
language === 'tsx' || language === 'html';
|
|
167
182
|
for (let j = 0; j < pos; j++) {
|
|
168
183
|
const ch = line[j];
|
|
169
184
|
if (ch === '\\') { j++; continue; }
|
|
185
|
+
if (inBacktick && jsTemplates) {
|
|
186
|
+
if (interpDepth === 0) {
|
|
187
|
+
if (ch === '$' && line[j + 1] === '{') { interpDepth = 1; j++; }
|
|
188
|
+
else if (ch === '`') inBacktick = false;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (ch === '{') interpDepth++;
|
|
192
|
+
else if (ch === '}') interpDepth--;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
170
195
|
if (ch === '"' && !inSingle && !inBacktick) inDouble = !inDouble;
|
|
171
|
-
if (ch === "'" && !inDouble && !inBacktick)
|
|
196
|
+
if (ch === "'" && !inDouble && !inBacktick) {
|
|
197
|
+
if (language === 'rust') {
|
|
198
|
+
// Char literal ('x', '\n', '\u{1F600}') — skip past it; a
|
|
199
|
+
// non-closing apostrophe is a lifetime and never opens a
|
|
200
|
+
// string (Rust strings are double-quoted only).
|
|
201
|
+
const m = line.slice(j).match(/^'(?:\\u\{[0-9a-fA-F_]+\}|\\.|[^'\\])'/);
|
|
202
|
+
if (m) j += m[0].length - 1;
|
|
203
|
+
} else {
|
|
204
|
+
inSingle = !inSingle;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
172
207
|
if (ch === '`' && !inDouble && !inSingle) inBacktick = !inBacktick;
|
|
173
208
|
}
|
|
174
|
-
return inSingle || inDouble || inBacktick;
|
|
209
|
+
return inSingle || inDouble || (inBacktick && interpDepth === 0);
|
|
175
210
|
}
|
|
176
211
|
|
|
177
212
|
/**
|
|
@@ -591,9 +626,16 @@ function deadcode(index, options = {}) {
|
|
|
591
626
|
for (let i = 0; i < lines.length; i++) {
|
|
592
627
|
const line = lines[i];
|
|
593
628
|
if (!line.includes(name)) continue;
|
|
594
|
-
// Skip line if entirely inside a line comment
|
|
595
|
-
|
|
596
|
-
|
|
629
|
+
// Skip line if entirely inside a line comment — the
|
|
630
|
+
// markers are language-shaped (fix #259, clap-measured):
|
|
631
|
+
// `#` comments PYTHON ONLY (a Rust attribute line
|
|
632
|
+
// `#[arg(value_parser = helper)]` is code, and the old
|
|
633
|
+
// skip dropped every reference on it — clap's derive-
|
|
634
|
+
// attribute callbacks claimed FALSE-DEAD); `//` comments
|
|
635
|
+
// everywhere EXCEPT Python, where it is floor division.
|
|
636
|
+
const isPython = fileEntry.language === 'python';
|
|
637
|
+
const commentIdx = isPython ? -1 : line.indexOf('//');
|
|
638
|
+
const hashIdx = isPython ? line.indexOf('#') : -1;
|
|
597
639
|
let searchFrom = 0;
|
|
598
640
|
while (searchFrom < line.length) {
|
|
599
641
|
const pos = line.indexOf(name, searchFrom);
|
|
@@ -611,7 +653,7 @@ function deadcode(index, options = {}) {
|
|
|
611
653
|
// Skip if inside a string literal — EXCEPT class-
|
|
612
654
|
// kind names in Python (fix #253a): `x: "Foo"`
|
|
613
655
|
// forward references are real type references.
|
|
614
|
-
if (isInsideString(line, pos) &&
|
|
656
|
+
if (isInsideString(line, pos, fileEntry.language) &&
|
|
615
657
|
!(fileEntry.language === 'python' && classKindNames.has(name))) continue;
|
|
616
658
|
// Property/field access (preceded by '.'), not a
|
|
617
659
|
// call: resolve the RECEIVER (fix #216, express-
|
|
@@ -778,6 +820,15 @@ function deadcode(index, options = {}) {
|
|
|
778
820
|
|
|
779
821
|
const mods = symbol.modifiers || [];
|
|
780
822
|
|
|
823
|
+
// Ambient declaration files are never dead CODE (fix #267,
|
|
824
|
+
// zustand-measured: `interface ImportMeta` in src/types.d.ts —
|
|
825
|
+
// global lib merging UCN cannot see): .d.ts content is erased at
|
|
826
|
+
// compile time and describes external/global shapes — nothing in
|
|
827
|
+
// it is deletable code, so nothing in it is claimable.
|
|
828
|
+
if (symbol.relativePath.endsWith('.d.ts')) {
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
|
|
781
832
|
// Language-specific entry points (called by runtime/test runner, not user code)
|
|
782
833
|
// Each language module declares its own isEntryPoint() rules.
|
|
783
834
|
const langModule = getLanguageModule(lang);
|
package/core/entrypoints.js
CHANGED
|
@@ -1104,6 +1104,12 @@ function computeReachability(index) {
|
|
|
1104
1104
|
}
|
|
1105
1105
|
}
|
|
1106
1106
|
|
|
1107
|
+
// The BFS runs thousands of findCallees calls — the per-operation caches
|
|
1108
|
+
// (call counts, usage totals, content) halve its cost. Wrap so callers
|
|
1109
|
+
// outside a command op (evals, direct API use) get the same warm path;
|
|
1110
|
+
// _beginOp nests, so command-op callers are unaffected.
|
|
1111
|
+
index._beginOp?.();
|
|
1112
|
+
try {
|
|
1107
1113
|
const reachable = new Set();
|
|
1108
1114
|
const entryPoints = detectEntrypoints(index);
|
|
1109
1115
|
|
|
@@ -1208,8 +1214,10 @@ function computeReachability(index) {
|
|
|
1208
1214
|
|
|
1209
1215
|
// BFS: walk callees of every reachable symbol.
|
|
1210
1216
|
// findCallees returns full symbol objects for every callee with file/startLine.
|
|
1211
|
-
|
|
1212
|
-
|
|
1217
|
+
// Index-based traversal — queue.shift() is O(n) per dequeue and the
|
|
1218
|
+
// queue reaches thousands of entries; same FIFO order.
|
|
1219
|
+
for (let qi = 0; qi < queue.length; qi++) {
|
|
1220
|
+
const sym = queue[qi];
|
|
1213
1221
|
if (!sym.file || sym.startLine == null) continue;
|
|
1214
1222
|
|
|
1215
1223
|
let callees;
|
|
@@ -1242,6 +1250,9 @@ function computeReachability(index) {
|
|
|
1242
1250
|
// Cleared in saveCache after a successful write.
|
|
1243
1251
|
index.reachabilityDirty = true;
|
|
1244
1252
|
return reachable;
|
|
1253
|
+
} finally {
|
|
1254
|
+
index._endOp?.();
|
|
1255
|
+
}
|
|
1245
1256
|
}
|
|
1246
1257
|
|
|
1247
1258
|
/**
|
package/core/imports.js
CHANGED
|
@@ -145,6 +145,15 @@ function resolveImport(importPath, fromFile, config = {}) {
|
|
|
145
145
|
const fullPath = path.join(config.root, modulePath);
|
|
146
146
|
const resolved = resolveFilePath(fullPath, getExtensions('python'));
|
|
147
147
|
if (resolved) return resolved;
|
|
148
|
+
// PEP-517 src layout (fix #269, click-measured): the installed
|
|
149
|
+
// package lives under src/ — `import click` from tests/ resolves
|
|
150
|
+
// to src/click/__init__.py. Without this, the module-ownership
|
|
151
|
+
// discipline judged the project's OWN package provably external
|
|
152
|
+
// (excluded every `click.get_binary_stream(...)` test caller —
|
|
153
|
+
// a false zero-caller answer).
|
|
154
|
+
const srcPath = path.join(config.root, 'src', modulePath);
|
|
155
|
+
const srcResolved = resolveFilePath(srcPath, getExtensions('python'));
|
|
156
|
+
if (srcResolved) return srcResolved;
|
|
148
157
|
}
|
|
149
158
|
|
|
150
159
|
return null; // External package
|
|
@@ -289,6 +298,42 @@ function resolveGoImport(importPath, fromFile, projectRoot) {
|
|
|
289
298
|
// Cache for Rust crate roots (Cargo.toml locations)
|
|
290
299
|
const cargoCache = new Map();
|
|
291
300
|
|
|
301
|
+
// Workspace crate registry (fix #258): Cargo [package] name → crate source
|
|
302
|
+
// root for EVERY crate in the project tree, so cross-crate workspace imports
|
|
303
|
+
// (`use clap::Command` from clap_bench/) resolve like own-package imports.
|
|
304
|
+
// One bounded scan per project root, cached.
|
|
305
|
+
const workspaceCrateCache = new Map();
|
|
306
|
+
const _WORKSPACE_SCAN_PRUNE = new Set([
|
|
307
|
+
'node_modules', '.git', 'target', 'vendor', 'dist', 'build', '.ucn-cache',
|
|
308
|
+
]);
|
|
309
|
+
|
|
310
|
+
function workspaceCrateRegistry(projectRoot) {
|
|
311
|
+
if (workspaceCrateCache.has(projectRoot)) {
|
|
312
|
+
return workspaceCrateCache.get(projectRoot);
|
|
313
|
+
}
|
|
314
|
+
const registry = new Map();
|
|
315
|
+
const walk = (dir, depth) => {
|
|
316
|
+
if (depth > 6) return;
|
|
317
|
+
let entries;
|
|
318
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
319
|
+
catch { return; }
|
|
320
|
+
for (const e of entries) {
|
|
321
|
+
if (e.isDirectory()) {
|
|
322
|
+
if (_WORKSPACE_SCAN_PRUNE.has(e.name) || e.name.startsWith('.')) continue;
|
|
323
|
+
walk(path.join(dir, e.name), depth + 1);
|
|
324
|
+
} else if (e.name === 'Cargo.toml') {
|
|
325
|
+
const info = findCargoRoot(dir);
|
|
326
|
+
if (info && info.packageName && !registry.has(info.packageName)) {
|
|
327
|
+
registry.set(info.packageName, info);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
walk(projectRoot, 0);
|
|
333
|
+
workspaceCrateCache.set(projectRoot, registry);
|
|
334
|
+
return registry;
|
|
335
|
+
}
|
|
336
|
+
|
|
292
337
|
/**
|
|
293
338
|
* Find the nearest Cargo.toml and return the crate's source root
|
|
294
339
|
* @param {string} startDir - Directory to start searching from
|
|
@@ -311,13 +356,24 @@ function findCargoRoot(startDir) {
|
|
|
311
356
|
// integration tests/benches/examples (`use mypkg::...` in
|
|
312
357
|
// tests/*.rs — fix #246); `-` normalizes to `_` in code.
|
|
313
358
|
let packageName = null;
|
|
359
|
+
const targetDirs = [];
|
|
314
360
|
try {
|
|
315
361
|
const toml = fs.readFileSync(cargoPath, 'utf-8');
|
|
316
362
|
const pkgSection = toml.split(/^\s*\[/m).find(s => s.startsWith('package]'));
|
|
317
363
|
const m = pkgSection && pkgSection.match(/^\s*name\s*=\s*"([^"]+)"/m);
|
|
318
364
|
if (m) packageName = m[1].replace(/-/g, '_');
|
|
365
|
+
// Explicit target roots (fix #260b, ripgrep-measured): a
|
|
366
|
+
// manifest may root its targets OUTSIDE src/ — ripgrep's
|
|
367
|
+
// `[[bin]] path = "crates/core/main.rs"` puts the whole bin
|
|
368
|
+
// module tree under crates/core, so `crate::messages` from
|
|
369
|
+
// crates/core/flags/parse.rs resolves THERE, never at src/.
|
|
370
|
+
// Collect every declared .rs target path's directory.
|
|
371
|
+
for (const tm of toml.matchAll(/^\s*path\s*=\s*"([^"]+\.rs)"/gm)) {
|
|
372
|
+
const tDir = path.dirname(path.resolve(dir, tm[1]));
|
|
373
|
+
if (!targetDirs.includes(tDir)) targetDirs.push(tDir);
|
|
374
|
+
}
|
|
319
375
|
} catch { /* unreadable Cargo.toml — no package identity */ }
|
|
320
|
-
const result = { root: dir, srcDir: fs.existsSync(srcDir) ? srcDir : dir, packageName };
|
|
376
|
+
const result = { root: dir, srcDir: fs.existsSync(srcDir) ? srcDir : dir, packageName, targetDirs };
|
|
321
377
|
cargoCache.set(startDir, result);
|
|
322
378
|
return result;
|
|
323
379
|
}
|
|
@@ -389,17 +445,30 @@ function resolveRustModulePath(dir, segments) {
|
|
|
389
445
|
function resolveRustImport(importPath, fromFile, projectRoot) {
|
|
390
446
|
const fromDir = path.dirname(fromFile);
|
|
391
447
|
|
|
392
|
-
// crate:: paths - resolve from the crate's src/ directory
|
|
448
|
+
// crate:: paths - resolve from the crate's src/ directory, or from a
|
|
449
|
+
// manifest-declared target root (fix #260b): a `[[bin]] path =
|
|
450
|
+
// "crates/core/main.rs"` roots the module tree at crates/core — the
|
|
451
|
+
// importing file's crate root dir is the DEEPEST declared target dir
|
|
452
|
+
// that is an ancestor of the file (module files live under their crate
|
|
453
|
+
// root; integration tests are separate crates and can't use crate::).
|
|
393
454
|
if (importPath.startsWith('crate::')) {
|
|
394
455
|
const cargo = findCargoRoot(fromDir);
|
|
395
456
|
if (!cargo) return null;
|
|
396
457
|
|
|
397
458
|
const rest = importPath.slice('crate::'.length);
|
|
398
459
|
const segments = rest.split('::');
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
460
|
+
const candidates = [cargo.srcDir, ...(cargo.targetDirs || [])]
|
|
461
|
+
.filter(d => fromDir === d || fromDir.startsWith(d + path.sep))
|
|
462
|
+
.sort((a, b) => b.length - a.length);
|
|
463
|
+
if (candidates.length === 0) candidates.push(cargo.srcDir);
|
|
464
|
+
for (const cand of candidates) {
|
|
465
|
+
// `use crate::ITEM` where ITEM is declared in the crate root file
|
|
466
|
+
// has no ITEM.rs — the import points at lib.rs/main.rs itself.
|
|
467
|
+
const hit = resolveRustModulePath(cand, segments) ||
|
|
468
|
+
rustModuleOwnFile(cand, fromFile);
|
|
469
|
+
if (hit) return hit;
|
|
470
|
+
}
|
|
471
|
+
return null;
|
|
403
472
|
}
|
|
404
473
|
|
|
405
474
|
// Own-package-name paths (fix #246): integration tests, benches, and
|
|
@@ -424,6 +493,28 @@ function resolveRustImport(importPath, fromFile, projectRoot) {
|
|
|
424
493
|
if (fallback) return fallback;
|
|
425
494
|
}
|
|
426
495
|
}
|
|
496
|
+
// Cross-crate WORKSPACE imports (fix #258, clap-measured): a sibling
|
|
497
|
+
// workspace member's [package] name resolves into that crate's source
|
|
498
|
+
// tree (`use clap::Command` from clap_bench/, ripgrep's grep-* crates
|
|
499
|
+
// importing each other). Own-package and 2015-edition child-module
|
|
500
|
+
// cases are handled above; a name matching this crate's own package
|
|
501
|
+
// inside src/ never reaches here as a workspace lookup.
|
|
502
|
+
if (firstSeg && (!cargo || firstSeg !== cargo.packageName)) {
|
|
503
|
+
// A local child module of the same name wins over a workspace
|
|
504
|
+
// crate (mod declarations resolve in the plain branch below).
|
|
505
|
+
const localChild = fs.existsSync(path.join(fromDir, firstSeg + '.rs')) ||
|
|
506
|
+
fs.existsSync(path.join(fromDir, firstSeg, 'mod.rs'));
|
|
507
|
+
if (!localChild) {
|
|
508
|
+
const registry = workspaceCrateRegistry(projectRoot);
|
|
509
|
+
const member = registry.get(firstSeg);
|
|
510
|
+
if (member && (!cargo || member.root !== cargo.root)) {
|
|
511
|
+
const restSegs = importPath.split('::').slice(1);
|
|
512
|
+
const resolved = restSegs.length > 0 ? resolveRustModulePath(member.srcDir, restSegs) : null;
|
|
513
|
+
const fallback = resolved || rustModuleOwnFile(member.srcDir, fromFile);
|
|
514
|
+
if (fallback) return fallback;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
427
518
|
}
|
|
428
519
|
|
|
429
520
|
// super:: paths - resolve relative to parent module
|
|
@@ -819,5 +910,6 @@ module.exports = {
|
|
|
819
910
|
extractExports,
|
|
820
911
|
resolveImport,
|
|
821
912
|
resolveFilePath,
|
|
913
|
+
resolveRustImport,
|
|
822
914
|
findGoModule
|
|
823
915
|
};
|
package/core/project.js
CHANGED
|
@@ -79,6 +79,7 @@ class ProjectIndex {
|
|
|
79
79
|
this._opContentCache = new Map();
|
|
80
80
|
this._opUsagesCache = new Map();
|
|
81
81
|
this._opCallsCountCache = new Map();
|
|
82
|
+
this._opUsageTotalsCache = new Map();
|
|
82
83
|
this._opEnclosingFnCache = new Map();
|
|
83
84
|
this._opTreeCache = new Map();
|
|
84
85
|
this._opLinesCache = new Map();
|
|
@@ -94,6 +95,7 @@ class ProjectIndex {
|
|
|
94
95
|
this._opContentCache = null;
|
|
95
96
|
this._opUsagesCache = null;
|
|
96
97
|
this._opCallsCountCache = null;
|
|
98
|
+
this._opUsageTotalsCache = null;
|
|
97
99
|
this._opEnclosingFnCache = null;
|
|
98
100
|
this._opTreeCache = null;
|
|
99
101
|
this._opLinesCache = null;
|
|
@@ -314,7 +316,7 @@ class ProjectIndex {
|
|
|
314
316
|
const disableParallel = workersSetting === 0 || envWorkers === 0;
|
|
315
317
|
let usedParallel = false;
|
|
316
318
|
|
|
317
|
-
if (!disableParallel && files.length >
|
|
319
|
+
if (!disableParallel && files.length > 150) {
|
|
318
320
|
try {
|
|
319
321
|
const { parallelBuild } = require('./parallel-build');
|
|
320
322
|
const result = parallelBuild(this, files, {
|
|
@@ -482,7 +484,13 @@ class ProjectIndex {
|
|
|
482
484
|
// the bare name for the whole file).
|
|
483
485
|
importBindings: imports.flatMap(i => (i.names || [])
|
|
484
486
|
.filter(n => n && n !== '*' && n !== '_' && n !== '.')
|
|
485
|
-
.map(n =>
|
|
487
|
+
.map(n => {
|
|
488
|
+
// Rename pairing (fix #269): `{ validate: validateSchema }
|
|
489
|
+
// = require('./validation')` — the record's local alias
|
|
490
|
+
// pins to ITS module, not any module exporting the name.
|
|
491
|
+
const rn = (i.renames || []).find(r => r.original === n);
|
|
492
|
+
return { name: n, module: i.module, ...(rn && { alias: rn.local }) };
|
|
493
|
+
})),
|
|
486
494
|
exports: exports.map(e => e.name),
|
|
487
495
|
exportDetails: exports,
|
|
488
496
|
symbols: [],
|
|
@@ -541,15 +549,22 @@ class ProjectIndex {
|
|
|
541
549
|
// members) — external-contract detection needs the NAME, not
|
|
542
550
|
// just the traitImpl flag (fix #210).
|
|
543
551
|
...(item.traitName && { traitName: item.traitName }),
|
|
544
|
-
...(item.isSignature && { isSignature: true })
|
|
552
|
+
...(item.isSignature && { isSignature: true }),
|
|
553
|
+
...(item.memberAssigned && { memberAssigned: true })
|
|
545
554
|
};
|
|
546
555
|
fileEntry.symbols.push(symbol);
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
556
|
+
// Property-assignment defs (fix #269: Reply.prototype.serialize
|
|
557
|
+
// = function) declare no lexical name — a bare reference in the
|
|
558
|
+
// file can never bind them, so they never enter the bindings
|
|
559
|
+
// table (the symbol stays indexed and method-reachable).
|
|
560
|
+
if (!item.memberAssigned) {
|
|
561
|
+
fileEntry.bindings.push({
|
|
562
|
+
id: symbol.bindingId,
|
|
563
|
+
name: symbol.name,
|
|
564
|
+
type: symbol.type,
|
|
565
|
+
startLine: symbol.startLine
|
|
566
|
+
});
|
|
567
|
+
}
|
|
553
568
|
|
|
554
569
|
if (!this.symbols.has(item.name)) {
|
|
555
570
|
this.symbols.set(item.name, []);
|
|
@@ -1158,6 +1173,17 @@ class ProjectIndex {
|
|
|
1158
1173
|
if (!this.calleeIndex) this.buildCalleeIndex();
|
|
1159
1174
|
const hasFilters = options.exclude && options.exclude.length > 0;
|
|
1160
1175
|
|
|
1176
|
+
// Per-operation memo of the whole fast-path result: the callee
|
|
1177
|
+
// tiebreaker (findCallees priority 6) and reachability BFS ask
|
|
1178
|
+
// for the same (name, defFile) totals thousands of times per
|
|
1179
|
+
// walk. Result objects are read-only by contract.
|
|
1180
|
+
const memoKey = !hasFilters && this._opUsageTotalsCache
|
|
1181
|
+
? `${name}\0${defFile}` : null;
|
|
1182
|
+
if (memoKey) {
|
|
1183
|
+
const hit = this._opUsageTotalsCache.get(memoKey);
|
|
1184
|
+
if (hit) return hit;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1161
1187
|
// Pre-compute which files can reference THIS specific definition
|
|
1162
1188
|
const importersSet = this.exportGraph.get(defFile) || new Set();
|
|
1163
1189
|
const defEntry = this.files.get(defFile);
|
|
@@ -1231,11 +1257,15 @@ class ProjectIndex {
|
|
|
1231
1257
|
imports++;
|
|
1232
1258
|
}
|
|
1233
1259
|
}
|
|
1234
|
-
// Same-package: files in same directory don't need imports to
|
|
1260
|
+
// Same-package: files in same directory don't need imports to
|
|
1261
|
+
// reference symbols. dirToFiles is canonical-ordered — same
|
|
1262
|
+
// iteration order as the full index scan it replaces.
|
|
1235
1263
|
if (isDirectoryScope) {
|
|
1236
1264
|
const pkgDir = defDir;
|
|
1237
|
-
for (const
|
|
1238
|
-
if (fp === defFile || !fp.endsWith('.go')
|
|
1265
|
+
for (const fp of this.dirToFiles?.get(pkgDir) || []) {
|
|
1266
|
+
if (fp === defFile || !fp.endsWith('.go')) continue;
|
|
1267
|
+
const fe = this.files.get(fp);
|
|
1268
|
+
if (!fe) continue;
|
|
1239
1269
|
if (hasFilters && !this.matchesFilters(fe.relativePath, { exclude: options.exclude })) continue;
|
|
1240
1270
|
// Check if already counted as importer
|
|
1241
1271
|
if (importersSet.has(fp)) continue;
|
|
@@ -1252,7 +1282,9 @@ class ProjectIndex {
|
|
|
1252
1282
|
}
|
|
1253
1283
|
|
|
1254
1284
|
const total = calls + definitions + imports;
|
|
1255
|
-
|
|
1285
|
+
const result = { total, calls, definitions, imports, references: 0 };
|
|
1286
|
+
if (memoKey) this._opUsageTotalsCache.set(memoKey, result);
|
|
1287
|
+
return result;
|
|
1256
1288
|
}
|
|
1257
1289
|
|
|
1258
1290
|
// Detailed path: full AST-based counting (original algorithm)
|
package/languages/go.js
CHANGED
|
@@ -678,6 +678,8 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
678
678
|
const closureScopes = new Map();
|
|
679
679
|
// Track variable -> type mappings per function scope (scopeStartLine -> Map<varName, typeName>)
|
|
680
680
|
const scopeTypes = new Map();
|
|
681
|
+
// Names whose scope type is a New*-prefix GUESS (fix #266) — per scope
|
|
682
|
+
const scopeGuesses = new Map();
|
|
681
683
|
// Track function-typed parameter names per scope (scopeStartLine -> Set<name>)
|
|
682
684
|
const funcParamScopes = new Map();
|
|
683
685
|
|
|
@@ -822,6 +824,21 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
822
824
|
}
|
|
823
825
|
return undefined;
|
|
824
826
|
};
|
|
827
|
+
// Is the FIRST scope-chain hit for this variable a New*-prefix GUESS
|
|
828
|
+
// (fix #266, viper-measured)? `registry := NewCodecRegistry()` types
|
|
829
|
+
// registry as 'CodecRegistry' by NAME CONVENTION — the actual return
|
|
830
|
+
// annotation says *DefaultCodecRegistry. Guess-grade types help
|
|
831
|
+
// resolution but must never be exclusion evidence; findCallers lets
|
|
832
|
+
// the compiler-checked return-type flow map override them.
|
|
833
|
+
const isGuessedType = (varName) => {
|
|
834
|
+
for (let i = functionStack.length - 1; i >= 0; i--) {
|
|
835
|
+
const typeMap = scopeTypes.get(functionStack[i].startLine);
|
|
836
|
+
if (typeMap?.has(varName)) {
|
|
837
|
+
return !!scopeGuesses.get(functionStack[i].startLine)?.has(varName);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return false;
|
|
841
|
+
};
|
|
825
842
|
|
|
826
843
|
// fix #203 (Go): is a bare-identifier function REFERENCE shadowed by an
|
|
827
844
|
// enclosing func-literal/function parameter, method receiver, range/init
|
|
@@ -957,6 +974,7 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
957
974
|
for (let vi = 0; vi < Math.min(names.length, values.length); vi++) {
|
|
958
975
|
const val = values[vi];
|
|
959
976
|
let typeName = null;
|
|
977
|
+
let typeGuessed = false;
|
|
960
978
|
// &Type{...} or Type{...}
|
|
961
979
|
if (val.type === 'composite_literal') {
|
|
962
980
|
typeName = extractTypeName(val.childForFieldName('type'));
|
|
@@ -978,8 +996,14 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
978
996
|
? callFuncNode.childForFieldName('field')?.text
|
|
979
997
|
: null;
|
|
980
998
|
if (callName && /^New[A-Z]/.test(callName)) {
|
|
999
|
+
// NAME CONVENTION, not compiler truth (fix
|
|
1000
|
+
// #266): NewCodecRegistry() returns
|
|
1001
|
+
// *DefaultCodecRegistry. Marked a guess so
|
|
1002
|
+
// the return-type flow map can override it
|
|
1003
|
+
// and exclusions never trust it.
|
|
981
1004
|
typeName = callName.slice(3);
|
|
982
1005
|
if (!typeName || !/^[A-Z]/.test(typeName)) typeName = null;
|
|
1006
|
+
if (typeName) typeGuessed = true;
|
|
983
1007
|
} else if (callFuncNode.type === 'identifier' &&
|
|
984
1008
|
callFuncNode.text === 'new') {
|
|
985
1009
|
// buf := new(bytes.Buffer) — the builtin
|
|
@@ -1000,7 +1024,16 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1000
1024
|
}
|
|
1001
1025
|
}
|
|
1002
1026
|
}
|
|
1003
|
-
if (typeName)
|
|
1027
|
+
if (typeName) {
|
|
1028
|
+
typeMap.set(names[vi], typeName);
|
|
1029
|
+
let gset = scopeGuesses.get(scopeKey);
|
|
1030
|
+
if (typeGuessed) {
|
|
1031
|
+
if (!gset) { gset = new Set(); scopeGuesses.set(scopeKey, gset); }
|
|
1032
|
+
gset.add(names[vi]);
|
|
1033
|
+
} else if (gset) {
|
|
1034
|
+
gset.delete(names[vi]); // compiler-true retype clears the guess
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1004
1037
|
}
|
|
1005
1038
|
}
|
|
1006
1039
|
}
|
|
@@ -1021,7 +1054,10 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1021
1054
|
if (!typeName) return;
|
|
1022
1055
|
for (let j = 0; j < spec.namedChildCount; j++) {
|
|
1023
1056
|
const id = spec.namedChild(j);
|
|
1024
|
-
if (id.type === 'identifier')
|
|
1057
|
+
if (id.type === 'identifier') {
|
|
1058
|
+
varTypeMap.set(id.text, typeName);
|
|
1059
|
+
scopeGuesses.get(scopeKey)?.delete(id.text); // declared type clears any guess
|
|
1060
|
+
}
|
|
1025
1061
|
}
|
|
1026
1062
|
};
|
|
1027
1063
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -1150,7 +1186,7 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1150
1186
|
// fix #202: one-hop declared-field receivers — h.inner.Run().
|
|
1151
1187
|
// receiverRoot/Field/RootType let findCallers hop to the
|
|
1152
1188
|
// field's declared struct-field type cross-file.
|
|
1153
|
-
let receiverRoot, receiverFieldName, receiverRootType;
|
|
1189
|
+
let receiverRoot, receiverFieldName, receiverRootType, receiverRootTypeGuessed;
|
|
1154
1190
|
if (!receiver && operandNode?.type === 'selector_expression') {
|
|
1155
1191
|
const rootNode = operandNode.childForFieldName('operand');
|
|
1156
1192
|
const fldNode = operandNode.childForFieldName('field');
|
|
@@ -1159,6 +1195,9 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1159
1195
|
receiverRoot = rootNode.text;
|
|
1160
1196
|
receiverFieldName = fldNode.text;
|
|
1161
1197
|
receiverRootType = getReceiverType(rootNode.text);
|
|
1198
|
+
if (receiverRootType && isGuessedType(rootNode.text)) {
|
|
1199
|
+
receiverRootTypeGuessed = true;
|
|
1200
|
+
}
|
|
1162
1201
|
}
|
|
1163
1202
|
}
|
|
1164
1203
|
// Chained receiver (fix #220, cobra-measured): the receiver
|
|
@@ -1167,16 +1206,22 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1167
1206
|
// declared return (*pflag.FlagSet → external → routed).
|
|
1168
1207
|
// Package-qualified producers (os.CreateTemp().Name())
|
|
1169
1208
|
// carry the qualifier for strict import-package resolution.
|
|
1170
|
-
let receiverCall, receiverCallIsMethod, receiverCallReceiver;
|
|
1209
|
+
let receiverCall, receiverCallIsMethod, receiverCallReceiver, receiverCallLine;
|
|
1171
1210
|
if (!receiver && !receiverFieldName && operandNode?.type === 'call_expression') {
|
|
1172
1211
|
const prodFunc = operandNode.childForFieldName('function');
|
|
1173
1212
|
if (prodFunc?.type === 'identifier') {
|
|
1174
1213
|
receiverCall = prodFunc.text;
|
|
1214
|
+
// Producer link (fix #258): plain-call records carry
|
|
1215
|
+
// the call node's start line
|
|
1216
|
+
receiverCallLine = operandNode.startPosition.row + 1;
|
|
1175
1217
|
} else if (prodFunc?.type === 'selector_expression') {
|
|
1176
1218
|
const pf = prodFunc.childForFieldName('field');
|
|
1177
1219
|
const po = prodFunc.childForFieldName('operand');
|
|
1178
1220
|
if (pf) {
|
|
1179
1221
|
receiverCall = pf.text;
|
|
1222
|
+
// Selector records report the FIELD's own line
|
|
1223
|
+
// (fix #223 name-node convention)
|
|
1224
|
+
receiverCallLine = pf.startPosition.row + 1;
|
|
1180
1225
|
if (po?.type === 'identifier' && importAliases.has(po.text)) {
|
|
1181
1226
|
receiverCallReceiver = po.text;
|
|
1182
1227
|
} else {
|
|
@@ -1198,11 +1243,14 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1198
1243
|
isMethod: !isPkgCall,
|
|
1199
1244
|
receiver,
|
|
1200
1245
|
...(receiverType && { receiverType }),
|
|
1246
|
+
...(receiverType && isGuessedType(receiver) && { receiverTypeGuessed: true }),
|
|
1201
1247
|
...(receiverFieldName && { receiverRoot, receiverField: receiverFieldName }),
|
|
1202
1248
|
...(receiverFieldName && receiverRootType && { receiverRootType }),
|
|
1249
|
+
...(receiverFieldName && receiverRootType && receiverRootTypeGuessed && { receiverRootTypeGuessed: true }),
|
|
1203
1250
|
...(receiverCall && { receiverCall }),
|
|
1204
1251
|
...(receiverCallIsMethod && { receiverCallIsMethod: true }),
|
|
1205
1252
|
...(receiverCallReceiver && { receiverCallReceiver }),
|
|
1253
|
+
...(receiverCallLine && { receiverCallLine }),
|
|
1206
1254
|
argCount,
|
|
1207
1255
|
...(argSpread && { argSpread: true }),
|
|
1208
1256
|
...(assigned && { assignedTo: assigned.assignedTo }),
|
|
@@ -1282,6 +1330,7 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1282
1330
|
isMethod: true,
|
|
1283
1331
|
receiver,
|
|
1284
1332
|
...(receiverType && { receiverType }),
|
|
1333
|
+
...(receiverType && isGuessedType(receiver) && { receiverTypeGuessed: true }),
|
|
1285
1334
|
enclosingFunction,
|
|
1286
1335
|
isPotentialCallback: true,
|
|
1287
1336
|
uncertain: false
|
|
@@ -1337,6 +1386,7 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1337
1386
|
isMethod: true,
|
|
1338
1387
|
receiver,
|
|
1339
1388
|
...(receiverType && { receiverType }),
|
|
1389
|
+
...(receiverType && isGuessedType(receiver) && { receiverTypeGuessed: true }),
|
|
1340
1390
|
enclosingFunction,
|
|
1341
1391
|
isPotentialCallback: true,
|
|
1342
1392
|
uncertain: false
|
|
@@ -1431,6 +1481,7 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1431
1481
|
isMethod: true,
|
|
1432
1482
|
receiver,
|
|
1433
1483
|
...(receiverType && { receiverType }),
|
|
1484
|
+
...(receiverType && isGuessedType(receiver) && { receiverTypeGuessed: true }),
|
|
1434
1485
|
enclosingFunction,
|
|
1435
1486
|
isPotentialCallback: true,
|
|
1436
1487
|
uncertain: false,
|
|
@@ -1469,6 +1520,7 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1469
1520
|
if (leaving) {
|
|
1470
1521
|
closureScopes.delete(leaving.startLine);
|
|
1471
1522
|
scopeTypes.delete(leaving.startLine);
|
|
1523
|
+
scopeGuesses.delete(leaving.startLine);
|
|
1472
1524
|
}
|
|
1473
1525
|
}
|
|
1474
1526
|
}
|
package/languages/java.js
CHANGED
|
@@ -1131,11 +1131,14 @@ function findCallsInCode(code, parser) {
|
|
|
1131
1131
|
// Chained receiver (fix #220): the receiver IS a call —
|
|
1132
1132
|
// getConfig().validate() — record the producer so findCallers
|
|
1133
1133
|
// can type it from the declared return annotation.
|
|
1134
|
-
let receiverCall, receiverCallIsMethod;
|
|
1134
|
+
let receiverCall, receiverCallIsMethod, receiverCallLine;
|
|
1135
1135
|
if (!receiver && !receiverFieldName && objNode?.type === 'method_invocation') {
|
|
1136
1136
|
const prodName = objNode.childForFieldName('name');
|
|
1137
1137
|
if (prodName) {
|
|
1138
1138
|
receiverCall = prodName.text;
|
|
1139
|
+
// Producer link (fix #258): records report the name
|
|
1140
|
+
// node's own line
|
|
1141
|
+
receiverCallLine = prodName.startPosition.row + 1;
|
|
1139
1142
|
if (objNode.childForFieldName('object')) receiverCallIsMethod = true;
|
|
1140
1143
|
}
|
|
1141
1144
|
}
|
|
@@ -1155,6 +1158,7 @@ function findCallsInCode(code, parser) {
|
|
|
1155
1158
|
...(receiverFieldName && receiverRootType && { receiverRootType }),
|
|
1156
1159
|
...(receiverCall && { receiverCall }),
|
|
1157
1160
|
...(receiverCallIsMethod && { receiverCallIsMethod: true }),
|
|
1161
|
+
...(receiverCallLine && { receiverCallLine }),
|
|
1158
1162
|
argCount: callArgs.argCount,
|
|
1159
1163
|
...(callArgs.argKinds && { argKinds: callArgs.argKinds }),
|
|
1160
1164
|
...(assignedTo && { assignedTo }),
|