ucn 5.5.3 → 5.6.1

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 (77) hide show
  1. package/.claude/skills/ucn/SKILL.md +37 -12
  2. package/.claude/skills/ucn/references/commands.md +18 -14
  3. package/.claude/skills/ucn/references/resolution-rules.md +4 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +4 -4
  5. package/CHANGELOG.md +485 -0
  6. package/README.md +66 -29
  7. package/cli/flags.js +184 -0
  8. package/cli/index.js +137 -324
  9. package/core/account.js +44 -87
  10. package/core/analysis.js +274 -108
  11. package/core/ast-analysis.js +2 -0
  12. package/core/bridge.js +14 -0
  13. package/core/cache.js +103 -16
  14. package/core/call-fit.js +130 -0
  15. package/core/callers.js +353 -71
  16. package/core/capture.js +363 -0
  17. package/core/change-kinds.js +1107 -0
  18. package/core/check.js +1150 -139
  19. package/core/command-contracts.js +8 -7
  20. package/core/constructed-class.js +1195 -0
  21. package/core/contract-membership.js +142 -0
  22. package/core/deadcode.js +455 -106
  23. package/core/deletion.js +421 -0
  24. package/core/discovery.js +104 -9
  25. package/core/entrypoint-toml.js +110 -0
  26. package/core/entrypoints.js +12 -6
  27. package/core/evidence-bands.js +68 -0
  28. package/core/evidence-bands.json +44 -0
  29. package/core/execute.js +172 -161
  30. package/core/hot-summary.js +241 -0
  31. package/core/index-ir.js +5 -3
  32. package/core/ir.js +6 -2
  33. package/core/member-signatures.js +789 -0
  34. package/core/outgoing-check.js +116 -0
  35. package/core/output/analysis-ext.js +20 -10
  36. package/core/output/analysis.js +98 -60
  37. package/core/output/check.js +101 -37
  38. package/core/output/doctor.js +34 -1
  39. package/core/output/find.js +2 -1
  40. package/core/output/lines.js +63 -3
  41. package/core/output/public.js +29 -11
  42. package/core/output/refactoring.js +32 -0
  43. package/core/output/reporting.js +19 -2
  44. package/core/output/search.js +14 -2
  45. package/core/output/tracing.js +36 -8
  46. package/core/output-budget.js +146 -61
  47. package/core/parallel-build.js +3 -3
  48. package/core/project.js +106 -23
  49. package/core/provenance-type.js +12 -5
  50. package/core/public-command.js +35 -9
  51. package/core/python-binding-identity.js +707 -0
  52. package/core/python-modules.js +6 -0
  53. package/core/reflection.js +248 -31
  54. package/core/registry.js +623 -72
  55. package/core/reporting.js +69 -34
  56. package/core/review-items.js +75 -0
  57. package/core/runtime-entrypoints.js +159 -0
  58. package/core/search.js +148 -164
  59. package/core/shared.js +0 -45
  60. package/core/snapshot.js +350 -0
  61. package/core/source-universe.js +707 -0
  62. package/core/test-units.js +3698 -0
  63. package/core/tracing.js +270 -658
  64. package/core/verify.js +63 -22
  65. package/languages/adapter.js +8 -3
  66. package/languages/c-family.js +12 -0
  67. package/languages/csharp.js +634 -0
  68. package/languages/go.js +120 -0
  69. package/languages/index.js +466 -4
  70. package/languages/java.js +712 -0
  71. package/languages/javascript.js +1498 -6
  72. package/languages/lexical-scope.js +68 -0
  73. package/languages/python.js +1708 -3
  74. package/languages/rust.js +209 -104
  75. package/languages/static-js.js +88 -0
  76. package/mcp/server.js +44 -110
  77. package/package.json +11 -5
package/languages/rust.js CHANGED
@@ -287,7 +287,10 @@ const RUST_TRANSPARENT_FN_ATTRIBUTES = new Set([
287
287
  ]);
288
288
 
289
289
  function rustAttributePath(node) {
290
- return node && ['identifier', 'scoped_identifier'].includes(node.type) ? node.text.replace(/\s+/g, '') : null;
290
+ // Raw identifiers change the spelling, not the attribute's identity.
291
+ return node && ['identifier', 'scoped_identifier'].includes(node.type)
292
+ ? node.text.replace(/\s+/g, '').split('::').map(part => part.startsWith('r#') ? part.slice(2) : part).join('::')
293
+ : null;
291
294
  }
292
295
 
293
296
  /** Outer attribute paths of an item; `cfg_attr(pred, a, b)` yields a, b. */
@@ -314,7 +317,7 @@ function rustOuterAttributePaths(itemNode) {
314
317
  continue;
315
318
  }
316
319
  if (group === 0 || !expectPath) continue;
317
- if (token.type === 'identifier' || token.type === '::') current += token.text;
320
+ if (token.type === 'identifier' || token.type === '::') current += rustAttributePath(token) || token.text;
318
321
  else { if (current) paths.push(current); current = ''; expectPath = false; }
319
322
  }
320
323
  if (current) paths.push(current);
@@ -602,93 +605,42 @@ function extractVisibility(text) {
602
605
  /**
603
606
  * Extract attributes from a function node (e.g., #[test], #[tokio::main])
604
607
  * @param {Node} node - AST node
605
- * @param {string} code - Source code
606
608
  * @returns {string[]} Array of attribute names
607
609
  */
608
- function extractAttributes(node, codeOrLines) {
610
+ function outerAttributeNodes(node) {
609
611
  const attributes = [];
610
- const lines = Array.isArray(codeOrLines) ? codeOrLines : codeOrLines.split('\n');
611
-
612
- // Look at lines before the function for attributes
613
- const startLine = node.startPosition.row;
614
- for (let i = startLine - 1; i >= 0 && i >= startLine - 5; i--) {
615
- const line = lines[i]?.trim();
616
- if (!line) break;
617
- if (line.startsWith('#[')) {
618
- // Extract attribute name (e.g., #[test] -> test, #[tokio::main] -> tokio::main)
619
- const match = line.match(/#\[([^\]]+)\]/);
620
- if (match) {
621
- const attrContent = match[1];
622
- // Get just the attribute name (without arguments)
623
- const attrName = attrContent.split('(')[0].trim();
624
- // Skip compiler hint attributes that aren't semantically meaningful for display
625
- const SKIP_ATTRS = new Set(['allow', 'deny', 'warn', 'forbid', 'cfg_attr', 'doc']);
626
- if (!SKIP_ATTRS.has(attrName)) {
627
- attributes.push(attrName);
628
- }
629
- }
630
- } else if (!line.startsWith('//')) {
631
- // Stop at non-comment, non-attribute lines
632
- break;
633
- }
612
+ for (let sibling = node.previousNamedSibling; sibling; sibling = sibling.previousNamedSibling) {
613
+ if (sibling.type === 'line_comment' || sibling.type === 'block_comment') continue;
614
+ if (sibling.type !== 'attribute_item') break;
615
+ const attribute = sibling.namedChildren.find(child => child.type === 'attribute');
616
+ if (attribute) attributes.unshift(attribute);
634
617
  }
635
-
636
618
  return attributes;
637
619
  }
638
620
 
639
- /**
640
- * Extract attributes WITH their argument tokens (for routing decorator detection).
641
- * Returns array of { name, args: rawArgString } objects.
642
- * #[get("/users")] → [{ name: 'get', args: '"/users"' }]
643
- * #[tokio::main] → [{ name: 'tokio::main', args: null }]
644
- *
645
- * @param {Node} node - Function AST node
646
- * @param {string|string[]} codeOrLines - Source code or pre-split lines
647
- * @returns {Array<{name: string, args: string|null}>}
648
- */
649
- function extractAttributesWithArgs(node, codeOrLines) {
650
- const result = [];
651
- const lines = Array.isArray(codeOrLines) ? codeOrLines : codeOrLines.split('\n');
652
-
653
- const startLine = node.startPosition.row;
654
- for (let i = startLine - 1; i >= 0 && i >= startLine - 5; i--) {
655
- const line = lines[i]?.trim();
656
- if (!line) break;
657
- if (line.startsWith('#[')) {
658
- // Match #[name(...args...)] or #[name]
659
- // Need to handle nested parens; use a simple bracket-matching approach.
660
- // (The line is trimmed: only a line ending in ']' can match; a
661
- // macro expansion's generated line can be very long.)
662
- const m = line.charCodeAt(line.length - 1) === 93 ? line.match(/^#\[(.+)\]\s*$/) : null;
663
- if (m) {
664
- const attrContent = m[1];
665
- const parenIdx = attrContent.indexOf('(');
666
- if (parenIdx === -1) {
667
- result.unshift({ name: attrContent.trim(), args: null });
668
- } else {
669
- const name = attrContent.slice(0, parenIdx).trim();
670
- // Extract content within outer parens (find matching close)
671
- let depth = 0;
672
- let endIdx = -1;
673
- for (let k = parenIdx; k < attrContent.length; k++) {
674
- const ch = attrContent[k];
675
- if (ch === '(') depth++;
676
- else if (ch === ')') {
677
- depth--;
678
- if (depth === 0) { endIdx = k; break; }
679
- }
680
- }
681
- const args = endIdx > parenIdx
682
- ? attrContent.slice(parenIdx + 1, endIdx).trim()
683
- : attrContent.slice(parenIdx + 1).trim();
684
- result.unshift({ name, args });
685
- }
686
- }
687
- } else if (!line.startsWith('//')) {
688
- break;
621
+ function extractAttributes(node) {
622
+ const skip = new Set(['allow', 'deny', 'warn', 'forbid', 'cfg_attr', 'doc']);
623
+ // Preserve the established nearest-first modifier ordering. Ownership
624
+ // comes from adjacent AST siblings, never preceding source lines.
625
+ return extractAttributesWithArgs(node).reverse().map(attribute => attribute.name)
626
+ .filter(name => !skip.has(name));
627
+ }
628
+
629
+ /** Written outer attributes attached to this item, in source order. */
630
+ function extractAttributesWithArgs(node) {
631
+ const attributes = [];
632
+ for (const attribute of outerAttributeNodes(node)) {
633
+ let name = rustAttributePath(attribute.namedChild(0));
634
+ if (!name) continue;
635
+ const argumentsNode = attribute.childForFieldName('arguments');
636
+ // The existing attribute record keeps name-value attributes whole
637
+ // (notably path = "module.rs", which module resolution consumes).
638
+ if (!argumentsNode && attribute.children.some(child => child.type === '=')) {
639
+ name += attribute.text.slice(attribute.namedChild(0).endIndex - attribute.startIndex).trimEnd();
689
640
  }
641
+ attributes.push({ name, args: argumentsNode ? argumentsNode.text.slice(1, -1).trim() : null });
690
642
  }
691
- return result;
643
+ return attributes;
692
644
  }
693
645
 
694
646
  // --- Module-scope constants for state object detection ---
@@ -701,30 +653,14 @@ function extractAttributesWithArgs(node, codeOrLines) {
701
653
  * functions inside a `mod tests` block as test entry points even when
702
654
  * they don't carry a direct `#[test]` attribute (BUG-CY).
703
655
  */
704
- function _isInsideCfgTestModule(node, lines) {
705
- let parent = node.parent;
706
- while (parent) {
707
- if (parent.type === 'mod_item') {
708
- const startRow = parent.startPosition.row;
709
- // Look at preceding lines for #[cfg(test)] or #[cfg(any(test,...))] / #[cfg(all(...,test,...))]
710
- for (let i = startRow - 1; i >= 0 && i >= startRow - 5; i--) {
711
- const line = lines[i]?.trim();
712
- if (!line) break;
713
- if (line.startsWith('#[')) {
714
- // Match #[cfg(...)] forms that include a `test` predicate.
715
- // Conservatively look for the literal token `test` inside the cfg(...) args.
716
- const m = line.match(/#\[\s*cfg\s*\(([^\]]*)\)\s*\]/);
717
- if (m) {
718
- const args = m[1];
719
- // Word-boundary match for `test` to avoid matching e.g. `testing_module`.
720
- if (/\btest\b/.test(args)) return true;
721
- }
722
- } else if (!line.startsWith('//')) {
723
- break;
724
- }
725
- }
726
- }
727
- parent = parent.parent;
656
+ function _isInsideCfgTestModule(node) {
657
+ const namesTest = part => part.type === 'identifier' && rustAttributePath(part) === 'test' ||
658
+ part.namedChildren.some(namesTest);
659
+ for (let parent = node.parent; parent; parent = parent.parent) {
660
+ if (parent.type !== 'mod_item') continue;
661
+ if (outerAttributeNodes(parent).some(attribute =>
662
+ rustAttributePath(attribute.namedChild(0)) === 'cfg' &&
663
+ attribute.childForFieldName('arguments')?.namedChildren.some(namesTest))) return true;
728
664
  }
729
665
  return false;
730
666
  }
@@ -1978,6 +1914,9 @@ function extractTraitMembers(traitNode, codeOrLines) {
1978
1914
  sourceSpan,
1979
1915
  memberType: 'method',
1980
1916
  isMethod: true,
1917
+ // A trait method without a body is one every impl must
1918
+ // supply; one with a body is a provided default.
1919
+ ...(child.type === 'function_signature_item' && { requiredMember: true }),
1981
1920
  ...(qualifiers.async && { isAsync: true }),
1982
1921
  modifiers: [...(traitVisibility ? [traitVisibility] : []), ...(qualifiers.async ? ['async'] : [])],
1983
1922
  ...(futureReturn && { futureReturn }),
@@ -6389,6 +6328,171 @@ function rustMacroNamespaceName(node, parent, attributeNode) {
6389
6328
  return !next || next.type !== '::';
6390
6329
  }
6391
6330
 
6331
+
6332
+ // Attribute macros that declare one test per case; the names the harness
6333
+ // lists come from the expansion, not from the function.
6334
+ const RUST_CASE_ATTRIBUTES = new Set(['rstest', 'test_case', 'test_matrix', 'parameterized', 'test_casing',
6335
+ 'apply', 'quickcheck', 'proptest', 'test_resources', 'datatest']);
6336
+
6337
+ // Attributes of the language that leave a test as it is. Any other is a
6338
+ // macro that rewrites the function: what then runs is not read.
6339
+ const RUST_INERT_ATTRIBUTES = new Set(['test', 'ignore', 'should_panic', 'cfg', 'cfg_attr', 'allow', 'warn', 'deny',
6340
+ 'forbid', 'expect', 'doc', 'inline', 'cold', 'must_use', 'deprecated', 'track_caller', 'no_mangle', 'case',
6341
+ 'values', 'timeout']);
6342
+
6343
+ /** The comma-separated syntax groups of an attribute's token tree. */
6344
+ function rustAttributeGroups(argumentsNode) {
6345
+ const groups = [[]];
6346
+ for (const child of argumentsNode?.children || []) {
6347
+ if (['(', ')', '[', ']', '{', '}', 'line_comment', 'block_comment'].includes(child.type)) continue;
6348
+ if (child.type === ',') groups.push([]);
6349
+ else groups[groups.length - 1].push(child);
6350
+ }
6351
+ return groups.filter(group => group.length > 0);
6352
+ }
6353
+
6354
+ /** An attribute written inside cfg_attr is token-tree syntax, not an attribute node. */
6355
+ function rustNestedAttribute(nodes) {
6356
+ let name = '';
6357
+ let args = null;
6358
+ for (const node of nodes) {
6359
+ if (node.type === 'token_tree') { args = node; break; }
6360
+ if (['identifier', 'scoped_identifier', '::'].includes(node.type)) name += rustAttributePath(node) || node.text;
6361
+ else break;
6362
+ }
6363
+ return { name, args };
6364
+ }
6365
+
6366
+ // Only configuration facts intrinsic to a test build are decided here.
6367
+ // Feature, target and user cfg values remain unknown, including their negations.
6368
+ function rustTestCfgValue(nodes) {
6369
+ if (nodes.length === 1 && nodes[0].type === 'identifier' && rustAttributePath(nodes[0]) === 'test') return true;
6370
+ const { name, args } = rustNestedAttribute(nodes);
6371
+ if (!args) return null;
6372
+ const values = rustAttributeGroups(args).map(rustTestCfgValue);
6373
+ if (name === 'all') return values.includes(false) ? false : values.includes(null) ? null : true;
6374
+ if (name === 'any') return values.includes(true) ? true : values.includes(null) ? null : false;
6375
+ if (name === 'not' && values.length === 1) return values[0] === null ? null : !values[0];
6376
+ return null;
6377
+ }
6378
+
6379
+ /** Possible attributes cfg_attr adds to this exact declaration, never a neighbor. */
6380
+ function rustConditionalTestAttributes(node) {
6381
+ const attributes = [];
6382
+ const expand = (name, args, active) => {
6383
+ if (active === false) return;
6384
+ if (name !== 'cfg_attr') {
6385
+ if (name) attributes.push({ name, certain: active === true });
6386
+ return;
6387
+ }
6388
+ const [condition, ...nested] = rustAttributeGroups(args);
6389
+ const value = condition ? rustTestCfgValue(condition) : null;
6390
+ const enabled = value === false ? false : active === null || value === null ? null : true;
6391
+ for (const group of nested) {
6392
+ const attribute = rustNestedAttribute(group);
6393
+ expand(attribute.name, attribute.args, enabled);
6394
+ }
6395
+ };
6396
+ for (const attribute of outerAttributeNodes(node)) {
6397
+ if (rustAttributePath(attribute.namedChild(0)) === 'cfg_attr') {
6398
+ expand('cfg_attr', attribute.childForFieldName('arguments'), true);
6399
+ }
6400
+ }
6401
+ return attributes;
6402
+ }
6403
+
6404
+ /**
6405
+ * The tests a Rust file declares, as the test harness lists them: a
6406
+ * function carrying #[test] or an attribute whose path ends in ::test
6407
+ * (tokio::test and the like), named by the modules it sits in. A function
6408
+ * carrying a case-generating attribute is a test whose listed names are
6409
+ * not its own. A test under a cfg other than cfg(test) is compiled only
6410
+ * sometimes.
6411
+ */
6412
+ function testUnits(fileEntry, helpers) {
6413
+ const units = [];
6414
+ const holding = new Set();
6415
+ const attributesOf = symbol => symbol.attributesWithArgs || [];
6416
+ let conditionalAttributes;
6417
+ const conditionalAttributesOf = symbol => {
6418
+ if (!attributesOf(symbol).some(attribute => attribute.name === 'cfg_attr')) return [];
6419
+ if (!conditionalAttributes) {
6420
+ conditionalAttributes = new Map();
6421
+ for (const node of helpers.tree()?.rootNode.descendantsOfType('function_item') || []) {
6422
+ conditionalAttributes.set(node.startIndex, rustConditionalTestAttributes(node));
6423
+ }
6424
+ }
6425
+ return conditionalAttributes.get(symbol.sourceSpan?.start) || [];
6426
+ };
6427
+ for (const symbol of fileEntry.symbols || []) {
6428
+ if (symbol.type !== 'function' || symbol.macroExpansion) continue;
6429
+ const attributes = attributesOf(symbol);
6430
+ // The attribute's path: `ignore = "why"` is the attribute `ignore`.
6431
+ const directNames = attributes.map(attribute => String(attribute.name).split(/[\s=(]/)[0]);
6432
+ const conditional = conditionalAttributesOf(symbol);
6433
+ const names = [...directNames, ...conditional.map(attribute => attribute.name)];
6434
+ const ignored = directNames.includes('ignore') || conditional.some(attribute => attribute.name === 'ignore' && attribute.certain);
6435
+ const plain = names.some(name => /(^|::)test$/.test(name));
6436
+ const cases = names.some(name => RUST_CASE_ATTRIBUTES.has(name.split('::').pop()));
6437
+ if (!plain && !cases) continue;
6438
+ const chain = helpers.enclosing(symbol);
6439
+ if (chain.some(outer => outer.type !== 'module')) continue;
6440
+ const gates = chain.filter(outer => (outer.modifiers || []).includes('cfg')).length;
6441
+ const ownGate = attributes.some(attribute => /^cfg(_attr)?$/.test(String(attribute.name)) &&
6442
+ String(attribute.args || '').replace(/[\s()]/g, '') !== 'test');
6443
+ const outerGate = chain.some(outer => attributesOf(outer).some(attribute =>
6444
+ /^cfg(_attr)?$/.test(String(attribute.name)) && String(attribute.args || '').replace(/[\s()]/g, '') !== 'test'));
6445
+ const inTestModule = (symbol.modifiers || []).includes('cfg_test_module');
6446
+ // An attribute that is none of the language's is a macro that rewrites the function.
6447
+ const rewritten = names.some(name => !/(^|::)test$/.test(name) &&
6448
+ !RUST_INERT_ATTRIBUTES.has(name) && !RUST_CASE_ATTRIBUTES.has(name.split('::').pop()) &&
6449
+ !/^(clippy|rustfmt|rustc|diagnostic)(::|_)/.test(name));
6450
+ for (const outer of chain) holding.add(outer);
6451
+ units.push({
6452
+ kind: 'test', name: symbol.name, suite: chain.map(outer => outer.name),
6453
+ line: symbol.nameLine || symbol.startLine, startLine: symbol.startLine,
6454
+ endLine: symbol.endLine ?? symbol.startLine, sourceSpan: symbol.sourceSpan,
6455
+ // Declared, and left out of a run unless asked for.
6456
+ ...(ignored && { ignored: true, skipped: true }),
6457
+ ...((ownGate || outerGate || gates > (inTestModule ? 1 : 0)) && { conditional: true, notInEveryBuild: true }),
6458
+ ...(!(ownGate || outerGate || gates > (inTestModule ? 1 : 0)) && rewritten && { conditional: true }),
6459
+ // What a macro makes of the function is not read: none, one
6460
+ // or several tests.
6461
+ ...(rewritten && { several: true }),
6462
+ ...(cases && { dynamic: 'generated-cases' }),
6463
+ });
6464
+ }
6465
+ for (const outer of holding) {
6466
+ units.push({
6467
+ kind: 'suite', name: outer.name, suite: helpers.enclosing(outer).map(up => up.name),
6468
+ line: outer.nameLine || outer.startLine, startLine: outer.startLine,
6469
+ endLine: outer.endLine ?? outer.startLine, sourceSpan: outer.sourceSpan,
6470
+ });
6471
+ }
6472
+ // Tests a macro makes are not listed: a function a macro of the
6473
+ // project expands to (its attributes stand in the macro), and an
6474
+ // invocation among the items whose tokens name a test attribute
6475
+ // (proptest! { #[test] fn ... }).
6476
+ let more = (fileEntry.symbols || []).some(symbol => symbol.type === 'function' && symbol.macroExpansion &&
6477
+ !units.some(unit => unit.kind === 'test' && unit.startLine === symbol.startLine && unit.name === symbol.name));
6478
+ const tree = more ? null : helpers.tree();
6479
+ const namesTest = node => (node.type === 'identifier' && /(^|_)test$/.test(node.text)) || node.namedChildren.some(namesTest);
6480
+ const items = node => {
6481
+ for (const child of node.namedChildren) {
6482
+ if (more) return;
6483
+ if (child.type === 'macro_invocation') {
6484
+ if (namesTest(child)) more = true;
6485
+ } else if (child.type === 'expression_statement' && child.namedChildren[0]?.type === 'macro_invocation') {
6486
+ if (namesTest(child)) more = true;
6487
+ } else if (child.type === 'mod_item' || child.type === 'declaration_list') {
6488
+ items(child);
6489
+ }
6490
+ }
6491
+ };
6492
+ if (tree) items(tree.rootNode);
6493
+ return { units, inheriting: more };
6494
+ }
6495
+
6392
6496
  /**
6393
6497
  * Classify a Rust symbol as a runtime entry point of a specific kind.
6394
6498
  * Returns 'test' | 'main' | 'framework' | null.
@@ -6444,5 +6548,6 @@ module.exports = {
6444
6548
  findUsagesInCode,
6445
6549
  isEntryPoint,
6446
6550
  getEntryPointKind,
6551
+ testUnits,
6447
6552
  parse
6448
6553
  };
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ const { sameNode } = require('./utils');
4
+ const SCOPES = new Set(['program', 'statement_block', 'switch_body', 'for_statement', 'for_in_statement',
5
+ 'function_declaration', 'generator_function_declaration', 'function_expression', 'generator_function',
6
+ 'arrow_function', 'method_definition', 'catch_clause', 'class', 'class_declaration']);
7
+ const FUNCTIONS = new Set(['function_declaration', 'generator_function_declaration', 'function_expression',
8
+ 'generator_function', 'arrow_function', 'method_definition']);
9
+
10
+ function hasName(pattern, name) {
11
+ return !!pattern && (pattern.text === name ||
12
+ pattern.descendantsOfType(['identifier', 'shorthand_property_identifier_pattern']).some(id => id.text === name));
13
+ }
14
+
15
+ /** Decode a JavaScript literal without evaluating project code. */
16
+ function jsString(node) {
17
+ if (!node || !['string', 'template_string'].includes(node.type) ||
18
+ node.namedChildren.some(child => child.type === 'template_substitution')) return null;
19
+ const escapes = { n: '\n', t: '\t', r: '\r', b: '\b', f: '\f', v: '\v', 0: '\0' };
20
+ return node.text.slice(1, -1).replace(/\\(u\{[0-9a-fA-F]+\}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|[0-3][0-7]{0,2}|[4-7][0-7]?|\r?\n|[\s\S])/g, (whole, escape) => {
21
+ if (/^[0-7]+$/.test(escape)) return String.fromCharCode(parseInt(escape, 8));
22
+ if (escape[0] === 'u' || escape[0] === 'x') {
23
+ const code = parseInt(escape.replace(/^[ux]\{?|\}$/g, ''), 16);
24
+ return code <= 0x10ffff ? String.fromCodePoint(code) : whole;
25
+ }
26
+ if (escape === '\n' || escape === '\r\n') return '';
27
+ return Object.hasOwn(escapes, escape) ? escapes[escape] : escape;
28
+ });
29
+ }
30
+
31
+ /** One stable const binding, with block and parameter shadowing. */
32
+ function jsConstValue(node, constructedMutable = false) {
33
+ if (node?.type !== 'identifier') return null;
34
+ const name = node.text;
35
+ for (let scope = node.parent; scope; scope = scope.parent) {
36
+ if (scope.type === 'with_statement') return null;
37
+ if (!SCOPES.has(scope.type)) continue;
38
+ // Named function/generator/class expressions bind their own name in
39
+ // their body; it never denotes an outer const of the same spelling.
40
+ if (['function_expression', 'generator_function', 'function_declaration',
41
+ 'generator_function_declaration', 'class', 'class_declaration'].includes(scope.type) &&
42
+ scope.childForFieldName('name')?.text === name) return null;
43
+ const params = scope.childForFieldName('parameters') || scope.childForFieldName('parameter');
44
+ if (hasName(params, name) || (scope.type === 'for_in_statement' && hasName(scope.childForFieldName('left'), name))) return null;
45
+ if (FUNCTIONS.has(scope.type)) {
46
+ let shadow = false;
47
+ const scanVars = current => {
48
+ for (const child of current.namedChildren) {
49
+ if (FUNCTIONS.has(child.type)) continue;
50
+ if (child.type === 'variable_declarator' && child.parent?.type === 'variable_declaration' &&
51
+ hasName(child.childForFieldName('name'), name)) shadow = true;
52
+ scanVars(child);
53
+ }
54
+ };
55
+ scanVars(scope);
56
+ if (shadow) return null;
57
+ }
58
+ const bindings = [];
59
+ const walk = current => {
60
+ for (const child of current.namedChildren) {
61
+ if (['function_declaration', 'generator_function_declaration', 'class_declaration'].includes(child.type) && child.childForFieldName('name')?.text === name) bindings.push(child);
62
+ if (SCOPES.has(child.type) && !sameNode(child, scope)) continue;
63
+ if (child.type === 'variable_declarator' && hasName(child.childForFieldName('name'), name)) bindings.push(child);
64
+ else if (child.type === 'import_statement' && hasName(child, name)) bindings.push(child);
65
+ walk(child);
66
+ }
67
+ };
68
+ walk(scope);
69
+ if (bindings.length === 0) continue;
70
+ if (bindings.length !== 1) return null;
71
+ const binding = bindings[0];
72
+ if (binding.type !== 'variable_declarator' || binding.childForFieldName('name')?.type !== 'identifier' ||
73
+ binding.endIndex > node.startIndex) return null;
74
+ const value = binding.childForFieldName('value');
75
+ if (binding.parent?.type === 'lexical_declaration' && binding.parent.child(0)?.type === 'const') return value;
76
+ if (!constructedMutable || value?.type !== 'new_expression') return null;
77
+ const writes = node.tree.rootNode.descendantsOfType(['assignment_expression', 'augmented_assignment_expression', 'update_expression']);
78
+ if (writes.some(write => hasName(write.childForFieldName('left') || write.childForFieldName('argument'), name))) return null;
79
+ return value;
80
+ }
81
+ return null;
82
+ }
83
+
84
+ function jsStaticKey(node) {
85
+ return jsString(node) ?? jsString(jsConstValue(node));
86
+ }
87
+
88
+ module.exports = { jsString, jsConstValue, jsStaticKey };