ucn 5.1.1 → 5.2.0

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.
@@ -313,6 +313,7 @@ const allSourceTreeBySelected = new WeakMap();
313
313
  // name merely to rediscover the same handful of macro definitions. Trees are
314
314
  // immutable, so retain the exact AST node set without retaining dead trees.
315
315
  const macroDefinitionsByTree = new WeakMap();
316
+ const macroParamEffectsByDefinition = new WeakMap();
316
317
 
317
318
  function macroDefinitionNodes(tree) {
318
319
  const cached = macroDefinitionsByTree.get(tree);
@@ -964,6 +965,24 @@ function isTemplateDependentCallable(node) {
964
965
  return false;
965
966
  }
966
967
 
968
+ // `template <> bool f<bool>(...)` is a FULL specialization: the same compiler
969
+ // symbol as its primary template, selected by substitution rather than
970
+ // overload resolution (fix #299). Every enclosing template head must be
971
+ // empty — one non-empty parameter list means a member of a class template
972
+ // (partial-specialization territory for classes; ordinary dependence here).
973
+ function isFullSpecializationCallable(node) {
974
+ let sawTemplateHead = false;
975
+ for (let parent = node?.parent; parent; parent = parent.parent) {
976
+ if (parent.type === 'template_declaration') {
977
+ const params = parent.childForFieldName('parameters');
978
+ if (!params || params.namedChildCount > 0) return false;
979
+ sawTemplateHead = true;
980
+ }
981
+ if (parent.type === 'translation_unit') break;
982
+ }
983
+ return sawTemplateHead;
984
+ }
985
+
967
986
  // Full type text for a NAMED parameter: the parameter's own text with the
968
987
  // name removed — `const char *s` → `const char *`, `int **pp` → `int **`,
969
988
  // `void (*cb)(int)` → `void (*)(int)`. Reading around the identifier keeps
@@ -1217,6 +1236,9 @@ function memberFromNode(node, className, access, lines, mode) {
1217
1236
  ...(mode === 'cpp' && isTemplateDependentCallable(node) && {
1218
1237
  templateDependent: true,
1219
1238
  }),
1239
+ ...(mode === 'cpp' && isFullSpecializationCallable(node) && {
1240
+ isSpecialization: true,
1241
+ }),
1220
1242
  ...(mode === 'cpp' && cLanguageLinkage(node) && {
1221
1243
  linkage: cLanguageLinkage(node),
1222
1244
  }),
@@ -1516,6 +1538,9 @@ function findFunctionsInTree(code, tree, mode, sourceLines = null) {
1516
1538
  ...(mode === 'cpp' && isTemplateDependentCallable(node) && {
1517
1539
  templateDependent: true,
1518
1540
  }),
1541
+ ...(mode === 'cpp' && isFullSpecializationCallable(node) && {
1542
+ isSpecialization: true,
1543
+ }),
1519
1544
  ...(returnedConcreteType && { returnedConcreteType }),
1520
1545
  ...(mode === 'cpp' && cLanguageLinkage(node) && {
1521
1546
  linkage: cLanguageLinkage(node),
@@ -1658,7 +1683,112 @@ function findStateObjects(code, parser) {
1658
1683
  item => `${item.name}:${item.startLine}`) : primary;
1659
1684
  }
1660
1685
 
1661
- function findMacrosInTree(tree, lines) {
1686
+ function collectMacroParameterEffects(nestedTree, parameters) {
1687
+ const effects = [];
1688
+ traverseTree(nestedTree.rootNode, node => {
1689
+ // Token-paste after a global qualifier (`::_##call`) is not C++
1690
+ // until preprocessing. tree-sitter preserves the `#call` token as
1691
+ // a preprocessor node next to an AST global qualified identifier;
1692
+ // that exact shape still proves the substituted callable is
1693
+ // globally qualified.
1694
+ if (node.type === 'preproc_directive' &&
1695
+ node.text.startsWith('#')) {
1696
+ const paramIndex = parameters.indexOf(node.text.slice(1));
1697
+ let globalSibling = false;
1698
+ let container = node.parent;
1699
+ for (let hops = 0; container && hops < 3 && !globalSibling;
1700
+ hops++, container = container.parent) {
1701
+ const stack = [...(container.namedChildren || [])];
1702
+ while (stack.length > 0 && !globalSibling) {
1703
+ const sibling = stack.pop();
1704
+ if (sibling === node) continue;
1705
+ if (sibling.type === 'qualified_identifier' &&
1706
+ sibling.children?.[0]?.type === '::') {
1707
+ globalSibling = true;
1708
+ break;
1709
+ }
1710
+ for (const child of sibling.namedChildren || []) {
1711
+ stack.push(child);
1712
+ }
1713
+ }
1714
+ }
1715
+ if (paramIndex >= 0 && globalSibling) {
1716
+ effects.push({
1717
+ paramIndex,
1718
+ kind: 'qualified',
1719
+ qualifier: 'global',
1720
+ });
1721
+ }
1722
+ return true;
1723
+ }
1724
+ if (node.type !== 'identifier') return true;
1725
+ const paramIndex = parameters.indexOf(node.text);
1726
+ if (paramIndex < 0) return true;
1727
+ const parent = node.parent;
1728
+ if (parent?.type === 'qualified_identifier' &&
1729
+ parent.childForFieldName('name') === node) {
1730
+ const scope = parent.childForFieldName('scope');
1731
+ effects.push({
1732
+ paramIndex,
1733
+ kind: 'qualified',
1734
+ qualifier: scope?.text || 'global',
1735
+ });
1736
+ return true;
1737
+ }
1738
+ if (parent?.type === 'argument_list' &&
1739
+ parent.parent?.type === 'call_expression') {
1740
+ const wrapper = callIdentity(
1741
+ parent.parent.childForFieldName('function'));
1742
+ if (!wrapper.name || !isMacroToken(wrapper.name)) return true;
1743
+ const args = (parent.namedChildren || [])
1744
+ .filter(child => !child.type.endsWith('comment'));
1745
+ const argIndex = args.findIndex(argument =>
1746
+ argument.startIndex <= node.startIndex &&
1747
+ node.endIndex <= argument.endIndex);
1748
+ if (argIndex >= 0) {
1749
+ effects.push({
1750
+ paramIndex,
1751
+ kind: 'forwarded',
1752
+ macro: wrapper.name,
1753
+ argIndex,
1754
+ });
1755
+ }
1756
+ }
1757
+ return true;
1758
+ });
1759
+ return effects;
1760
+ }
1761
+
1762
+ function macroParameterEffects(definitionNode, valueNode, paramsNode, parser) {
1763
+ if (definitionNode && macroParamEffectsByDefinition.has(definitionNode)) {
1764
+ return macroParamEffectsByDefinition.get(definitionNode);
1765
+ }
1766
+ if (!valueNode || !paramsNode || !parser) return [];
1767
+ const parameters = (paramsNode.namedChildren || [])
1768
+ .filter(child => child.type === 'identifier')
1769
+ .map(child => child.text);
1770
+ if (parameters.length === 0) return [];
1771
+ const body = valueNode.text.replace(/\\(?=\r?\n)/g, ' ');
1772
+ // Cheap parse-avoidance only; every semantic decision below comes from
1773
+ // the replacement list's tree. Ordinary value/punctuation macros need no
1774
+ // extra native tree.
1775
+ if (!body.includes('::') &&
1776
+ !/[A-Z_][A-Z0-9_]*\s*\(/.test(body)) return [];
1777
+ const prefix = 'void __ucn_macro_effect__(void) {\n';
1778
+ const synthetic = `${prefix}${body}\n;}`;
1779
+ const nestedTree = safeParse(parser, synthetic, undefined, PARSE_OPTIONS);
1780
+ try {
1781
+ const effects = collectMacroParameterEffects(nestedTree, parameters);
1782
+ if (definitionNode) {
1783
+ macroParamEffectsByDefinition.set(definitionNode, effects);
1784
+ }
1785
+ return effects;
1786
+ } finally {
1787
+ nestedTree.delete?.();
1788
+ }
1789
+ }
1790
+
1791
+ function findMacrosInTree(tree, lines, parser) {
1662
1792
  const macros = [];
1663
1793
  for (const node of macroDefinitionNodes(tree)) {
1664
1794
  const nameNode = node.childForFieldName('name') ||
@@ -1684,6 +1814,17 @@ function findMacrosInTree(tree, lines) {
1684
1814
  : undefined,
1685
1815
  modifiers: [],
1686
1816
  functionLike: node.type === 'preproc_function_def',
1817
+ ...(() => {
1818
+ const effects = macroParameterEffects(
1819
+ node,
1820
+ node.childForFieldName('value') ||
1821
+ (node.namedChildren || []).find(child =>
1822
+ child.type === 'preproc_arg'),
1823
+ paramsNode,
1824
+ parser);
1825
+ return effects.length > 0
1826
+ ? { macroParamEffects: effects } : {};
1827
+ })(),
1687
1828
  docstring: extractJSDocstring(lines, startLine),
1688
1829
  });
1689
1830
  }
@@ -1693,10 +1834,10 @@ function findMacrosInTree(tree, lines) {
1693
1834
  function findMacros(code, parser) {
1694
1835
  const tree = parseTree(parser, code);
1695
1836
  const lines = code.split('\n');
1696
- const primary = findMacrosInTree(tree, lines);
1837
+ const primary = findMacrosInTree(tree, lines, parser);
1697
1838
  const literal = literalRecoveryTree(parser, code, tree);
1698
1839
  return literal ? mergeExtracted(primary,
1699
- findMacrosInTree(literal, lines),
1840
+ findMacrosInTree(literal, lines, parser),
1700
1841
  item => `${item.name}:${item.startLine}:${item.functionLike ? 1 : 0}`) : primary;
1701
1842
  }
1702
1843
 
@@ -1789,6 +1930,23 @@ function buildVariableTypes(tree) {
1789
1930
  // block declaration.
1790
1931
  const memberReceiverUses = [];
1791
1932
  const ambiguousDirectInitializers = [];
1933
+ const declaratorStaticType = (type, declarator) => {
1934
+ // The declaration's type node carries only the base type. Preserve
1935
+ // array rank from the AST declarator for overload arguments:
1936
+ // `wchar_t format_str[]; runtime(format_str)` passes a wide-character
1937
+ // array (and decays to wchar_t*), not a scalar wchar_t. Receiver
1938
+ // typing continues to use the base `type`; only static argument shape
1939
+ // consumes this fuller spelling.
1940
+ let arrays = 0;
1941
+ const stack = [declarator];
1942
+ while (stack.length > 0) {
1943
+ const current = stack.pop();
1944
+ if (!current) continue;
1945
+ if (current.type === 'array_declarator') arrays++;
1946
+ for (const child of current.namedChildren || []) stack.push(child);
1947
+ }
1948
+ return arrays > 0 ? `${type}${'[]'.repeat(arrays)}` : type;
1949
+ };
1792
1950
  const addBindings = (node, type, pointeeType, scope, declarators) => {
1793
1951
  for (const declarator of declarators) {
1794
1952
  const identity = declaratorIdentity(declarator);
@@ -1796,6 +1954,7 @@ function buildVariableTypes(tree) {
1796
1954
  bindings.push({
1797
1955
  name: identity.name,
1798
1956
  type,
1957
+ staticType: declaratorStaticType(type, declarator),
1799
1958
  ...(pointeeType && { pointeeType }),
1800
1959
  declaredAt: node.type === 'parameter_declaration'
1801
1960
  ? scope.startIndex : declarator.startIndex,
@@ -1893,6 +2052,7 @@ function buildVariableTypes(tree) {
1893
2052
  };
1894
2053
  return {
1895
2054
  get: (name, atNode) => resolveBinding(name, atNode)?.type,
2055
+ getStatic: (name, atNode) => resolveBinding(name, atNode)?.staticType,
1896
2056
  getPointee: (name, atNode) =>
1897
2057
  resolveBinding(name, atNode)?.pointeeType,
1898
2058
  has: (name, atNode) => resolveBinding(name, atNode) !== undefined,
@@ -1963,7 +2123,8 @@ function staticArgumentKind(node, variableTypes) {
1963
2123
  if (node.type === 'true' || node.type === 'false') return 'bool';
1964
2124
  if (node.type === 'null' || node.type === 'nullptr') return 'null';
1965
2125
  if (node.type === 'identifier') {
1966
- const type = variableTypes?.get(node.text, node);
2126
+ const type = variableTypes?.getStatic(node.text, node) ||
2127
+ variableTypes?.get(node.text, node);
1967
2128
  return type ? `type:${type}` : 'expr';
1968
2129
  }
1969
2130
  if (node.type === 'compound_literal_expression') {
@@ -1998,6 +2159,18 @@ function staticArgumentKind(node, variableTypes) {
1998
2159
  const type = typeName(typeNode);
1999
2160
  if (type) return `type:${type}`;
2000
2161
  }
2162
+ // Bare-identifier producers are name-resolvable (fix #299B): mark
2163
+ // them `bcall:` so the overload discipline can type the argument
2164
+ // from the producer's declared return type. A local callable
2165
+ // variable shadows the project name — those record plain 'expr'
2166
+ // (the #203 localShadow rule at kind-recording time). Member,
2167
+ // qualified, and template-explicit producers keep `call:NAME`.
2168
+ if (identity.name && !identity.isMethod && !identity.isPathCall &&
2169
+ identity.nameNode && IDENTIFIER_NODES.has(identity.nameNode.type) &&
2170
+ fnNode && IDENTIFIER_NODES.has(fnNode.type)) {
2171
+ if (variableTypes?.get(identity.name, node)) return 'expr';
2172
+ return `bcall:${identity.name}`;
2173
+ }
2001
2174
  return identity.name ? `call:${identity.name}` : 'expr';
2002
2175
  }
2003
2176
  return 'expr';
@@ -2202,6 +2375,46 @@ function fieldReceiverPath(node) {
2202
2375
  return { root: base.root, fields: [...base.fields, field.text] };
2203
2376
  }
2204
2377
 
2378
+ /** Macro invocations whose argument expression contains this call node. */
2379
+ function enclosingMacroArguments(node) {
2380
+ const wrappers = [];
2381
+ let current = node;
2382
+ let hops = 0;
2383
+ while (current?.parent && hops++ < 24) {
2384
+ const argumentsNode = current.parent;
2385
+ const outerCall = argumentsNode?.type === 'argument_list'
2386
+ ? argumentsNode.parent : null;
2387
+ if (outerCall?.type === 'call_expression') {
2388
+ const wrapper = callIdentity(
2389
+ outerCall.childForFieldName('function'));
2390
+ if (wrapper.name && isMacroToken(wrapper.name)) {
2391
+ const args = (argumentsNode.namedChildren || [])
2392
+ .filter(child => !child.type.endsWith('comment'));
2393
+ const argIndex = args.findIndex(argument =>
2394
+ argument.startIndex <= node.startIndex &&
2395
+ node.endIndex <= argument.endIndex);
2396
+ if (argIndex >= 0) {
2397
+ wrappers.push({ name: wrapper.name, argIndex });
2398
+ }
2399
+ }
2400
+ current = outerCall;
2401
+ continue;
2402
+ }
2403
+ // Once the walk leaves an expression, no outer macro invocation can
2404
+ // contain this call. Stopping here keeps the common non-macro path
2405
+ // constant-depth instead of climbing every call to the translation
2406
+ // unit (material on template-heavy fmt headers).
2407
+ if (/(?:statement|declaration|definition)$/.test(
2408
+ argumentsNode.type) ||
2409
+ argumentsNode.type === 'translation_unit' ||
2410
+ argumentsNode.type === 'init_declarator') {
2411
+ break;
2412
+ }
2413
+ current = current.parent;
2414
+ }
2415
+ return wrappers;
2416
+ }
2417
+
2205
2418
  function findCallsInTree(code, parser, _options = {}, existingTree = null,
2206
2419
  includeMacroBodies = true) {
2207
2420
  const tree = existingTree || parseTree(parser, code);
@@ -2281,6 +2494,7 @@ function findCallsInTree(code, parser, _options = {}, existingTree = null,
2281
2494
  : undefined;
2282
2495
  const assignedTo = assignmentTargetOf(node);
2283
2496
  const compileTimeOnly = compileTimeOnlyContext(node);
2497
+ const macroArguments = enclosingMacroArguments(node);
2284
2498
  calls.push({
2285
2499
  name: identity.name,
2286
2500
  line: identity.nameNode?.startPosition.row + 1 || node.startPosition.row + 1,
@@ -2295,6 +2509,7 @@ function findCallsInTree(code, parser, _options = {}, existingTree = null,
2295
2509
  explicitTemplateCall: true,
2296
2510
  }),
2297
2511
  ...(compileTimeOnly && { compileTimeOnly }),
2512
+ ...(macroArguments.length > 0 && { macroArguments }),
2298
2513
  ...(directReceiverType && { receiverType: directReceiverType }),
2299
2514
  ...(receiverCall && {
2300
2515
  receiverCall,
@@ -2474,9 +2689,10 @@ function findMacroBodyCalls(tree, code, parser, onlyName = null) {
2474
2689
  if (onlyName && !valueNode.text.includes(onlyName)) continue;
2475
2690
  const paramsNode = node.childForFieldName('parameters') ||
2476
2691
  (node.namedChildren || []).find(child => child.type === 'preproc_params');
2477
- const parameters = new Set((paramsNode?.namedChildren || [])
2692
+ const parameterNames = (paramsNode?.namedChildren || [])
2478
2693
  .filter(child => child.type === 'identifier')
2479
- .map(child => child.text));
2694
+ .map(child => child.text);
2695
+ const parameters = new Set(parameterNames);
2480
2696
  // Replace only the continuation backslash. Keeping the newline and
2481
2697
  // every other byte makes source-line/column and span remapping exact.
2482
2698
  const body = valueNode.text.replace(/\\(?=\r?\n)/g, ' ');
@@ -2490,6 +2706,12 @@ function findMacroBodyCalls(tree, code, parser, onlyName = null) {
2490
2706
  const nestedTree = safeParse(parser, synthetic, undefined, PARSE_OPTIONS);
2491
2707
  let nested;
2492
2708
  try {
2709
+ macroParamEffectsByDefinition.set(
2710
+ node,
2711
+ body.includes('::') ||
2712
+ /[A-Z_][A-Z0-9_]*\s*\(/.test(body)
2713
+ ? collectMacroParameterEffects(nestedTree, parameterNames)
2714
+ : []);
2493
2715
  nested = findCallsInCode(synthetic, parser, {}, nestedTree, false);
2494
2716
  } finally {
2495
2717
  nestedTree.delete?.();
@@ -2725,10 +2947,10 @@ function parse(code, parser, mode, options = {}) {
2725
2947
  item => `${item.name}:${item.startLine}`)
2726
2948
  : findStateObjectsInTree(tree, lines),
2727
2949
  macros: literal
2728
- ? mergeExtracted(findMacrosInTree(tree, lines),
2729
- findMacrosInTree(literal, lines),
2950
+ ? mergeExtracted(findMacrosInTree(tree, lines, parser),
2951
+ findMacrosInTree(literal, lines, parser),
2730
2952
  item => `${item.name}:${item.startLine}:${item.functionLike ? 1 : 0}`)
2731
- : findMacrosInTree(tree, lines),
2953
+ : findMacrosInTree(tree, lines, parser),
2732
2954
  imports,
2733
2955
  exports: [
2734
2956
  ...functions
@@ -1155,10 +1155,21 @@ function findCallsInCode(code, parser) {
1155
1155
  : functionNode?.type === 'conditional_access_expression'
1156
1156
  ? functionNode.childForFieldName('condition') || functionNode.namedChild(0)
1157
1157
  : null;
1158
- const receiverTypeInfo = receiverTypeFromNode(receiverNode, variableTypes) ||
1159
- normalizeReceiverType(receiverRoot && variableTypes.get(receiverRoot));
1160
- const receiverType = receiverTypeInfo?.name;
1161
1158
  const unwrappedReceiverNode = unwrapReceiverNode(receiverNode);
1159
+ // A root variable's type is the receiver type only for a direct
1160
+ // `value.Method()` call. For `value.Property.Method()` the static
1161
+ // receiver type is the property's declared type, not `value`'s
1162
+ // type. Preserve that shape as a field path for query-time
1163
+ // declaration walking; collapsing it to the root class falsely
1164
+ // confirms sibling overrides (Newtonsoft JProperty.Value is a
1165
+ // JToken, not a JProperty).
1166
+ const receiverTypeInfo = receiverTypeFromNode(
1167
+ receiverNode, variableTypes) ||
1168
+ (unwrappedReceiverNode?.type === 'identifier'
1169
+ ? normalizeReceiverType(
1170
+ receiverRoot && variableTypes.get(receiverRoot))
1171
+ : null);
1172
+ const receiverType = receiverTypeInfo?.name;
1162
1173
  const receiverCastThis = receiverCastIsThis(receiverNode);
1163
1174
  const receiverIsTypeQualified = !!(identity.isMethod &&
1164
1175
  unwrappedReceiverNode?.type === 'identifier' &&