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
package/core/search.js CHANGED
@@ -72,28 +72,28 @@ function appendTextComplements(index, {
72
72
  .filter(usage => usage.file === filePath)
73
73
  .map(usage => usage.line));
74
74
  const pattern = literalNameRegex(name);
75
+ let syntax;
75
76
  for (let idx = 0; idx < lines.length; idx++) {
76
77
  const line = lines[idx];
77
78
  const match = pattern.exec(line);
78
79
  if (!match || represented.has(idx + 1)) continue;
79
80
  const lineNum = idx + 1;
80
- const commentOrString = index.isCommentOrStringAtPosition(
81
- content, lineNum, match.index, filePath);
81
+ syntax ||= require('./text-usage-syntax').textUsageSyntax(index, filePath, fileEntry, content, name);
82
+ const textKind = syntax.kind(lineNum, match.index);
82
83
  // A literal match that no AST identifier record claimed and that is
83
84
  // not inside a comment or string is non-code text: JSX children,
84
85
  // HTML markup and attributes, regex bodies. The ACCOUNT counts these
85
86
  // lines as other-text; the escape hatch must list them too. A line
86
87
  // the AST scan classified and then deliberately dropped (Rust enum
87
88
  // variants against a struct pin, #234) is code, not text: skip it.
88
- if (!commentOrString &&
89
- index.isIdentifierAtPosition(content, lineNum, match.index, filePath, name)) continue;
89
+ if (textKind === 'identifier') continue;
90
90
  const usage = {
91
91
  file: filePath,
92
92
  relativePath: fileEntry.relativePath,
93
93
  line: lineNum,
94
94
  content: line,
95
95
  usageType: 'text',
96
- textKind: commentOrString ? 'comment-or-string' : 'markup-or-text',
96
+ textKind,
97
97
  isDefinition: false,
98
98
  };
99
99
  if (context > 0) {
@@ -106,6 +106,7 @@ function appendTextComplements(index, {
106
106
  }
107
107
  usagesList.push(usage);
108
108
  }
109
+ syntax?.save();
109
110
  }
110
111
 
111
112
  /**
@@ -339,9 +340,11 @@ function usages(index, name, options = {}) {
339
340
  options.internalEvidence ? { lexicalScopes: true } : undefined);
340
341
  if (astUsages !== null) {
341
342
  for (const u of astUsages) {
342
- // Skip if this is a definition line (already added above)
343
+ // Public usages groups declaration lines into their definition
344
+ // entry. Refactoring needs every other AST token on that line.
343
345
  if (definitions.some(d => d.file === filePath &&
344
- (d.startLine === u.line || d.nameLine === u.line))) {
346
+ (d.startLine === u.line || d.nameLine === u.line)) &&
347
+ (!options.internalEvidence || u.usageType === 'definition' || u.declarationName)) {
345
348
  continue;
346
349
  }
347
350
 
@@ -375,13 +378,22 @@ function usages(index, name, options = {}) {
375
378
  // plan gains precision without changing that surface.
376
379
  ...(options.internalEvidence && {
377
380
  ...(Number.isInteger(u.column) && { column: u.column }),
381
+ ...(u.receiverType && {
382
+ receiverType: u.receiverType,
383
+ ...(u.receiverTypeQualifier && { receiverTypeQualifier: u.receiverTypeQualifier }),
384
+ ...(Number.isInteger(u.receiverTypeArgs) && { receiverTypeArgs: u.receiverTypeArgs }),
385
+ }),
378
386
  ...(u.receiverIsModule && { receiverIsModule: true }),
379
387
  ...(u.receiverLocalBinding && { receiverLocalBinding: true }),
388
+ // A path-qualified reference (`T::m`, fix #422).
389
+ ...(u.scopedReference && { scopedReference: true }),
380
390
  ...(u.namespace && { namespace: u.namespace }),
381
391
  ...(u.scopeBinding && { scopeBinding: u.scopeBinding }),
382
392
  ...(u.scopeDefLines && { scopeDefLines: u.scopeDefLines }),
383
393
  ...(u.shorthandProperty && { shorthandProperty: true }),
384
394
  ...(u.importAlias && { importAlias: true }),
395
+ ...(u.modulePath && { modulePath: true }),
396
+ ...(u.importSource && { importSource: true }),
385
397
  }),
386
398
  ...(callerSym && {
387
399
  callerName: callerSym.name,
@@ -587,7 +599,7 @@ function search(index, term, options = {}) {
587
599
 
588
600
  // Use AST-based filtering for codeOnly mode when language is supported
589
601
  if (options.codeOnly) {
590
- const language = detectLanguage(filePath);
602
+ const language = fileEntry.language || detectLanguage(filePath, index.root);
591
603
  if (language) {
592
604
  try {
593
605
  const parser = getParser(language);
@@ -1323,6 +1335,11 @@ function tests(index, nameOrFile, options = {}) {
1323
1335
  } else if (!isFilePath) {
1324
1336
  targetDefs = index.find(nameOrFile, { exact: true, className: options.className });
1325
1337
  }
1338
+ // A pinned definition among same-name ones (an overload, a constructor
1339
+ // beside its class and siblings, fix #430): calls are this definition's
1340
+ // only when the caller engine attributes them to it.
1341
+ const pinnedFamily = !isFilePath && options.definition && targetDefs.length > 1;
1342
+ if (!isFilePath && options.definition) targetDefs = [options.definition];
1326
1343
 
1327
1344
  // Find all test files
1328
1345
  const testFiles = [];
@@ -1522,6 +1539,7 @@ function tests(index, nameOrFile, options = {}) {
1522
1539
  // own definition (fix #246).
1523
1540
  if (localShadow && !usage.receiver && usage.usageType !== 'import') continue;
1524
1541
 
1542
+ if (pinnedFamily && usage.usageType === 'call') continue;
1525
1543
  // A line is claimed only by a usage that is recorded: two
1526
1544
  // same-kind tokens on one line (`a.m = b.m`) are judged
1527
1545
  // separately, so a rejected first token never hides a
@@ -1550,10 +1568,14 @@ function tests(index, nameOrFile, options = {}) {
1550
1568
  // className scoping for references: require class-associated receiver
1551
1569
  if (className && (matchType === 'reference' || matchType === 'string-ref' ||
1552
1570
  matchType === 'unverified-reference')) {
1553
- // Bare references (no receiver) like `fn = save` have no class
1554
- // association — skip them. Only keep member-access references
1555
- // where the receiver matches the target class.
1556
- if (!usage.receiver && matchType !== 'unverified-reference') continue;
1571
+ // A bare name can bind an earlier class-body method.
1572
+ // Other references need a receiver matching the target.
1573
+ if (!usage.receiver && matchType !== 'unverified-reference') {
1574
+ if (!classBodyReferenceTargets(index, testPath, entry.language, usage, targetDefs)) continue;
1575
+ seenLines.add(lineKey);
1576
+ matches.push({ line: usage.line, content: lineContent.trim(), matchType: 'reference' });
1577
+ continue;
1578
+ }
1557
1579
  if (!usage.receiver && matchType === 'unverified-reference') {
1558
1580
  seenLines.add(lineKey);
1559
1581
  matches.push({
@@ -1685,7 +1707,9 @@ function tests(index, nameOrFile, options = {}) {
1685
1707
  if (!isFilePath && targetDefs.length > 0) {
1686
1708
  const testInfo = new Map(testFiles.map(t => [t.path, t]));
1687
1709
  const callerSites = [];
1688
- const globallyUniqueTarget = index.find(searchTerm, { exact: true }).length === 1;
1710
+ // A pinned definition's unverified sites were adjudicated against it
1711
+ // alone, like a globally unique name's (fix #430).
1712
+ const globallyUniqueTarget = !!options.definition || index.find(searchTerm, { exact: true }).length === 1;
1689
1713
  for (const def of targetDefs) {
1690
1714
  const raw = index.findCallers(searchTerm, {
1691
1715
  targetDefinitions: [def],
@@ -1750,15 +1774,27 @@ function tests(index, nameOrFile, options = {}) {
1750
1774
  // receiver identity the accessor engine proves are test links; the
1751
1775
  // unverified ones join only for a globally unique name, as above.
1752
1776
  const accessorDefs = targetDefs.filter(isAccessorDefinition);
1753
- if (accessorDefs.length > 0) {
1777
+ // A nested type's annotations and qualifiers are type references,
1778
+ // not instance-member accesses of its containing class. Reuse the
1779
+ // same language lookup as impact instead of requiring a receiver.
1780
+ const nestedTypeDefs = targetDefs.filter(def => def.enclosingType &&
1781
+ ['class', 'struct', 'interface', 'type', 'enum', 'trait', 'record', 'union'].includes(def.type));
1782
+ if (accessorDefs.length > 0 || nestedTypeDefs.length > 0) {
1754
1783
  const testPaths = new Set(testFiles.map(t => t.path));
1755
1784
  const globallyUnique = index.find(searchTerm, { exact: true }).length === 1;
1756
- for (const def of accessorDefs) {
1757
- const refs = findAccessorReferences(index, searchTerm, def, { files: testPaths });
1785
+ for (const def of [...accessorDefs, ...nestedTypeDefs]) {
1786
+ const types = nestedTypeDefs.includes(def)
1787
+ ? require('./analysis').findTypeReferences(index, searchTerm, def, { files: testPaths }) : null;
1788
+ const typeSite = site => ({ ...site, absoluteFile: path.join(index.root, site.file) });
1789
+ const refs = types ? {
1790
+ confirmed: types.byFile.flatMap(group => group.sites).map(typeSite),
1791
+ unverified: types.unverifiedSites.map(typeSite),
1792
+ } : findAccessorReferences(index, searchTerm, def, { files: testPaths });
1758
1793
  if (!refs) continue;
1759
1794
  const rows = [
1760
1795
  ...refs.confirmed.map(r => ({ ...r, tierName: 'confirmed' })),
1761
- ...(globallyUnique ? refs.unverified.map(r => ({ ...r, tierName: 'unverified' })) : []),
1796
+ ...(globallyUnique || types && options.definition
1797
+ ? refs.unverified.map(r => ({ ...r, tierName: 'unverified' })) : []),
1762
1798
  ];
1763
1799
  for (const ref of rows) {
1764
1800
  const info = testInfo.get(ref.absoluteFile);
@@ -2092,6 +2128,23 @@ function _buildInstanceTypeMap(index, filePath, content, targetClassName) {
2092
2128
  return typeMap;
2093
2129
  }
2094
2130
 
2131
+ /** A bare name bound by one preceding declaration in a Python class body. */
2132
+ function classBodyReferenceTargets(index, file, language, usage, targets) {
2133
+ if (!langTraits(language)?.assignmentDeclaresLocal || !targets.length) return false;
2134
+ const tree = index._getParsedTree(file, index._readFile(file), language);
2135
+ if (!tree) return false;
2136
+ const node = tree.rootNode.namedDescendantForPosition({ row: usage.line - 1, column: usage.column });
2137
+ const owner = require('./value-references').pythonClassBodyOf(node);
2138
+ if (!owner) return false;
2139
+ const { pythonBindingSites, pythonClassBodyOrder } = require('../languages/lexical-scope');
2140
+ const bindings = pythonBindingSites(owner, node.text);
2141
+ if (bindings.length !== 1 || pythonClassBodyOrder(owner, node, node.text) !== 'class') return false;
2142
+ const binding = bindings[0];
2143
+ return targets.some(def => def.file === file && def.name === node.text &&
2144
+ def.startLine >= owner.startPosition.row + 1 && def.endLine <= owner.endPosition.row + 1 &&
2145
+ binding.startPosition.row + 1 === (def.nameLine || def.startLine));
2146
+ }
2147
+
2095
2148
  /**
2096
2149
  * Check if a usage's receiver matches the target className.
2097
2150
  * Uses direct receiver check and instance type map for indirect bindings.
package/core/shared.js CHANGED
@@ -7,6 +7,11 @@ const path = require('path');
7
7
  const { isTestFile } = require('./discovery');
8
8
  const { detectLanguage } = require('./parser');
9
9
 
10
+ // Modules read lazily (load cycles), each resolved once: a require() per
11
+ // call in hot paths was measurable (fix #436).
12
+ let lazyLanguagesModuleCache = null;
13
+ const lazyLanguagesModule = () => lazyLanguagesModuleCache || (lazyLanguagesModuleCache = require('../languages'));
14
+
10
15
  /**
11
16
  * Security containment check (fix #285): is `candidate` inside `root`?
12
17
  *
@@ -63,7 +68,7 @@ function isTestPath(rp) {
63
68
  // has a `.spec` form, only C's patterns name `.h`): no header-language
64
69
  // detection, which walks the project directory (fix #396).
65
70
  const language = path.extname(rp).toLowerCase() === '.h' ? 'c' : detectLanguage(rp);
66
- const { langTraits } = require('../languages');
71
+ const { langTraits } = lazyLanguagesModule();
67
72
  const specConvention = langTraits(language)?.testFileCandidates?.('sample', '')
68
73
  .some(candidate => candidate.includes('.spec'));
69
74
  return isTestFile(rp, language) ||
@@ -148,6 +153,22 @@ function escapeRegExp(text) {
148
153
 
149
154
  // Symbol types that are not callable (used to filter class/struct/type declarations from call analysis)
150
155
  const NON_CALLABLE_TYPES = new Set(['class', 'struct', 'interface', 'type', 'enum', 'trait', 'state', 'impl', 'field']);
156
+ const VALUE_SYMBOL_KINDS = new Set(['field', 'private field', 'constant', 'state', 'variant']);
157
+
158
+ /**
159
+ * A member a call can name (fix #423): a callable kind, or a class field the
160
+ * parser read as a function (`reset = () => {..}`, `isMethod`), which
161
+ * `form.reset()` and `this.reset()` call like a method.
162
+ */
163
+ function isCallableMember(definition) {
164
+ return !!definition && (!NON_CALLABLE_TYPES.has(definition.type) ||
165
+ (definition.type === 'field' && definition.isMethod === true));
166
+ }
167
+
168
+ function isValueDefinition(definition) {
169
+ return !!definition && !definition.isMethod &&
170
+ (VALUE_SYMBOL_KINDS.has(definition.type) || VALUE_SYMBOL_KINDS.has(definition.memberType));
171
+ }
151
172
 
152
173
  /**
153
174
  * Every function-shaped symbol kind across the parsers (fix #251 — stats
@@ -501,7 +522,81 @@ function isMacroNamespaceDefinition(def) {
501
522
  modifiers.includes('proc_macro_derive');
502
523
  }
503
524
 
525
+ /** The names the inverted call index records, in first-occurrence order. */
526
+ function calleeNames(calls) {
527
+ const names = new Set();
528
+ for (const call of calls || []) {
529
+ names.add(call.name);
530
+ if (call.resolvedName) names.add(call.resolvedName);
531
+ for (const name of call.resolvedNames || []) names.add(name);
532
+ if (call.ownClass) names.add(call.ownClass.name);
533
+ if (call.receiverIsTypeQualified && call.receiver) names.add(call.receiver);
534
+ }
535
+ return names;
536
+ }
537
+
538
+ // Declarations whose line extent holds a line, by symbol list (fix #418:
539
+ // per-site scans of a whole symbol table made construction and namespace
540
+ // lookups quadratic in large files). An index belongs to one list; a list
541
+ // whose length or ends changed gets a new one.
542
+ const SPAN_INDEXES = new WeakMap();
543
+
544
+ function spanIndexOf(symbols) {
545
+ const known = SPAN_INDEXES.get(symbols);
546
+ if (known && known.length === symbols.length && known.first === symbols[0] &&
547
+ known.last === symbols[symbols.length - 1]) return known;
548
+ const regular = [], irregular = [];
549
+ for (let i = 0; i < symbols.length; i++) {
550
+ const symbol = symbols[i];
551
+ if (Number.isFinite(symbol?.startLine) && Number.isFinite(symbol?.endLine)) regular.push(i);
552
+ else irregular.push(i);
553
+ }
554
+ regular.sort((a, b) => symbols[a].startLine - symbols[b].startLine || a - b);
555
+ const starts = regular.map(i => symbols[i].startLine), ends = regular.map(i => symbols[i].endLine);
556
+ const maxEnd = new Array(regular.length);
557
+ const build = (lo, hi) => {
558
+ if (lo >= hi) return -Infinity;
559
+ const mid = (lo + hi) >> 1;
560
+ return (maxEnd[mid] = Math.max(ends[mid], build(lo, mid), build(mid + 1, hi)));
561
+ };
562
+ build(0, regular.length);
563
+ const index = { length: symbols.length, first: symbols[0], last: symbols[symbols.length - 1],
564
+ regular, starts, ends, maxEnd, irregular };
565
+ SPAN_INDEXES.set(symbols, index);
566
+ return index;
567
+ }
568
+
569
+ /**
570
+ * The symbols of `symbols` whose extent holds `line` (the comparisons
571
+ * `!(startLine > line) && !(endLine < line)`), in list order.
572
+ */
573
+ function symbolsSpanning(symbols, line) {
574
+ if (!Array.isArray(symbols) || symbols.length === 0) return [];
575
+ const index = spanIndexOf(symbols);
576
+ const found = [];
577
+ const visit = (lo, hi) => {
578
+ if (lo >= hi) return;
579
+ const mid = (lo + hi) >> 1;
580
+ if (index.maxEnd[mid] < line) return;
581
+ visit(lo, mid);
582
+ if (index.starts[mid] > line) return;
583
+ if (index.ends[mid] >= line) found.push(index.regular[mid]);
584
+ visit(mid + 1, hi);
585
+ };
586
+ if (Number.isFinite(line)) visit(0, index.regular.length);
587
+ else for (let i = 0; i < index.regular.length; i++) {
588
+ if (!(index.starts[i] > line) && !(index.ends[i] < line)) found.push(index.regular[i]);
589
+ }
590
+ for (const i of index.irregular) {
591
+ const symbol = symbols[i];
592
+ if (!(symbol?.startLine > line) && !(symbol?.endLine < line)) found.push(i);
593
+ }
594
+ return found.sort((a, b) => a - b).map(i => symbols[i]);
595
+ }
596
+
504
597
  module.exports = { literalNameRegex,
598
+ symbolsSpanning,
599
+ calleeNames,
505
600
  pickBestDefinition,
506
601
  addTestExclusions,
507
602
  escapeRegExp,
@@ -511,6 +606,8 @@ module.exports = { literalNameRegex,
511
606
  classDispatchNames,
512
607
  maskBlockComments,
513
608
  NON_CALLABLE_TYPES,
609
+ isCallableMember,
610
+ isValueDefinition,
514
611
  CALLABLE_SYMBOL_KINDS,
515
612
  formatSymbolHandle,
516
613
  parseSymbolHandle,
@@ -0,0 +1,201 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+
6
+ // Bounded memos and source identities shared by the query-time syntax
7
+ // caches (fix #418). A memo keys a value by the exact input that determines
8
+ // it (a source text, a parsed view), never by a binding or ownership verdict.
9
+
10
+ /** A least-recently-used map bounded by entry count and by total weight. */
11
+ class BoundedMap {
12
+ constructor({ maxEntries = Infinity, maxWeight = Infinity, weigh = null } = {}) {
13
+ this.maxEntries = maxEntries;
14
+ this.maxWeight = maxWeight;
15
+ this.weigh = weigh;
16
+ this.entries = new Map();
17
+ this.weight = 0;
18
+ }
19
+
20
+ get size() { return this.entries.size; }
21
+
22
+ has(key) { return this.entries.has(key); }
23
+
24
+ /** The value, made most recently used; undefined when absent. */
25
+ get(key) {
26
+ const entry = this.entries.get(key);
27
+ if (entry === undefined) return undefined;
28
+ this.entries.delete(key);
29
+ this.entries.set(key, entry);
30
+ return entry.value;
31
+ }
32
+
33
+ /** Store a value; one heavier than the whole bound is not kept. */
34
+ set(key, value) {
35
+ const weight = this.weigh ? this.weigh(value, key) : 0;
36
+ this.delete(key);
37
+ if (weight > this.maxWeight) return false;
38
+ while (this.entries.size > 0 && (this.entries.size >= this.maxEntries ||
39
+ this.weight + weight > this.maxWeight)) {
40
+ this.delete(this.entries.keys().next().value);
41
+ }
42
+ this.entries.set(key, { value, weight });
43
+ this.weight += weight;
44
+ return true;
45
+ }
46
+
47
+ delete(key) {
48
+ const entry = this.entries.get(key);
49
+ if (entry === undefined) return false;
50
+ this.entries.delete(key);
51
+ this.weight -= entry.weight;
52
+ return true;
53
+ }
54
+
55
+ clear() {
56
+ this.entries.clear();
57
+ this.weight = 0;
58
+ }
59
+
60
+ *[Symbol.iterator]() {
61
+ for (const [key, entry] of this.entries) yield [key, entry.value];
62
+ }
63
+ }
64
+
65
+ /**
66
+ * A memo of facts derived from a source text in one language. The text is
67
+ * the key itself, so a lookup never builds a key string as long as the
68
+ * source; its weight counts the retained text.
69
+ */
70
+ class SourceMemo {
71
+ constructor({ maxEntries = 256, maxWeight = 32 * 1024 * 1024, weigh = () => 0 } = {}) {
72
+ this.memo = new BoundedMap({ maxEntries, maxWeight,
73
+ weigh: (byLanguage, source) => source.length * 2 + [...byLanguage.values()].reduce(
74
+ (sum, value) => sum + weigh(value), 0) });
75
+ }
76
+
77
+ get(language, source) {
78
+ return this.memo.get(source)?.get(language);
79
+ }
80
+
81
+ set(language, source, value) {
82
+ const byLanguage = new Map(this.memo.get(source) || []);
83
+ byLanguage.set(language, value);
84
+ this.memo.set(source, byLanguage);
85
+ return value;
86
+ }
87
+ }
88
+
89
+ // Parsed trees of recently read sources, by grammar. A tree is immutable
90
+ // and collected by the runtime; a reader never edits one. Readers that ask
91
+ // for whole-file syntax at many sites of the same few files share a parse
92
+ // (fix #418: an overload check parsed both of its files for every argument).
93
+ const parsedSources = new SourceMemo({ maxEntries: 32, maxWeight: 64 * 1024 * 1024 });
94
+
95
+ /** The tree of `source` in the grammar `language`, or null when it cannot parse. */
96
+ function parsedSource(language, source) {
97
+ if (typeof source !== 'string') return null;
98
+ let tree = parsedSources.get(language, source);
99
+ if (tree === undefined) {
100
+ const { getParser, safeParse } = require('../languages');
101
+ tree = safeParse(getParser(language), source) || null;
102
+ parsedSources.set(language, source, tree);
103
+ }
104
+ return tree;
105
+ }
106
+
107
+ // One stat per file per operation (fix #365), for every reader of the
108
+ // operation (fix #418: the call-name cache stated a file per query).
109
+ const OPERATION_STATS = new WeakMap(); // operation content cache -> Map<file, mtimeMs>
110
+
111
+ /** The file's mtime, read once per operation; throws as statSync does. */
112
+ function fileMtime(index, file) {
113
+ const scope = index._opContentCache;
114
+ let memo = scope ? OPERATION_STATS.get(scope) : null;
115
+ if (scope && !memo) OPERATION_STATS.set(scope, memo = new Map());
116
+ let mtime = memo?.get(file);
117
+ if (mtime === undefined) {
118
+ mtime = fs.statSync(file).mtimeMs;
119
+ memo?.set(file, mtime);
120
+ }
121
+ return mtime;
122
+ }
123
+
124
+ /** The SHA-256 of the JSON of `parts`: a view or option identity. */
125
+ function digestOf(parts) {
126
+ return crypto.createHash('sha256').update(JSON.stringify(parts)).digest('hex');
127
+ }
128
+
129
+ /**
130
+ * Does `content` equal the source the index read for `file`? One hash per
131
+ * file and content in an operation (every syntax cache of an operation
132
+ * asks it of the same files).
133
+ */
134
+ function indexedContent(index, file, entry, content) {
135
+ if (!entry?.hash || typeof content !== 'string') return false;
136
+ const memo = !index.isProvenanceFactIndex && typeof index._opMemo === 'function'
137
+ ? index._opMemo('indexedContent', () => new Map()) : null;
138
+ const saved = memo?.get(file);
139
+ if (saved && saved.content === content && saved.hash === entry.hash) return saved.fresh;
140
+ const fresh = crypto.createHash('md5').update(content).digest('hex') === entry.hash;
141
+ memo?.set(file, { content, hash: entry.hash, fresh });
142
+ return fresh;
143
+ }
144
+
145
+ /**
146
+ * Are the type names written in `file` usable as the index read them (trait
147
+ * `typeNameSourceCanonical` of `language`)? The index's own parse of the
148
+ * same content tells whether it had errors, so most sources need no second
149
+ * parse (fix #418). `lines` (`{ startLine, endLine }`) asks about the names
150
+ * of that line range only: a parse error confined to another member does not
151
+ * reach them (fix #421).
152
+ */
153
+ function typeNamesCanonical(index, file, canonical, language, lines = null) {
154
+ const source = index._readFile(file);
155
+ // The verdict is the same with or without the hint, so a witness view
156
+ // takes it unrecorded from its live index; its replay derives it again.
157
+ const hint = live => {
158
+ const entry = live.files.get(file);
159
+ return entry?.language === language && indexedContent(live, file, entry, source)
160
+ ? !!entry.parseRecovery : undefined;
161
+ };
162
+ return canonical(source, () => index.isProvenanceFactIndex ? index.unrecordedHint(hint) : hint(index), lines);
163
+ }
164
+
165
+ /** The parsed views of a file: grammar, configured views, recovery blanks and external macro scope. */
166
+ function parseViewParts(entry, language = entry.language) {
167
+ return [language, entry.conditionalViews, entry.recoveryBlanks, entry.externalMacroKey];
168
+ }
169
+
170
+ /**
171
+ * Persisted source-only syntax caches (fix #415): each family's mode
172
+ * prefix and the weight of one stored value in the usage-result LRU.
173
+ * A producer names its mode through `syntaxMode`, so a version bump in a
174
+ * prefix keeps its weight.
175
+ */
176
+ const SYNTAX_FAMILIES = Object.freeze({
177
+ callToken: { prefix: 'call-token-v1', weight: 'rows' },
178
+ declarationToken: { prefix: 'declaration-token-v1', weight: 'rows' },
179
+ contractSyntax: { prefix: 'contract-syntax-v1', weight: 'json' },
180
+ usageText: { prefix: 'usage-text-v2', weight: 'json' },
181
+ enumSyntax: { prefix: 'enum-syntax-v2', weight: 'json' },
182
+ valueLocal: { prefix: 'value-local-v3', weight: 'json' },
183
+ valueBodies: { prefix: 'value-bodies-v4', weight: 'json' },
184
+ classFields: { prefix: 'class-fields-v1', weight: 'json' },
185
+ });
186
+
187
+ /** A family's mode: its prefix, an optional qualifier and the digest of `parts`. */
188
+ function syntaxMode(family, parts, qualifier = null) {
189
+ return `${SYNTAX_FAMILIES[family].prefix}:${qualifier === null ? '' : `${qualifier}:`}${digestOf(parts)}`;
190
+ }
191
+
192
+ function syntaxWeightKind(mode) {
193
+ const prefix = String(mode).split(':', 1)[0];
194
+ for (const family of Object.values(SYNTAX_FAMILIES)) {
195
+ if (family.prefix === prefix) return family.weight;
196
+ }
197
+ return null;
198
+ }
199
+
200
+ module.exports = { BoundedMap, SourceMemo, parsedSource, fileMtime, digestOf, indexedContent, typeNamesCanonical, parseViewParts,
201
+ SYNTAX_FAMILIES, syntaxMode, syntaxWeightKind };
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ const cache = require('./cache');
4
+ const { indexedContent, parseViewParts, syntaxMode } = require('./source-memo');
5
+
6
+ /** Source positions only; no binding or ownership decisions are retained. */
7
+ function textUsageSyntax(index, file, entry, content, name) {
8
+ const eligible = entry.hash && index._usageResultCache && !index.isProvenanceFactIndex &&
9
+ indexedContent(index, file, entry, content);
10
+ const mode = eligible ? syntaxMode('usageText', parseViewParts(entry)) : null;
11
+ const stored = eligible ? cache.getUsageResult(index, file, name, mode) : null;
12
+ const facts = new Map();
13
+ if (Array.isArray(stored) && stored.every(fact => Array.isArray(fact) && fact.length === 3 &&
14
+ Number.isInteger(fact[0]) && fact[0] > 0 && Number.isInteger(fact[1]) && fact[1] >= 0 &&
15
+ ['identifier', 'comment-or-string', 'markup-or-text'].includes(fact[2]))) {
16
+ for (const fact of stored) facts.set(fact[0], fact);
17
+ }
18
+ let dirty = false;
19
+ return {
20
+ kind(line, column) {
21
+ const saved = facts.get(line);
22
+ if (saved?.[1] === column) return saved[2];
23
+ const kind = index.isCommentOrStringAtPosition(content, line, column, file)
24
+ ? 'comment-or-string'
25
+ : index.isIdentifierAtPosition(content, line, column, file, name)
26
+ ? 'identifier' : 'markup-or-text';
27
+ facts.set(line, [line, column, kind]);
28
+ dirty = true;
29
+ return kind;
30
+ },
31
+ save() {
32
+ if (eligible && dirty) cache.storeUsageResult(index, file, name, mode,
33
+ [...facts.values()].sort((a, b) => a[0] - b[0]));
34
+ },
35
+ };
36
+ }
37
+
38
+ module.exports = { textUsageSyntax };
package/core/tracing.js CHANGED
@@ -231,6 +231,11 @@ function trace(index, name, options = {}) {
231
231
  }
232
232
  }
233
233
  }
234
+ // A construction whose constructor body is undecided (fix #421):
235
+ // its class edge is a child, the review a leaf beside it.
236
+ if (callees.initializerReviews && callees.initializerReviews.length > 0) {
237
+ node.initializerReviews = callees.initializerReviews;
238
+ }
234
239
  for (const callee of callees.slice(0, maxChildren)) {
235
240
  // callee already has the best-matched definition from findCallees
236
241
  const childTree = buildTree(callee, currentDepth + 1, 'down');
@@ -238,7 +243,8 @@ function trace(index, name, options = {}) {
238
243
  node.children.push({
239
244
  ...childTree,
240
245
  callCount: callee.callCount,
241
- weight: callee.weight
246
+ weight: callee.weight,
247
+ ...(callee.initializerOf && { constructs: callee.initializerOf }),
242
248
  });
243
249
  }
244
250
  }