ucn 5.5.1 → 5.5.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.
Files changed (97) hide show
  1. package/.claude/skills/ucn/SKILL.md +35 -9
  2. package/.claude/skills/ucn/references/commands.md +40 -18
  3. package/.claude/skills/ucn/references/resolution-rules.md +126 -20
  4. package/.claude/skills/ucn/references/trust-contract.md +5 -3
  5. package/README.md +32 -21
  6. package/cli/index.js +2 -1
  7. package/core/accessors.js +341 -54
  8. package/core/account.js +74 -20
  9. package/core/analysis.js +257 -125
  10. package/core/ast-analysis.js +12 -3
  11. package/core/bridge.js +15 -65
  12. package/core/brief.js +8 -4
  13. package/core/build-worker.js +2 -1
  14. package/core/cache.js +137 -6
  15. package/core/call-name-cache.js +134 -0
  16. package/core/callable-scope.js +21 -0
  17. package/core/callers.js +4726 -940
  18. package/core/check.js +22 -5
  19. package/core/class-identity.js +399 -12
  20. package/core/client-receivers.js +189 -0
  21. package/core/constructor-calls.js +831 -0
  22. package/core/constructor-hierarchy.js +147 -0
  23. package/core/constructor-protocol.js +196 -0
  24. package/core/contract-membership.js +346 -140
  25. package/core/cpp-conversion-constraints.js +123 -0
  26. package/core/cpp-conversion-syntax.js +216 -0
  27. package/core/cpp-conversions.js +414 -0
  28. package/core/cpp-scope.js +245 -44
  29. package/core/deadcode.js +61 -16
  30. package/core/decorator-binding.js +137 -3
  31. package/core/enum-reference-syntax.js +134 -0
  32. package/core/execute.js +17 -1
  33. package/core/external-macros.js +42 -10
  34. package/core/go-packages.js +171 -0
  35. package/core/graph-build.js +2 -0
  36. package/core/graph.js +75 -59
  37. package/core/ignore-rules.js +2 -1
  38. package/core/imports.js +145 -19
  39. package/core/index-ir.js +41 -4
  40. package/core/inprocess-clients.js +41 -15
  41. package/core/ir.js +27 -6
  42. package/core/macro-expansion.js +38 -2
  43. package/core/out-of-class-members.js +182 -0
  44. package/core/output/analysis-ext.js +4 -2
  45. package/core/output/analysis.js +89 -20
  46. package/core/output/graph.js +12 -3
  47. package/core/output/lines.js +4 -3
  48. package/core/output/public.js +2 -0
  49. package/core/output/reporting.js +12 -1
  50. package/core/output/shared.js +2 -0
  51. package/core/output/tracing.js +28 -22
  52. package/core/output-budget.js +2 -2
  53. package/core/parallel-build.js +52 -1
  54. package/core/project.js +217 -112
  55. package/core/provenance-constructor-base.js +161 -0
  56. package/core/provenance-facts.js +257 -27
  57. package/core/provenance-overload.js +458 -54
  58. package/core/provenance-type.js +487 -0
  59. package/core/provenance.js +234 -15
  60. package/core/public-command.js +17 -2
  61. package/core/query-bodies.js +159 -0
  62. package/core/reflection.js +270 -2
  63. package/core/rename-token-syntax.js +111 -0
  64. package/core/reporting.js +13 -7
  65. package/core/request-helpers.js +63 -7
  66. package/core/rust-macro-expansion.js +151 -19
  67. package/core/rust-modules.js +1 -1
  68. package/core/rust-names.js +607 -0
  69. package/core/search.js +70 -17
  70. package/core/shared.js +98 -1
  71. package/core/source-memo.js +201 -0
  72. package/core/text-usage-syntax.js +38 -0
  73. package/core/tracing.js +7 -1
  74. package/core/type-denotation.js +124 -37
  75. package/core/type-references.js +615 -246
  76. package/core/value-references.js +2777 -0
  77. package/core/verify.js +1097 -281
  78. package/languages/adapter.js +28 -4
  79. package/languages/c-family.js +1118 -164
  80. package/languages/constructor-bases.js +105 -0
  81. package/languages/cpp-platform.js +26 -0
  82. package/languages/cpp-site-syntax.js +83 -0
  83. package/languages/csharp-scope.js +439 -0
  84. package/languages/csharp.js +428 -68
  85. package/languages/go.js +385 -207
  86. package/languages/html.js +4 -2
  87. package/languages/index.js +397 -2
  88. package/languages/java-platform.js +226 -0
  89. package/languages/java-unicode.js +269 -0
  90. package/languages/java.js +461 -117
  91. package/languages/javascript.js +418 -66
  92. package/languages/lexical-scope.js +190 -23
  93. package/languages/python.js +404 -54
  94. package/languages/rust.js +510 -82
  95. package/languages/utils.js +67 -8
  96. package/mcp/server.js +8 -2
  97. package/package.json +3 -3
@@ -195,9 +195,23 @@ class PythonClientResolver {
195
195
 
196
196
  /** The fixture named `name` a test in `file` requests: same module first,
197
197
  * then conftest.py files from the test's directory up to the root. */
198
- fixtureDef(file, name) {
199
- const key = `${file}\0${name}`;
198
+ fixtureDef(file, name, requester = null) {
199
+ let owner = requester?.parent;
200
+ while (owner && owner.type !== 'class_definition') owner = owner.parent;
201
+ const key = `${file}\0${name}\0${owner?.startIndex ?? ''}`;
200
202
  if (this.fixtureMemo.has(key)) return this.fixtureMemo.get(key);
203
+ this.fixtureMemo.set(key, null);
204
+ if (owner) {
205
+ const matches = namedChildren(field(owner, 'body')).map(stmt =>
206
+ stmt.type === 'decorated_definition' ? field(stmt, 'definition') : stmt)
207
+ .filter(fn => fn?.type === 'function_definition' && field(fn, 'name')?.text === name);
208
+ if (matches.length) {
209
+ const found = matches.length === 1 && this.isFixture(file, matches[0])
210
+ ? { file, fnNode: matches[0] } : null;
211
+ this.fixtureMemo.set(key, found);
212
+ return found;
213
+ }
214
+ }
201
215
  let found = this.moduleFixture(file, name);
202
216
  if (!found) {
203
217
  for (let dir = path.dirname(file); dir.startsWith(this.index.root); dir = path.dirname(dir)) {
@@ -213,7 +227,8 @@ class PythonClientResolver {
213
227
  return found;
214
228
  }
215
229
 
216
- moduleFixture(file, name) {
230
+ moduleFixture(file, name, depth = 0) {
231
+ if (depth > MAX_HOPS) return null;
217
232
  const root = this.sess.root(file);
218
233
  if (!root) return null;
219
234
  const hits = [];
@@ -222,7 +237,12 @@ class PythonClientResolver {
222
237
  if (fn?.type === 'function_definition' && field(fn, 'name')?.text === name &&
223
238
  this.isFixture(file, fn)) hits.push(fn);
224
239
  }
225
- return hits.length === 1 ? { file, fnNode: hits[0] } : null;
240
+ if (hits.length) return hits.length === 1 ? { file, fnNode: hits[0] } : null;
241
+ const entry = this.sess.entry(file);
242
+ const bindings = (entry?.importBindings || []).filter(b => (b.alias || b.name) === name && !b.deferred);
243
+ if (bindings.length !== 1 || bindings[0].kind !== 'from') return null;
244
+ const relative = entry.moduleResolved?.[bindings[0].module];
245
+ return relative ? this.moduleFixture(path.join(this.index.root, relative), bindings[0].name, depth + 1) : null;
226
246
  }
227
247
 
228
248
  /** The expression a fixture provides: its single yielded/returned value,
@@ -253,11 +273,14 @@ class PythonClientResolver {
253
273
  if (!expr || expr.type !== 'identifier' || hops > MAX_HOPS) return { file, expr };
254
274
  if (fnNode) {
255
275
  const values = boundValues(field(fnNode, 'body'), expr.text, this.bindingMemo);
256
- if (values === null) return null;
276
+ if (values === null) {
277
+ const decl = routeGraph.findDecl(this.sess, file, expr.text, expr);
278
+ return decl?.kind === 'function' ? { file, expr: decl.valueNode } : null;
279
+ }
257
280
  if (values.length === 1) return this.followBinding(file, fnNode, unwrap(values[0].node), hops + 1);
258
281
  if (values.length > 1) return null;
259
282
  if (isParameterOf(fnNode, expr.text)) {
260
- const fixture = this.fixtureDef(file, expr.text);
283
+ const fixture = this.fixtureDef(file, expr.text, fnNode);
261
284
  return fixture ? this.fixtureValue(fixture.file, fixture.fnNode, hops + 1) : null;
262
285
  }
263
286
  }
@@ -319,7 +342,7 @@ class PythonClientResolver {
319
342
  if (expr.type !== 'identifier') return null;
320
343
  const fnNode = enclosingFunctionNode(expr);
321
344
  if (fnNode && isParameterOf(fnNode, expr.text)) {
322
- const fixture = this.fixtureDef(file, expr.text);
345
+ const fixture = this.fixtureDef(file, expr.text, fnNode);
323
346
  if (!fixture) return null;
324
347
  const value = this.fixtureValue(fixture.file, fixture.fnNode, hops + 1);
325
348
  if (!value?.expr) return null;
@@ -345,13 +368,8 @@ class PythonClientResolver {
345
368
  return null;
346
369
  }
347
370
 
348
- /**
349
- * { appKey } for a request `receiver.verb(path)` at `line` of `file` made
350
- * through an in-process client, { inProcess: true } when the client is
351
- * built from an app that did not resolve, else null (not a client built
352
- * from an app).
353
- */
354
- resolve(file, line, receiver, name) {
371
+ /** Values bound to a request receiver, without assuming they are clients. */
372
+ clientValues(file, line, receiver, name) {
355
373
  const root = this.sess.root(file);
356
374
  if (!root) return null;
357
375
  const sites = requestCallNodes(root, line, receiver, name);
@@ -365,7 +383,7 @@ class PythonClientResolver {
365
383
  if (values.length > 0) {
366
384
  clients = this.bindingInEffect(values, receiverNode).map(node => ({ file, expr: unwrap(node) }));
367
385
  } else if (isParameterOf(fnNode, receiver)) {
368
- const fixture = this.fixtureDef(file, receiver);
386
+ const fixture = this.fixtureDef(file, receiver, fnNode);
369
387
  const value = fixture ? this.fixtureValue(fixture.file, fixture.fnNode, 1) : null;
370
388
  clients = value ? [value] : [];
371
389
  }
@@ -376,6 +394,14 @@ class PythonClientResolver {
376
394
  clients = this.bindingInEffect(values, receiverNode).map(node => ({ file, expr: unwrap(node) }));
377
395
  }
378
396
  }
397
+ const values = clients?.map(c => this.followBinding(c.file,
398
+ enclosingFunctionNode(c.expr), c.expr, 0));
399
+ return values?.every(Boolean) ? values : null;
400
+ }
401
+
402
+ /** The served app key, an unresolved in-process app, or null. */
403
+ resolve(file, line, receiver, name) {
404
+ const clients = this.clientValues(file, line, receiver, name);
379
405
  if (!clients || clients.length === 0 || clients.some(c => c.expr?.type !== 'call')) return null;
380
406
  const keys = clients.map(c => this.clientAppOnce(c.file, c.expr));
381
407
  if (keys.every(key => key && key === keys[0])) {
package/core/ir.js CHANGED
@@ -62,8 +62,8 @@ function normalizeSymbol(symbol, family, language, kind, owner = null) {
62
62
  modifiers: [...(symbol.modifiers || [])],
63
63
  };
64
64
  const passthrough = [
65
- 'docstring', 'returnedFunctionResult', 'isFunctionVariable', 'paramTypes',
66
- 'isAsync', 'isGenerator', 'generics', 'ownerGenerics', 'genericBounds', 'extends', 'implements', 'indent',
65
+ 'docstring', 'sourceSpan', 'returnedFunctionResult', 'isFunctionVariable', 'paramTypes',
66
+ 'isAsync', 'isGenerator', 'generics', 'ownerGenerics', 'genericBounds', 'typeParameterConstraints', 'extends', 'implements', 'indent',
67
67
  'isNested', 'enclosingType', 'isMethod', 'memberType', 'fieldType', 'embedded',
68
68
  'aliasOf', 'aliasQualifier', 'aliasMembers', 'aliasTypeText', 'aliasTypeParameters', 'aliasTypeDefaults', 'derefTarget', 'decorators', 'decoratorsWithArgs',
69
69
  'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
@@ -82,7 +82,9 @@ function normalizeSymbol(symbol, family, language, kind, owner = null) {
82
82
  'generatedByMacro',
83
83
  'typeArity', 'ownerTypeArity',
84
84
  'annotationType', 'valueShape', 'friendOf', 'templateParams', 'delegateParams',
85
- 'typedefName',
85
+ 'typedefName', 'forwardDeclaration', 'anonymousNamespace', 'specialization',
86
+ 'memberConst', 'refQualifier', 'fieldPointers', 'memberPaths', 'instanceAttribute',
87
+ 'ambientAugmentation',
86
88
  ];
87
89
  for (const field of passthrough) {
88
90
  if (symbol[field] !== undefined && symbol[field] !== null) {
@@ -110,9 +112,15 @@ function createFileIR({
110
112
  const append = (symbol, family, kind, owner = null) => {
111
113
  // Members one macro invocation declares share its lines (fix #385):
112
114
  // their own binding ids keep overloads apart.
113
- const key = `${owner || symbol.className || ''}\0${symbol.name}\0${kind}\0` +
114
- `${symbol.startLine}\0${symbol.endLine}` +
115
- (symbol.generatedByMacro ? `\0${symbol.bindingId || ''}` : '');
115
+ const key = JSON.stringify([
116
+ owner || symbol.className || '', symbol.name, kind,
117
+ symbol.startLine, symbol.endLine, symbol.namespace || '',
118
+ symbol.enclosingType || '', symbol.lexicalScopeStartLine ?? null,
119
+ symbol.lexicalScopeEndLine ?? null, symbol.anonymousNamespace || false,
120
+ symbol.typeArity ?? null, symbol.ownerTypeArity ?? null,
121
+ symbol.params ?? null,
122
+ symbol.generatedByMacro ? symbol.bindingId || '' : '',
123
+ ]);
116
124
  if (seen.has(key)) return;
117
125
  seen.add(key);
118
126
  normalizedSymbols.push(normalizeSymbol(symbol, family, language, kind, owner));
@@ -236,6 +244,10 @@ function createFileIR({
236
244
  ...(parsed.asyncClosureNames?.length > 0 && {
237
245
  asyncClosureNames: [...parsed.asyncClosureNames],
238
246
  }),
247
+ // Rust item-position macro invocations of each module (fix #422).
248
+ ...(parsed.itemMacros?.length > 0 && {
249
+ itemMacros: parsed.itemMacros.map(item => ({ ...item })),
250
+ }),
239
251
  // Shapes of calls whose value is not consumed where it is produced
240
252
  // (fix #371/#372): audit-async reads a file's call records only when
241
253
  // one of these can reach a future producer.
@@ -249,12 +261,18 @@ function createFileIR({
249
261
  ...(parsed.reflectionSites?.length > 0 && {
250
262
  reflectionSites: parsed.reflectionSites.map(item => ({ ...item })),
251
263
  }),
264
+ ...(Array.isArray(parsed.computedDispatchSites) && {
265
+ computedDispatchSites: parsed.computedDispatchSites.map(item => ({ ...item })),
266
+ }),
252
267
  ...(parsed.macroScopeMarkers?.length > 0 && {
253
268
  macroScopeMarkers: parsed.macroScopeMarkers.map(item => ({ ...item })),
254
269
  }),
255
270
  ...(parsed.cppUsings?.length > 0 && {
256
271
  cppUsings: parsed.cppUsings.map(item => ({ ...item })),
257
272
  }),
273
+ // Type-parameter names a file declares (fix #421), when the parser
274
+ // read them from the raw source.
275
+ ...(Array.isArray(parsed.typeParameterNames) && { typeParameterNames: [...parsed.typeParameterNames] }),
258
276
  ...(parsed.languageFeatures?.length > 0 && {
259
277
  languageFeatures: [...parsed.languageFeatures],
260
278
  }),
@@ -289,6 +307,9 @@ function createFileIR({
289
307
  }),
290
308
  // The byte ranges the C/C++ recovery blanked (fix #387): a query
291
309
  // rebuilds the recovered tree with one parse.
310
+ ...(Array.isArray(parsed.cppBaseSyntax) && { cppBaseSyntax: parsed.cppBaseSyntax.map(row => ({
311
+ ...row, syntax: row.syntax && { ...row.syntax, namespaces: [...row.syntax.namespaces], classes: [...row.syntax.classes] },
312
+ })) }),
292
313
  ...(Array.isArray(parsed.recoveryBlanks) && {
293
314
  recoveryBlanks: parsed.recoveryBlanks.map(range => [...range]),
294
315
  }),
@@ -384,6 +384,37 @@ function macroBodyTokens(index, def) {
384
384
  return expansionState(index).tokensOf(def);
385
385
  }
386
386
 
387
+ class DeclarationChoice extends Error {
388
+ constructor(name, definitions) { super(name); this.definitions = definitions; }
389
+ }
390
+
391
+ /** Expand every bounded macro configuration of a declaration fragment. */
392
+ function expandDeclaration(index, file, line, source) {
393
+ const state = expansionState(index);
394
+ const pending = [new Map()], variants = [];
395
+ while (pending.length) {
396
+ const choices = pending.pop();
397
+ const site = { file, line, lines: index._getFileLines(file), fileState: {},
398
+ lookupMemo: new Map(), usedDefs: new Set(), budget: MAX_TOKENS, ambiguous: false,
399
+ declarationChoices: choices };
400
+ try {
401
+ const tokens = expandTokens(index, state, site, lexPP(source), new Set(), 0);
402
+ const text = tokens.map(token => token.v).join(' ');
403
+ const complete = !site.ambiguous && !tokens.some(token => token.opaque || token.noexpand);
404
+ if (!variants.some(variant => variant.text === text)) variants.push({ text, complete });
405
+ } catch (error) {
406
+ if (error instanceof DeclarationChoice) {
407
+ if (pending.length + variants.length + error.definitions.length > MAX_VARIANTS) {
408
+ variants.push({ text: source, complete: false }); break;
409
+ }
410
+ for (const definition of error.definitions) pending.push(new Map(choices).set(error.message, definition));
411
+ } else if (error instanceof BlindExpansion) variants.push({ text: source, complete: false });
412
+ else throw error;
413
+ }
414
+ }
415
+ return { ...variants[0], complete: variants.length === 1 && variants[0].complete, variants };
416
+ }
417
+
387
418
  /** Does an invocation with `args` fit the macro's parameter list? */
388
419
  function arityFits(def, args) {
389
420
  const params = def.ppParams || [];
@@ -508,7 +539,12 @@ function expandTokens(index, state, site, tokens, hide, depth) {
508
539
  out.push({ ...t, noexpand: true });
509
540
  continue;
510
541
  }
511
- const scope = definitionsInScope(index, state, site, t.v);
542
+ let scope = definitionsInScope(index, state, site, t.v);
543
+ if (site.declarationChoices && scope.defs.length > 1 && !scope.truncated) {
544
+ const chosen = site.declarationChoices.get(t.v);
545
+ if (!chosen) throw new DeclarationChoice(t.v, scope.defs);
546
+ scope = { defs: [chosen], ambiguous: false, truncated: false };
547
+ }
512
548
  let def = scope.defs[0];
513
549
  if (!def) {
514
550
  out.push(t);
@@ -1413,4 +1449,4 @@ function macroExpansionSummary(index) {
1413
1449
  }
1414
1450
 
1415
1451
  module.exports = { withMacroExpansion, macroExpansionSummary, expansionState, persistableMacroExpansion,
1416
- macroDefinitionsAt, macroBodyTokens, codeTokens, pastedNameSites };
1452
+ macroDefinitionsAt, macroBodyTokens, expandDeclaration, codeTokens, pastedNameSites };
@@ -0,0 +1,182 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * core/out-of-class-members.js - constructors defined outside their class
5
+ * body (fix #421): the C++ header + source layout, where `W::W(int)` in
6
+ * w.cpp defines the `W(int)` its class declares in w.h (trait
7
+ * `outOfClassMemberDefinitions`).
8
+ *
9
+ * The constructor family of a class is its in-class declarations; an
10
+ * out-of-class definition holds the body of exactly one of them. Linkage is
11
+ * by the owner's scope path, textual visibility of the class (same file or
12
+ * include closure) and the declaration's parameter signature. Anything less
13
+ * certain links nothing: the caller keeps its review result.
14
+ */
15
+
16
+ const { langTraits } = require('../languages');
17
+
18
+ const OWNER_KINDS = new Set(['class', 'struct', 'union']);
19
+
20
+ const scopePath = def => [def.namespace, def.enclosingType].filter(Boolean).join('::');
21
+
22
+ const outOfClassLanguage = (index, file) => !!langTraits(index.files.get(file)?.language)?.outOfClassMemberDefinitions;
23
+ const applies = (index, def) => !!def?.className && outOfClassLanguage(index, def.file);
24
+
25
+ function memo(index, name) {
26
+ return index.isProvenanceFactIndex ? null : index._opMemo?.(name, () => new WeakMap());
27
+ }
28
+
29
+ /** A definition outside every class body (its symbol's span lies in no owner). */
30
+ function isOutOfClass(index, def) {
31
+ const { ownerDefinitionOf } = require('./class-identity');
32
+ return !def.isSignature && !ownerDefinitionOf(index, def);
33
+ }
34
+
35
+ /**
36
+ * The class an out-of-class constructor definition belongs to: the one
37
+ * class of its name whose scope path equals the definition's qualifier and
38
+ * that the definition's file sees. Null when none or several qualify.
39
+ */
40
+ function definitionOwner(index, def) {
41
+ if (!applies(index, def) || !isOutOfClass(index, def)) return null;
42
+ const saved = memo(index, 'outOfClassOwner');
43
+ if (saved?.has(def)) return saved.get(def);
44
+ const visible = require('./cpp-scope').includeClosure(index, def.file);
45
+ const path = scopePath(def);
46
+ const owners = (index.symbols.get(def.className) || []).filter(owner => OWNER_KINDS.has(owner.type) &&
47
+ !owner.forwardDeclaration && scopePath(owner) === path && visible.has(owner.file));
48
+ const owner = owners.length === 1 ? owners[0] : null;
49
+ saved?.set(def, owner);
50
+ return owner;
51
+ }
52
+
53
+ /** The in-class constructor declaration an out-of-class definition defines. */
54
+ function declarationOf(index, def) {
55
+ const owner = definitionOwner(index, def);
56
+ if (!owner) return null;
57
+ const { ownConstructors } = require('./constructor-calls');
58
+ const { _cFamilySignatureKey } = require('./callers');
59
+ const key = _cFamilySignatureKey(def);
60
+ const declared = ownConstructors(index, owner).filter(member => member.isSignature &&
61
+ _cFamilySignatureKey(member) === key);
62
+ return declared.length === 1 ? declared[0] : null;
63
+ }
64
+
65
+ /**
66
+ * The definition holding the body of an in-class constructor declaration:
67
+ * the declaration itself when it has a body, else its one linked
68
+ * out-of-class definition (null when there is none or several).
69
+ */
70
+ function bodyDefinitionOf(index, declaration) {
71
+ if (!declaration?.isSignature || !applies(index, declaration)) return declaration;
72
+ const saved = memo(index, 'outOfClassBody');
73
+ if (saved?.has(declaration)) return saved.get(declaration);
74
+ const definitions = (index.symbols.get(declaration.name) || []).filter(def => def !== declaration &&
75
+ def.type === declaration.type && def.className === declaration.className && !def.isSignature &&
76
+ declarationOf(index, def) === declaration);
77
+ const body = definitions.length === 1 ? definitions[0] : null;
78
+ saved?.set(declaration, body);
79
+ return body;
80
+ }
81
+
82
+ /** The out-of-class constructor definition enclosing a call record of `file`. */
83
+ function enclosingDefinition(index, file, call, owner) {
84
+ const enclosing = call.enclosingFunction;
85
+ if (!enclosing || !outOfClassLanguage(index, file)) return null;
86
+ const matches = (index.files.get(file)?.symbols || []).filter(def => def.name === owner.name &&
87
+ def.className === owner.name && !def.isSignature && def.startLine === enclosing.startLine &&
88
+ def.endLine === enclosing.endLine && require('./constructor-calls').isConstructor(def, index));
89
+ return matches.length === 1 && definitionOwner(index, matches[0]) === owner ? matches[0] : null;
90
+ }
91
+
92
+ /**
93
+ * Does a mem-initializer of an out-of-class constructor definition
94
+ * (parser `memberInitializer`) initialize a data member rather than
95
+ * construct a base (fix #421)? A name that is no project type, or a field
96
+ * of the definition's class, is a member; the record is then no call.
97
+ */
98
+ function initializesMember(index, file, call, namesType) {
99
+ if (!call.memberInitializer) return false;
100
+ if (!namesType(call.name)) return true;
101
+ const enclosing = call.enclosingFunction;
102
+ if (!enclosing) return false;
103
+ const { isConstructor } = require('./constructor-calls');
104
+ const definitions = (index.files.get(file)?.symbols || []).filter(def => !def.isSignature &&
105
+ def.startLine === enclosing.startLine && def.endLine === enclosing.endLine && isConstructor(def, index));
106
+ const owner = definitions.length === 1 ? definitionOwner(index, definitions[0]) : null;
107
+ return !!owner && (index.symbols.get(call.name) || []).some(def => def.className === owner.name &&
108
+ def.file === owner.file && def.startLine >= owner.startLine && def.endLine <= owner.endLine &&
109
+ (def.type === 'field' || def.memberType === 'field'));
110
+ }
111
+
112
+ /**
113
+ * The project bases an out-of-class constructor definition leaves to
114
+ * implicit construction (fix #421): its class's written bases that none of
115
+ * its mem-initializers names, none when it delegates. `isProjectType(name)`
116
+ * keeps the bases the project declares.
117
+ */
118
+ function omittedBases(index, def, isProjectType) {
119
+ const owner = definitionOwner(index, def);
120
+ if (!owner?.extends) return [];
121
+ const { getCachedCalls } = require('./callers');
122
+ const { splitParentList } = require('./graph-build');
123
+ const initializers = (getCachedCalls(index, def.file) || []).filter(call => call.constructorSyntax === 'delegation' &&
124
+ call.line >= def.startLine && call.line <= def.endLine &&
125
+ call.enclosingFunction?.startLine === def.startLine && call.enclosingFunction?.endLine === def.endLine);
126
+ if (initializers.some(call => call.ctorDelegation === 'this')) return [];
127
+ const initialized = new Set(initializers.map(call => call.name));
128
+ return splitParentList(owner.extends).map(spelling => {
129
+ const parts = String(spelling).replace(/<.*$/s, '').trim().split('::');
130
+ const name = parts.pop().trim();
131
+ return { name, qualifier: parts.join('::') || null };
132
+ }).filter(base => base.name && !initialized.has(base.name) && isProjectType(base.name));
133
+ }
134
+
135
+ /**
136
+ * Call records for the omitted base initializers of an out-of-class
137
+ * constructor definition (fix #421): the base's default constructor runs
138
+ * there, read from the class declaration in another place, so the site is
139
+ * a visible unverified candidate (`implicitOutOfClass`), never a confirmed
140
+ * edge.
141
+ */
142
+ function implicitBaseRecords(index, def, isProjectType) {
143
+ if (!applies(index, def) || def.isSignature || !require('./constructor-calls').isConstructor(def, index)) return [];
144
+ const saved = memo(index, 'outOfClassImplicitBases');
145
+ if (saved?.has(def)) return saved.get(def);
146
+ const records = omittedBases(index, def, isProjectType).map(base => ({
147
+ name: base.name, line: def.startLine, isMethod: false, isConstructor: true,
148
+ constructorSyntax: 'implicit', argCount: 0, argKinds: [],
149
+ ...(base.qualifier && { constructorQualifier: base.qualifier }),
150
+ implicitOutOfClass: { owner: def.className },
151
+ enclosingFunction: { name: def.name, startLine: def.startLine, endLine: def.endLine, className: def.className },
152
+ }));
153
+ saved?.set(def, records);
154
+ return records;
155
+ }
156
+
157
+ /**
158
+ * The out-of-class constructor definitions of classes derived from `base`
159
+ * that construct it implicitly (fix #421), as unverified caller rows.
160
+ */
161
+ function implicitBaseCallerRows(index, base, isProjectType) {
162
+ if (!outOfClassLanguage(index, base.file)) return [];
163
+ const rows = [];
164
+ const derived = new Set((index.extendedByGraph?.get(base.name) || [])
165
+ .map(child => typeof child === 'string' ? child : child.name));
166
+ for (const name of [...derived].sort()) {
167
+ for (const def of index.symbols.get(name) || []) {
168
+ if (def.className !== name || def.isSignature || !require('./constructor-calls').isConstructor(def, index)) continue;
169
+ const owner = definitionOwner(index, def);
170
+ if (!owner || !implicitBaseRecords(index, def, isProjectType).some(record => record.name === base.name)) continue;
171
+ // The derived class's written base must be this class.
172
+ const { resolveClassRef } = require('./class-identity');
173
+ const ref = resolveClassRef(index, base.name, owner.file);
174
+ if (ref?.def && ref.def !== base) continue;
175
+ rows.push(def);
176
+ }
177
+ }
178
+ return rows;
179
+ }
180
+
181
+ module.exports = { definitionOwner, declarationOf, bodyDefinitionOf, enclosingDefinition, initializesMember,
182
+ implicitBaseRecords, implicitBaseCallerRows };
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  const { dynamicImportsNote, formatLineRanges, unverifiedReasonLabel, advisoryLine } = require('./shared');
6
- const { formatAccountLines, formatCalleeAccountLine, unverifiedCalleeLines } = require('./analysis');
6
+ const { formatAccountLines, formatCalleeAccountLine, unverifiedCalleeLines, initializerReviewLines } = require('./analysis');
7
7
 
8
8
  /**
9
9
  * Format related command output - text
@@ -146,6 +146,7 @@ function formatSmart(smart, options = {}) {
146
146
  // v4 callee contract: unresolved calls inside the target — visible with
147
147
  // reasons (they may be dependencies smart could not inline).
148
148
  lines.push(...unverifiedCalleeLines(smart.unverifiedCallees, false));
149
+ lines.push(...initializerReviewLines(smart.initializerReviews, false));
149
150
  const smartCalleeAcct = formatCalleeAccountLine(smart.meta && smart.meta.calleeAccount);
150
151
  if (smartCalleeAcct) lines.push(`\n${smartCalleeAcct}`);
151
152
 
@@ -185,7 +186,8 @@ function formatSmartJson(result) {
185
186
  types: result.types || [],
186
187
  // v4 callee contract: visible unresolved-call entries, reconciled
187
188
  // by meta.calleeAccount.
188
- unverifiedCallees: result.unverifiedCallees || []
189
+ unverifiedCallees: result.unverifiedCallees || [],
190
+ ...(result.initializerReviews?.length && { initializerReviews: result.initializerReviews }),
189
191
  }
190
192
  });
191
193
  }