ucn 4.2.2 → 4.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -11
- package/core/build-worker.js +11 -2
- package/core/cache.js +21 -1
- package/core/callers.js +618 -43
- package/core/deadcode.js +25 -2
- package/core/entrypoints.js +9 -1
- package/core/output/analysis.js +4 -0
- package/core/project.js +13 -3
- package/core/search.js +149 -26
- package/languages/java.js +32 -3
- package/languages/javascript.js +257 -34
- package/languages/python.js +77 -10
- package/languages/rust.js +99 -1
- package/package.json +4 -4
package/languages/javascript.js
CHANGED
|
@@ -429,6 +429,53 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
429
429
|
return true;
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
+
// Named function expressions used as callbacks have a real lexical
|
|
433
|
+
// definition even when no variable owns them: `test('x', function run()
|
|
434
|
+
// {})`. Property/variable assignments are handled by their binding
|
|
435
|
+
// branches below; indexing the expression name there as a second symbol
|
|
436
|
+
// would manufacture a duplicate public definition.
|
|
437
|
+
if (node.type === 'function_expression' || node.type === 'generator_function') {
|
|
438
|
+
const parent = node.parent;
|
|
439
|
+
const isBoundValue = (parent?.type === 'variable_declarator' &&
|
|
440
|
+
sameNode(parent.childForFieldName('value'), node)) ||
|
|
441
|
+
(parent?.type === 'assignment_expression' &&
|
|
442
|
+
sameNode(parent.childForFieldName('right'), node)) ||
|
|
443
|
+
(parent?.type === 'pair' && sameNode(parent.childForFieldName('value'), node));
|
|
444
|
+
const nameNode = node.childForFieldName('name');
|
|
445
|
+
if (!isBoundValue && nameNode && !processedRanges.has(rangeKey)) {
|
|
446
|
+
processedRanges.add(rangeKey);
|
|
447
|
+
const paramsNode = node.childForFieldName('parameters');
|
|
448
|
+
const { startLine, endLine, indent } = nodeToLocation(node, lines);
|
|
449
|
+
const returnType = extractReturnType(node);
|
|
450
|
+
const generics = extractGenerics(node);
|
|
451
|
+
const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
|
|
452
|
+
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
|
|
453
|
+
const docstring = extractJSDocstring(lines, startLine);
|
|
454
|
+
functions.push({
|
|
455
|
+
name: nameNode.text,
|
|
456
|
+
params: extractParams(paramsNode),
|
|
457
|
+
paramsStructured,
|
|
458
|
+
startLine,
|
|
459
|
+
endLine,
|
|
460
|
+
indent,
|
|
461
|
+
isArrow: false,
|
|
462
|
+
isGenerator: isGenerator(node),
|
|
463
|
+
isAsync: node.text.trimStart().startsWith('async '),
|
|
464
|
+
modifiers: [],
|
|
465
|
+
// ECMA-262: a FunctionExpression's BindingIdentifier is in
|
|
466
|
+
// scope only within its own body — the name creates no
|
|
467
|
+
// file-level binding (never enters the bindings table) and
|
|
468
|
+
// the expression is consumed where it appears (argument /
|
|
469
|
+
// value position), so deadcode never audits it.
|
|
470
|
+
bodyScopedName: true,
|
|
471
|
+
...typeAnno,
|
|
472
|
+
...(generics && { generics }),
|
|
473
|
+
...(docstring && { docstring })
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
|
|
432
479
|
// TypeScript function signatures (e.g., in .d.ts files)
|
|
433
480
|
if (node.type === 'function_signature') {
|
|
434
481
|
if (processedRanges.has(rangeKey)) return false;
|
|
@@ -613,7 +660,10 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
613
660
|
}
|
|
614
661
|
parent = parent.parent;
|
|
615
662
|
}
|
|
616
|
-
|
|
663
|
+
// A nested property assignment still defines that object's
|
|
664
|
+
// callable member (`reply.send = () => {}`). Only a nested bare
|
|
665
|
+
// assignment lacks a new symbol binding and stays excluded.
|
|
666
|
+
if (!isTopLevel && leftNode?.type !== 'member_expression') return true;
|
|
617
667
|
}
|
|
618
668
|
|
|
619
669
|
const rightNode = node.childForFieldName('right');
|
|
@@ -1388,6 +1438,87 @@ function parse(code, parser) {
|
|
|
1388
1438
|
return true; // always continue, never skip subtrees
|
|
1389
1439
|
});
|
|
1390
1440
|
|
|
1441
|
+
// Some valid overload-heavy TypeScript files exceed the grammar's error
|
|
1442
|
+
// recovery budget. tree-sitter then returns a whole-file ERROR root and
|
|
1443
|
+
// flattens later declarations into unrelated type nodes without throwing.
|
|
1444
|
+
// Recover from AST tokens, not source patterns: top-level declaration
|
|
1445
|
+
// tokens define bounded fragments which are reparsed by the same grammar.
|
|
1446
|
+
// This keeps the AST-only contract while preventing a valid declaration
|
|
1447
|
+
// near the end of one difficult type file from disappearing silently.
|
|
1448
|
+
if (tree.rootNode.hasError) {
|
|
1449
|
+
const declarationTokens = [];
|
|
1450
|
+
const startsDeclaration = new Set([
|
|
1451
|
+
'export', 'declare', 'async', 'function', 'class', 'abstract',
|
|
1452
|
+
'interface', 'type', 'enum', 'namespace', 'module',
|
|
1453
|
+
'const', 'let', 'var'
|
|
1454
|
+
]);
|
|
1455
|
+
const stack = [tree.rootNode];
|
|
1456
|
+
while (stack.length > 0) {
|
|
1457
|
+
const node = stack.pop();
|
|
1458
|
+
if (node.childCount === 0) {
|
|
1459
|
+
// In severe recovery, a keyword itself can be downgraded to
|
|
1460
|
+
// an identifier token. Its AST position/text still supplies
|
|
1461
|
+
// a safe declaration boundary; semantic extraction remains
|
|
1462
|
+
// entirely delegated to the reparsed fragment.
|
|
1463
|
+
const recoveredKeyword = node.type === 'identifier' &&
|
|
1464
|
+
startsDeclaration.has(node.text);
|
|
1465
|
+
if (node.startPosition.column === 0 &&
|
|
1466
|
+
(startsDeclaration.has(node.type) || recoveredKeyword)) {
|
|
1467
|
+
declarationTokens.push(node);
|
|
1468
|
+
}
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
const children = node.children;
|
|
1472
|
+
for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
|
|
1473
|
+
}
|
|
1474
|
+
declarationTokens.sort((a, b) => a.startIndex - b.startIndex);
|
|
1475
|
+
|
|
1476
|
+
const recoveredFunctions = [], recoveredClasses = [], recoveredState = [];
|
|
1477
|
+
for (let i = 0; i < declarationTokens.length; i++) {
|
|
1478
|
+
const token = declarationTokens[i];
|
|
1479
|
+
const next = declarationTokens[i + 1];
|
|
1480
|
+
if (next && next.startIndex === token.startIndex) continue;
|
|
1481
|
+
const fragment = code.slice(token.startIndex, next?.startIndex ?? code.length);
|
|
1482
|
+
if (!fragment.trim()) continue;
|
|
1483
|
+
const fragmentTree = parseTree(parser, fragment);
|
|
1484
|
+
const fragmentLines = fragment.split('\n');
|
|
1485
|
+
const ff = [], fc = [], fs = [];
|
|
1486
|
+
const pf = new Set(), pc = new Set();
|
|
1487
|
+
traverseTreeCached(fragmentTree.rootNode, (node) => {
|
|
1488
|
+
_processFunction(node, ff, pf, fragmentLines);
|
|
1489
|
+
_processClass(node, fc, pc, fragmentLines);
|
|
1490
|
+
_processState(node, fs, fragmentLines);
|
|
1491
|
+
return true;
|
|
1492
|
+
});
|
|
1493
|
+
const lineOffset = token.startPosition.row;
|
|
1494
|
+
const shiftLines = (value) => {
|
|
1495
|
+
if (!value || typeof value !== 'object') return;
|
|
1496
|
+
if (Array.isArray(value)) {
|
|
1497
|
+
for (const item of value) shiftLines(item);
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1501
|
+
if (Number.isInteger(child) && /Line$/.test(key)) value[key] = child + lineOffset;
|
|
1502
|
+
else if (child && typeof child === 'object') shiftLines(child);
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
for (const item of ff) { shiftLines(item); recoveredFunctions.push(item); }
|
|
1506
|
+
for (const item of fc) { shiftLines(item); recoveredClasses.push(item); }
|
|
1507
|
+
for (const item of fs) { shiftLines(item); recoveredState.push(item); }
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
const mergeUnique = (target, additions, kind) => {
|
|
1511
|
+
const seen = new Set(target.map(item => `${kind}\0${item.name}\0${item.startLine}`));
|
|
1512
|
+
for (const item of additions) {
|
|
1513
|
+
const key = `${kind}\0${item.name}\0${item.startLine}`;
|
|
1514
|
+
if (!seen.has(key)) { seen.add(key); target.push(item); }
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1517
|
+
mergeUnique(functions, recoveredFunctions, 'function');
|
|
1518
|
+
mergeUnique(classes, recoveredClasses, 'class');
|
|
1519
|
+
mergeUnique(stateObjects, recoveredState, 'state');
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1391
1522
|
functions.sort((a, b) => a.startLine - b.startLine);
|
|
1392
1523
|
classes.sort((a, b) => a.startLine - b.startLine);
|
|
1393
1524
|
stateObjects.sort((a, b) => a.startLine - b.startLine);
|
|
@@ -1519,6 +1650,13 @@ function jsConstructorTypeName(ctorNode) {
|
|
|
1519
1650
|
return undefined;
|
|
1520
1651
|
}
|
|
1521
1652
|
|
|
1653
|
+
function jsConstructorTypeQualifier(ctorNode) {
|
|
1654
|
+
if (ctorNode?.type !== 'member_expression') return undefined;
|
|
1655
|
+
let root = ctorNode.childForFieldName('object');
|
|
1656
|
+
while (root?.type === 'member_expression') root = root.childForFieldName('object');
|
|
1657
|
+
return root?.type === 'identifier' ? root.text : undefined;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1522
1660
|
function findCallsInCode(code, parser) {
|
|
1523
1661
|
const tree = parseTree(parser, code);
|
|
1524
1662
|
const calls = [];
|
|
@@ -1530,6 +1668,7 @@ function findCallsInCode(code, parser) {
|
|
|
1530
1668
|
const aliases = new Map(); // aliasName -> [{ target, declarationIndex, scopeStart, scopeEnd }]
|
|
1531
1669
|
const nonCallableNames = new Set(); // Track names assigned non-callable values
|
|
1532
1670
|
const localVarTypes = new Map(); // Track local variable types: varName -> typeName (for receiverType inference)
|
|
1671
|
+
const localVarTypeQualifiers = new Map(); // qualifier provenance for new ns.Type()
|
|
1533
1672
|
// Names whose type came from a DECLARED annotation (TS `x: Foo` / typed
|
|
1534
1673
|
// params). The compiler enforces assignability for these, so reassignment
|
|
1535
1674
|
// never stales them; inferred types (literal/new) DO stale and are
|
|
@@ -1538,6 +1677,7 @@ function findCallsInCode(code, parser) {
|
|
|
1538
1677
|
const declaredTypeVarsStack = [];
|
|
1539
1678
|
const moduleAliases = new Set(); // Names bound to MODULES (import * as ns / const pkg = require(...))
|
|
1540
1679
|
const localVarTypesStack = []; // Stack for function-scoped save/restore of localVarTypes
|
|
1680
|
+
const localVarTypeQualifiersStack = [];
|
|
1541
1681
|
|
|
1542
1682
|
// Helper: extract first string-arg literal from a call_expression node.
|
|
1543
1683
|
// Used by route extraction to capture path arg of fetch('/path'), app.get('/path', handler) etc.
|
|
@@ -1750,20 +1890,31 @@ function findCallsInCode(code, parser) {
|
|
|
1750
1890
|
return best && best.target;
|
|
1751
1891
|
};
|
|
1752
1892
|
|
|
1753
|
-
|
|
1893
|
+
const _patternDeclaresName = (pattern, name) => {
|
|
1894
|
+
if (!pattern) return false;
|
|
1895
|
+
if ((pattern.type === 'identifier' ||
|
|
1896
|
+
pattern.type === 'shorthand_property_identifier_pattern') &&
|
|
1897
|
+
pattern.text === name) return true;
|
|
1898
|
+
if (pattern.type === 'pair_pattern' || pattern.type === 'pair') {
|
|
1899
|
+
return _patternDeclaresName(pattern.childForFieldName('value'), name);
|
|
1900
|
+
}
|
|
1901
|
+
if (pattern.type === 'assignment_pattern') {
|
|
1902
|
+
return _patternDeclaresName(
|
|
1903
|
+
pattern.childForFieldName('left') || pattern.childForFieldName('pattern'), name);
|
|
1904
|
+
}
|
|
1905
|
+
for (let i = 0; i < pattern.namedChildCount; i++) {
|
|
1906
|
+
if (_patternDeclaresName(pattern.namedChild(i), name)) return true;
|
|
1907
|
+
}
|
|
1908
|
+
return false;
|
|
1909
|
+
};
|
|
1910
|
+
|
|
1911
|
+
// fix #203: does a declaration node declare `name` (including nested destructuring)?
|
|
1754
1912
|
const _declaresName = (declNode, name) => {
|
|
1755
1913
|
for (let i = 0; i < declNode.namedChildCount; i++) {
|
|
1756
1914
|
const d = declNode.namedChild(i);
|
|
1757
1915
|
if (d.type !== 'variable_declarator') continue;
|
|
1758
1916
|
const nameNode = d.childForFieldName('name');
|
|
1759
|
-
if (nameNode
|
|
1760
|
-
if (nameNode && (nameNode.type === 'object_pattern' || nameNode.type === 'array_pattern')) {
|
|
1761
|
-
for (let j = 0; j < nameNode.namedChildCount; j++) {
|
|
1762
|
-
const el = nameNode.namedChild(j);
|
|
1763
|
-
if ((el.type === 'identifier' || el.type === 'shorthand_property_identifier_pattern') &&
|
|
1764
|
-
el.text === name) return true;
|
|
1765
|
-
}
|
|
1766
|
-
}
|
|
1917
|
+
if (_patternDeclaresName(nameNode, name)) return true;
|
|
1767
1918
|
}
|
|
1768
1919
|
return false;
|
|
1769
1920
|
};
|
|
@@ -1797,27 +1948,22 @@ function findCallsInCode(code, parser) {
|
|
|
1797
1948
|
_declaresName(init, name)) return true;
|
|
1798
1949
|
} else if (p.type === 'for_in_statement') {
|
|
1799
1950
|
const left = p.childForFieldName('left');
|
|
1800
|
-
if (left
|
|
1951
|
+
if (_patternDeclaresName(left, name)) return true;
|
|
1801
1952
|
if (left && (left.type === 'lexical_declaration' || left.type === 'variable_declaration') &&
|
|
1802
1953
|
_declaresName(left, name)) return true;
|
|
1803
1954
|
} else if (p.type === 'catch_clause') {
|
|
1804
1955
|
const param = p.childForFieldName('parameter');
|
|
1805
|
-
if (param
|
|
1956
|
+
if (_patternDeclaresName(param, name)) return true;
|
|
1806
1957
|
} else if (p.type === 'arrow_function' || p.type === 'function_expression' ||
|
|
1807
1958
|
p.type === 'function_declaration' || p.type === 'function' ||
|
|
1808
1959
|
p.type === 'method_definition' || p.type === 'generator_function' ||
|
|
1809
1960
|
p.type === 'generator_function_declaration') {
|
|
1810
1961
|
const params = p.childForFieldName('parameters') || p.childForFieldName('parameter');
|
|
1811
1962
|
if (params) {
|
|
1812
|
-
if (params
|
|
1963
|
+
if (_patternDeclaresName(params, name)) return true;
|
|
1813
1964
|
for (let i = 0; i < params.namedChildCount; i++) {
|
|
1814
1965
|
const prm = params.namedChild(i);
|
|
1815
|
-
if (prm
|
|
1816
|
-
if (prm.type === 'assignment_pattern' || prm.type === 'required_parameter' ||
|
|
1817
|
-
prm.type === 'optional_parameter') {
|
|
1818
|
-
const l = prm.childForFieldName('left') || prm.childForFieldName('pattern');
|
|
1819
|
-
if (l?.type === 'identifier' && l.text === name) return true;
|
|
1820
|
-
}
|
|
1966
|
+
if (_patternDeclaresName(prm, name)) return true;
|
|
1821
1967
|
}
|
|
1822
1968
|
}
|
|
1823
1969
|
}
|
|
@@ -1825,6 +1971,21 @@ function findCallsInCode(code, parser) {
|
|
|
1825
1971
|
return false;
|
|
1826
1972
|
};
|
|
1827
1973
|
|
|
1974
|
+
const isConditionalReassignment = node => {
|
|
1975
|
+
for (let p = node.parent; p && !isFunctionNode(p); p = p.parent) {
|
|
1976
|
+
if (p.type === 'if_statement') {
|
|
1977
|
+
const condition = p.childForFieldName('condition');
|
|
1978
|
+
if (!condition || node.startIndex < condition.startIndex ||
|
|
1979
|
+
node.endIndex > condition.endIndex) return true;
|
|
1980
|
+
}
|
|
1981
|
+
if (p.type === 'switch_case' || p.type === 'ternary_expression' ||
|
|
1982
|
+
p.type === 'for_statement' || p.type === 'for_in_statement' ||
|
|
1983
|
+
p.type === 'while_statement' || p.type === 'do_statement' ||
|
|
1984
|
+
p.type === 'catch_clause') return true;
|
|
1985
|
+
}
|
|
1986
|
+
return false;
|
|
1987
|
+
};
|
|
1988
|
+
|
|
1828
1989
|
traverseTree(tree.rootNode, (node) => {
|
|
1829
1990
|
// Track module-alias bindings: `import * as ns from "./m"` binds ns to a
|
|
1830
1991
|
// MODULE — method calls through it dispatch to module exports, never to
|
|
@@ -1843,6 +2004,7 @@ function findCallsInCode(code, parser) {
|
|
|
1843
2004
|
});
|
|
1844
2005
|
// Save localVarTypes so inner declarations don't leak to sibling functions
|
|
1845
2006
|
localVarTypesStack.push(new Map(localVarTypes));
|
|
2007
|
+
localVarTypeQualifiersStack.push(new Map(localVarTypeQualifiers));
|
|
1846
2008
|
declaredTypeVarsStack.push(new Set(declaredTypeVars));
|
|
1847
2009
|
}
|
|
1848
2010
|
|
|
@@ -1896,6 +2058,10 @@ function findCallsInCode(code, parser) {
|
|
|
1896
2058
|
const ctorName = jsConstructorTypeName(initNode.childForFieldName('constructor'));
|
|
1897
2059
|
if (ctorName) {
|
|
1898
2060
|
localVarTypes.set(nameNode.text, ctorName);
|
|
2061
|
+
const qualifier = jsConstructorTypeQualifier(
|
|
2062
|
+
initNode.childForFieldName('constructor'));
|
|
2063
|
+
if (qualifier) localVarTypeQualifiers.set(nameNode.text, qualifier);
|
|
2064
|
+
else localVarTypeQualifiers.delete(nameNode.text);
|
|
1899
2065
|
}
|
|
1900
2066
|
}
|
|
1901
2067
|
// Track TypeScript type annotations: const x: Foo = ...
|
|
@@ -1941,15 +2107,21 @@ function findCallsInCode(code, parser) {
|
|
|
1941
2107
|
if (right?.type === 'new_expression') {
|
|
1942
2108
|
nonCallableNames.add(left.text);
|
|
1943
2109
|
const ctorName = jsConstructorTypeName(right.childForFieldName('constructor'));
|
|
1944
|
-
if (ctorName) {
|
|
2110
|
+
if (ctorName && !isConditionalReassignment(node)) {
|
|
1945
2111
|
localVarTypes.set(left.text, ctorName);
|
|
2112
|
+
const qualifier = jsConstructorTypeQualifier(
|
|
2113
|
+
right.childForFieldName('constructor'));
|
|
2114
|
+
if (qualifier) localVarTypeQualifiers.set(left.text, qualifier);
|
|
2115
|
+
else localVarTypeQualifiers.delete(left.text);
|
|
1946
2116
|
} else if (!declaredTypeVars.has(left.text)) {
|
|
1947
2117
|
localVarTypes.delete(left.text);
|
|
2118
|
+
localVarTypeQualifiers.delete(left.text);
|
|
1948
2119
|
}
|
|
1949
2120
|
} else if (right && JS_LITERAL_ASSIGN_TYPES[right.type]) {
|
|
1950
2121
|
// Literal reassignment re-types the variable (fix #262)
|
|
1951
2122
|
if (!declaredTypeVars.has(left.text)) {
|
|
1952
2123
|
localVarTypes.set(left.text, JS_LITERAL_ASSIGN_TYPES[right.type]);
|
|
2124
|
+
localVarTypeQualifiers.delete(left.text);
|
|
1953
2125
|
}
|
|
1954
2126
|
} else if (localVarTypes.has(left.text) && !declaredTypeVars.has(left.text)) {
|
|
1955
2127
|
// Rebinding without a known type makes any previously
|
|
@@ -1957,6 +2129,7 @@ function findCallsInCode(code, parser) {
|
|
|
1957
2129
|
// semantics (#218d). Annotation-declared types survive:
|
|
1958
2130
|
// the TS compiler enforces assignability for those.
|
|
1959
2131
|
localVarTypes.delete(left.text);
|
|
2132
|
+
localVarTypeQualifiers.delete(left.text);
|
|
1960
2133
|
}
|
|
1961
2134
|
}
|
|
1962
2135
|
// Handler-registration references (fix #252, the #221 family's
|
|
@@ -2012,9 +2185,17 @@ function findCallsInCode(code, parser) {
|
|
|
2012
2185
|
|
|
2013
2186
|
// Handle regular function calls: foo(), obj.foo(), foo.call()
|
|
2014
2187
|
if (node.type === 'call_expression') {
|
|
2015
|
-
|
|
2188
|
+
let funcNode = node.childForFieldName('function');
|
|
2016
2189
|
if (!funcNode) return true;
|
|
2017
2190
|
|
|
2191
|
+
// tree-sitter-typescript represents `await obj.method<T>()` with
|
|
2192
|
+
// the await_expression inside the call's function field. Unwrap
|
|
2193
|
+
// it so generic awaited calls use the same AST call path as every
|
|
2194
|
+
// other method invocation.
|
|
2195
|
+
if (funcNode.type === 'await_expression' && funcNode.namedChildCount === 1) {
|
|
2196
|
+
funcNode = funcNode.namedChild(0);
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2018
2199
|
const enclosingFunction = getCurrentEnclosingFunction();
|
|
2019
2200
|
let uncertain = false;
|
|
2020
2201
|
// optional chaining implies possible non-call
|
|
@@ -2089,12 +2270,25 @@ function findCallsInCode(code, parser) {
|
|
|
2089
2270
|
const innerProp = objNode.childForFieldName('property');
|
|
2090
2271
|
const innerObj = objNode.childForFieldName('object');
|
|
2091
2272
|
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;
|
|
2092
2277
|
calls.push({
|
|
2093
2278
|
name: innerProp.text,
|
|
2094
2279
|
line: node.startPosition.row + 1,
|
|
2095
2280
|
isMethod: true,
|
|
2096
2281
|
boundCall: true,
|
|
2097
|
-
receiver:
|
|
2282
|
+
receiver: boundReceiver,
|
|
2283
|
+
...(boundReceiverType && { receiverType: boundReceiverType }),
|
|
2284
|
+
...(innerObj?.type === 'identifier' &&
|
|
2285
|
+
localVarTypeQualifiers.has(innerObj.text) && {
|
|
2286
|
+
receiverTypeQualifier: localVarTypeQualifiers.get(innerObj.text),
|
|
2287
|
+
}),
|
|
2288
|
+
...(innerObj?.type === 'identifier' &&
|
|
2289
|
+
isShadowedByLocal(innerObj, innerObj.text) && {
|
|
2290
|
+
receiverLocalBinding: true,
|
|
2291
|
+
}),
|
|
2098
2292
|
enclosingFunction,
|
|
2099
2293
|
uncertain
|
|
2100
2294
|
});
|
|
@@ -2115,13 +2309,15 @@ function findCallsInCode(code, parser) {
|
|
|
2115
2309
|
// field's DECLARED type annotation. `this`-rooted hops
|
|
2116
2310
|
// resolve their root type query-side (the enclosing
|
|
2117
2311
|
// class); identifier roots type from local annotations.
|
|
2118
|
-
let receiverRoot, receiverFieldName, receiverRootType;
|
|
2312
|
+
let receiverRoot, receiverFieldName, receiverRootType, receiverBindingNode;
|
|
2313
|
+
if (receiver && objNode?.type === 'identifier') receiverBindingNode = objNode;
|
|
2119
2314
|
if (!receiver && objNode && objNode.type === 'member_expression') {
|
|
2120
2315
|
const rootNode = objNode.childForFieldName('object');
|
|
2121
2316
|
const fldNode = objNode.childForFieldName('property');
|
|
2122
2317
|
if (fldNode && rootNode &&
|
|
2123
2318
|
(rootNode.type === 'identifier' || rootNode.type === 'this')) {
|
|
2124
2319
|
receiverRoot = rootNode.text;
|
|
2320
|
+
receiverBindingNode = rootNode.type === 'identifier' ? rootNode : undefined;
|
|
2125
2321
|
receiverFieldName = fldNode.text;
|
|
2126
2322
|
if (rootNode.type === 'identifier') {
|
|
2127
2323
|
receiverRootType = localVarTypes.get(rootNode.text);
|
|
@@ -2164,9 +2360,19 @@ function findCallsInCode(code, parser) {
|
|
|
2164
2360
|
}
|
|
2165
2361
|
// Literal receivers carry their builtin type: [].map() can
|
|
2166
2362
|
// never be a project class method
|
|
2363
|
+
// A freshly constructed receiver has an exact runtime
|
|
2364
|
+
// type as well: new Service().start(). Recording it here
|
|
2365
|
+
// avoids treating the call as an untyped method dispatch.
|
|
2366
|
+
const constructedReceiverType = objNode?.type === 'new_expression'
|
|
2367
|
+
? jsConstructorTypeName(objNode.childForFieldName('constructor'))
|
|
2368
|
+
: undefined;
|
|
2369
|
+
const constructedReceiverQualifier = objNode?.type === 'new_expression'
|
|
2370
|
+
? jsConstructorTypeQualifier(objNode.childForFieldName('constructor'))
|
|
2371
|
+
: undefined;
|
|
2167
2372
|
const receiverType = receiver
|
|
2168
2373
|
? localVarTypes.get(receiver)
|
|
2169
|
-
: (
|
|
2374
|
+
: (constructedReceiverType ||
|
|
2375
|
+
(objNode ? JS_LITERAL_RECEIVER_TYPES[objNode.type] : undefined));
|
|
2170
2376
|
// Module receiver (ns.helper()) — unless locally shadowed
|
|
2171
2377
|
// by a typed instance binding
|
|
2172
2378
|
const receiverIsModule = !!receiver && moduleAliases.has(receiver) &&
|
|
@@ -2184,7 +2390,15 @@ function findCallsInCode(code, parser) {
|
|
|
2184
2390
|
isMethod: true,
|
|
2185
2391
|
receiver,
|
|
2186
2392
|
...(receiverType && { receiverType }),
|
|
2393
|
+
...((constructedReceiverQualifier ||
|
|
2394
|
+
(receiver && localVarTypeQualifiers.get(receiver))) && {
|
|
2395
|
+
receiverTypeQualifier: constructedReceiverQualifier ||
|
|
2396
|
+
localVarTypeQualifiers.get(receiver),
|
|
2397
|
+
}),
|
|
2187
2398
|
...(receiverIsModule && { receiverIsModule: true }),
|
|
2399
|
+
...(receiverBindingNode &&
|
|
2400
|
+
isShadowedByLocal(receiverBindingNode, receiverBindingNode.text) &&
|
|
2401
|
+
{ receiverLocalBinding: true }),
|
|
2188
2402
|
...(receiverFieldName && { receiverRoot, receiverField: receiverFieldName }),
|
|
2189
2403
|
...(receiverFieldName && receiverRootType && { receiverRootType }),
|
|
2190
2404
|
...(receiverCall && { receiverCall }),
|
|
@@ -2315,6 +2529,7 @@ function findCallsInCode(code, parser) {
|
|
|
2315
2529
|
line: node.startPosition.row + 1,
|
|
2316
2530
|
isMethod: false,
|
|
2317
2531
|
isConstructor: true,
|
|
2532
|
+
...(isShadowedByLocal(ctorNode, ctorNode.text) && { localShadow: true }),
|
|
2318
2533
|
enclosingFunction
|
|
2319
2534
|
});
|
|
2320
2535
|
} else if (ctorNode.type === 'member_expression') {
|
|
@@ -2426,6 +2641,11 @@ function findCallsInCode(code, parser) {
|
|
|
2426
2641
|
localVarTypes.clear();
|
|
2427
2642
|
for (const [k, v] of saved) localVarTypes.set(k, v);
|
|
2428
2643
|
}
|
|
2644
|
+
const savedQualifiers = localVarTypeQualifiersStack.pop();
|
|
2645
|
+
if (savedQualifiers) {
|
|
2646
|
+
localVarTypeQualifiers.clear();
|
|
2647
|
+
for (const [k, v] of savedQualifiers) localVarTypeQualifiers.set(k, v);
|
|
2648
|
+
}
|
|
2429
2649
|
const savedDeclared = declaredTypeVarsStack.pop();
|
|
2430
2650
|
if (savedDeclared) {
|
|
2431
2651
|
declaredTypeVars.clear();
|
|
@@ -2733,11 +2953,17 @@ function findImportsInCode(code, parser) {
|
|
|
2733
2953
|
|
|
2734
2954
|
// Check parent for variable name
|
|
2735
2955
|
let parent = node.parent;
|
|
2956
|
+
let defaultLike = false;
|
|
2736
2957
|
if (parent && parent.type === 'variable_declarator') {
|
|
2737
2958
|
const nameNode = parent.childForFieldName('name');
|
|
2738
2959
|
if (nameNode) {
|
|
2739
2960
|
if (nameNode.type === 'identifier') {
|
|
2740
2961
|
names.push(nameNode.text);
|
|
2962
|
+
// `const app = require('./app')` binds the
|
|
2963
|
+
// value assigned to `module.exports`, not a
|
|
2964
|
+
// named property called `app`. Preserve that
|
|
2965
|
+
// distinction for exact import ownership.
|
|
2966
|
+
defaultLike = true;
|
|
2741
2967
|
} else if (nameNode.type === 'object_pattern') {
|
|
2742
2968
|
// Destructuring: const { a, b } = require('x')
|
|
2743
2969
|
for (let i = 0; i < nameNode.namedChildCount; i++) {
|
|
@@ -2762,6 +2988,7 @@ function findImportsInCode(code, parser) {
|
|
|
2762
2988
|
|
|
2763
2989
|
if (modulePath) {
|
|
2764
2990
|
imports.push({ module: modulePath, names, type: 'require', line, dynamic,
|
|
2991
|
+
...(defaultLike && { defaultLike: true }),
|
|
2765
2992
|
// Per-import rename pairing (fix #269): the flat
|
|
2766
2993
|
// importAliases list loses WHICH module a renamed
|
|
2767
2994
|
// name came from — `{ validate: validateSchema }`
|
|
@@ -3046,12 +3273,14 @@ function findUsagesInCode(code, name, parser, tree) {
|
|
|
3046
3273
|
|
|
3047
3274
|
visitNameNodes(tree, code, name, (node) => {
|
|
3048
3275
|
// Look for identifier, property_identifier (method names in obj.method() calls),
|
|
3276
|
+
// private_property_identifier (#method definitions and calls),
|
|
3049
3277
|
// type_identifier (TypeScript type annotations), shorthand_property_identifier_pattern
|
|
3050
3278
|
// (destructured names in `const { name } = require(...)`), and
|
|
3051
3279
|
// shorthand_property_identifier (value-position shorthand — CJS export
|
|
3052
3280
|
// objects `module.exports = { helper }` and option objects `f({ helper })`
|
|
3053
3281
|
// reference the symbol but produced no usage record at all, fix #241)
|
|
3054
3282
|
const isIdentifier = node.type === 'identifier' || node.type === 'property_identifier' ||
|
|
3283
|
+
node.type === 'private_property_identifier' ||
|
|
3055
3284
|
node.type === 'type_identifier' || node.type === 'shorthand_property_identifier_pattern' ||
|
|
3056
3285
|
node.type === 'shorthand_property_identifier';
|
|
3057
3286
|
if (!isIdentifier || node.text !== name) {
|
|
@@ -3155,17 +3384,11 @@ function findUsagesInCode(code, name, parser, tree) {
|
|
|
3155
3384
|
// Property access (method call): a.name() - the name after dot
|
|
3156
3385
|
else if (parent.type === 'member_expression' &&
|
|
3157
3386
|
sameNode(parent.childForFieldName('property'), node)) {
|
|
3158
|
-
//
|
|
3387
|
+
// Preserve the receiver and let the project-aware usage layer
|
|
3388
|
+
// decide ownership. A spelling such as `util` or `path` can be
|
|
3389
|
+
// either a standard module or a local project namespace, which
|
|
3390
|
+
// cannot be decided correctly from this file's AST alone.
|
|
3159
3391
|
const object = parent.childForFieldName('object');
|
|
3160
|
-
const builtins = [
|
|
3161
|
-
// JS built-in objects
|
|
3162
|
-
'JSON', 'Math', 'console', 'Object', 'Array', 'String', 'Number', 'Date', 'RegExp', 'Promise', 'Reflect', 'Proxy', 'Map', 'Set', 'WeakMap', 'WeakSet', 'Symbol', 'Intl', 'WebAssembly', 'Atomics', 'SharedArrayBuffer', 'ArrayBuffer', 'DataView', 'Int8Array', 'Uint8Array', 'Uint8ClampedArray', 'Int16Array', 'Uint16Array', 'Int32Array', 'Uint32Array', 'Float32Array', 'Float64Array', 'BigInt64Array', 'BigUint64Array', 'Error', 'EvalError', 'RangeError', 'ReferenceError', 'SyntaxError', 'TypeError', 'URIError', 'URL', 'URLSearchParams',
|
|
3163
|
-
// Node.js core modules
|
|
3164
|
-
'path', 'fs', 'os', 'http', 'https', 'net', 'dgram', 'dns', 'tls', 'crypto', 'zlib', 'stream', 'util', 'events', 'buffer', 'child_process', 'cluster', 'readline', 'repl', 'vm', 'assert', 'querystring', 'url', 'punycode', 'string_decoder', 'timers', 'tty', 'v8', 'perf_hooks', 'worker_threads', 'inspector', 'trace_events', 'async_hooks', 'process'
|
|
3165
|
-
];
|
|
3166
|
-
if (object && object.type === 'identifier' && builtins.includes(object.text)) {
|
|
3167
|
-
return true; // Skip built-in method calls
|
|
3168
|
-
}
|
|
3169
3392
|
// Check if this is a method call
|
|
3170
3393
|
const grandparent = parent.parent;
|
|
3171
3394
|
if (grandparent && grandparent.type === 'call_expression') {
|