ucn 4.2.3 → 5.0.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 (72) hide show
  1. package/.claude/skills/ucn/SKILL.md +89 -77
  2. package/.claude/skills/ucn/references/commands.md +62 -68
  3. package/.claude/skills/ucn/references/trust-contract.md +31 -6
  4. package/README.md +438 -305
  5. package/assets/demo.svg +31 -0
  6. package/cli/index.js +430 -1385
  7. package/core/account.js +144 -34
  8. package/core/analysis.js +182 -72
  9. package/core/ast-analysis.js +279 -0
  10. package/core/bridge.js +205 -24
  11. package/core/brief.js +27 -58
  12. package/core/build-worker.js +21 -140
  13. package/core/cache.js +513 -11
  14. package/core/callers.js +4920 -456
  15. package/core/check.js +13 -4
  16. package/core/command-contracts.js +402 -0
  17. package/core/compilation-database.js +276 -0
  18. package/core/confidence.js +4 -1
  19. package/core/deadcode.js +397 -19
  20. package/core/discovery.js +359 -46
  21. package/core/entrypoints.js +195 -41
  22. package/core/execute.js +887 -81
  23. package/core/graph-build.js +162 -7
  24. package/core/graph.js +53 -77
  25. package/core/imports.js +65 -6
  26. package/core/index-ir.js +138 -0
  27. package/core/ir.js +195 -0
  28. package/core/output/analysis.js +212 -22
  29. package/core/output/brief.js +23 -0
  30. package/core/output/check.js +4 -0
  31. package/core/output/doctor.js +37 -6
  32. package/core/output/endpoints.js +5 -2
  33. package/core/output/extraction.js +24 -12
  34. package/core/output/find.js +141 -36
  35. package/core/output/graph.js +11 -5
  36. package/core/output/public.js +462 -0
  37. package/core/output/refactoring.js +42 -10
  38. package/core/output/reporting.js +97 -20
  39. package/core/output/search.js +24 -16
  40. package/core/output/shared.js +22 -1
  41. package/core/output/tracing.js +30 -15
  42. package/core/output-budget.js +295 -0
  43. package/core/output.js +1 -0
  44. package/core/parallel-build.js +44 -11
  45. package/core/parser.js +3 -3
  46. package/core/project.js +384 -187
  47. package/core/public-command.js +47 -0
  48. package/core/registry.js +247 -117
  49. package/core/reporting.js +312 -290
  50. package/core/search.js +317 -185
  51. package/core/semantic-provider.js +110 -0
  52. package/core/stacktrace.js +25 -0
  53. package/core/tracing.js +101 -51
  54. package/core/trust-matrix.js +19 -40
  55. package/core/verify.js +534 -37
  56. package/languages/adapter.js +218 -0
  57. package/languages/c-family.js +2791 -0
  58. package/languages/c.js +3 -0
  59. package/languages/cpp.js +3 -0
  60. package/languages/csharp.js +1402 -0
  61. package/languages/go.js +60 -21
  62. package/languages/html.js +2 -2
  63. package/languages/index.js +85 -7
  64. package/languages/java.js +396 -13
  65. package/languages/javascript.js +199 -19
  66. package/languages/python.js +964 -22
  67. package/languages/rust.js +1317 -152
  68. package/languages/utils.js +40 -3
  69. package/mcp/server.js +254 -636
  70. package/package.json +39 -22
  71. package/eslint.config.js +0 -43
  72. package/jsconfig.json +0 -10
@@ -379,6 +379,62 @@ function extractDecoratorsWithArgs(node) {
379
379
 
380
380
  // --- Single-pass helpers: extracted from find* callbacks ---
381
381
 
382
+ const FUNCTION_SCOPE_NODES = new Set([
383
+ 'function_declaration', 'generator_function_declaration',
384
+ 'function_expression', 'generator_function', 'arrow_function',
385
+ 'method_definition',
386
+ ]);
387
+
388
+ function lexicalOwnerRange(node) {
389
+ for (let parent = node?.parent; parent; parent = parent.parent) {
390
+ if (!FUNCTION_SCOPE_NODES.has(parent.type)) continue;
391
+ const body = parent.childForFieldName('body') || parent;
392
+ return {
393
+ lexicalScopeStartLine: body.startPosition.row + 1,
394
+ lexicalScopeEndLine: body.endPosition.row + 1,
395
+ };
396
+ }
397
+ return {};
398
+ }
399
+
400
+ /**
401
+ * Concrete runtime value returned by a function when every reachable return
402
+ * constructs the same class. Nested functions/classes are separate scopes.
403
+ * This complements (never guesses beyond) a declared interface return type.
404
+ */
405
+ function extractReturnedConcreteType(node) {
406
+ const body = node.childForFieldName('body');
407
+ if (!body) return null;
408
+ const types = [];
409
+ let incomplete = false;
410
+ const stack = [body];
411
+ while (stack.length > 0) {
412
+ const current = stack.pop();
413
+ if (current !== body && FUNCTION_SCOPE_NODES.has(current.type)) continue;
414
+ if (current.type === 'class_declaration' || current.type === 'class') continue;
415
+ if (current.type === 'return_statement') {
416
+ const value = current.namedChild(0);
417
+ if (value?.type !== 'new_expression') {
418
+ incomplete = true;
419
+ continue;
420
+ }
421
+ const constructor = value.childForFieldName('constructor');
422
+ if (!constructor ||
423
+ !['identifier', 'member_expression'].includes(constructor.type)) {
424
+ incomplete = true;
425
+ continue;
426
+ }
427
+ types.push(constructor.text.split('.').pop());
428
+ continue;
429
+ }
430
+ for (let i = current.namedChildCount - 1; i >= 0; i--) {
431
+ stack.push(current.namedChild(i));
432
+ }
433
+ }
434
+ return !incomplete && types.length > 0 && new Set(types).size === 1
435
+ ? types[0] : null;
436
+ }
437
+
382
438
  /**
383
439
  * Process a node for function extraction (single-pass helper)
384
440
  * Returns true if node was matched, false otherwise
@@ -397,6 +453,7 @@ function _processFunction(node, functions, processedRanges, lines) {
397
453
  if (nameNode) {
398
454
  const { startLine, endLine, indent } = nodeToLocation(node, lines);
399
455
  const returnType = extractReturnType(node);
456
+ const returnedConcreteType = extractReturnedConcreteType(node);
400
457
  const generics = extractGenerics(node);
401
458
  const docstring = extractJSDocstring(lines, startLine);
402
459
  const isGen = isGenerator(node);
@@ -421,7 +478,9 @@ function _processFunction(node, functions, processedRanges, lines) {
421
478
  isGenerator: isGen,
422
479
  isAsync,
423
480
  modifiers,
481
+ ...lexicalOwnerRange(node),
424
482
  ...typeAnno,
483
+ ...(returnedConcreteType && { returnedConcreteType }),
425
484
  ...(generics && { generics }),
426
485
  ...(docstring && { docstring })
427
486
  });
@@ -528,7 +587,8 @@ function _processFunction(node, functions, processedRanges, lines) {
528
587
 
529
588
  if (isArrow || isFnExpr) {
530
589
  processedRanges.add(rangeKey);
531
- const paramsNode = valueNode.childForFieldName('parameters');
590
+ const paramsNode = valueNode.childForFieldName('parameters') ||
591
+ valueNode.childForFieldName('parameter');
532
592
  const { startLine, endLine, indent } = nodeToLocation(node, lines);
533
593
  const returnType = extractReturnType(valueNode);
534
594
  const generics = extractGenerics(valueNode);
@@ -674,7 +734,16 @@ function _processFunction(node, functions, processedRanges, lines) {
674
734
  rightNode.type === 'generator_function';
675
735
 
676
736
  if (isArrow || isFnExpr) {
677
- const name = getAssignmentName(leftNode);
737
+ const isCommonJsDefault = leftNode.text === 'module.exports';
738
+ const expressionName = isFnExpr
739
+ ? rightNode.childForFieldName('name')?.text : null;
740
+ // `module.exports = function transformer(){}` exports the
741
+ // function expression, not a symbol called "exports". Keep
742
+ // its authored name when present; anonymous defaults use the
743
+ // same `default` identity exposed by the API surface.
744
+ const name = isCommonJsDefault
745
+ ? (expressionName || 'default')
746
+ : getAssignmentName(leftNode);
678
747
  if (name) {
679
748
  processedRanges.add(rangeKey);
680
749
  const paramsNode = rightNode.childForFieldName('parameters');
@@ -695,7 +764,7 @@ function _processFunction(node, functions, processedRanges, lines) {
695
764
  indent,
696
765
  isArrow,
697
766
  isGenerator: isGen,
698
- modifiers: [],
767
+ modifiers: isCommonJsDefault ? ['export'] : [],
699
768
  // A property-assignment def (Reply.prototype.serialize
700
769
  // = function, exports.h = () => ...) creates NO
701
770
  // lexical name — a bare call in the file can never
@@ -862,18 +931,26 @@ function _processClass(node, classes, processedRanges, lines) {
862
931
  if (nameNode) {
863
932
  const { startLine, endLine } = nodeToLocation(node, lines);
864
933
  const docstring = extractJSDocstring(lines, startLine);
934
+ const valueNode = node.childForFieldName('value');
865
935
  // `type ZodTypeAny = ZodType<any, any, any>;` — the alias IS the
866
936
  // aliased type. Record the base name so receivers annotated with
867
937
  // the alias validate against the base type's methods (fix #208,
868
938
  // TS parity with Rust/Go).
869
- const aliasOf = aliasBaseTypeName(node.childForFieldName('value'));
939
+ const aliasOf = aliasBaseTypeName(valueNode);
940
+ // Object type aliases are structural record declarations, not
941
+ // opaque labels. Index their declared fields just like interface
942
+ // fields so a compiler-typed hop such as
943
+ // `node: Node; node._source.unsubscribe()` can resolve Node's
944
+ // `_source: Signal` contract without name guessing.
945
+ const members = valueNode?.type === 'object_type'
946
+ ? extractTypeMembers(valueNode, lines) : [];
870
947
 
871
948
  classes.push({
872
949
  name: nameNode.text,
873
950
  startLine,
874
951
  endLine,
875
952
  type: 'type',
876
- members: [],
953
+ members,
877
954
  ...(aliasOf && { aliasOf }),
878
955
  ...(docstring && { docstring })
879
956
  });
@@ -1041,10 +1118,13 @@ function extractInterfaceExtends(interfaceNode) {
1041
1118
  * Extract interface members (method signatures, property signatures)
1042
1119
  */
1043
1120
  function extractInterfaceMembers(interfaceNode, code) {
1044
- const members = [];
1045
1121
  const bodyNode = interfaceNode.childForFieldName('body');
1046
- if (!bodyNode) return members;
1122
+ if (!bodyNode) return [];
1123
+ return extractTypeMembers(bodyNode, code);
1124
+ }
1047
1125
 
1126
+ function extractTypeMembers(bodyNode, code) {
1127
+ const members = [];
1048
1128
  for (let i = 0; i < bodyNode.namedChildCount; i++) {
1049
1129
  const child = bodyNode.namedChild(i);
1050
1130
 
@@ -1657,9 +1737,31 @@ function jsConstructorTypeQualifier(ctorNode) {
1657
1737
  return root?.type === 'identifier' ? root.text : undefined;
1658
1738
  }
1659
1739
 
1740
+ // CommonJS permits direct namespace calls without a local alias:
1741
+ // `require('./output').formatContextJson(...)`. Preserve the literal module
1742
+ // specifier on the outer call so the index can apply the same export-ownership
1743
+ // rules as `const output = require('./output'); output.formatContextJson()`.
1744
+ function jsLiteralRequireModule(node) {
1745
+ if (node?.type !== 'call_expression') return undefined;
1746
+ const fn = node.childForFieldName('function');
1747
+ if (fn?.type !== 'identifier' || fn.text !== 'require') return undefined;
1748
+ const args = node.childForFieldName('arguments');
1749
+ if (!args || args.namedChildCount !== 1) return undefined;
1750
+ const first = args.namedChild(0);
1751
+ return first?.type === 'string' ? first.text.slice(1, -1) : undefined;
1752
+ }
1753
+
1660
1754
  function findCallsInCode(code, parser) {
1661
1755
  const tree = parseTree(parser, code);
1662
1756
  const calls = [];
1757
+ const assignedMembers = new Set();
1758
+ traverseTreeCached(tree.rootNode, node => {
1759
+ if (node.type !== 'assignment_expression' &&
1760
+ node.type !== 'augmented_assignment_expression') return true;
1761
+ const left = node.childForFieldName('left');
1762
+ if (left?.type === 'member_expression') assignedMembers.add(left.text);
1763
+ return true;
1764
+ });
1663
1765
  const functionStack = []; // Stack of { name, startLine, endLine }
1664
1766
  // Local aliases with lexical ownership. A flat aliasName→target map leaks
1665
1767
  // block locals into the rest of a module (`let effect = batchedEffect`
@@ -1854,7 +1956,10 @@ function findCallsInCode(code, parser) {
1854
1956
  // Helper to get current enclosing function
1855
1957
  const getCurrentEnclosingFunction = () => {
1856
1958
  return functionStack.length > 0
1857
- ? { ...functionStack[functionStack.length - 1] }
1959
+ ? {
1960
+ ...functionStack[functionStack.length - 1],
1961
+ scopeChain: functionStack.map(scope => scope.startLine),
1962
+ }
1858
1963
  : null;
1859
1964
  };
1860
1965
 
@@ -2219,6 +2324,8 @@ function findCallsInCode(code, parser) {
2219
2324
  ...(resolvedName && { resolvedName }),
2220
2325
  ...(resolvedNames && { resolvedNames }),
2221
2326
  line: node.startPosition.row + 1,
2327
+ callStart: node.startIndex,
2328
+ callEnd: node.endIndex,
2222
2329
  isMethod: false,
2223
2330
  ...(assignedTo && { assignedTo }),
2224
2331
  enclosingFunction,
@@ -2235,6 +2342,8 @@ function findCallsInCode(code, parser) {
2235
2342
  calls.push({
2236
2343
  name: 'constructor',
2237
2344
  line: node.startPosition.row + 1,
2345
+ callStart: node.startIndex,
2346
+ callEnd: node.endIndex,
2238
2347
  isMethod: true,
2239
2348
  receiver: 'super',
2240
2349
  argCount: node.childForFieldName('arguments')?.namedChildCount ?? 0,
@@ -2270,10 +2379,17 @@ function findCallsInCode(code, parser) {
2270
2379
  const innerProp = objNode.childForFieldName('property');
2271
2380
  const innerObj = objNode.childForFieldName('object');
2272
2381
  if (innerProp) {
2273
- const boundReceiver = innerObj?.type === 'identifier'
2274
- ? innerObj.text : innerObj?.text;
2275
- const boundReceiverType = innerObj?.type === 'identifier'
2276
- ? localVarTypes.get(innerObj.text) : undefined;
2382
+ const prototypeOwner = innerObj?.type === 'member_expression' &&
2383
+ innerObj.childForFieldName('property')?.text === 'prototype' &&
2384
+ innerObj.childForFieldName('object')?.type === 'identifier'
2385
+ ? innerObj.childForFieldName('object').text
2386
+ : undefined;
2387
+ const boundReceiver = prototypeOwner ||
2388
+ (innerObj?.type === 'identifier'
2389
+ ? innerObj.text : innerObj?.text);
2390
+ const boundReceiverType = prototypeOwner ||
2391
+ (innerObj?.type === 'identifier'
2392
+ ? localVarTypes.get(innerObj.text) : undefined);
2277
2393
  calls.push({
2278
2394
  name: innerProp.text,
2279
2395
  line: node.startPosition.row + 1,
@@ -2310,6 +2426,7 @@ function findCallsInCode(code, parser) {
2310
2426
  // resolve their root type query-side (the enclosing
2311
2427
  // class); identifier roots type from local annotations.
2312
2428
  let receiverRoot, receiverFieldName, receiverRootType, receiverBindingNode;
2429
+ let receiverDeepPath = false;
2313
2430
  if (receiver && objNode?.type === 'identifier') receiverBindingNode = objNode;
2314
2431
  if (!receiver && objNode && objNode.type === 'member_expression') {
2315
2432
  const rootNode = objNode.childForFieldName('object');
@@ -2322,6 +2439,12 @@ function findCallsInCode(code, parser) {
2322
2439
  if (rootNode.type === 'identifier') {
2323
2440
  receiverRootType = localVarTypes.get(rootNode.text);
2324
2441
  }
2442
+ } else {
2443
+ // Preserve unresolved deeper member chains
2444
+ // (`client.req.query()`). Their terminal name
2445
+ // must not borrow a same-file method binding
2446
+ // while the root object's type is unknown.
2447
+ receiverDeepPath = true;
2325
2448
  }
2326
2449
  }
2327
2450
  // Chained receiver (fix #219): the receiver IS a call —
@@ -2329,6 +2452,7 @@ function findCallsInCode(code, parser) {
2329
2452
  // findCallers can type the receiver from its declared
2330
2453
  // return annotation (Promise<...> → Promise).
2331
2454
  let receiverCall, receiverCallIsMethod, receiverCallAwaited, receiverCallLine;
2455
+ let receiverCallStart, receiverCallEnd;
2332
2456
  {
2333
2457
  let recvNode = objNode;
2334
2458
  if (recvNode && recvNode.type === 'parenthesized_expression') {
@@ -2345,6 +2469,8 @@ function findCallsInCode(code, parser) {
2345
2469
  // Producer link (fix #258): plain-call
2346
2470
  // records carry the call node's start line
2347
2471
  receiverCallLine = recvNode.startPosition.row + 1;
2472
+ receiverCallStart = recvNode.startIndex;
2473
+ receiverCallEnd = recvNode.endIndex;
2348
2474
  } else if (prodFunc?.type === 'member_expression') {
2349
2475
  const prodProp = prodFunc.childForFieldName('property');
2350
2476
  if (prodProp) {
@@ -2353,6 +2479,8 @@ function findCallsInCode(code, parser) {
2353
2479
  // Method records report the property
2354
2480
  // node's own line
2355
2481
  receiverCallLine = prodProp.startPosition.row + 1;
2482
+ receiverCallStart = recvNode.startIndex;
2483
+ receiverCallEnd = recvNode.endIndex;
2356
2484
  }
2357
2485
  }
2358
2486
  }
@@ -2375,8 +2503,10 @@ function findCallsInCode(code, parser) {
2375
2503
  (objNode ? JS_LITERAL_RECEIVER_TYPES[objNode.type] : undefined));
2376
2504
  // Module receiver (ns.helper()) — unless locally shadowed
2377
2505
  // by a typed instance binding
2378
- const receiverIsModule = !!receiver && moduleAliases.has(receiver) &&
2379
- !localVarTypes.has(receiver);
2506
+ const receiverModuleSpecifier = jsLiteralRequireModule(objNode);
2507
+ const receiverIsModule = !!receiverModuleSpecifier ||
2508
+ (!!receiver && moduleAliases.has(receiver) &&
2509
+ !localVarTypes.has(receiver));
2380
2510
  const firstArg = getFirstStringArg(node);
2381
2511
  const argCount = getArgCount(node);
2382
2512
  const assignedTo = jsAssignmentTargetOf(node);
@@ -2387,6 +2517,8 @@ function findCallsInCode(code, parser) {
2387
2517
  // line — the account's ground set is keyed by the
2388
2518
  // name's line
2389
2519
  line: propNode.startPosition.row + 1,
2520
+ callStart: node.startIndex,
2521
+ callEnd: node.endIndex,
2390
2522
  isMethod: true,
2391
2523
  receiver,
2392
2524
  ...(receiverType && { receiverType }),
@@ -2396,15 +2528,22 @@ function findCallsInCode(code, parser) {
2396
2528
  localVarTypeQualifiers.get(receiver),
2397
2529
  }),
2398
2530
  ...(receiverIsModule && { receiverIsModule: true }),
2531
+ ...(receiverModuleSpecifier && { receiverModuleSpecifier }),
2532
+ ...(receiver && assignedMembers.has(`${receiver}.${propName}`) && {
2533
+ receiverMemberAssigned: true,
2534
+ }),
2399
2535
  ...(receiverBindingNode &&
2400
2536
  isShadowedByLocal(receiverBindingNode, receiverBindingNode.text) &&
2401
2537
  { receiverLocalBinding: true }),
2402
2538
  ...(receiverFieldName && { receiverRoot, receiverField: receiverFieldName }),
2403
2539
  ...(receiverFieldName && receiverRootType && { receiverRootType }),
2540
+ ...(receiverDeepPath && { receiverDeepPath: true }),
2404
2541
  ...(receiverCall && { receiverCall }),
2405
2542
  ...(receiverCallIsMethod && { receiverCallIsMethod: true }),
2406
2543
  ...(receiverCallAwaited && { receiverCallAwaited: true }),
2407
2544
  ...(receiverCallLine && { receiverCallLine }),
2545
+ ...(receiverCallStart != null && { receiverCallStart }),
2546
+ ...(receiverCallEnd != null && { receiverCallEnd }),
2408
2547
  ...(assignedTo && { assignedTo }),
2409
2548
  enclosingFunction,
2410
2549
  uncertain,
@@ -3212,10 +3351,16 @@ function findExportsInCode(code, parser) {
3212
3351
  for (let i = 0; i < rightNode.namedChildCount; i++) {
3213
3352
  const prop = rightNode.namedChild(i);
3214
3353
  if (prop.type === 'shorthand_property_identifier') {
3215
- exports.push({ name: prop.text, type: 'module.exports', line });
3354
+ exports.push({ name: prop.text, localName: prop.text, type: 'module.exports', line });
3216
3355
  } else if (prop.type === 'pair') {
3217
3356
  const key = prop.childForFieldName('key');
3218
- if (key) exports.push({ name: key.text, type: 'module.exports', line });
3357
+ const value = prop.childForFieldName('value');
3358
+ if (key) exports.push({
3359
+ name: key.text,
3360
+ ...(value?.type === 'identifier' && { localName: value.text }),
3361
+ type: 'module.exports',
3362
+ line,
3363
+ });
3219
3364
  } else if (prop.type === 'method_definition') {
3220
3365
  // Shorthand methods are exports too
3221
3366
  // (fix #252 — `module.exports =
@@ -3223,12 +3368,41 @@ function findExportsInCode(code, parser) {
3223
3368
  // export list, so deadcode audited a
3224
3369
  // require()-reachable function).
3225
3370
  const mName = prop.childForFieldName('name');
3226
- if (mName) exports.push({ name: mName.text, type: 'module.exports', line });
3371
+ if (mName) exports.push({ name: mName.text, localName: mName.text, type: 'module.exports', line });
3372
+ } else if (prop.type === 'spread_element') {
3373
+ // CommonJS barrel: `module.exports = {
3374
+ // ...require('./public') }`. This is the
3375
+ // CJS equivalent of `export * from` and
3376
+ // must retain its SOURCE so namespace
3377
+ // calls through the barrel can establish
3378
+ // name ownership. A dynamic spread stays
3379
+ // an explicitly unmodelable CJS surface.
3380
+ const value = prop.namedChild(0);
3381
+ const fn = value?.type === 'call_expression'
3382
+ ? value.childForFieldName('function') : null;
3383
+ const args = value?.type === 'call_expression'
3384
+ ? value.childForFieldName('arguments') : null;
3385
+ const first = args?.namedChild(0);
3386
+ if (fn?.type === 'identifier' && fn.text === 'require' &&
3387
+ first?.type === 'string') {
3388
+ exports.push({
3389
+ name: '*',
3390
+ type: 're-export-all',
3391
+ line,
3392
+ source: first.text.slice(1, -1),
3393
+ });
3394
+ } else {
3395
+ exports.push({
3396
+ name: '*',
3397
+ type: 'module.exports',
3398
+ line,
3399
+ });
3400
+ }
3227
3401
  }
3228
3402
  }
3229
3403
  } else if (rightNode && rightNode.type === 'identifier') {
3230
3404
  // module.exports = something
3231
- exports.push({ name: rightNode.text, type: 'module.exports', line });
3405
+ exports.push({ name: rightNode.text, localName: rightNode.text, type: 'module.exports', line });
3232
3406
  } else {
3233
3407
  exports.push({ name: 'default', type: 'module.exports', line });
3234
3408
  }
@@ -3238,7 +3412,13 @@ function findExportsInCode(code, parser) {
3238
3412
  // exports.name = ...
3239
3413
  if (objNode.text === 'exports') {
3240
3414
  const line = node.startPosition.row + 1;
3241
- exports.push({ name: propNode.text, type: 'exports', line });
3415
+ const rightNode = node.childForFieldName('right');
3416
+ exports.push({
3417
+ name: propNode.text,
3418
+ ...(rightNode?.type === 'identifier' && { localName: rightNode.text }),
3419
+ type: 'exports',
3420
+ line,
3421
+ });
3242
3422
  return true;
3243
3423
  }
3244
3424