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/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
- /** Does this CLASS DEF extend at least one base that is not in the project index? */
57
- function _classDefHasExternalBase(index, classDef, lang) {
58
- if (!classDef.extends) return false;
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 the method's enclosing class EXTEND at least one base that is NOT in the
65
- * project index? An out-of-tree base is a framework/library type UCN can't see;
66
- * via inheritance it may dispatch into a public method of the subclass
67
- * polymorphically (Starlette → build_middleware_stack) or by name convention
68
- * (Pydantic → bytes_schema). The class def is matched in the method's own file.
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
- * `implements` is deliberately NOT consulted: implementing an external
71
- * interface/trait makes only the INTERFACE'S OWN methods a contract (those
72
- * carry traitImpl/@Override markers handled by Rule A), not the class's
73
- * unrelated inherent methods. Counting it would wrongly shield, e.g., a Rust
74
- * struct's genuinely-dead inherent method just because the struct also
75
- * `impl Display for`s.
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 => _classDefHasExternalBase(index, cd, lang));
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 EXTENDS an unresolved base.
96
- * Private/underscore members are never external-contract surface, so a
97
- * genuinely-dead one stays claimable (the fix #211 shape predicate).
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
- function isInsideString(line, pos) {
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) inSingle = !inSingle;
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 (// or #)
595
- const commentIdx = line.indexOf('//');
596
- const hashIdx = line.indexOf('#');
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
- ? _classDefHasExternalBase(index, symbol, lang)
1014
+ ? _heritageReachesExternalBase(index, symbol, lang, false)
903
1015
  : overridesOutOfTreeBase(index, symbol);
904
1016
  if (isExternalContract && !options.includeExported) {
905
1017
  excludedExternalContract++;
@@ -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
- while (queue.length > 0) {
1212
- const sym = queue.shift();
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
- // `use crate::ITEM` where ITEM is declared in the crate root file has
400
- // no ITEM.rs the import points at lib.rs/main.rs itself.
401
- return resolveRustModulePath(cargo.srcDir, segments) ||
402
- rustModuleOwnFile(cargo.srcDir, fromFile);
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 > 500) {
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 => ({ name: n, module: i.module }))),
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
- fileEntry.bindings.push({
548
- id: symbol.bindingId,
549
- name: symbol.name,
550
- type: symbol.type,
551
- startLine: symbol.startLine
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 reference symbols
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 [fp, fe] of this.files) {
1238
- if (fp === defFile || !fp.endsWith('.go') || path.dirname(fp) !== pkgDir) continue;
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
- return { total, calls, definitions, imports, references: 0 };
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)