ucn 4.1.1 → 4.1.3
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 +20 -1
- package/core/callers.js +1210 -98
- package/core/deadcode.js +142 -30
- 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 +23 -9
- 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
|
@@ -53,32 +53,90 @@ function _baseIsExternal(index, bare, lang) {
|
|
|
53
53
|
return !(defs && defs.some(d => _CLASS_KINDS.includes(d.type)));
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const supers = Array.isArray(classDef.extends) ? classDef.extends : splitParentList(classDef.extends);
|
|
60
|
-
return supers.some(raw => _baseIsExternal(index, _bareBaseName(raw), lang));
|
|
61
|
-
}
|
|
56
|
+
// Bounded heritage-closure depth (fix #270) — matches the engine's other
|
|
57
|
+
// inheritance walks.
|
|
58
|
+
const _HERITAGE_WALK_DEPTH = 8;
|
|
62
59
|
|
|
63
60
|
/**
|
|
64
|
-
* Does
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
61
|
+
* Does this CLASS DEF reach at least one base that is NOT in the project
|
|
62
|
+
* index, walking `extends` chains transitively THROUGH resolved project types
|
|
63
|
+
* (fix #270)? One level was not enough — fastify-measured: CustomLoggerImpl
|
|
64
|
+
* implements CustomLogger (project) extends FastifyBaseLogger (project,
|
|
65
|
+
* .d.ts) extends Pick<BaseLogger, ...'silent'> (pino — external). The member
|
|
66
|
+
* surface the class must provide comes from the OUT-OF-TREE end of the chain,
|
|
67
|
+
* invisible without tsc, so a zero-usage impl member (`silent`) is contract
|
|
68
|
+
* surface — deleting it breaks the build. Same dispatch physics as one level;
|
|
69
|
+
* only the verdict moved from "direct parent external" to "heritage closure
|
|
70
|
+
* reaches external".
|
|
69
71
|
*
|
|
70
|
-
* `
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* `
|
|
72
|
+
* `followImplements` additionally walks the def's OWN `implements` clause
|
|
73
|
+
* (first hop only — the measured family; deeper implements hops are
|
|
74
|
+
* classified-deferred). Method claims pass true only when the member sits
|
|
75
|
+
* INSIDE the def's source range: TS/Java members live in the class body, so
|
|
76
|
+
* the implements contract constrains them. Rust surfaces trait impls as
|
|
77
|
+
* `implements` on the struct (rust.js), but inherent methods live in separate
|
|
78
|
+
* `impl X` blocks OUTSIDE the struct's range — an unrelated `impl Display
|
|
79
|
+
* for X` must never shield a genuinely-dead inherent method (the original
|
|
80
|
+
* reason implements was not consulted here at all; trait-impl members
|
|
81
|
+
* themselves carry traitImpl/@Override markers → Rule A). Class-kind claims
|
|
82
|
+
* pass false: deleting the whole class removes its implements clause with it.
|
|
83
|
+
*/
|
|
84
|
+
function _heritageReachesExternalBase(index, classDef, lang, followImplements) {
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
let frontier = [classDef];
|
|
87
|
+
for (let depth = 0; depth < _HERITAGE_WALK_DEPTH && frontier.length; depth++) {
|
|
88
|
+
const next = [];
|
|
89
|
+
for (const def of frontier) {
|
|
90
|
+
// Copy array-shaped extends — the implements push below must
|
|
91
|
+
// never mutate the symbol's own heritage data in the index.
|
|
92
|
+
const parents = def.extends
|
|
93
|
+
? (Array.isArray(def.extends) ? [...def.extends] : splitParentList(def.extends))
|
|
94
|
+
: [];
|
|
95
|
+
if (depth === 0 && followImplements && Array.isArray(def.implements)) {
|
|
96
|
+
parents.push(...def.implements);
|
|
97
|
+
}
|
|
98
|
+
for (const raw of parents) {
|
|
99
|
+
const bare = _bareBaseName(raw);
|
|
100
|
+
if (!bare || seen.has(bare)) continue;
|
|
101
|
+
seen.add(bare);
|
|
102
|
+
if (_baseIsExternal(index, bare, lang)) return true;
|
|
103
|
+
for (const pd of index.symbols.get(bare) || []) {
|
|
104
|
+
if (_CLASS_KINDS.includes(pd.type)) next.push(pd);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
frontier = next;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Does the method's enclosing class reach at least one base that is NOT in
|
|
115
|
+
* the project index through its heritage closure? An out-of-tree base is a
|
|
116
|
+
* framework/library type UCN can't see; via inheritance it may dispatch into
|
|
117
|
+
* a public method of the subclass polymorphically (Starlette →
|
|
118
|
+
* build_middleware_stack), by name convention (Pydantic → bytes_schema), or
|
|
119
|
+
* REQUIRE the member outright (fastify → CustomLoggerImpl.silent, via an
|
|
120
|
+
* implements chain ending in pino). The class def is matched in the method's
|
|
121
|
+
* own file; `implements` is walked only when the member sits inside the class
|
|
122
|
+
* def's own range (see _heritageReachesExternalBase — the Rust guard).
|
|
76
123
|
*/
|
|
77
124
|
function _classHasExternalBase(index, symbol) {
|
|
78
125
|
const lang = index.files.get(symbol.file)?.language;
|
|
126
|
+
// The implements hop shields only contract-SATISFIABLE members: an
|
|
127
|
+
// interface implementation must be public, so in explicit-visibility
|
|
128
|
+
// languages a package-private/non-pub member provably cannot implement
|
|
129
|
+
// any interface member (javac rejects it) — `implements Runnable` never
|
|
130
|
+
// shields a package-private helper. Implicit-public languages satisfy
|
|
131
|
+
// contracts with unmarked members; the caller's public-by-shape check
|
|
132
|
+
// already screened those. Compiler physics, not a heuristic.
|
|
133
|
+
const contractSatisfiable = langTraits(lang)?.implicitlyPublicMembers ||
|
|
134
|
+
(symbol.modifiers || []).includes('public');
|
|
79
135
|
const classDefs = (index.symbols.get(symbol.className) || []).filter(c =>
|
|
80
136
|
c.file === symbol.file && _CLASS_KINDS.includes(c.type));
|
|
81
|
-
return classDefs.some(cd =>
|
|
137
|
+
return classDefs.some(cd => _heritageReachesExternalBase(index, cd, lang,
|
|
138
|
+
contractSatisfiable &&
|
|
139
|
+
symbol.startLine >= cd.startLine && symbol.startLine <= cd.endLine));
|
|
82
140
|
}
|
|
83
141
|
|
|
84
142
|
/**
|
|
@@ -92,9 +150,12 @@ function _classHasExternalBase(index, symbol) {
|
|
|
92
150
|
* Rust `impl Trait for X`) AND a single project-wide method owner of the
|
|
93
151
|
* name (no in-project supertype defines it → the contract is external —
|
|
94
152
|
* the #210 ownerCount===1 rule).
|
|
95
|
-
* (B) a public-by-shape method whose class
|
|
96
|
-
*
|
|
97
|
-
*
|
|
153
|
+
* (B) a public-by-shape method whose class reaches an unresolved base
|
|
154
|
+
* through its heritage closure (extends chains walked transitively,
|
|
155
|
+
* plus the class's own `implements` clause when the member sits in the
|
|
156
|
+
* class body — fix #270). Private/underscore members are never
|
|
157
|
+
* external-contract surface, so a genuinely-dead one stays claimable
|
|
158
|
+
* (the fix #211 shape predicate).
|
|
98
159
|
* Data-driven, not language-keyed: classes without an `extends` clause (Go
|
|
99
160
|
* embedding, Rust structs / inherent impls) never trip (B), and Rust trait
|
|
100
161
|
* impls trip (A) via traitImpl — so new languages inherit correct behavior.
|
|
@@ -161,17 +222,52 @@ function nameOnlySelfRecursive(index, name) {
|
|
|
161
222
|
return true;
|
|
162
223
|
}
|
|
163
224
|
|
|
164
|
-
/** Check if a position in a line is inside a string literal (quotes/backticks)
|
|
165
|
-
|
|
225
|
+
/** Check if a position in a line is inside a string literal (quotes/backticks).
|
|
226
|
+
* Language-aware (fix #259, clap-measured): a Rust apostrophe is a LIFETIME
|
|
227
|
+
* unless it closes as a char literal within a few chars — `impl<E: Send +
|
|
228
|
+
* 'static> MyTrait` read everything after 'static as "inside a string" and
|
|
229
|
+
* dropped the line's only reference to MyTrait (FALSE-DEAD on clap's
|
|
230
|
+
* autoref-specialization traits). */
|
|
231
|
+
function isInsideString(line, pos, language) {
|
|
166
232
|
let inSingle = false, inDouble = false, inBacktick = false;
|
|
233
|
+
// Template-literal interpolations are CODE (fix #267, hono-measured:
|
|
234
|
+
// `${this.activeRouter.name}` was the getter's ONLY read — masked as
|
|
235
|
+
// string interior, the symbol claimed FALSE-DEAD). JS family only: Go
|
|
236
|
+
// backtick strings are raw, no interpolation. Brace-depth tracked so
|
|
237
|
+
// `${ {a: 1} }` closes at the right brace; quotes inside interpolations
|
|
238
|
+
// are not tracked — the misjudgment direction is UNMASK (counting a
|
|
239
|
+
// usage keeps the symbol alive), never masking code (#253 rule).
|
|
240
|
+
let interpDepth = 0;
|
|
241
|
+
const jsTemplates = language === 'javascript' || language === 'typescript' ||
|
|
242
|
+
language === 'tsx' || language === 'html';
|
|
167
243
|
for (let j = 0; j < pos; j++) {
|
|
168
244
|
const ch = line[j];
|
|
169
245
|
if (ch === '\\') { j++; continue; }
|
|
246
|
+
if (inBacktick && jsTemplates) {
|
|
247
|
+
if (interpDepth === 0) {
|
|
248
|
+
if (ch === '$' && line[j + 1] === '{') { interpDepth = 1; j++; }
|
|
249
|
+
else if (ch === '`') inBacktick = false;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (ch === '{') interpDepth++;
|
|
253
|
+
else if (ch === '}') interpDepth--;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
170
256
|
if (ch === '"' && !inSingle && !inBacktick) inDouble = !inDouble;
|
|
171
|
-
if (ch === "'" && !inDouble && !inBacktick)
|
|
257
|
+
if (ch === "'" && !inDouble && !inBacktick) {
|
|
258
|
+
if (language === 'rust') {
|
|
259
|
+
// Char literal ('x', '\n', '\u{1F600}') — skip past it; a
|
|
260
|
+
// non-closing apostrophe is a lifetime and never opens a
|
|
261
|
+
// string (Rust strings are double-quoted only).
|
|
262
|
+
const m = line.slice(j).match(/^'(?:\\u\{[0-9a-fA-F_]+\}|\\.|[^'\\])'/);
|
|
263
|
+
if (m) j += m[0].length - 1;
|
|
264
|
+
} else {
|
|
265
|
+
inSingle = !inSingle;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
172
268
|
if (ch === '`' && !inDouble && !inSingle) inBacktick = !inBacktick;
|
|
173
269
|
}
|
|
174
|
-
return inSingle || inDouble || inBacktick;
|
|
270
|
+
return inSingle || inDouble || (inBacktick && interpDepth === 0);
|
|
175
271
|
}
|
|
176
272
|
|
|
177
273
|
/**
|
|
@@ -591,9 +687,16 @@ function deadcode(index, options = {}) {
|
|
|
591
687
|
for (let i = 0; i < lines.length; i++) {
|
|
592
688
|
const line = lines[i];
|
|
593
689
|
if (!line.includes(name)) continue;
|
|
594
|
-
// Skip line if entirely inside a line comment
|
|
595
|
-
|
|
596
|
-
|
|
690
|
+
// Skip line if entirely inside a line comment — the
|
|
691
|
+
// markers are language-shaped (fix #259, clap-measured):
|
|
692
|
+
// `#` comments PYTHON ONLY (a Rust attribute line
|
|
693
|
+
// `#[arg(value_parser = helper)]` is code, and the old
|
|
694
|
+
// skip dropped every reference on it — clap's derive-
|
|
695
|
+
// attribute callbacks claimed FALSE-DEAD); `//` comments
|
|
696
|
+
// everywhere EXCEPT Python, where it is floor division.
|
|
697
|
+
const isPython = fileEntry.language === 'python';
|
|
698
|
+
const commentIdx = isPython ? -1 : line.indexOf('//');
|
|
699
|
+
const hashIdx = isPython ? line.indexOf('#') : -1;
|
|
597
700
|
let searchFrom = 0;
|
|
598
701
|
while (searchFrom < line.length) {
|
|
599
702
|
const pos = line.indexOf(name, searchFrom);
|
|
@@ -611,7 +714,7 @@ function deadcode(index, options = {}) {
|
|
|
611
714
|
// Skip if inside a string literal — EXCEPT class-
|
|
612
715
|
// kind names in Python (fix #253a): `x: "Foo"`
|
|
613
716
|
// forward references are real type references.
|
|
614
|
-
if (isInsideString(line, pos) &&
|
|
717
|
+
if (isInsideString(line, pos, fileEntry.language) &&
|
|
615
718
|
!(fileEntry.language === 'python' && classKindNames.has(name))) continue;
|
|
616
719
|
// Property/field access (preceded by '.'), not a
|
|
617
720
|
// call: resolve the RECEIVER (fix #216, express-
|
|
@@ -778,6 +881,15 @@ function deadcode(index, options = {}) {
|
|
|
778
881
|
|
|
779
882
|
const mods = symbol.modifiers || [];
|
|
780
883
|
|
|
884
|
+
// Ambient declaration files are never dead CODE (fix #267,
|
|
885
|
+
// zustand-measured: `interface ImportMeta` in src/types.d.ts —
|
|
886
|
+
// global lib merging UCN cannot see): .d.ts content is erased at
|
|
887
|
+
// compile time and describes external/global shapes — nothing in
|
|
888
|
+
// it is deletable code, so nothing in it is claimable.
|
|
889
|
+
if (symbol.relativePath.endsWith('.d.ts')) {
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
|
|
781
893
|
// Language-specific entry points (called by runtime/test runner, not user code)
|
|
782
894
|
// Each language module declares its own isEntryPoint() rules.
|
|
783
895
|
const langModule = getLanguageModule(lang);
|
|
@@ -899,7 +1011,7 @@ function deadcode(index, options = {}) {
|
|
|
899
1011
|
// by module path, pytest plugins by registration) — zero
|
|
900
1012
|
// in-project references is not evidence of deadness there.
|
|
901
1013
|
const isExternalContract = classAuditSet.has(symbol.type)
|
|
902
|
-
?
|
|
1014
|
+
? _heritageReachesExternalBase(index, symbol, lang, false)
|
|
903
1015
|
: overridesOutOfTreeBase(index, symbol);
|
|
904
1016
|
if (isExternalContract && !options.includeExported) {
|
|
905
1017
|
excludedExternalContract++;
|
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)
|