ucn 5.0.6 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -451,6 +451,12 @@ function buildInheritanceGraph(index) {
451
451
  }
452
452
  index.extendsGraph.get(symbol.name).push({
453
453
  file: filePath,
454
+ // Per-def anchor (fix #300, attrs-measured): five
455
+ // function-local `class C2Slots(...)` defs in one file
456
+ // carry DIFFERENT parents — file-granular lookup returned
457
+ // the first entry for all of them. startLine lets scope-
458
+ // aware consumers pick the def the call site actually sees.
459
+ startLine: symbol.startLine,
454
460
  parents: resolvedParents
455
461
  });
456
462
 
package/core/index-ir.js CHANGED
@@ -16,6 +16,7 @@ function createImportBindings(imports) {
16
16
  return {
17
17
  name,
18
18
  module: item.module,
19
+ ...(item.type && { kind: item.type }),
19
20
  ...(item.line != null && { line: item.line }),
20
21
  ...(rename && { alias: rename.local }),
21
22
  ...(item.defaultLike && { defaultLike: true }),
@@ -65,17 +66,19 @@ function createFileEntryFromIR({
65
66
 
66
67
  const OPTIONAL_SYMBOL_FIELDS = Object.freeze([
67
68
  'returnedFunctionResult', 'isFunctionVariable', 'paramTypes', 'isAsync',
68
- 'isGenerator', 'generics', 'extends', 'implements', 'indent', 'isNested',
69
+ 'isGenerator', 'generics', 'genericBounds', 'extends', 'implements', 'indent', 'isNested',
69
70
  'enclosingType', 'isMethod', 'receiver', 'memberType', 'fieldType',
70
71
  'aliasOf', 'derefTarget', 'decorators', 'decoratorsWithArgs',
71
72
  'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
72
- 'traitName', 'isSignature', 'memberAssigned', 'bodyScopedName',
73
+ 'traitName', 'isSignature', 'memberAssigned', 'assignedReceiver', 'bodyScopedName',
73
74
  'registryMember', 'registryContainer', 'namespace',
74
75
  'isExtensionMethod', 'extensionReceiver', 'explicitInterface',
75
76
  'lexicalScopeStartLine', 'lexicalScopeEndLine',
76
77
  'returnTypeQualifier', 'macroNeverReturns', 'callbackParamTypes', 'iteratorItemType',
77
78
  'returnedConcreteType', 'returnedConstructors', 'templateDependent',
78
- 'linkage', 'functionLike',
79
+ 'isSpecialization',
80
+ 'linkage', 'functionLike', 'callableAlias', 'exportedAlias',
81
+ 'aliasOwner', 'aliasMember', 'macroParamEffects',
79
82
  ]);
80
83
 
81
84
  function materializeSymbol(fileEntry, item) {
@@ -91,7 +94,9 @@ function materializeSymbol(fileEntry, item) {
91
94
  returnType: item.returnType,
92
95
  modifiers: item.modifiers,
93
96
  docstring: item.docstring,
94
- bindingId: `${fileEntry.relativePath}:${item.kind}:${item.startLine}`,
97
+ bindingId: item.id
98
+ ? `${fileEntry.relativePath}:${item.id}`
99
+ : `${fileEntry.relativePath}:${item.kind}:${item.startLine}`,
95
100
  ...(item.owner && { className: item.owner }),
96
101
  };
97
102
  for (const field of OPTIONAL_SYMBOL_FIELDS) {
@@ -109,7 +114,13 @@ function materializeSymbol(fileEntry, item) {
109
114
  function addIRSymbol(fileEntry, item, symbolTable = null) {
110
115
  const symbol = materializeSymbol(fileEntry, item);
111
116
  fileEntry.symbols.push(symbol);
112
- if (!item.memberAssigned && !item.bodyScopedName) {
117
+ // A Rust `impl X`/`impl Trait for X` block introduces NO name into any
118
+ // scope (fix #286b, cursive-measured: the impl symbol stole the bare-name
119
+ // binding of ColorPair from the cross-file struct, excluding a compiler-
120
+ // true composite-literal caller as other-definition). The struct/enum
121
+ // claim covers the impl — same discipline as deadcode's CLASS_AUDIT_KINDS.
122
+ if (!item.memberAssigned && !item.bodyScopedName && !item.exportedAlias &&
123
+ item.kind !== 'impl') {
113
124
  fileEntry.bindings.push({
114
125
  id: symbol.bindingId,
115
126
  name: symbol.name,
package/core/ir.js CHANGED
@@ -43,17 +43,19 @@ function normalizeSymbol(symbol, family, language, kind, owner = null) {
43
43
  };
44
44
  const passthrough = [
45
45
  'docstring', 'returnedFunctionResult', 'isFunctionVariable', 'paramTypes',
46
- 'isAsync', 'isGenerator', 'generics', 'extends', 'implements', 'indent',
46
+ 'isAsync', 'isGenerator', 'generics', 'genericBounds', 'extends', 'implements', 'indent',
47
47
  'isNested', 'enclosingType', 'isMethod', 'memberType', 'fieldType',
48
48
  'aliasOf', 'derefTarget', 'decorators', 'decoratorsWithArgs',
49
49
  'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
50
- 'traitName', 'isSignature', 'memberAssigned', 'bodyScopedName',
50
+ 'traitName', 'isSignature', 'memberAssigned', 'assignedReceiver', 'bodyScopedName',
51
51
  'registryMember', 'registryContainer', 'isConstructor',
52
52
  'isExtensionMethod', 'extensionReceiver', 'explicitInterface',
53
53
  'namespace', 'lexicalScopeStartLine', 'lexicalScopeEndLine',
54
54
  'returnTypeQualifier', 'macroNeverReturns', 'callbackParamTypes', 'iteratorItemType',
55
55
  'returnedConcreteType', 'returnedConstructors', 'templateDependent',
56
- 'linkage', 'functionLike',
56
+ 'isSpecialization',
57
+ 'linkage', 'functionLike', 'callableAlias', 'exportedAlias',
58
+ 'aliasOwner', 'aliasMember', 'macroParamEffects',
57
59
  ];
58
60
  for (const field of passthrough) {
59
61
  if (symbol[field] !== undefined && symbol[field] !== null) {
@@ -114,6 +116,51 @@ function createFileIR({
114
116
  for (const symbol of stateObjects) append(symbol, 'state', 'state');
115
117
  for (const symbol of macros) append(symbol,
116
118
  symbol.functionLike ? 'callable' : 'state', 'macro');
119
+ // An immutable module-scope member alias has the callable signature of
120
+ // the class member it captures: `const make = Widget.create`. Materialize
121
+ // the local value and each explicit export alias as real function symbols
122
+ // only when its member is static and every declared return type agrees.
123
+ // This is compiler-visible identity; mutable aliases and ambiguous
124
+ // overload returns were rejected by the parser/agreement gate above.
125
+ for (const alias of (parsed.callableAliases || [])) {
126
+ const sources = normalizedSymbols.filter(symbol =>
127
+ symbol.name === alias.member && symbol.owner === alias.owner &&
128
+ (symbol.params !== undefined || symbol.paramsStructured) &&
129
+ (symbol.modifiers?.includes('static') ||
130
+ String(symbol.memberType || symbol.kind).startsWith('static')) &&
131
+ symbol.returnType);
132
+ if (sources.length === 0) continue;
133
+ const sourceReturns = new Set(sources.map(source => source.returnType));
134
+ if (sourceReturns.size !== 1) continue;
135
+ const source = sources[0];
136
+ const exported = (parsed.exports || []).filter(item =>
137
+ !item.source && item.name === alias.name);
138
+ const exposed = exported.map(item => ({
139
+ name: item.type === 'default' ? 'default' : (item.alias || item.name),
140
+ line: item.line || alias.startLine,
141
+ }));
142
+ const localIsExported = exposed.some(item => item.name === alias.name);
143
+ const makeAlias = (name, startLine, isExported, exportedAlias = false) => ({
144
+ bindingId: `callable-alias:${alias.owner}.${alias.member}:${name}:${startLine}`,
145
+ name,
146
+ startLine,
147
+ endLine: startLine,
148
+ params: source.params,
149
+ ...(source.paramsStructured && { paramsStructured: source.paramsStructured }),
150
+ returnType: source.returnType,
151
+ modifiers: isExported ? ['export'] : [],
152
+ callableAlias: true,
153
+ ...(exportedAlias && { exportedAlias: true }),
154
+ aliasOwner: alias.owner,
155
+ aliasMember: alias.member,
156
+ });
157
+ append(makeAlias(alias.name, alias.startLine, localIsExported),
158
+ 'callable', 'function');
159
+ for (const item of exposed) {
160
+ if (item.name === alias.name) continue;
161
+ append(makeAlias(item.name, item.line, true, true), 'callable', 'function');
162
+ }
163
+ }
117
164
  const imports = [...(parsed.imports || [])];
118
165
  return {
119
166
  schemaVersion: IR_SCHEMA_VERSION,
@@ -74,6 +74,23 @@ function isTestEntry(entry) {
74
74
  return isTestPath(entry.relativePath || entry.file || '');
75
75
  }
76
76
 
77
+ function formatAmbiguityCandidates(lines, ambiguity) {
78
+ if (!ambiguity?.items?.length) return;
79
+ const owners = ambiguity.dispatchOwners
80
+ ? `; ${ambiguity.dispatchOwners} dispatch owners` : '';
81
+ lines.push(` competing definitions (${ambiguity.totalDefinitions}${owners}):`);
82
+ for (const candidate of ambiguity.items) {
83
+ const owner = candidate.owner
84
+ ? ` on ${candidate.owner}${candidate.memberAssignment ? ' (member assignment)' : ''}` : '';
85
+ const selected = candidate.selected ? ' [selected target]' : '';
86
+ lines.push(` - ${candidate.handle} — ${candidate.type}${owner}${selected}`);
87
+ }
88
+ if (ambiguity.truncated) {
89
+ lines.push(` (+${ambiguity.totalDefinitions - ambiguity.items.length} more — ` +
90
+ `use find ${ambiguity.name})`);
91
+ }
92
+ }
93
+
77
94
  /**
78
95
  * Render the conservation contract lines: ACCOUNT and CONTRACT (always),
79
96
  * WARNING (unparsed files containing the symbol), FILTERED (display-filter
@@ -273,6 +290,9 @@ function formatContextJson(context) {
273
290
  ? 'runtime-dispatch' : 'actionable-ambiguity'),
274
291
  ...(c.dispatchFamily && { dispatchFamily: c.dispatchFamily }),
275
292
  })),
293
+ ...(context.ambiguityCandidates && {
294
+ ambiguityCandidates: context.ambiguityCandidates,
295
+ }),
276
296
  ...(context.warnings && { warnings: context.warnings })
277
297
  }
278
298
  });
@@ -327,6 +347,9 @@ function formatContextJson(context) {
327
347
  ? 'runtime-dispatch' : 'actionable-ambiguity'),
328
348
  ...(c.dispatchFamily && { dispatchFamily: c.dispatchFamily }),
329
349
  })),
350
+ ...(context.ambiguityCandidates && {
351
+ ambiguityCandidates: context.ambiguityCandidates,
352
+ }),
330
353
  callees: callees.map(c => ({
331
354
  name: c.name,
332
355
  type: c.type,
@@ -427,6 +450,7 @@ function formatContext(ctx, options = {}) {
427
450
  const typeUnverified = ctx.unverifiedCallers || [];
428
451
  if (typeUnverified.length > 0) {
429
452
  lines.push(`\nCALLERS — UNVERIFIED (${typeUnverified.length}) — call syntax, no binding/receiver evidence:`);
453
+ formatAmbiguityCandidates(lines, ctx.ambiguityCandidates);
430
454
  const cap = 10;
431
455
  let shown = 0;
432
456
  for (const u of typeUnverified) {
@@ -653,6 +677,7 @@ function formatContext(ctx, options = {}) {
653
677
  // Always visible and capped at 10 one-liners unless --all.
654
678
  if (actionableUnverified.length > 0) {
655
679
  lines.push(`${compact ? '' : '\n'}CALLERS — UNVERIFIED (${actionableUnverified.length}) — call syntax, no binding/receiver evidence:`);
680
+ formatAmbiguityCandidates(lines, ctx.ambiguityCandidates);
656
681
  const cap = (ctx.meta && ctx.meta.all) ? Infinity : 10;
657
682
  let shown = 0;
658
683
  for (const u of actionableUnverified) {
@@ -70,7 +70,7 @@ function formatPlan(plan, options = {}) {
70
70
  lines.push(` Files affected: ${plan.filesAffected}`);
71
71
  if (plan.changeSummary) {
72
72
  const summary = plan.changeSummary;
73
- lines.push(` Definition ${summary.definitions}, calls/references ${summary.calls}, imports ${summary.imports}, exports ${summary.exports}; manual review required for ${summary.reviewRequired} of these changes`);
73
+ lines.push(` Definition ${summary.definitions}, calls ${summary.calls}, references ${summary.references || 0}, imports ${summary.imports}, exports ${summary.exports}; manual review required for ${summary.reviewRequired} of these changes`);
74
74
  }
75
75
  if (plan.unchangedSites > 0) {
76
76
  lines.push(` ${plan.unchangedSites} existing call site${plan.unchangedSites === 1 ? '' : 's'} require no edit because the new parameter has a default.`);
@@ -338,7 +338,7 @@ function formatGitLine(git) {
338
338
  * Display label for an unverified-tier entry's reason. Dispatch-tiered
339
339
  * entries (nominal languages) carry attribution metadata: the declared
340
340
  * supertype the call dispatches through (dispatchVia) and how many
341
- * same-name definitions the dispatch could land on (dispatchCandidates).
341
+ * distinct same-name owners the dispatch could land on (dispatchCandidates).
342
342
  */
343
343
  function unverifiedReasonLabel(entry) {
344
344
  if (!entry || !entry.reason) return '';
@@ -356,7 +356,7 @@ function unverifiedReasonLabel(entry) {
356
356
  : `possible-dispatch via ${entry.dispatchVia}`;
357
357
  }
358
358
  if (entry.reason === 'method-ambiguous' && entry.dispatchCandidates > 1) {
359
- return `method-ambiguous — ${entry.dispatchCandidates} same-name definitions`;
359
+ return `method-ambiguous — ${entry.dispatchCandidates} dispatch owners`;
360
360
  }
361
361
  if (entry.reason === 'overload-ambiguous' && entry.dispatchCandidates > 1) {
362
362
  return `overload-ambiguous — 1 of ${entry.dispatchCandidates} applicable overloads`;
package/core/project.js CHANGED
@@ -444,6 +444,9 @@ class ProjectIndex {
444
444
  this._completenessCache = null;
445
445
  this._attrTypeCache = null;
446
446
  this._computedDispatchBlindspots = null;
447
+ this._cppVisibleFilesCache?.clear();
448
+ this._cppTargetVisibilityCache?.clear();
449
+ this._cppMacroParamOutcomesCache?.clear();
447
450
  // Endpoints cache (server routes / client requests / bridges) becomes
448
451
  // stale when files change; clear on every rebuild.
449
452
  this._endpointsCache = null;
@@ -589,6 +592,13 @@ class ProjectIndex {
589
592
  return false;
590
593
  }
591
594
 
595
+ // These query caches depend on project symbols and include closure.
596
+ // A changed file can alter either even before a full rebuild reaches
597
+ // its graph phase, so no prior call-identity verdict may survive.
598
+ this._cppVisibleFilesCache?.clear();
599
+ this._cppTargetVisibilityCache?.clear();
600
+ this._cppMacroParamOutcomesCache?.clear();
601
+
592
602
  if (existing) {
593
603
  this.removeFileSymbols(filePath);
594
604
  }
@@ -942,6 +952,28 @@ class ProjectIndex {
942
952
  return entries;
943
953
  }
944
954
 
955
+ /**
956
+ * Def-exact inheritance parents (fix #300): resolve parents for ONE
957
+ * specific class definition by (file, startLine). Same-name classes in
958
+ * one file (function-local test classes) carry different parents; the
959
+ * file-granular lookup above conflates them. Returns null when no entry
960
+ * matches — for a known def that means it extends nothing (entries are
961
+ * only recorded for extends-bearing defs), which callers must treat as
962
+ * "no parents", never fall back to a sibling def's edges.
963
+ * @param {string} className
964
+ * @param {string} contextFile
965
+ * @param {number} defStartLine
966
+ * @returns {string[]|null}
967
+ */
968
+ _getInheritanceParentsAt(className, contextFile, defStartLine) {
969
+ const entries = this.extendsGraph.get(className);
970
+ if (!entries || entries.length === 0) return null;
971
+ if (typeof entries[0] !== 'object' || entries[0].file === undefined) return null;
972
+ const match = entries.find(e => e.file === contextFile &&
973
+ e.startLine === defStartLine);
974
+ return match ? match.parents : null;
975
+ }
976
+
945
977
  /**
946
978
  * Resolve which file a class is defined in, preferring contextFile.
947
979
  * Used during inheritance BFS to find grandparent chains.
package/core/search.js CHANGED
@@ -352,6 +352,15 @@ function usages(index, name, options = {}) {
352
352
  usageType: u.usageType,
353
353
  isDefinition: false,
354
354
  ...(u.receiver && { receiver: u.receiver }),
355
+ // Refactoring internals need the exact AST token and
356
+ // receiver provenance, but the public usages JSON is a
357
+ // stable line-oriented inventory. Keep this opt-in so
358
+ // plan gains precision without changing that surface.
359
+ ...(options.internalEvidence && {
360
+ ...(Number.isInteger(u.column) && { column: u.column }),
361
+ ...(u.receiverIsModule && { receiverIsModule: true }),
362
+ ...(u.receiverLocalBinding && { receiverLocalBinding: true }),
363
+ }),
355
364
  ...(callerSym && {
356
365
  callerName: callerSym.name,
357
366
  callerStartLine: callerSym.startLine