ucn 5.5.1 → 5.5.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.
Files changed (93) hide show
  1. package/.claude/skills/ucn/SKILL.md +35 -9
  2. package/.claude/skills/ucn/references/commands.md +38 -16
  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 +30 -21
  6. package/cli/index.js +1 -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/brief.js +8 -4
  12. package/core/build-worker.js +2 -1
  13. package/core/cache.js +137 -6
  14. package/core/call-name-cache.js +134 -0
  15. package/core/callable-scope.js +21 -0
  16. package/core/callers.js +4726 -940
  17. package/core/check.js +22 -5
  18. package/core/class-identity.js +399 -12
  19. package/core/constructor-calls.js +831 -0
  20. package/core/constructor-hierarchy.js +147 -0
  21. package/core/constructor-protocol.js +196 -0
  22. package/core/contract-membership.js +346 -140
  23. package/core/cpp-conversion-constraints.js +123 -0
  24. package/core/cpp-conversion-syntax.js +216 -0
  25. package/core/cpp-conversions.js +414 -0
  26. package/core/cpp-scope.js +245 -44
  27. package/core/deadcode.js +35 -15
  28. package/core/decorator-binding.js +137 -3
  29. package/core/enum-reference-syntax.js +134 -0
  30. package/core/execute.js +17 -1
  31. package/core/external-macros.js +42 -10
  32. package/core/go-packages.js +171 -0
  33. package/core/graph-build.js +2 -0
  34. package/core/graph.js +75 -59
  35. package/core/ignore-rules.js +2 -1
  36. package/core/imports.js +145 -19
  37. package/core/index-ir.js +41 -4
  38. package/core/ir.js +27 -6
  39. package/core/macro-expansion.js +38 -2
  40. package/core/out-of-class-members.js +182 -0
  41. package/core/output/analysis-ext.js +4 -2
  42. package/core/output/analysis.js +89 -20
  43. package/core/output/graph.js +12 -3
  44. package/core/output/lines.js +4 -3
  45. package/core/output/public.js +1 -0
  46. package/core/output/reporting.js +6 -1
  47. package/core/output/shared.js +2 -0
  48. package/core/output/tracing.js +28 -22
  49. package/core/output-budget.js +2 -2
  50. package/core/parallel-build.js +52 -1
  51. package/core/project.js +217 -112
  52. package/core/provenance-constructor-base.js +161 -0
  53. package/core/provenance-facts.js +257 -27
  54. package/core/provenance-overload.js +458 -54
  55. package/core/provenance-type.js +487 -0
  56. package/core/provenance.js +234 -15
  57. package/core/public-command.js +17 -2
  58. package/core/query-bodies.js +159 -0
  59. package/core/reflection.js +255 -2
  60. package/core/rename-token-syntax.js +111 -0
  61. package/core/reporting.js +13 -7
  62. package/core/rust-macro-expansion.js +151 -19
  63. package/core/rust-modules.js +1 -1
  64. package/core/rust-names.js +607 -0
  65. package/core/search.js +70 -17
  66. package/core/shared.js +98 -1
  67. package/core/source-memo.js +201 -0
  68. package/core/text-usage-syntax.js +38 -0
  69. package/core/tracing.js +7 -1
  70. package/core/type-denotation.js +124 -37
  71. package/core/type-references.js +615 -246
  72. package/core/value-references.js +2777 -0
  73. package/core/verify.js +1097 -281
  74. package/languages/adapter.js +28 -4
  75. package/languages/c-family.js +1118 -164
  76. package/languages/constructor-bases.js +105 -0
  77. package/languages/cpp-platform.js +26 -0
  78. package/languages/cpp-site-syntax.js +83 -0
  79. package/languages/csharp-scope.js +439 -0
  80. package/languages/csharp.js +428 -68
  81. package/languages/go.js +385 -207
  82. package/languages/html.js +4 -2
  83. package/languages/index.js +397 -2
  84. package/languages/java-platform.js +226 -0
  85. package/languages/java-unicode.js +269 -0
  86. package/languages/java.js +461 -117
  87. package/languages/javascript.js +418 -66
  88. package/languages/lexical-scope.js +190 -23
  89. package/languages/python.js +404 -54
  90. package/languages/rust.js +510 -82
  91. package/languages/utils.js +67 -8
  92. package/mcp/server.js +8 -2
  93. package/package.json +2 -2
@@ -177,12 +177,16 @@ function returnedDecorator(fnNode, family) {
177
177
  /**
178
178
  * Resolve a decorator's head name to one project callable definition, or null.
179
179
  */
180
- function projectDecoratorDef(index, def, head, rest) {
180
+ function projectDecoratorDef(index, def, head, rest, implementation = false) {
181
181
  const { _nameBindingReaches, _moduleAttributeBindingReaches } = require('./callers');
182
182
  const target = rest ? rest.slice(1) : head;
183
183
  if (rest && rest.includes('.', 1)) return null;
184
+ // `@overload` stubs declare signatures of the implementation beside
185
+ // them; the implementation decides what a call returns (fix #434).
184
186
  const candidates = (index.symbols.get(target) || []).filter(candidate =>
185
- !candidate.className && /function|method/.test(candidate.type || 'function'));
187
+ !candidate.className && /function|method/.test(candidate.type || 'function') &&
188
+ !(implementation && (candidate.decorators || []).some(name =>
189
+ /^(typing\.)?overload$/.test(String(typeof name === 'string' ? name : name?.name || '')))));
186
190
  const reaching = candidates.filter(candidate => {
187
191
  const targets = new Set([candidate.file]);
188
192
  if (!rest) {
@@ -257,4 +261,134 @@ function nameBindingDecorators(index, def, memo = new Map()) {
257
261
  return unproven;
258
262
  }
259
263
 
260
- module.exports = { nameBindingDecorators };
264
+ // A Python `if` condition testing the parameter against None: true for
265
+ // `p is None`, false for `p is not None`, null for anything else.
266
+ function noneTest(condition, p) {
267
+ if (condition?.type !== 'comparison_operator' || condition.namedChildCount !== 2) return null;
268
+ const [left, right] = condition.namedChildren;
269
+ if (left.type !== 'identifier' || left.text !== p || right.type !== 'none') return null;
270
+ const operators = condition.children.filter(child => !child.isNamed).map(child => child.type);
271
+ if (operators.length === 1 && operators[0] === 'is') return true;
272
+ if (operators.length === 2 && operators[0] === 'is' && operators[1] === 'not') return false;
273
+ return null;
274
+ }
275
+
276
+ /**
277
+ * The return statements of a Python function body that run when the
278
+ * parameter `p` is None (`pIsNone`) or is a class (fix #434): branches of
279
+ * `if p is None` / `if p is not None` the value rules out are skipped;
280
+ * nested definitions, lambdas and classes run elsewhere.
281
+ */
282
+ function pythonReturnsUnder(block, p, pIsNone, out = []) {
283
+ for (const statement of namedChildrenOf(block)) {
284
+ if (statement.type === 'return_statement') { out.push(statement); continue; }
285
+ if (/^(function_definition|class_definition|decorated_definition)$/.test(statement.type)) continue;
286
+ if (statement.type === 'if_statement') {
287
+ const test = noneTest(statement.childForFieldName('condition'), p);
288
+ if (test !== null) {
289
+ if (test === pIsNone) pythonReturnsUnder(statement.childForFieldName('consequence'), p, pIsNone, out);
290
+ else {
291
+ for (const alternative of namedChildrenOf(statement).filter(child =>
292
+ child.type === 'else_clause' || child.type === 'elif_clause')) {
293
+ if (alternative.type === 'elif_clause') return null;
294
+ pythonReturnsUnder(alternative.childForFieldName('body'), p, pIsNone, out);
295
+ }
296
+ }
297
+ continue;
298
+ }
299
+ }
300
+ let invalid = false;
301
+ traverseTree(statement, node => {
302
+ if (invalid) return false;
303
+ if (/^(function_definition|class_definition|lambda)$/.test(node.type)) return false;
304
+ if (node.type === 'return_statement') out.push(node);
305
+ if (node.type === 'yield') invalid = true;
306
+ return true;
307
+ });
308
+ if (invalid) return null;
309
+ }
310
+ return out;
311
+ }
312
+
313
+ /** Nested function definitions of a body, by name (not inside other defs). */
314
+ function nestedDefinitions(fnNode) {
315
+ const nested = new Map();
316
+ const body = fnNode.childForFieldName('body');
317
+ traverseTree(body, node => {
318
+ if (sameNode(node, body)) return true;
319
+ const definition = node.type === 'decorated_definition' ? node.childForFieldName('definition') : node;
320
+ if (definition?.type === 'function_definition') {
321
+ const name = definition.childForFieldName('name');
322
+ if (name && node.type === 'function_definition') nested.set(name.text, definition);
323
+ return false;
324
+ }
325
+ return !/^(class_definition|lambda)$/.test(node.type);
326
+ });
327
+ return nested;
328
+ }
329
+
330
+ /**
331
+ * Python: decorator `fnNode` applied to a class returns that class (fix
332
+ * #434): every return reachable when its first parameter holds the class
333
+ * returns the parameter, or passes it first to a nested function that
334
+ * does. `pIsNone` evaluates the factory form (`@d(..)` with the class
335
+ * parameter left at its None default): every reachable return then gives
336
+ * a decorator that returns its argument.
337
+ */
338
+ function pythonReturnsItsClass(fnNode, scope, depth = 0, pIsNone = false) {
339
+ if (!fnNode || depth > 4) return false;
340
+ const p = firstParamName(fnNode, 'python');
341
+ const body = fnNode.childForFieldName('body');
342
+ if (!p || !body) return false;
343
+ const nested = new Map([...scope, ...nestedDefinitions(fnNode)]);
344
+ const returns = pythonReturnsUnder(body, p, pIsNone);
345
+ if (!returns || returns.length === 0) return false;
346
+ return returns.every(statement => {
347
+ const value = statement.namedChild(0);
348
+ if (pIsNone) {
349
+ const decorator = value?.type === 'identifier' ? nested.get(value.text) : null;
350
+ return !!decorator && pythonReturnsItsClass(decorator, nested, depth + 1);
351
+ }
352
+ if (value?.type === 'identifier') return value.text === p;
353
+ if (value?.type !== 'call') return false;
354
+ const callee = value.childForFieldName('function');
355
+ const first = namedChildrenOf(value.childForFieldName('arguments'))[0];
356
+ const inner = callee?.type === 'identifier' ? nested.get(callee.text) : null;
357
+ return !!inner && first?.type === 'identifier' && first.text === p &&
358
+ pythonReturnsItsClass(inner, nested, depth + 1);
359
+ });
360
+ }
361
+
362
+ /**
363
+ * Whether a project class decorator returns the class it decorates (fix
364
+ * #434): `@register` / `@rich_repr` / `@rich_repr(angular=True)` keep the
365
+ * class (and its synthesized initializer) in place.
366
+ */
367
+ function classPreservingDecorator(index, classDef, text) {
368
+ const fileEntry = index.files.get(classDef.file);
369
+ if (fileEntry?.language !== 'python') return false;
370
+ const call = text.indexOf('(');
371
+ const expr = (call >= 0 ? text.slice(0, call) : text).trim();
372
+ if (!/^[A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)?$/.test(expr)) return false;
373
+ const dot = expr.indexOf('.');
374
+ const head = dot < 0 ? expr : expr.slice(0, dot);
375
+ const rest = dot < 0 ? '' : expr.slice(dot);
376
+ const target = projectDecoratorDef(index, classDef, head, rest, true);
377
+ if (!target) return false;
378
+ const tree = treeOf(index, target.file);
379
+ const fnNode = tree ? functionNodeAt(tree, target.nameLine || target.startLine, target.name) : null;
380
+ if (!fnNode) return false;
381
+ if (call < 0) return pythonReturnsItsClass(fnNode, new Map());
382
+ // The factory form: the class parameter is left at its None default
383
+ // when no positional argument is passed.
384
+ const args = text.slice(call + 1, text.lastIndexOf(')')).trim();
385
+ const positional = args.length > 0 && args.split(',').some(arg => !/^\s*[A-Za-z_]\w*\s*=/.test(arg));
386
+ const firstParam = namedChildrenOf(fnNode.childForFieldName('parameters'))[0];
387
+ if (!positional && firstParam?.type === 'default_parameter' &&
388
+ firstParam.childForFieldName('value')?.type === 'none') {
389
+ return pythonReturnsItsClass(fnNode, new Map(), 0, true);
390
+ }
391
+ return pythonReturnsItsClass(returnedDecorator(fnNode, 'python'), new Map());
392
+ }
393
+
394
+ module.exports = { nameBindingDecorators, classPreservingDecorator };
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+
3
+ const { langTraits } = require('../languages');
4
+ const cache = require('./cache');
5
+ const { indexedContent, parseViewParts, syntaxMode } = require('./source-memo');
6
+ const { TypeReferenceResolver } = require('./type-references');
7
+ const { classDefsNamed, findDeclaringClass } = require('./class-identity');
8
+
9
+ function validScope(value) {
10
+ return value && typeof value.namespace === 'string' &&
11
+ Number.isInteger(value.startIndex) && Number.isInteger(value.endIndex) &&
12
+ value.startIndex >= 0 && value.endIndex >= value.startIndex;
13
+ }
14
+
15
+ function validContext(value) {
16
+ return value == null || Array.isArray(value.scopes) && value.scopes.every(validScope) &&
17
+ Array.isArray(value.directives) && value.directives.every(detail => detail &&
18
+ typeof detail.type === 'string' && typeof detail.module === 'string' &&
19
+ Array.isArray(detail.names) && detail.names.every(name => typeof name === 'string') &&
20
+ (detail.namespace == null || typeof detail.namespace === 'string') &&
21
+ ['static', 'global', 'rooted'].every(key => detail[key] == null || typeof detail[key] === 'boolean') &&
22
+ Array.isArray(detail.usingScopes) && detail.usingScopes.every(validScope));
23
+ }
24
+
25
+ /** Persist syntax only; every owner and member verdict is recomputed. */
26
+ class EnumReferenceSyntax {
27
+ constructor(resolver) { this.resolver = resolver; this.states = new Map(); }
28
+
29
+ collecting(file) { return this.states.get(file)?.valid === true; }
30
+
31
+ capture(file, fact) {
32
+ const state = this.states.get(file);
33
+ if (!state?.valid) return;
34
+ if (!fact) state.valid = false;
35
+ else state.facts.push(fact);
36
+ }
37
+
38
+ read(file, entry, language, rows, content) {
39
+ const r = this.resolver, index = r.index;
40
+ if (file === r.def.file || r.owner?.def?.type !== 'enum' ||
41
+ !langTraits(language)?.staticEnumReferenceSyntax || !entry.hash ||
42
+ !index._usageResultCache || index.isProvenanceFactIndex ||
43
+ !indexedContent(index, file, entry, content)) return null;
44
+ const mode = syntaxMode('enumSyntax', [...parseViewParts(entry, language), rows]);
45
+ this.states.set(file, { valid: true, facts: [], mode });
46
+ const value = cache.getUsageResult(index, file, r.name, mode);
47
+ if (!Array.isArray(value) || value.length !== 3 || value[0] !== 2 ||
48
+ !Array.isArray(value[1]) || !Array.isArray(value[2])) return null;
49
+ const lines = new Map(), wanted = new Set(rows);
50
+ for (const record of value[1]) {
51
+ if (!Array.isArray(record) || record.length !== 4 || !wanted.has(record[0]) ||
52
+ lines.has(record[0]) || !Number.isInteger(record[1]) || record[1] < 0 ||
53
+ typeof record[2] !== 'boolean' || record[3] !== 0) return null;
54
+ lines.set(record[0], { edits: [], reviews: [], skips: 0, skipColumns: [],
55
+ text: record[2], tokens: record[1], macroBody: false });
56
+ }
57
+ if (lines.size !== rows.length) return null;
58
+ const counts = new Map(), references = [];
59
+ for (const fact of value[2]) {
60
+ if (!fact || !wanted.has(fact.line) || !Number.isInteger(fact.column) || fact.column < 0 ||
61
+ !Number.isInteger(fact.at) || fact.at < 0 || content.slice(fact.at, fact.at + r.name.length) !== r.name ||
62
+ !['member', 'type', 'opaque'].includes(fact.kind) || typeof fact.accessKind !== 'string' ||
63
+ fact.kind !== 'opaque' && (typeof fact.receiver !== 'string' || !fact.receiver) ||
64
+ fact.kind === 'opaque' && (!['macro', 'template'].includes(fact.sourceRole) ||
65
+ fact.receiver != null || fact.context != null || fact.owner != null) ||
66
+ !validContext(fact.context) || fact.owner != null &&
67
+ (typeof fact.owner.name !== 'string' || !Number.isInteger(fact.owner.startLine) ||
68
+ !Number.isInteger(fact.owner.endLine) || fact.owner.startLine < 1 ||
69
+ fact.owner.endLine < fact.owner.startLine)) return null;
70
+ if (fact.kind !== 'opaque' && langTraits(language).enumReferenceContext && fact.context == null) return null;
71
+ counts.set(fact.line, (counts.get(fact.line) || 0) + 1);
72
+ }
73
+ for (const [line, slot] of lines) if ((counts.get(line) || 0) !== slot.tokens) return null;
74
+ for (const fact of value[2]) {
75
+ const lookup = fact.kind === 'opaque' ? null : this.ownerOf(file, language, fact);
76
+ if (fact.kind !== 'opaque' && !lookup) return null;
77
+ const verdict = fact.kind === 'opaque'
78
+ ? { verdict: 'unknown', reason: 'value-binding-unresolved' } : r.memberAt(lookup.owner);
79
+ const slot = lines.get(fact.line);
80
+ if (verdict.verdict === 'yes') slot.edits.push(fact.column);
81
+ else if (verdict.verdict === 'unknown') slot.reviews.push({ column: fact.column, reason: verdict.reason });
82
+ else { slot.skips++; slot.skipColumns.push(fact.column); }
83
+ references.push({ file: entry.relativePath, absoluteFile: file,
84
+ line: fact.line, column: fact.column, expression: index.getLineContent(file, fact.line).trim(),
85
+ ...(fact.kind === 'member' && { receiver: fact.receiver }), accessKind: fact.accessKind,
86
+ verdict: verdict.verdict, reason: verdict.reason });
87
+ }
88
+ r.references.push(...references);
89
+ return lines;
90
+ }
91
+
92
+ ownerOf(file, language, fact) {
93
+ const r = this.resolver, index = r.index;
94
+ if (fact.kind === 'member' && langTraits(language).bareCallReachesMethods) {
95
+ let enclosing = null;
96
+ if (fact.owner) {
97
+ const entries = classDefsNamed(index, fact.owner.name).entries.filter(e =>
98
+ e.def.file === file && e.def.startLine <= fact.owner.startLine &&
99
+ e.def.endLine >= fact.owner.endLine);
100
+ if (new Set(entries.map(e => e.key)).size !== 1) return null;
101
+ enclosing = { name: entries[0].def.name, key: entries[0].key, def: entries[0].def };
102
+ } else if (index.findEnclosingFunction(file, fact.line, true)?.className) return null;
103
+ const members = (index.symbols.get(fact.receiver) || []).filter(d => d.className);
104
+ if (enclosing && findDeclaringClass(index, enclosing, members)) return { owner: null };
105
+ }
106
+ if ((index.symbols.get(fact.receiver) || []).some(d => d.type === 'type')) return null;
107
+ const matches = [];
108
+ for (const entry of classDefsNamed(index, fact.receiver).entries) {
109
+ // Same-file declarations also need the native lexical scope relation.
110
+ if (entry.def.file === file) return null;
111
+ const sub = new TypeReferenceResolver(index, entry.def, fact.receiver);
112
+ sub.siteFile = file;
113
+ if (fact.context) sub.siteSyntax = { file, line: fact.line, arity: 0, context: fact.context };
114
+ const verdict = sub.resolveName(file, fact.line, fact.receiver);
115
+ if (verdict.verdict === 'unknown') return { owner: null };
116
+ if (verdict.verdict === 'yes') matches.push(entry);
117
+ }
118
+ if (new Set(matches.map(e => e.key)).size !== 1) return { owner: null };
119
+ return { owner: { name: matches[0].def.name, key: matches[0].key, def: matches[0].def } };
120
+ }
121
+
122
+ remember(file, lines) {
123
+ const state = this.states.get(file);
124
+ if (!state?.valid) return;
125
+ const counts = new Map();
126
+ for (const fact of state.facts) counts.set(fact.line, (counts.get(fact.line) || 0) + 1);
127
+ if ([...lines].some(([line, slot]) => slot.macroBody || slot.stringAnnotation ||
128
+ slot.tokens !== (counts.get(line) || 0) || slot.tokens === 0 && !slot.text)) return;
129
+ const value = [2, [...lines].map(([line, slot]) => [line, slot.tokens, slot.text, 0]), state.facts];
130
+ cache.storeUsageResult(this.resolver.index, file, this.resolver.name, state.mode, value);
131
+ }
132
+ }
133
+
134
+ module.exports = { EnumReferenceSyntax };
package/core/execute.js CHANGED
@@ -16,6 +16,7 @@ const { addTestExclusions, pickBestDefinition, parseSymbolHandle, looksLikeHandl
16
16
  const { cleanHtmlScriptTags, detectLanguage } = require('./parser');
17
17
  const { renderExpandItem } = require('./expand-cache');
18
18
  const { CANONICAL_COMMANDS } = require('./registry');
19
+ const { sourceRangeTarget } = require('./public-command');
19
20
  const { isAccessorDefinition } = require('./accessors');
20
21
  const { describeError } = require('./errors');
21
22
 
@@ -1547,7 +1548,7 @@ const HANDLERS = {
1547
1548
  p.name.includes('/') || p.name.includes('\\') ||
1548
1549
  /\.(js|ts|py|go|java|rs)$/.test(p.name));
1549
1550
  if (!testsTargetIsFile) applyClassMethodSyntax(p);
1550
- if (testsTargetIsHandle && p.line) {
1551
+ if (!testsTargetIsFile && p.line) {
1551
1552
  const line = Number(p.line);
1552
1553
  const resolvedFile = p.file ? index.resolveFilePathForQuery(p.file) : null;
1553
1554
  const pinned = (index.symbols.get(p.name) || []).filter(d =>
@@ -1589,6 +1590,9 @@ const HANDLERS = {
1589
1590
  const result = index.tests(p.name, {
1590
1591
  callsOnly: p.callsOnly || false,
1591
1592
  className: p.className,
1593
+ // A line pin names one definition, including the file/line
1594
+ // parameters passed by a composed command such as show.
1595
+ ...(!testsTargetIsFile && p.line && testsResolution?.def && { definition: testsResolution.def }),
1592
1596
  // Bare ambiguous names use the same deterministic definition as
1593
1597
  // the rest of the semantic surface. The warning below discloses
1594
1598
  // that choice and gives the caller a stable file= escape hatch.
@@ -1681,6 +1685,7 @@ const HANDLERS = {
1681
1685
  if (result.excludedDecorated != null) sliced.excludedDecorated = result.excludedDecorated;
1682
1686
  if (result.excludedExternalContract != null) sliced.excludedExternalContract = result.excludedExternalContract;
1683
1687
  if (result.excludedRuntimeContract != null) sliced.excludedRuntimeContract = result.excludedRuntimeContract;
1688
+ if (result.excludedConstructors != null) sliced.excludedConstructors = result.excludedConstructors;
1684
1689
  if (result.excludedDynamicDispatch != null) sliced.excludedDynamicDispatch = result.excludedDynamicDispatch;
1685
1690
  if (result.pythonImplicitExportFiles != null) sliced.pythonImplicitExportFiles = result.pythonImplicitExportFiles;
1686
1691
  if (result.computedDispatch != null) sliced.computedDispatch = result.computedDispatch;
@@ -2515,9 +2520,18 @@ function execute(index, command, params = {}) {
2515
2520
  // logged params object; a frozen object silently no-oped the
2516
2521
  // normalizers and returned wrong not-found answers).
2517
2522
  params = { ...params };
2523
+ if (command === 'source') params = sourceRangeTarget(params);
2518
2524
  // Listing completeness belongs to execution, including direct API calls.
2519
2525
  if (params.lines) params.all = true;
2526
+ // Composed commands share one source snapshot and its derived facts.
2527
+ // Child operations still balance their scopes; the outer command owns
2528
+ // the lifetime so summary/relationships/source do not repeat reads.
2529
+ let operation = false;
2520
2530
  try {
2531
+ if (typeof index?._beginOp === 'function' && typeof index?._endOp === 'function') {
2532
+ index._beginOp();
2533
+ operation = true;
2534
+ }
2521
2535
  if (PUBLIC_COMMAND_SET.has(command)) {
2522
2536
  const validationError = validatePublicParams(command, params);
2523
2537
  if (validationError) return { ok: false, error: validationError };
@@ -2564,6 +2578,8 @@ function execute(index, command, params = {}) {
2564
2578
  // A deliberate refusal keeps its message; an exception inside the
2565
2579
  // engine is marked internal on every surface (fix #394).
2566
2580
  return { ok: false, ...describeError(e) };
2581
+ } finally {
2582
+ if (operation) index._endOp();
2567
2583
  }
2568
2584
  }
2569
2585
 
@@ -115,7 +115,7 @@ function leadsWithProjectMacro(body, perName) {
115
115
  // `catch (...) { }` / `try`: statement fragments are read by the probe
116
116
  // even though they hold braces.
117
117
  function mayBeStatementFragment(body) {
118
- return /^(?:try|catch|if)\b/.test(body.trim());
118
+ return /^(?:try|catch|if|while|for|switch)\b/.test(body.trim());
119
119
  }
120
120
 
121
121
  /**
@@ -166,24 +166,53 @@ function projectMacroDictionary(index, files) {
166
166
  if (!readingMemo.has(key)) readingMemo.set(key, compute());
167
167
  return readingMemo.get(key);
168
168
  };
169
+ // An object-like body naming another project macro that reads as
170
+ // decoration reads as the rest of it (fix #427).
171
+ const { withoutDecorationMacros } = require('../languages/c-family');
172
+ const objectReading = new Map();
173
+ const objectReadingOf = name => {
174
+ if (objectReading.has(name)) return objectReading.get(name);
175
+ objectReading.set(name, null); // in progress
176
+ const slot = perName.get(name);
177
+ let reading = null;
178
+ if (slot && slot.objects.size > 0 && slot.functions.size === 0 && !slot.objects.has(null)) {
179
+ const bodies = [...slot.objects].sort(codeUnitCompare).map(body => withoutDecorationMacros(body,
180
+ word => word !== name && perName.has(word) && objectReadingOf(word) === 'decoration'));
181
+ if (bodies.every(body => !body || maySpecify(body) || mayBeStatementFragment(body)) &&
182
+ !bodies.some(body => leadsWithProjectMacro(body, perName))) {
183
+ reading = read(`o\0${bodies.join('\u0001')}`, () => objectMacroReading(parser, bodies));
184
+ }
185
+ }
186
+ objectReading.set(name, reading);
187
+ return reading;
188
+ };
169
189
  const objectBodies = [];
170
190
  const functionMacros = [];
191
+ // Function-like macros whose every expansion ends in a function head
192
+ // (fix #427): an invocation followed by a body defines the function the
193
+ // expansion names (`DEF_PRIMITIVE(name) { .. }`).
194
+ const functionHeads = [];
195
+ const { endsFunctionHead } = require('../languages/c-family');
171
196
  for (const name of [...perName.keys()].sort(codeUnitCompare)) {
172
197
  const slot = perName.get(name);
173
198
  // Every definition of the name is one kind; object bodies are short.
174
199
  if (slot.objects.size > 0 && slot.functions.size > 0) continue;
175
200
  if (slot.objects.size > 0) {
176
201
  if (slot.objects.has(null)) continue;
177
- const bodies = [...slot.objects].sort(codeUnitCompare);
178
- if (!bodies.every(body => !body || maySpecify(body) || mayBeStatementFragment(body))) continue;
179
- if (bodies.some(body => leadsWithProjectMacro(body, perName))) continue;
180
- const reading = read(`o\0${bodies.join('\u0001')}`, () => objectMacroReading(parser, bodies));
181
- if (reading === 'decoration' || reading === 'statement' || reading === 'if') objectBodies.push([name, bodies]);
202
+ const reading = objectReadingOf(name);
203
+ if (reading === 'decoration' || reading === 'statement' || reading === 'if') {
204
+ objectBodies.push([name, [...slot.objects].sort(codeUnitCompare).map(body => withoutDecorationMacros(body,
205
+ word => word !== name && perName.has(word) && objectReadingOf(word) === 'decoration'))]);
206
+ }
182
207
  continue;
183
208
  }
184
209
  const templates = [...slot.functions.values()]
185
210
  .sort((a, b) => codeUnitCompare(JSON.stringify(a), JSON.stringify(b)));
186
211
  if (new Set(templates.map(template => template.params.length)).size !== 1) continue;
212
+ if (templates.every(template => template.body && endsFunctionHead(template.body)) &&
213
+ !templates.some(template => leadsWithProjectMacro(template.body, perName))) {
214
+ functionHeads.push([name, templates]);
215
+ }
187
216
  // Only an attribute or return-type macro (its expansion reads as
188
217
  // specifiers or a type) can explain a misread declaration; statement
189
218
  // macros such as test assertions read as calls.
@@ -197,8 +226,8 @@ function projectMacroDictionary(index, files) {
197
226
  if (reading === 'decoration' || reading === 'type') functionMacros.push([name, templates]);
198
227
  }
199
228
  const byLanguage = {};
200
- if (objectBodies.length > 0 || functionMacros.length > 0) {
201
- const entries = { objectBodies, functionMacros };
229
+ if (objectBodies.length > 0 || functionMacros.length > 0 || functionHeads.length > 0) {
230
+ const entries = { objectBodies, functionMacros, functionHeads };
202
231
  byLanguage.c = entries;
203
232
  byLanguage.cpp = entries;
204
233
  }
@@ -222,6 +251,7 @@ function contextFor(dictionary, language, excluded = null) {
222
251
  key: `${dictionary.key}-${[...excluded].sort(codeUnitCompare).join(',')}`,
223
252
  objectBodies: new Map(entry.objectBodies.filter(keep)),
224
253
  functionMacros: new Map(entry.functionMacros.filter(keep)),
254
+ functionHeads: new Map((entry.functionHeads || []).filter(keep)),
225
255
  consulted: new Set(),
226
256
  };
227
257
  }
@@ -233,7 +263,8 @@ function contextFor(dictionary, language, excluded = null) {
233
263
  }
234
264
  let maps = byLanguage.get(language);
235
265
  if (!maps) {
236
- maps = { objectBodies: new Map(entry.objectBodies), functionMacros: new Map(entry.functionMacros) };
266
+ maps = { objectBodies: new Map(entry.objectBodies), functionMacros: new Map(entry.functionMacros),
267
+ functionHeads: new Map(entry.functionHeads || []) };
237
268
  byLanguage.set(language, maps);
238
269
  }
239
270
  return { key: dictionary.key, ...maps, consulted: new Set() };
@@ -245,8 +276,9 @@ function keyOfNames(dictionary, language, names) {
245
276
  const entry = dictionary?.byLanguage?.[language];
246
277
  const objects = new Map(entry?.objectBodies || []);
247
278
  const functions = new Map(entry?.functionMacros || []);
279
+ const heads = new Map(entry?.functionHeads || []);
248
280
  const parts = [...new Set(names)].sort(codeUnitCompare)
249
- .map(name => [name, objects.get(name) || null, functions.get(name) || null]);
281
+ .map(name => [name, objects.get(name) || null, functions.get(name) || null, heads.get(name) || null]);
250
282
  return crypto.createHash('sha1').update(JSON.stringify(parts)).digest('base64url');
251
283
  }
252
284
 
@@ -0,0 +1,171 @@
1
+ /**
2
+ * core/go-packages.js - Go package identity and build configurations.
3
+ *
4
+ * A Go package is the set of files of one directory that share a package
5
+ * clause: `package x` files (test files included, for `go test`) and the
6
+ * external test package `package x_test` of the same directory are two
7
+ * packages, and a non-test file never sees a `_test.go` declaration. One
8
+ * package-level identifier denotes one entity in a build configuration; files
9
+ * that declare it under different `//go:build` constraints (or GOOS/GOARCH
10
+ * file-name suffixes) are configuration alternatives of one item (fix #425).
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const path = require('path');
16
+
17
+ const GO_OS = new Set(['aix', 'android', 'darwin', 'dragonfly', 'freebsd', 'hurd', 'illumos', 'ios', 'js',
18
+ 'linux', 'nacl', 'netbsd', 'openbsd', 'plan9', 'solaris', 'wasip1', 'windows', 'zos']);
19
+ const GO_ARCH = new Set(['386', 'amd64', 'amd64p32', 'arm', 'arm64', 'arm64be', 'armbe', 'loong64', 'mips',
20
+ 'mips64', 'mips64le', 'mips64p32', 'mips64p32le', 'mipsle', 'ppc', 'ppc64', 'ppc64le', 'riscv', 'riscv64',
21
+ 's390', 's390x', 'sparc', 'sparc64', 'wasm']);
22
+
23
+ // Package-level type kinds a method set belongs to.
24
+ const GO_TYPE_KINDS = new Set(['struct', 'interface', 'type', 'class', 'enum']);
25
+
26
+ /**
27
+ * The build configurations a Go file belongs to (fix #399): its
28
+ * `//go:build` expression plus the GOOS/GOARCH its file name implies
29
+ * (`x_linux.go`, `x_windows_amd64_test.go`); '' when every build compiles it.
30
+ */
31
+ function goFileConstraint(index, file) {
32
+ const memo = index._opMemo?.('goFileConstraint', () => new Map());
33
+ const cached = memo?.get(file);
34
+ if (cached !== undefined) return cached;
35
+ const constraint = goFileConstraintUncached(index, file);
36
+ memo?.set(file, constraint);
37
+ return constraint;
38
+ }
39
+
40
+ function goFileConstraintUncached(index, file) {
41
+ const tag = index.files.get(file)?.buildConstraint || '';
42
+ const parts = path.basename(file, '.go').replace(/_test$/, '').split('_');
43
+ let implied = '';
44
+ if (parts.length >= 3 && GO_OS.has(parts[parts.length - 2]) && GO_ARCH.has(parts[parts.length - 1])) {
45
+ implied = `${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
46
+ } else if (parts.length >= 2 && (GO_OS.has(parts[parts.length - 1]) || GO_ARCH.has(parts[parts.length - 1]))) {
47
+ implied = parts[parts.length - 1];
48
+ }
49
+ return tag || implied ? `${tag}|${implied}` : '';
50
+ }
51
+
52
+ const isTestFile = file => file.endsWith('_test.go');
53
+
54
+ /**
55
+ * Does a package-level declaration of `declFile` belong to the package a
56
+ * bare name in `siteFile` is resolved in? Same directory, same package
57
+ * clause (`x` and `x_test` are two packages), and a `_test.go` declaration
58
+ * only for a test file. A file whose clause is unknown keeps the directory
59
+ * rule.
60
+ */
61
+ function goSamePackage(index, siteFile, declFile) {
62
+ if (!siteFile || !declFile) return false;
63
+ if (siteFile === declFile) return true;
64
+ if (path.dirname(siteFile) !== path.dirname(declFile)) return false;
65
+ const site = index.files.get(siteFile)?.packageName;
66
+ const decl = index.files.get(declFile)?.packageName;
67
+ if (site && decl && site !== decl) return false;
68
+ return isTestFile(siteFile) || !isTestFile(declFile);
69
+ }
70
+
71
+ /**
72
+ * The directories of the project packages `siteFile` dot-imports
73
+ * (`import . "p"`), whose exported package-level names its file block holds.
74
+ */
75
+ function goDotImportDirs(index, siteFile) {
76
+ const memo = index._opMemo?.('goDotImportDirs', () => new Map());
77
+ if (memo?.has(siteFile)) return memo.get(siteFile);
78
+ const entry = index.files.get(siteFile);
79
+ let dirs = null;
80
+ for (const detail of entry?.importDetails || []) {
81
+ if (detail.type !== 'dot-import' || !detail.module) continue;
82
+ const rel = entry.moduleResolved?.[detail.module];
83
+ if (!rel) continue;
84
+ const abs = path.isAbsolute(rel) ? rel : path.join(index.root, rel);
85
+ (dirs || (dirs = new Set())).add(path.dirname(abs));
86
+ }
87
+ memo?.set(siteFile, dirs);
88
+ return dirs;
89
+ }
90
+
91
+ /**
92
+ * Does a bare name in `siteFile` reach the package-level declaration `def`?
93
+ * Its own package's declarations (goSamePackage), or an exported non-test
94
+ * declaration of a package the file dot-imports (Go spec, Import
95
+ * declarations: the external test package `x_test` that dot-imports `x` sees
96
+ * x's exported names unqualified).
97
+ */
98
+ function goBareNameReaches(index, siteFile, def) {
99
+ if (!def?.file) return false;
100
+ if (goSamePackage(index, siteFile, def.file)) return true;
101
+ if (isTestFile(def.file) || !/^\p{Lu}/u.test(def.name || '')) return false;
102
+ const dirs = goDotImportDirs(index, siteFile);
103
+ if (!dirs || !dirs.has(path.dirname(def.file))) return false;
104
+ // The imported package is the directory's non-test package.
105
+ return !String(index.files.get(def.file)?.packageName || '').endsWith('_test');
106
+ }
107
+
108
+ /**
109
+ * Are two declarations configuration alternatives of one Go item? Files of
110
+ * one package that both declare a package-level name compile together only
111
+ * in a program that does not build (the name would be redeclared), so two
112
+ * such declarations under different file constraints are one item in two
113
+ * build configurations: same name, same receiver type, one package, one
114
+ * test side, and at least one constrained file.
115
+ */
116
+ function goBuildAlternatives(index, a, b) {
117
+ if (!a || !b || a === b || a.file === b.file || a.name !== b.name) return false;
118
+ if (path.dirname(a.file) !== path.dirname(b.file)) return false;
119
+ if (isTestFile(a.file) !== isTestFile(b.file)) return false;
120
+ const pa = index.files.get(a.file)?.packageName;
121
+ const pb = index.files.get(b.file)?.packageName;
122
+ if (!pa || pa !== pb) return false;
123
+ if (a.lexicalScopeStartLine != null || b.lexicalScopeStartLine != null) return false;
124
+ if (a.isNested || b.isNested) return false;
125
+ const ownerOf = d => String(d.receiver || d.className || '').replace(/^\*/, '').replace(/\[.*$/, '') || null;
126
+ if (ownerOf(a) !== ownerOf(b)) return false;
127
+ // A function and a type of one name are not alternatives of one item.
128
+ if (GO_TYPE_KINDS.has(a.type) !== GO_TYPE_KINDS.has(b.type)) return false;
129
+ const ca = goFileConstraint(index, a.file);
130
+ const cb = goFileConstraint(index, b.file);
131
+ return ca !== cb && !!(ca || cb);
132
+ }
133
+
134
+ /**
135
+ * The alternatives of one item a site in `siteFile` reaches: a site in a
136
+ * file whose constraint is one alternative's own compiles only with that
137
+ * alternative (the site's file and that alternative's file build together,
138
+ * so no other alternative does); any other site reaches each of them.
139
+ */
140
+ function goAlternativesAt(index, siteFile, alternatives) {
141
+ if (!siteFile || alternatives.length < 2) return alternatives;
142
+ const own = alternatives.filter(d => d.file === siteFile);
143
+ if (own.length > 0) return own;
144
+ const site = goFileConstraint(index, siteFile);
145
+ if (!site) return alternatives;
146
+ const same = alternatives.filter(d => goFileConstraint(index, d.file) === site);
147
+ return same.length > 0 ? same : alternatives;
148
+ }
149
+
150
+ /**
151
+ * Package-level types named `name` the package of `siteFile` declares
152
+ * (fix #425): what a receiver no local binds (`T.M`, `(*T).M`) denotes when
153
+ * the package declares a type of that name.
154
+ */
155
+ function goPackageTypes(index, siteFile, name) {
156
+ if (!name) return [];
157
+ return (index.symbols.get(name) || []).filter(d => d.file && GO_TYPE_KINDS.has(d.type) &&
158
+ d.lexicalScopeStartLine == null && !d.className && !d.receiver &&
159
+ index.files.get(d.file)?.language === 'go' && goSamePackage(index, siteFile, d.file));
160
+ }
161
+
162
+ module.exports = {
163
+ goFileConstraint,
164
+ goSamePackage,
165
+ goDotImportDirs,
166
+ goBareNameReaches,
167
+ goBuildAlternatives,
168
+ goAlternativesAt,
169
+ goPackageTypes,
170
+ GO_TYPE_KINDS,
171
+ };
@@ -604,6 +604,8 @@ function _applyGoImportNames(index, filePath, fileEntry, moduleResolved, dirToGo
604
604
  * Build inheritance relationship graphs
605
605
  */
606
606
  function buildInheritanceGraph(index) {
607
+ index._implicitConversionOwners = null;
608
+ index._constructionAliases = null;
607
609
  index.extendsGraph.clear();
608
610
  index.extendedByGraph.clear();
609
611