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/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
- function isInsideString(line, pos) {
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) inSingle = !inSingle;
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 (// or #)
595
- const commentIdx = line.indexOf('//');
596
- const hashIdx = line.indexOf('#');
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);
@@ -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)
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) typeMap.set(names[vi], 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') varTypeMap.set(id.text, typeName);
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 }),