ucn 5.0.6 → 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.
- package/.claude/skills/ucn/SKILL.md +12 -5
- package/.claude/skills/ucn/references/commands.md +2 -2
- package/.claude/skills/ucn/references/trust-contract.md +3 -2
- package/README.md +31 -14
- package/core/analysis.js +66 -1
- package/core/bridge.js +2 -1
- package/core/cache.js +64 -8
- package/core/callers.js +1722 -90
- package/core/graph-build.js +6 -0
- package/core/index-ir.js +16 -5
- package/core/ir.js +50 -3
- package/core/output/analysis.js +25 -0
- package/core/output/refactoring.js +1 -1
- package/core/output/shared.js +2 -2
- package/core/project.js +32 -0
- package/core/search.js +9 -0
- package/core/verify.js +1084 -36
- package/languages/c-family.js +231 -9
- package/languages/csharp.js +14 -3
- package/languages/go.js +473 -71
- package/languages/javascript.js +288 -21
- package/languages/python.js +443 -97
- package/languages/rust.js +87 -4
- package/languages/utils.js +11 -0
- package/mcp/server.js +100 -104
- package/mcp/stdio-server.js +296 -0
- package/package.json +10 -8
package/languages/javascript.js
CHANGED
|
@@ -435,6 +435,34 @@ function extractReturnedConcreteType(node) {
|
|
|
435
435
|
? types[0] : null;
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
+
/**
|
|
439
|
+
* Whether every explicit value-producing return yields the current receiver.
|
|
440
|
+
* A fallthrough/empty return cannot feed a subsequent chained call, so it does
|
|
441
|
+
* not compete with `this`; any other returned value makes the result unknown.
|
|
442
|
+
*/
|
|
443
|
+
function returnsReceiverSelf(node) {
|
|
444
|
+
const body = node.childForFieldName('body');
|
|
445
|
+
if (!body) return false;
|
|
446
|
+
let sawSelf = false;
|
|
447
|
+
const stack = [body];
|
|
448
|
+
while (stack.length > 0) {
|
|
449
|
+
const current = stack.pop();
|
|
450
|
+
if (current !== body && FUNCTION_SCOPE_NODES.has(current.type)) continue;
|
|
451
|
+
if (current.type === 'class_declaration' || current.type === 'class') continue;
|
|
452
|
+
if (current.type === 'return_statement') {
|
|
453
|
+
const value = current.namedChild(0);
|
|
454
|
+
if (!value) continue;
|
|
455
|
+
if (value.type !== 'this') return false;
|
|
456
|
+
sawSelf = true;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
for (let i = current.namedChildCount - 1; i >= 0; i--) {
|
|
460
|
+
stack.push(current.namedChild(i));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return sawSelf;
|
|
464
|
+
}
|
|
465
|
+
|
|
438
466
|
/**
|
|
439
467
|
* Process a node for function extraction (single-pass helper)
|
|
440
468
|
* Returns true if node was matched, false otherwise
|
|
@@ -618,6 +646,7 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
618
646
|
isGenerator: isGen,
|
|
619
647
|
isAsync,
|
|
620
648
|
modifiers,
|
|
649
|
+
...lexicalOwnerRange(node),
|
|
621
650
|
...typeAnno,
|
|
622
651
|
...(generics && { generics }),
|
|
623
652
|
...(docstring && { docstring })
|
|
@@ -699,8 +728,14 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
699
728
|
if (processedRanges.has(rangeKey)) return false;
|
|
700
729
|
|
|
701
730
|
const leftNode = node.childForFieldName('left');
|
|
702
|
-
const
|
|
703
|
-
leftNode.
|
|
731
|
+
const assignedObject = leftNode?.type === 'member_expression'
|
|
732
|
+
? leftNode.childForFieldName('object') : null;
|
|
733
|
+
const prototypeBase = assignedObject?.type === 'member_expression' &&
|
|
734
|
+
assignedObject.childForFieldName('property')?.text === 'prototype'
|
|
735
|
+
? assignedObject.childForFieldName('object') : null;
|
|
736
|
+
const prototypeOwner = prototypeBase?.type === 'identifier'
|
|
737
|
+
? prototypeBase.text : null;
|
|
738
|
+
const isPrototypeAssignment = !!prototypeOwner;
|
|
704
739
|
|
|
705
740
|
// For non-prototype assignments, check if nested
|
|
706
741
|
if (!isPrototypeAssignment) {
|
|
@@ -748,7 +783,8 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
748
783
|
processedRanges.add(rangeKey);
|
|
749
784
|
const paramsNode = rightNode.childForFieldName('parameters');
|
|
750
785
|
const { startLine, endLine, indent } = nodeToLocation(node, lines);
|
|
751
|
-
const returnType = extractReturnType(rightNode)
|
|
786
|
+
const returnType = extractReturnType(rightNode) ||
|
|
787
|
+
(prototypeOwner && returnsReceiverSelf(rightNode) ? 'this' : null);
|
|
752
788
|
const generics = extractGenerics(rightNode);
|
|
753
789
|
const docstring = extractJSDocstring(lines, startLine);
|
|
754
790
|
const isGen = isGenerator(rightNode);
|
|
@@ -774,9 +810,14 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
774
810
|
// assignments carry their class so typed-receiver
|
|
775
811
|
// method resolution reaches them.
|
|
776
812
|
...(leftNode.type === 'member_expression' && { memberAssigned: true }),
|
|
813
|
+
// One-hop member assignments record the object they
|
|
814
|
+
// patch (fix #286a: `console.log = () => {}` — the
|
|
815
|
+
// builtin-global exclusion must see cross-file that
|
|
816
|
+
// the project rebinds this global's member).
|
|
777
817
|
...(leftNode.type === 'member_expression' &&
|
|
778
|
-
|
|
779
|
-
{
|
|
818
|
+
leftNode.childForFieldName('object')?.type === 'identifier' &&
|
|
819
|
+
{ assignedReceiver: leftNode.childForFieldName('object').text }),
|
|
820
|
+
...(prototypeOwner && { className: prototypeOwner, isMethod: true }),
|
|
780
821
|
...typeAnno,
|
|
781
822
|
...(generics && { generics }),
|
|
782
823
|
...(docstring && { docstring })
|
|
@@ -1258,7 +1299,8 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
1258
1299
|
}
|
|
1259
1300
|
|
|
1260
1301
|
const isAsync = text.match(/^\s*(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:override\s+)?async\s/) !== null;
|
|
1261
|
-
const returnType = extractReturnType(child)
|
|
1302
|
+
const returnType = extractReturnType(child) ||
|
|
1303
|
+
(returnsReceiverSelf(child) ? 'this' : null);
|
|
1262
1304
|
const docstring = extractJSDocstring(code, startLine);
|
|
1263
1305
|
const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
|
|
1264
1306
|
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, code, startLine, true);
|
|
@@ -1372,6 +1414,8 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
1372
1414
|
const name = nameNode.text;
|
|
1373
1415
|
const valueNode = child.childForFieldName('value');
|
|
1374
1416
|
const isArrow = valueNode && valueNode.type === 'arrow_function';
|
|
1417
|
+
const isStatic = Array.from({ length: child.childCount }, (_, ci) => child.child(ci))
|
|
1418
|
+
.some(part => part.type === 'static');
|
|
1375
1419
|
|
|
1376
1420
|
// Collect decorators — children of the field node (TS) or preceding siblings (JS)
|
|
1377
1421
|
const fieldDecorators = [];
|
|
@@ -1410,6 +1454,7 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
1410
1454
|
startLine,
|
|
1411
1455
|
endLine,
|
|
1412
1456
|
memberType: name.startsWith('#') ? 'private' : 'field',
|
|
1457
|
+
...(isStatic && { modifiers: ['static'] }),
|
|
1413
1458
|
isArrow: true,
|
|
1414
1459
|
isMethod: true, // Arrow fields are callable like methods
|
|
1415
1460
|
...typeAnno,
|
|
@@ -1427,6 +1472,7 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
1427
1472
|
startLine,
|
|
1428
1473
|
endLine,
|
|
1429
1474
|
memberType: name.startsWith('#') ? 'private field' : 'field',
|
|
1475
|
+
...(isStatic && { modifiers: ['static'] }),
|
|
1430
1476
|
...(fieldType && { fieldType }),
|
|
1431
1477
|
...(fieldDecorators.length > 0 && { decorators: fieldDecorators })
|
|
1432
1478
|
// Not a method - regular field
|
|
@@ -1484,6 +1530,48 @@ function _processState(node, objects, lines) {
|
|
|
1484
1530
|
return false;
|
|
1485
1531
|
}
|
|
1486
1532
|
|
|
1533
|
+
/**
|
|
1534
|
+
* Record immutable module-scope aliases of a statically named class member.
|
|
1535
|
+
*
|
|
1536
|
+
* `const make = Widget.create` preserves the member's compiler-visible
|
|
1537
|
+
* callable signature. The normalized IR can therefore expose both the local
|
|
1538
|
+
* callable value and any `export { make as widget }` surface without guessing
|
|
1539
|
+
* from a later call spelling. Mutable/local/object aliases deliberately stay
|
|
1540
|
+
* out: they need data-flow evidence, not a declaration-shape shortcut.
|
|
1541
|
+
*/
|
|
1542
|
+
function _processCallableAlias(node, aliases) {
|
|
1543
|
+
if (node.type !== 'lexical_declaration' || !isModuleScope(node)) return false;
|
|
1544
|
+
const declarationKind = node.child(0)?.text;
|
|
1545
|
+
if (declarationKind !== 'const') return false;
|
|
1546
|
+
|
|
1547
|
+
let matched = false;
|
|
1548
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1549
|
+
const declarator = node.namedChild(i);
|
|
1550
|
+
if (declarator.type !== 'variable_declarator') continue;
|
|
1551
|
+
const nameNode = declarator.childForFieldName('name');
|
|
1552
|
+
let valueNode = declarator.childForFieldName('value');
|
|
1553
|
+
if (nameNode?.type !== 'identifier' || !valueNode) continue;
|
|
1554
|
+
while (valueNode && ['parenthesized_expression', 'as_expression',
|
|
1555
|
+
'satisfies_expression', 'type_assertion'].includes(valueNode.type)) {
|
|
1556
|
+
valueNode = valueNode.namedChild(0);
|
|
1557
|
+
}
|
|
1558
|
+
if (valueNode?.type !== 'member_expression') continue;
|
|
1559
|
+
const owner = valueNode.childForFieldName('object');
|
|
1560
|
+
const member = valueNode.childForFieldName('property');
|
|
1561
|
+
if (owner?.type !== 'identifier' ||
|
|
1562
|
+
!['identifier', 'property_identifier'].includes(member?.type)) continue;
|
|
1563
|
+
aliases.push({
|
|
1564
|
+
name: nameNode.text,
|
|
1565
|
+
owner: owner.text,
|
|
1566
|
+
member: member.text,
|
|
1567
|
+
startLine: declarator.startPosition.row + 1,
|
|
1568
|
+
endLine: declarator.endPosition.row + 1,
|
|
1569
|
+
});
|
|
1570
|
+
matched = true;
|
|
1571
|
+
}
|
|
1572
|
+
return matched;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1487
1575
|
/**
|
|
1488
1576
|
* Find state objects (CONFIG, constants, etc.)
|
|
1489
1577
|
*/
|
|
@@ -1508,13 +1596,14 @@ function findStateObjects(code, parser) {
|
|
|
1508
1596
|
function parse(code, parser) {
|
|
1509
1597
|
const tree = parseTree(parser, code);
|
|
1510
1598
|
const lines = code.split('\n');
|
|
1511
|
-
const functions = [], classes = [], stateObjects = [];
|
|
1599
|
+
const functions = [], classes = [], stateObjects = [], callableAliases = [];
|
|
1512
1600
|
const processedFn = new Set(), processedCls = new Set();
|
|
1513
1601
|
|
|
1514
1602
|
traverseTreeCached(tree.rootNode, (node) => {
|
|
1515
1603
|
_processFunction(node, functions, processedFn, lines);
|
|
1516
1604
|
_processClass(node, classes, processedCls, lines);
|
|
1517
1605
|
_processState(node, stateObjects, lines);
|
|
1606
|
+
_processCallableAlias(node, callableAliases);
|
|
1518
1607
|
return true; // always continue, never skip subtrees
|
|
1519
1608
|
});
|
|
1520
1609
|
|
|
@@ -1554,6 +1643,7 @@ function parse(code, parser) {
|
|
|
1554
1643
|
declarationTokens.sort((a, b) => a.startIndex - b.startIndex);
|
|
1555
1644
|
|
|
1556
1645
|
const recoveredFunctions = [], recoveredClasses = [], recoveredState = [];
|
|
1646
|
+
const recoveredCallableAliases = [];
|
|
1557
1647
|
for (let i = 0; i < declarationTokens.length; i++) {
|
|
1558
1648
|
const token = declarationTokens[i];
|
|
1559
1649
|
const next = declarationTokens[i + 1];
|
|
@@ -1562,12 +1652,13 @@ function parse(code, parser) {
|
|
|
1562
1652
|
if (!fragment.trim()) continue;
|
|
1563
1653
|
const fragmentTree = parseTree(parser, fragment);
|
|
1564
1654
|
const fragmentLines = fragment.split('\n');
|
|
1565
|
-
const ff = [], fc = [], fs = [];
|
|
1655
|
+
const ff = [], fc = [], fs = [], fa = [];
|
|
1566
1656
|
const pf = new Set(), pc = new Set();
|
|
1567
1657
|
traverseTreeCached(fragmentTree.rootNode, (node) => {
|
|
1568
1658
|
_processFunction(node, ff, pf, fragmentLines);
|
|
1569
1659
|
_processClass(node, fc, pc, fragmentLines);
|
|
1570
1660
|
_processState(node, fs, fragmentLines);
|
|
1661
|
+
_processCallableAlias(node, fa);
|
|
1571
1662
|
return true;
|
|
1572
1663
|
});
|
|
1573
1664
|
const lineOffset = token.startPosition.row;
|
|
@@ -1585,6 +1676,7 @@ function parse(code, parser) {
|
|
|
1585
1676
|
for (const item of ff) { shiftLines(item); recoveredFunctions.push(item); }
|
|
1586
1677
|
for (const item of fc) { shiftLines(item); recoveredClasses.push(item); }
|
|
1587
1678
|
for (const item of fs) { shiftLines(item); recoveredState.push(item); }
|
|
1679
|
+
for (const item of fa) { shiftLines(item); recoveredCallableAliases.push(item); }
|
|
1588
1680
|
}
|
|
1589
1681
|
|
|
1590
1682
|
const mergeUnique = (target, additions, kind) => {
|
|
@@ -1597,11 +1689,21 @@ function parse(code, parser) {
|
|
|
1597
1689
|
mergeUnique(functions, recoveredFunctions, 'function');
|
|
1598
1690
|
mergeUnique(classes, recoveredClasses, 'class');
|
|
1599
1691
|
mergeUnique(stateObjects, recoveredState, 'state');
|
|
1692
|
+
const aliasKeys = new Set(callableAliases.map(alias =>
|
|
1693
|
+
`${alias.name}\0${alias.owner}\0${alias.member}\0${alias.startLine}`));
|
|
1694
|
+
for (const alias of recoveredCallableAliases) {
|
|
1695
|
+
const key = `${alias.name}\0${alias.owner}\0${alias.member}\0${alias.startLine}`;
|
|
1696
|
+
if (!aliasKeys.has(key)) {
|
|
1697
|
+
aliasKeys.add(key);
|
|
1698
|
+
callableAliases.push(alias);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1600
1701
|
}
|
|
1601
1702
|
|
|
1602
1703
|
functions.sort((a, b) => a.startLine - b.startLine);
|
|
1603
1704
|
classes.sort((a, b) => a.startLine - b.startLine);
|
|
1604
1705
|
stateObjects.sort((a, b) => a.startLine - b.startLine);
|
|
1706
|
+
callableAliases.sort((a, b) => a.startLine - b.startLine);
|
|
1605
1707
|
|
|
1606
1708
|
return {
|
|
1607
1709
|
language: 'javascript',
|
|
@@ -1609,6 +1711,7 @@ function parse(code, parser) {
|
|
|
1609
1711
|
functions,
|
|
1610
1712
|
classes,
|
|
1611
1713
|
stateObjects,
|
|
1714
|
+
callableAliases,
|
|
1612
1715
|
...(tree.rootNode.hasError && { parseRecovery: true }),
|
|
1613
1716
|
imports: [], // Handled by core/imports.js
|
|
1614
1717
|
exports: [] // Handled by core/imports.js
|
|
@@ -1653,6 +1756,40 @@ const JS_LITERAL_ASSIGN_TYPES = {
|
|
|
1653
1756
|
// Predefined TS types that pin a receiver; any/unknown/object say nothing.
|
|
1654
1757
|
const TS_PREDEFINED_RECEIVER_TYPES = new Set(['string', 'number', 'boolean', 'bigint', 'symbol']);
|
|
1655
1758
|
|
|
1759
|
+
/**
|
|
1760
|
+
* Companion to tsTypeName: the namespace qualifier that owns the annotated
|
|
1761
|
+
* name (fix #286e — `app: ns.Flask` must carry 'ns' so two same-name classes
|
|
1762
|
+
* resolve by declaration origin, not directory proximity).
|
|
1763
|
+
*/
|
|
1764
|
+
function tsTypeQualifier(node) {
|
|
1765
|
+
if (!node) return undefined;
|
|
1766
|
+
switch (node.type) {
|
|
1767
|
+
case 'nested_type_identifier': {
|
|
1768
|
+
const last = node.namedChild(node.namedChildCount - 1);
|
|
1769
|
+
const start = node.startIndex;
|
|
1770
|
+
return last && last.startIndex > start
|
|
1771
|
+
? node.text.slice(0, last.startIndex - start).replace(/\.$/, '') || undefined
|
|
1772
|
+
: undefined;
|
|
1773
|
+
}
|
|
1774
|
+
case 'generic_type':
|
|
1775
|
+
return tsTypeQualifier(node.namedChild(0));
|
|
1776
|
+
case 'union_type': {
|
|
1777
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1778
|
+
const c = node.namedChild(i);
|
|
1779
|
+
if (c.type === 'nested_type_identifier' || c.type === 'generic_type') {
|
|
1780
|
+
const q = tsTypeQualifier(c);
|
|
1781
|
+
if (q) return q;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
return undefined;
|
|
1785
|
+
}
|
|
1786
|
+
case 'parenthesized_type':
|
|
1787
|
+
return tsTypeQualifier(node.namedChild(0));
|
|
1788
|
+
default:
|
|
1789
|
+
return undefined;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1656
1793
|
/**
|
|
1657
1794
|
* Extract a single concrete type name from a TS type node. Conservative by
|
|
1658
1795
|
* design: a wrong type would exclude true callers downstream
|
|
@@ -1717,6 +1854,34 @@ function jsAssignmentTargetOf(callNode) {
|
|
|
1717
1854
|
return undefined;
|
|
1718
1855
|
}
|
|
1719
1856
|
|
|
1857
|
+
/**
|
|
1858
|
+
* For-of iteration target of a call used as the iterable:
|
|
1859
|
+
* `for (const x of getItems())`. The loop variable holds an ELEMENT of the
|
|
1860
|
+
* producer's result, never the return value — provenance only (fix #294).
|
|
1861
|
+
*/
|
|
1862
|
+
function jsIterTargetOf(callNode) {
|
|
1863
|
+
let n = callNode;
|
|
1864
|
+
let p = n.parent;
|
|
1865
|
+
if (p && p.type === 'await_expression') { n = p; p = n.parent; }
|
|
1866
|
+
if (!p || p.type !== 'for_in_statement') return undefined;
|
|
1867
|
+
if (p.childForFieldName('operator')?.text !== 'of') return undefined;
|
|
1868
|
+
const right = p.childForFieldName('right');
|
|
1869
|
+
if (!right || right.id !== n.id) return undefined;
|
|
1870
|
+
const left = p.childForFieldName('left');
|
|
1871
|
+
if (!left) return undefined;
|
|
1872
|
+
const names = [];
|
|
1873
|
+
if (left.type === 'identifier') {
|
|
1874
|
+
names.push(left.text);
|
|
1875
|
+
} else if (left.type === 'array_pattern') {
|
|
1876
|
+
for (let i = 0; i < left.namedChildCount; i++) {
|
|
1877
|
+
const c = left.namedChild(i);
|
|
1878
|
+
if (c.type === 'identifier') names.push(c.text);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
if (names.length === 0) return undefined;
|
|
1882
|
+
return { first: names[0], rest: names.slice(1) };
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1720
1885
|
/**
|
|
1721
1886
|
* Type name from a new-expression constructor node: new Foo() or new pkg.Foo().
|
|
1722
1887
|
*/
|
|
@@ -2180,6 +2345,12 @@ function findCallsInCode(code, parser) {
|
|
|
2180
2345
|
if (typeName) {
|
|
2181
2346
|
localVarTypes.set(nameNode.text, typeName);
|
|
2182
2347
|
declaredTypeVars.add(nameNode.text);
|
|
2348
|
+
const annotationQualifier = tsTypeQualifier(typeId);
|
|
2349
|
+
if (annotationQualifier) {
|
|
2350
|
+
localVarTypeQualifiers.set(nameNode.text, annotationQualifier);
|
|
2351
|
+
} else {
|
|
2352
|
+
localVarTypeQualifiers.delete(nameNode.text);
|
|
2353
|
+
}
|
|
2183
2354
|
}
|
|
2184
2355
|
} else if (initNode && JS_LITERAL_ASSIGN_TYPES[initNode.type]) {
|
|
2185
2356
|
// Literal declaration types the variable (fix #262):
|
|
@@ -2200,6 +2371,12 @@ function findCallsInCode(code, parser) {
|
|
|
2200
2371
|
if (typeName) {
|
|
2201
2372
|
localVarTypes.set(pat.text, typeName);
|
|
2202
2373
|
declaredTypeVars.add(pat.text);
|
|
2374
|
+
const annotationQualifier = tsTypeQualifier(inner);
|
|
2375
|
+
if (annotationQualifier) {
|
|
2376
|
+
localVarTypeQualifiers.set(pat.text, annotationQualifier);
|
|
2377
|
+
} else {
|
|
2378
|
+
localVarTypeQualifiers.delete(pat.text);
|
|
2379
|
+
}
|
|
2203
2380
|
}
|
|
2204
2381
|
}
|
|
2205
2382
|
}
|
|
@@ -2314,7 +2491,18 @@ function findCallsInCode(code, parser) {
|
|
|
2314
2491
|
const resolvedName = typeof alias === 'string' ? alias : undefined;
|
|
2315
2492
|
const resolvedNames = Array.isArray(alias) ? alias : undefined;
|
|
2316
2493
|
const firstArg = getFirstStringArg(node);
|
|
2317
|
-
|
|
2494
|
+
let assignedTo = jsAssignmentTargetOf(node);
|
|
2495
|
+
let assignedIterFields = {};
|
|
2496
|
+
if (!assignedTo) {
|
|
2497
|
+
const iterTarget = jsIterTargetOf(node);
|
|
2498
|
+
if (iterTarget) {
|
|
2499
|
+
assignedTo = iterTarget.first;
|
|
2500
|
+
assignedIterFields = {
|
|
2501
|
+
assignedIter: true,
|
|
2502
|
+
...(iterTarget.rest.length > 0 && { assignedTupleRest: iterTarget.rest }),
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2318
2506
|
// MEDIUM-5: capture explicit method for fetch(url, { method }).
|
|
2319
2507
|
const optionsMethod = funcNode.text === 'fetch'
|
|
2320
2508
|
? getOptionsMethod(node, 1)
|
|
@@ -2328,6 +2516,7 @@ function findCallsInCode(code, parser) {
|
|
|
2328
2516
|
callEnd: node.endIndex,
|
|
2329
2517
|
isMethod: false,
|
|
2330
2518
|
...(assignedTo && { assignedTo }),
|
|
2519
|
+
...assignedIterFields,
|
|
2331
2520
|
enclosingFunction,
|
|
2332
2521
|
uncertain,
|
|
2333
2522
|
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp }),
|
|
@@ -2509,7 +2698,19 @@ function findCallsInCode(code, parser) {
|
|
|
2509
2698
|
!localVarTypes.has(receiver));
|
|
2510
2699
|
const firstArg = getFirstStringArg(node);
|
|
2511
2700
|
const argCount = getArgCount(node);
|
|
2512
|
-
|
|
2701
|
+
let assignedTo = jsAssignmentTargetOf(node);
|
|
2702
|
+
let assignedIterFields = {};
|
|
2703
|
+
if (!assignedTo) {
|
|
2704
|
+
const iterTarget = jsIterTargetOf(node);
|
|
2705
|
+
if (iterTarget) {
|
|
2706
|
+
assignedTo = iterTarget.first;
|
|
2707
|
+
assignedIterFields = {
|
|
2708
|
+
assignedIter: true,
|
|
2709
|
+
...(iterTarget.rest.length > 0 &&
|
|
2710
|
+
{ assignedTupleRest: iterTarget.rest }),
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2513
2714
|
calls.push({
|
|
2514
2715
|
name: propName,
|
|
2515
2716
|
// Multi-line chains (builder.x()\n.y()) must report
|
|
@@ -2545,6 +2746,7 @@ function findCallsInCode(code, parser) {
|
|
|
2545
2746
|
...(receiverCallStart != null && { receiverCallStart }),
|
|
2546
2747
|
...(receiverCallEnd != null && { receiverCallEnd }),
|
|
2547
2748
|
...(assignedTo && { assignedTo }),
|
|
2749
|
+
...assignedIterFields,
|
|
2548
2750
|
enclosingFunction,
|
|
2549
2751
|
uncertain,
|
|
2550
2752
|
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp }),
|
|
@@ -2593,11 +2795,17 @@ function findCallsInCode(code, parser) {
|
|
|
2593
2795
|
const propNode = arg.childForFieldName('property');
|
|
2594
2796
|
const objNode = arg.childForFieldName('object');
|
|
2595
2797
|
if (propNode && !SKIP_IDENTS.has(propNode.text)) {
|
|
2798
|
+
const hofRecv = objNode?.type === 'identifier' ? objNode.text : undefined;
|
|
2799
|
+
// Same typing evidence as the general-arg
|
|
2800
|
+
// member-value shape (fix #295) — tier must
|
|
2801
|
+
// not depend on argument position (#234).
|
|
2802
|
+
const hofRecvType = hofRecv ? localVarTypes.get(hofRecv) : undefined;
|
|
2596
2803
|
calls.push({
|
|
2597
2804
|
name: propNode.text,
|
|
2598
2805
|
line: arg.startPosition.row + 1,
|
|
2599
2806
|
isMethod: true,
|
|
2600
|
-
receiver:
|
|
2807
|
+
receiver: hofRecv,
|
|
2808
|
+
...(hofRecvType && { receiverType: hofRecvType }),
|
|
2601
2809
|
isFunctionReference: true,
|
|
2602
2810
|
enclosingFunction
|
|
2603
2811
|
});
|
|
@@ -2628,6 +2836,29 @@ function findCallsInCode(code, parser) {
|
|
|
2628
2836
|
enclosingFunction
|
|
2629
2837
|
});
|
|
2630
2838
|
}
|
|
2839
|
+
// Method-value references (fix #295): `expectRaises(t.check, null)`
|
|
2840
|
+
// passes the method value t.check — one-hop identifier
|
|
2841
|
+
// receivers, typed from the same localVarTypes evidence
|
|
2842
|
+
// method calls use. No isPotentialCallback: the record
|
|
2843
|
+
// rides the MAIN path's full receiver physics (the HOF
|
|
2844
|
+
// member-value convention above).
|
|
2845
|
+
if (arg.type === 'member_expression') {
|
|
2846
|
+
const mvProp = arg.childForFieldName('property');
|
|
2847
|
+
const mvObj = arg.childForFieldName('object');
|
|
2848
|
+
if (mvProp && mvObj?.type === 'identifier' &&
|
|
2849
|
+
!SKIP_IDENTS.has(mvProp.text) && !SKIP_IDENTS.has(mvObj.text)) {
|
|
2850
|
+
const mvType = localVarTypes.get(mvObj.text);
|
|
2851
|
+
calls.push({
|
|
2852
|
+
name: mvProp.text,
|
|
2853
|
+
line: arg.startPosition.row + 1,
|
|
2854
|
+
isMethod: true,
|
|
2855
|
+
receiver: mvObj.text,
|
|
2856
|
+
...(mvType && { receiverType: mvType }),
|
|
2857
|
+
isFunctionReference: true,
|
|
2858
|
+
enclosingFunction
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2631
2862
|
// Scan object literal args for function refs in property values
|
|
2632
2863
|
// e.g., doRequest({onSuccess: handleSuccess, onError: handleError})
|
|
2633
2864
|
if (arg.type === 'object') {
|
|
@@ -3163,6 +3394,22 @@ function findImportsInCode(code, parser) {
|
|
|
3163
3394
|
return imports;
|
|
3164
3395
|
}
|
|
3165
3396
|
|
|
3397
|
+
/**
|
|
3398
|
+
* Return the local symbol created or referenced by a statically-owned
|
|
3399
|
+
* CommonJS property assignment. Dynamic expressions deliberately return
|
|
3400
|
+
* undefined: `exports.x = factory()` and `exports.x = other.y` may be
|
|
3401
|
+
* re-exports or arbitrary values, so they are not exclusion-grade identity.
|
|
3402
|
+
*/
|
|
3403
|
+
function cjsAssignedLocalName(valueNode, exposedName) {
|
|
3404
|
+
if (!valueNode) return undefined;
|
|
3405
|
+
if (valueNode.type === 'identifier') return valueNode.text;
|
|
3406
|
+
if (['function_expression', 'generator_function', 'arrow_function', 'class']
|
|
3407
|
+
.includes(valueNode.type)) {
|
|
3408
|
+
return valueNode.childForFieldName('name')?.text || exposedName;
|
|
3409
|
+
}
|
|
3410
|
+
return undefined;
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3166
3413
|
/**
|
|
3167
3414
|
* Find all exports in JavaScript/TypeScript code using tree-sitter AST
|
|
3168
3415
|
* @param {string} code - Source code to analyze
|
|
@@ -3355,12 +3602,15 @@ function findExportsInCode(code, parser) {
|
|
|
3355
3602
|
} else if (prop.type === 'pair') {
|
|
3356
3603
|
const key = prop.childForFieldName('key');
|
|
3357
3604
|
const value = prop.childForFieldName('value');
|
|
3358
|
-
if (key)
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3605
|
+
if (key) {
|
|
3606
|
+
const localName = cjsAssignedLocalName(value, key.text);
|
|
3607
|
+
exports.push({
|
|
3608
|
+
name: key.text,
|
|
3609
|
+
...(localName && { localName }),
|
|
3610
|
+
type: 'module.exports',
|
|
3611
|
+
line,
|
|
3612
|
+
});
|
|
3613
|
+
}
|
|
3364
3614
|
} else if (prop.type === 'method_definition') {
|
|
3365
3615
|
// Shorthand methods are exports too
|
|
3366
3616
|
// (fix #252 — `module.exports =
|
|
@@ -3402,9 +3652,21 @@ function findExportsInCode(code, parser) {
|
|
|
3402
3652
|
}
|
|
3403
3653
|
} else if (rightNode && rightNode.type === 'identifier') {
|
|
3404
3654
|
// module.exports = something
|
|
3405
|
-
exports.push({ name: rightNode.text, localName: rightNode.text,
|
|
3655
|
+
exports.push({ name: rightNode.text, localName: rightNode.text,
|
|
3656
|
+
type: 'module.exports', defaultLike: true, line });
|
|
3406
3657
|
} else {
|
|
3407
|
-
|
|
3658
|
+
// A named function/class expression still exports
|
|
3659
|
+
// one callable default. Preserve its LOCAL symbol
|
|
3660
|
+
// identity so `require('./mod')()` resolves back
|
|
3661
|
+
// to the declaration; anonymous expressions use
|
|
3662
|
+
// the parser's synthetic `default` symbol.
|
|
3663
|
+
const localName = rightNode &&
|
|
3664
|
+
['function_expression', 'generator_function', 'class'].includes(rightNode.type)
|
|
3665
|
+
? rightNode.childForFieldName('name')?.text
|
|
3666
|
+
: undefined;
|
|
3667
|
+
exports.push({ name: 'default',
|
|
3668
|
+
...(localName && { localName }),
|
|
3669
|
+
type: 'module.exports', defaultLike: true, line });
|
|
3408
3670
|
}
|
|
3409
3671
|
return true;
|
|
3410
3672
|
}
|
|
@@ -3413,9 +3675,10 @@ function findExportsInCode(code, parser) {
|
|
|
3413
3675
|
if (objNode.text === 'exports') {
|
|
3414
3676
|
const line = node.startPosition.row + 1;
|
|
3415
3677
|
const rightNode = node.childForFieldName('right');
|
|
3678
|
+
const localName = cjsAssignedLocalName(rightNode, propNode.text);
|
|
3416
3679
|
exports.push({
|
|
3417
3680
|
name: propNode.text,
|
|
3418
|
-
...(
|
|
3681
|
+
...(localName && { localName }),
|
|
3419
3682
|
type: 'exports',
|
|
3420
3683
|
line,
|
|
3421
3684
|
});
|
|
@@ -3425,7 +3688,11 @@ function findExportsInCode(code, parser) {
|
|
|
3425
3688
|
// module.exports.name = ...
|
|
3426
3689
|
if (objNode.type === 'member_expression' && objNode.text === 'module.exports') {
|
|
3427
3690
|
const line = node.startPosition.row + 1;
|
|
3428
|
-
|
|
3691
|
+
const rightNode = node.childForFieldName('right');
|
|
3692
|
+
const localName = cjsAssignedLocalName(rightNode, propNode.text);
|
|
3693
|
+
exports.push({ name: propNode.text,
|
|
3694
|
+
...(localName && { localName }),
|
|
3695
|
+
type: 'module.exports', line });
|
|
3429
3696
|
return true;
|
|
3430
3697
|
}
|
|
3431
3698
|
}
|