ucn 4.2.2 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +89 -77
- package/.claude/skills/ucn/references/commands.md +62 -68
- package/.claude/skills/ucn/references/trust-contract.md +31 -6
- package/README.md +445 -300
- package/assets/demo.svg +31 -0
- package/cli/index.js +430 -1385
- package/core/account.js +144 -34
- package/core/analysis.js +182 -72
- package/core/ast-analysis.js +279 -0
- package/core/bridge.js +205 -24
- package/core/brief.js +27 -58
- package/core/build-worker.js +21 -131
- package/core/cache.js +533 -11
- package/core/callers.js +5533 -494
- package/core/check.js +13 -4
- package/core/command-contracts.js +402 -0
- package/core/compilation-database.js +276 -0
- package/core/confidence.js +4 -1
- package/core/deadcode.js +421 -20
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +204 -42
- package/core/execute.js +887 -81
- package/core/graph-build.js +162 -7
- package/core/graph.js +53 -77
- package/core/imports.js +65 -6
- package/core/index-ir.js +138 -0
- package/core/ir.js +195 -0
- package/core/output/analysis.js +216 -22
- package/core/output/brief.js +23 -0
- package/core/output/check.js +4 -0
- package/core/output/doctor.js +37 -6
- package/core/output/endpoints.js +5 -2
- package/core/output/extraction.js +24 -12
- package/core/output/find.js +141 -36
- package/core/output/graph.js +11 -5
- package/core/output/public.js +462 -0
- package/core/output/refactoring.js +42 -10
- package/core/output/reporting.js +97 -20
- package/core/output/search.js +24 -16
- package/core/output/shared.js +22 -1
- package/core/output/tracing.js +30 -15
- package/core/output-budget.js +295 -0
- package/core/output.js +1 -0
- package/core/parallel-build.js +44 -11
- package/core/parser.js +3 -3
- package/core/project.js +384 -177
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +371 -116
- package/core/semantic-provider.js +110 -0
- package/core/stacktrace.js +25 -0
- package/core/tracing.js +101 -51
- package/core/trust-matrix.js +19 -40
- package/core/verify.js +534 -37
- package/languages/adapter.js +218 -0
- package/languages/c-family.js +2791 -0
- package/languages/c.js +3 -0
- package/languages/cpp.js +3 -0
- package/languages/csharp.js +1402 -0
- package/languages/go.js +60 -21
- package/languages/html.js +2 -2
- package/languages/index.js +85 -7
- package/languages/java.js +428 -16
- package/languages/javascript.js +452 -49
- package/languages/python.js +1041 -32
- package/languages/rust.js +1415 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +41 -24
- package/eslint.config.js +0 -43
- package/jsconfig.json +0 -10
package/languages/javascript.js
CHANGED
|
@@ -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,6 +478,55 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
421
478
|
isGenerator: isGen,
|
|
422
479
|
isAsync,
|
|
423
480
|
modifiers,
|
|
481
|
+
...lexicalOwnerRange(node),
|
|
482
|
+
...typeAnno,
|
|
483
|
+
...(returnedConcreteType && { returnedConcreteType }),
|
|
484
|
+
...(generics && { generics }),
|
|
485
|
+
...(docstring && { docstring })
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Named function expressions used as callbacks have a real lexical
|
|
492
|
+
// definition even when no variable owns them: `test('x', function run()
|
|
493
|
+
// {})`. Property/variable assignments are handled by their binding
|
|
494
|
+
// branches below; indexing the expression name there as a second symbol
|
|
495
|
+
// would manufacture a duplicate public definition.
|
|
496
|
+
if (node.type === 'function_expression' || node.type === 'generator_function') {
|
|
497
|
+
const parent = node.parent;
|
|
498
|
+
const isBoundValue = (parent?.type === 'variable_declarator' &&
|
|
499
|
+
sameNode(parent.childForFieldName('value'), node)) ||
|
|
500
|
+
(parent?.type === 'assignment_expression' &&
|
|
501
|
+
sameNode(parent.childForFieldName('right'), node)) ||
|
|
502
|
+
(parent?.type === 'pair' && sameNode(parent.childForFieldName('value'), node));
|
|
503
|
+
const nameNode = node.childForFieldName('name');
|
|
504
|
+
if (!isBoundValue && nameNode && !processedRanges.has(rangeKey)) {
|
|
505
|
+
processedRanges.add(rangeKey);
|
|
506
|
+
const paramsNode = node.childForFieldName('parameters');
|
|
507
|
+
const { startLine, endLine, indent } = nodeToLocation(node, lines);
|
|
508
|
+
const returnType = extractReturnType(node);
|
|
509
|
+
const generics = extractGenerics(node);
|
|
510
|
+
const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
|
|
511
|
+
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
|
|
512
|
+
const docstring = extractJSDocstring(lines, startLine);
|
|
513
|
+
functions.push({
|
|
514
|
+
name: nameNode.text,
|
|
515
|
+
params: extractParams(paramsNode),
|
|
516
|
+
paramsStructured,
|
|
517
|
+
startLine,
|
|
518
|
+
endLine,
|
|
519
|
+
indent,
|
|
520
|
+
isArrow: false,
|
|
521
|
+
isGenerator: isGenerator(node),
|
|
522
|
+
isAsync: node.text.trimStart().startsWith('async '),
|
|
523
|
+
modifiers: [],
|
|
524
|
+
// ECMA-262: a FunctionExpression's BindingIdentifier is in
|
|
525
|
+
// scope only within its own body — the name creates no
|
|
526
|
+
// file-level binding (never enters the bindings table) and
|
|
527
|
+
// the expression is consumed where it appears (argument /
|
|
528
|
+
// value position), so deadcode never audits it.
|
|
529
|
+
bodyScopedName: true,
|
|
424
530
|
...typeAnno,
|
|
425
531
|
...(generics && { generics }),
|
|
426
532
|
...(docstring && { docstring })
|
|
@@ -481,7 +587,8 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
481
587
|
|
|
482
588
|
if (isArrow || isFnExpr) {
|
|
483
589
|
processedRanges.add(rangeKey);
|
|
484
|
-
const paramsNode = valueNode.childForFieldName('parameters')
|
|
590
|
+
const paramsNode = valueNode.childForFieldName('parameters') ||
|
|
591
|
+
valueNode.childForFieldName('parameter');
|
|
485
592
|
const { startLine, endLine, indent } = nodeToLocation(node, lines);
|
|
486
593
|
const returnType = extractReturnType(valueNode);
|
|
487
594
|
const generics = extractGenerics(valueNode);
|
|
@@ -613,7 +720,10 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
613
720
|
}
|
|
614
721
|
parent = parent.parent;
|
|
615
722
|
}
|
|
616
|
-
|
|
723
|
+
// A nested property assignment still defines that object's
|
|
724
|
+
// callable member (`reply.send = () => {}`). Only a nested bare
|
|
725
|
+
// assignment lacks a new symbol binding and stays excluded.
|
|
726
|
+
if (!isTopLevel && leftNode?.type !== 'member_expression') return true;
|
|
617
727
|
}
|
|
618
728
|
|
|
619
729
|
const rightNode = node.childForFieldName('right');
|
|
@@ -624,7 +734,16 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
624
734
|
rightNode.type === 'generator_function';
|
|
625
735
|
|
|
626
736
|
if (isArrow || isFnExpr) {
|
|
627
|
-
const
|
|
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);
|
|
628
747
|
if (name) {
|
|
629
748
|
processedRanges.add(rangeKey);
|
|
630
749
|
const paramsNode = rightNode.childForFieldName('parameters');
|
|
@@ -645,7 +764,7 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
645
764
|
indent,
|
|
646
765
|
isArrow,
|
|
647
766
|
isGenerator: isGen,
|
|
648
|
-
modifiers: [],
|
|
767
|
+
modifiers: isCommonJsDefault ? ['export'] : [],
|
|
649
768
|
// A property-assignment def (Reply.prototype.serialize
|
|
650
769
|
// = function, exports.h = () => ...) creates NO
|
|
651
770
|
// lexical name — a bare call in the file can never
|
|
@@ -812,18 +931,26 @@ function _processClass(node, classes, processedRanges, lines) {
|
|
|
812
931
|
if (nameNode) {
|
|
813
932
|
const { startLine, endLine } = nodeToLocation(node, lines);
|
|
814
933
|
const docstring = extractJSDocstring(lines, startLine);
|
|
934
|
+
const valueNode = node.childForFieldName('value');
|
|
815
935
|
// `type ZodTypeAny = ZodType<any, any, any>;` — the alias IS the
|
|
816
936
|
// aliased type. Record the base name so receivers annotated with
|
|
817
937
|
// the alias validate against the base type's methods (fix #208,
|
|
818
938
|
// TS parity with Rust/Go).
|
|
819
|
-
const aliasOf = aliasBaseTypeName(
|
|
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) : [];
|
|
820
947
|
|
|
821
948
|
classes.push({
|
|
822
949
|
name: nameNode.text,
|
|
823
950
|
startLine,
|
|
824
951
|
endLine,
|
|
825
952
|
type: 'type',
|
|
826
|
-
members
|
|
953
|
+
members,
|
|
827
954
|
...(aliasOf && { aliasOf }),
|
|
828
955
|
...(docstring && { docstring })
|
|
829
956
|
});
|
|
@@ -991,10 +1118,13 @@ function extractInterfaceExtends(interfaceNode) {
|
|
|
991
1118
|
* Extract interface members (method signatures, property signatures)
|
|
992
1119
|
*/
|
|
993
1120
|
function extractInterfaceMembers(interfaceNode, code) {
|
|
994
|
-
const members = [];
|
|
995
1121
|
const bodyNode = interfaceNode.childForFieldName('body');
|
|
996
|
-
if (!bodyNode) return
|
|
1122
|
+
if (!bodyNode) return [];
|
|
1123
|
+
return extractTypeMembers(bodyNode, code);
|
|
1124
|
+
}
|
|
997
1125
|
|
|
1126
|
+
function extractTypeMembers(bodyNode, code) {
|
|
1127
|
+
const members = [];
|
|
998
1128
|
for (let i = 0; i < bodyNode.namedChildCount; i++) {
|
|
999
1129
|
const child = bodyNode.namedChild(i);
|
|
1000
1130
|
|
|
@@ -1388,6 +1518,87 @@ function parse(code, parser) {
|
|
|
1388
1518
|
return true; // always continue, never skip subtrees
|
|
1389
1519
|
});
|
|
1390
1520
|
|
|
1521
|
+
// Some valid overload-heavy TypeScript files exceed the grammar's error
|
|
1522
|
+
// recovery budget. tree-sitter then returns a whole-file ERROR root and
|
|
1523
|
+
// flattens later declarations into unrelated type nodes without throwing.
|
|
1524
|
+
// Recover from AST tokens, not source patterns: top-level declaration
|
|
1525
|
+
// tokens define bounded fragments which are reparsed by the same grammar.
|
|
1526
|
+
// This keeps the AST-only contract while preventing a valid declaration
|
|
1527
|
+
// near the end of one difficult type file from disappearing silently.
|
|
1528
|
+
if (tree.rootNode.hasError) {
|
|
1529
|
+
const declarationTokens = [];
|
|
1530
|
+
const startsDeclaration = new Set([
|
|
1531
|
+
'export', 'declare', 'async', 'function', 'class', 'abstract',
|
|
1532
|
+
'interface', 'type', 'enum', 'namespace', 'module',
|
|
1533
|
+
'const', 'let', 'var'
|
|
1534
|
+
]);
|
|
1535
|
+
const stack = [tree.rootNode];
|
|
1536
|
+
while (stack.length > 0) {
|
|
1537
|
+
const node = stack.pop();
|
|
1538
|
+
if (node.childCount === 0) {
|
|
1539
|
+
// In severe recovery, a keyword itself can be downgraded to
|
|
1540
|
+
// an identifier token. Its AST position/text still supplies
|
|
1541
|
+
// a safe declaration boundary; semantic extraction remains
|
|
1542
|
+
// entirely delegated to the reparsed fragment.
|
|
1543
|
+
const recoveredKeyword = node.type === 'identifier' &&
|
|
1544
|
+
startsDeclaration.has(node.text);
|
|
1545
|
+
if (node.startPosition.column === 0 &&
|
|
1546
|
+
(startsDeclaration.has(node.type) || recoveredKeyword)) {
|
|
1547
|
+
declarationTokens.push(node);
|
|
1548
|
+
}
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
const children = node.children;
|
|
1552
|
+
for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
|
|
1553
|
+
}
|
|
1554
|
+
declarationTokens.sort((a, b) => a.startIndex - b.startIndex);
|
|
1555
|
+
|
|
1556
|
+
const recoveredFunctions = [], recoveredClasses = [], recoveredState = [];
|
|
1557
|
+
for (let i = 0; i < declarationTokens.length; i++) {
|
|
1558
|
+
const token = declarationTokens[i];
|
|
1559
|
+
const next = declarationTokens[i + 1];
|
|
1560
|
+
if (next && next.startIndex === token.startIndex) continue;
|
|
1561
|
+
const fragment = code.slice(token.startIndex, next?.startIndex ?? code.length);
|
|
1562
|
+
if (!fragment.trim()) continue;
|
|
1563
|
+
const fragmentTree = parseTree(parser, fragment);
|
|
1564
|
+
const fragmentLines = fragment.split('\n');
|
|
1565
|
+
const ff = [], fc = [], fs = [];
|
|
1566
|
+
const pf = new Set(), pc = new Set();
|
|
1567
|
+
traverseTreeCached(fragmentTree.rootNode, (node) => {
|
|
1568
|
+
_processFunction(node, ff, pf, fragmentLines);
|
|
1569
|
+
_processClass(node, fc, pc, fragmentLines);
|
|
1570
|
+
_processState(node, fs, fragmentLines);
|
|
1571
|
+
return true;
|
|
1572
|
+
});
|
|
1573
|
+
const lineOffset = token.startPosition.row;
|
|
1574
|
+
const shiftLines = (value) => {
|
|
1575
|
+
if (!value || typeof value !== 'object') return;
|
|
1576
|
+
if (Array.isArray(value)) {
|
|
1577
|
+
for (const item of value) shiftLines(item);
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1581
|
+
if (Number.isInteger(child) && /Line$/.test(key)) value[key] = child + lineOffset;
|
|
1582
|
+
else if (child && typeof child === 'object') shiftLines(child);
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1585
|
+
for (const item of ff) { shiftLines(item); recoveredFunctions.push(item); }
|
|
1586
|
+
for (const item of fc) { shiftLines(item); recoveredClasses.push(item); }
|
|
1587
|
+
for (const item of fs) { shiftLines(item); recoveredState.push(item); }
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
const mergeUnique = (target, additions, kind) => {
|
|
1591
|
+
const seen = new Set(target.map(item => `${kind}\0${item.name}\0${item.startLine}`));
|
|
1592
|
+
for (const item of additions) {
|
|
1593
|
+
const key = `${kind}\0${item.name}\0${item.startLine}`;
|
|
1594
|
+
if (!seen.has(key)) { seen.add(key); target.push(item); }
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
mergeUnique(functions, recoveredFunctions, 'function');
|
|
1598
|
+
mergeUnique(classes, recoveredClasses, 'class');
|
|
1599
|
+
mergeUnique(stateObjects, recoveredState, 'state');
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1391
1602
|
functions.sort((a, b) => a.startLine - b.startLine);
|
|
1392
1603
|
classes.sort((a, b) => a.startLine - b.startLine);
|
|
1393
1604
|
stateObjects.sort((a, b) => a.startLine - b.startLine);
|
|
@@ -1519,9 +1730,38 @@ function jsConstructorTypeName(ctorNode) {
|
|
|
1519
1730
|
return undefined;
|
|
1520
1731
|
}
|
|
1521
1732
|
|
|
1733
|
+
function jsConstructorTypeQualifier(ctorNode) {
|
|
1734
|
+
if (ctorNode?.type !== 'member_expression') return undefined;
|
|
1735
|
+
let root = ctorNode.childForFieldName('object');
|
|
1736
|
+
while (root?.type === 'member_expression') root = root.childForFieldName('object');
|
|
1737
|
+
return root?.type === 'identifier' ? root.text : undefined;
|
|
1738
|
+
}
|
|
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
|
+
|
|
1522
1754
|
function findCallsInCode(code, parser) {
|
|
1523
1755
|
const tree = parseTree(parser, code);
|
|
1524
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
|
+
});
|
|
1525
1765
|
const functionStack = []; // Stack of { name, startLine, endLine }
|
|
1526
1766
|
// Local aliases with lexical ownership. A flat aliasName→target map leaks
|
|
1527
1767
|
// block locals into the rest of a module (`let effect = batchedEffect`
|
|
@@ -1530,6 +1770,7 @@ function findCallsInCode(code, parser) {
|
|
|
1530
1770
|
const aliases = new Map(); // aliasName -> [{ target, declarationIndex, scopeStart, scopeEnd }]
|
|
1531
1771
|
const nonCallableNames = new Set(); // Track names assigned non-callable values
|
|
1532
1772
|
const localVarTypes = new Map(); // Track local variable types: varName -> typeName (for receiverType inference)
|
|
1773
|
+
const localVarTypeQualifiers = new Map(); // qualifier provenance for new ns.Type()
|
|
1533
1774
|
// Names whose type came from a DECLARED annotation (TS `x: Foo` / typed
|
|
1534
1775
|
// params). The compiler enforces assignability for these, so reassignment
|
|
1535
1776
|
// never stales them; inferred types (literal/new) DO stale and are
|
|
@@ -1538,6 +1779,7 @@ function findCallsInCode(code, parser) {
|
|
|
1538
1779
|
const declaredTypeVarsStack = [];
|
|
1539
1780
|
const moduleAliases = new Set(); // Names bound to MODULES (import * as ns / const pkg = require(...))
|
|
1540
1781
|
const localVarTypesStack = []; // Stack for function-scoped save/restore of localVarTypes
|
|
1782
|
+
const localVarTypeQualifiersStack = [];
|
|
1541
1783
|
|
|
1542
1784
|
// Helper: extract first string-arg literal from a call_expression node.
|
|
1543
1785
|
// Used by route extraction to capture path arg of fetch('/path'), app.get('/path', handler) etc.
|
|
@@ -1714,7 +1956,10 @@ function findCallsInCode(code, parser) {
|
|
|
1714
1956
|
// Helper to get current enclosing function
|
|
1715
1957
|
const getCurrentEnclosingFunction = () => {
|
|
1716
1958
|
return functionStack.length > 0
|
|
1717
|
-
? {
|
|
1959
|
+
? {
|
|
1960
|
+
...functionStack[functionStack.length - 1],
|
|
1961
|
+
scopeChain: functionStack.map(scope => scope.startLine),
|
|
1962
|
+
}
|
|
1718
1963
|
: null;
|
|
1719
1964
|
};
|
|
1720
1965
|
|
|
@@ -1750,20 +1995,31 @@ function findCallsInCode(code, parser) {
|
|
|
1750
1995
|
return best && best.target;
|
|
1751
1996
|
};
|
|
1752
1997
|
|
|
1753
|
-
|
|
1998
|
+
const _patternDeclaresName = (pattern, name) => {
|
|
1999
|
+
if (!pattern) return false;
|
|
2000
|
+
if ((pattern.type === 'identifier' ||
|
|
2001
|
+
pattern.type === 'shorthand_property_identifier_pattern') &&
|
|
2002
|
+
pattern.text === name) return true;
|
|
2003
|
+
if (pattern.type === 'pair_pattern' || pattern.type === 'pair') {
|
|
2004
|
+
return _patternDeclaresName(pattern.childForFieldName('value'), name);
|
|
2005
|
+
}
|
|
2006
|
+
if (pattern.type === 'assignment_pattern') {
|
|
2007
|
+
return _patternDeclaresName(
|
|
2008
|
+
pattern.childForFieldName('left') || pattern.childForFieldName('pattern'), name);
|
|
2009
|
+
}
|
|
2010
|
+
for (let i = 0; i < pattern.namedChildCount; i++) {
|
|
2011
|
+
if (_patternDeclaresName(pattern.namedChild(i), name)) return true;
|
|
2012
|
+
}
|
|
2013
|
+
return false;
|
|
2014
|
+
};
|
|
2015
|
+
|
|
2016
|
+
// fix #203: does a declaration node declare `name` (including nested destructuring)?
|
|
1754
2017
|
const _declaresName = (declNode, name) => {
|
|
1755
2018
|
for (let i = 0; i < declNode.namedChildCount; i++) {
|
|
1756
2019
|
const d = declNode.namedChild(i);
|
|
1757
2020
|
if (d.type !== 'variable_declarator') continue;
|
|
1758
2021
|
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
|
-
}
|
|
2022
|
+
if (_patternDeclaresName(nameNode, name)) return true;
|
|
1767
2023
|
}
|
|
1768
2024
|
return false;
|
|
1769
2025
|
};
|
|
@@ -1797,27 +2053,22 @@ function findCallsInCode(code, parser) {
|
|
|
1797
2053
|
_declaresName(init, name)) return true;
|
|
1798
2054
|
} else if (p.type === 'for_in_statement') {
|
|
1799
2055
|
const left = p.childForFieldName('left');
|
|
1800
|
-
if (left
|
|
2056
|
+
if (_patternDeclaresName(left, name)) return true;
|
|
1801
2057
|
if (left && (left.type === 'lexical_declaration' || left.type === 'variable_declaration') &&
|
|
1802
2058
|
_declaresName(left, name)) return true;
|
|
1803
2059
|
} else if (p.type === 'catch_clause') {
|
|
1804
2060
|
const param = p.childForFieldName('parameter');
|
|
1805
|
-
if (param
|
|
2061
|
+
if (_patternDeclaresName(param, name)) return true;
|
|
1806
2062
|
} else if (p.type === 'arrow_function' || p.type === 'function_expression' ||
|
|
1807
2063
|
p.type === 'function_declaration' || p.type === 'function' ||
|
|
1808
2064
|
p.type === 'method_definition' || p.type === 'generator_function' ||
|
|
1809
2065
|
p.type === 'generator_function_declaration') {
|
|
1810
2066
|
const params = p.childForFieldName('parameters') || p.childForFieldName('parameter');
|
|
1811
2067
|
if (params) {
|
|
1812
|
-
if (params
|
|
2068
|
+
if (_patternDeclaresName(params, name)) return true;
|
|
1813
2069
|
for (let i = 0; i < params.namedChildCount; i++) {
|
|
1814
2070
|
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
|
-
}
|
|
2071
|
+
if (_patternDeclaresName(prm, name)) return true;
|
|
1821
2072
|
}
|
|
1822
2073
|
}
|
|
1823
2074
|
}
|
|
@@ -1825,6 +2076,21 @@ function findCallsInCode(code, parser) {
|
|
|
1825
2076
|
return false;
|
|
1826
2077
|
};
|
|
1827
2078
|
|
|
2079
|
+
const isConditionalReassignment = node => {
|
|
2080
|
+
for (let p = node.parent; p && !isFunctionNode(p); p = p.parent) {
|
|
2081
|
+
if (p.type === 'if_statement') {
|
|
2082
|
+
const condition = p.childForFieldName('condition');
|
|
2083
|
+
if (!condition || node.startIndex < condition.startIndex ||
|
|
2084
|
+
node.endIndex > condition.endIndex) return true;
|
|
2085
|
+
}
|
|
2086
|
+
if (p.type === 'switch_case' || p.type === 'ternary_expression' ||
|
|
2087
|
+
p.type === 'for_statement' || p.type === 'for_in_statement' ||
|
|
2088
|
+
p.type === 'while_statement' || p.type === 'do_statement' ||
|
|
2089
|
+
p.type === 'catch_clause') return true;
|
|
2090
|
+
}
|
|
2091
|
+
return false;
|
|
2092
|
+
};
|
|
2093
|
+
|
|
1828
2094
|
traverseTree(tree.rootNode, (node) => {
|
|
1829
2095
|
// Track module-alias bindings: `import * as ns from "./m"` binds ns to a
|
|
1830
2096
|
// MODULE — method calls through it dispatch to module exports, never to
|
|
@@ -1843,6 +2109,7 @@ function findCallsInCode(code, parser) {
|
|
|
1843
2109
|
});
|
|
1844
2110
|
// Save localVarTypes so inner declarations don't leak to sibling functions
|
|
1845
2111
|
localVarTypesStack.push(new Map(localVarTypes));
|
|
2112
|
+
localVarTypeQualifiersStack.push(new Map(localVarTypeQualifiers));
|
|
1846
2113
|
declaredTypeVarsStack.push(new Set(declaredTypeVars));
|
|
1847
2114
|
}
|
|
1848
2115
|
|
|
@@ -1896,6 +2163,10 @@ function findCallsInCode(code, parser) {
|
|
|
1896
2163
|
const ctorName = jsConstructorTypeName(initNode.childForFieldName('constructor'));
|
|
1897
2164
|
if (ctorName) {
|
|
1898
2165
|
localVarTypes.set(nameNode.text, ctorName);
|
|
2166
|
+
const qualifier = jsConstructorTypeQualifier(
|
|
2167
|
+
initNode.childForFieldName('constructor'));
|
|
2168
|
+
if (qualifier) localVarTypeQualifiers.set(nameNode.text, qualifier);
|
|
2169
|
+
else localVarTypeQualifiers.delete(nameNode.text);
|
|
1899
2170
|
}
|
|
1900
2171
|
}
|
|
1901
2172
|
// Track TypeScript type annotations: const x: Foo = ...
|
|
@@ -1941,15 +2212,21 @@ function findCallsInCode(code, parser) {
|
|
|
1941
2212
|
if (right?.type === 'new_expression') {
|
|
1942
2213
|
nonCallableNames.add(left.text);
|
|
1943
2214
|
const ctorName = jsConstructorTypeName(right.childForFieldName('constructor'));
|
|
1944
|
-
if (ctorName) {
|
|
2215
|
+
if (ctorName && !isConditionalReassignment(node)) {
|
|
1945
2216
|
localVarTypes.set(left.text, ctorName);
|
|
2217
|
+
const qualifier = jsConstructorTypeQualifier(
|
|
2218
|
+
right.childForFieldName('constructor'));
|
|
2219
|
+
if (qualifier) localVarTypeQualifiers.set(left.text, qualifier);
|
|
2220
|
+
else localVarTypeQualifiers.delete(left.text);
|
|
1946
2221
|
} else if (!declaredTypeVars.has(left.text)) {
|
|
1947
2222
|
localVarTypes.delete(left.text);
|
|
2223
|
+
localVarTypeQualifiers.delete(left.text);
|
|
1948
2224
|
}
|
|
1949
2225
|
} else if (right && JS_LITERAL_ASSIGN_TYPES[right.type]) {
|
|
1950
2226
|
// Literal reassignment re-types the variable (fix #262)
|
|
1951
2227
|
if (!declaredTypeVars.has(left.text)) {
|
|
1952
2228
|
localVarTypes.set(left.text, JS_LITERAL_ASSIGN_TYPES[right.type]);
|
|
2229
|
+
localVarTypeQualifiers.delete(left.text);
|
|
1953
2230
|
}
|
|
1954
2231
|
} else if (localVarTypes.has(left.text) && !declaredTypeVars.has(left.text)) {
|
|
1955
2232
|
// Rebinding without a known type makes any previously
|
|
@@ -1957,6 +2234,7 @@ function findCallsInCode(code, parser) {
|
|
|
1957
2234
|
// semantics (#218d). Annotation-declared types survive:
|
|
1958
2235
|
// the TS compiler enforces assignability for those.
|
|
1959
2236
|
localVarTypes.delete(left.text);
|
|
2237
|
+
localVarTypeQualifiers.delete(left.text);
|
|
1960
2238
|
}
|
|
1961
2239
|
}
|
|
1962
2240
|
// Handler-registration references (fix #252, the #221 family's
|
|
@@ -2012,9 +2290,17 @@ function findCallsInCode(code, parser) {
|
|
|
2012
2290
|
|
|
2013
2291
|
// Handle regular function calls: foo(), obj.foo(), foo.call()
|
|
2014
2292
|
if (node.type === 'call_expression') {
|
|
2015
|
-
|
|
2293
|
+
let funcNode = node.childForFieldName('function');
|
|
2016
2294
|
if (!funcNode) return true;
|
|
2017
2295
|
|
|
2296
|
+
// tree-sitter-typescript represents `await obj.method<T>()` with
|
|
2297
|
+
// the await_expression inside the call's function field. Unwrap
|
|
2298
|
+
// it so generic awaited calls use the same AST call path as every
|
|
2299
|
+
// other method invocation.
|
|
2300
|
+
if (funcNode.type === 'await_expression' && funcNode.namedChildCount === 1) {
|
|
2301
|
+
funcNode = funcNode.namedChild(0);
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2018
2304
|
const enclosingFunction = getCurrentEnclosingFunction();
|
|
2019
2305
|
let uncertain = false;
|
|
2020
2306
|
// optional chaining implies possible non-call
|
|
@@ -2038,6 +2324,8 @@ function findCallsInCode(code, parser) {
|
|
|
2038
2324
|
...(resolvedName && { resolvedName }),
|
|
2039
2325
|
...(resolvedNames && { resolvedNames }),
|
|
2040
2326
|
line: node.startPosition.row + 1,
|
|
2327
|
+
callStart: node.startIndex,
|
|
2328
|
+
callEnd: node.endIndex,
|
|
2041
2329
|
isMethod: false,
|
|
2042
2330
|
...(assignedTo && { assignedTo }),
|
|
2043
2331
|
enclosingFunction,
|
|
@@ -2054,6 +2342,8 @@ function findCallsInCode(code, parser) {
|
|
|
2054
2342
|
calls.push({
|
|
2055
2343
|
name: 'constructor',
|
|
2056
2344
|
line: node.startPosition.row + 1,
|
|
2345
|
+
callStart: node.startIndex,
|
|
2346
|
+
callEnd: node.endIndex,
|
|
2057
2347
|
isMethod: true,
|
|
2058
2348
|
receiver: 'super',
|
|
2059
2349
|
argCount: node.childForFieldName('arguments')?.namedChildCount ?? 0,
|
|
@@ -2089,12 +2379,32 @@ function findCallsInCode(code, parser) {
|
|
|
2089
2379
|
const innerProp = objNode.childForFieldName('property');
|
|
2090
2380
|
const innerObj = objNode.childForFieldName('object');
|
|
2091
2381
|
if (innerProp) {
|
|
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);
|
|
2092
2393
|
calls.push({
|
|
2093
2394
|
name: innerProp.text,
|
|
2094
2395
|
line: node.startPosition.row + 1,
|
|
2095
2396
|
isMethod: true,
|
|
2096
2397
|
boundCall: true,
|
|
2097
|
-
receiver:
|
|
2398
|
+
receiver: boundReceiver,
|
|
2399
|
+
...(boundReceiverType && { receiverType: boundReceiverType }),
|
|
2400
|
+
...(innerObj?.type === 'identifier' &&
|
|
2401
|
+
localVarTypeQualifiers.has(innerObj.text) && {
|
|
2402
|
+
receiverTypeQualifier: localVarTypeQualifiers.get(innerObj.text),
|
|
2403
|
+
}),
|
|
2404
|
+
...(innerObj?.type === 'identifier' &&
|
|
2405
|
+
isShadowedByLocal(innerObj, innerObj.text) && {
|
|
2406
|
+
receiverLocalBinding: true,
|
|
2407
|
+
}),
|
|
2098
2408
|
enclosingFunction,
|
|
2099
2409
|
uncertain
|
|
2100
2410
|
});
|
|
@@ -2115,17 +2425,26 @@ function findCallsInCode(code, parser) {
|
|
|
2115
2425
|
// field's DECLARED type annotation. `this`-rooted hops
|
|
2116
2426
|
// resolve their root type query-side (the enclosing
|
|
2117
2427
|
// class); identifier roots type from local annotations.
|
|
2118
|
-
let receiverRoot, receiverFieldName, receiverRootType;
|
|
2428
|
+
let receiverRoot, receiverFieldName, receiverRootType, receiverBindingNode;
|
|
2429
|
+
let receiverDeepPath = false;
|
|
2430
|
+
if (receiver && objNode?.type === 'identifier') receiverBindingNode = objNode;
|
|
2119
2431
|
if (!receiver && objNode && objNode.type === 'member_expression') {
|
|
2120
2432
|
const rootNode = objNode.childForFieldName('object');
|
|
2121
2433
|
const fldNode = objNode.childForFieldName('property');
|
|
2122
2434
|
if (fldNode && rootNode &&
|
|
2123
2435
|
(rootNode.type === 'identifier' || rootNode.type === 'this')) {
|
|
2124
2436
|
receiverRoot = rootNode.text;
|
|
2437
|
+
receiverBindingNode = rootNode.type === 'identifier' ? rootNode : undefined;
|
|
2125
2438
|
receiverFieldName = fldNode.text;
|
|
2126
2439
|
if (rootNode.type === 'identifier') {
|
|
2127
2440
|
receiverRootType = localVarTypes.get(rootNode.text);
|
|
2128
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;
|
|
2129
2448
|
}
|
|
2130
2449
|
}
|
|
2131
2450
|
// Chained receiver (fix #219): the receiver IS a call —
|
|
@@ -2133,6 +2452,7 @@ function findCallsInCode(code, parser) {
|
|
|
2133
2452
|
// findCallers can type the receiver from its declared
|
|
2134
2453
|
// return annotation (Promise<...> → Promise).
|
|
2135
2454
|
let receiverCall, receiverCallIsMethod, receiverCallAwaited, receiverCallLine;
|
|
2455
|
+
let receiverCallStart, receiverCallEnd;
|
|
2136
2456
|
{
|
|
2137
2457
|
let recvNode = objNode;
|
|
2138
2458
|
if (recvNode && recvNode.type === 'parenthesized_expression') {
|
|
@@ -2149,6 +2469,8 @@ function findCallsInCode(code, parser) {
|
|
|
2149
2469
|
// Producer link (fix #258): plain-call
|
|
2150
2470
|
// records carry the call node's start line
|
|
2151
2471
|
receiverCallLine = recvNode.startPosition.row + 1;
|
|
2472
|
+
receiverCallStart = recvNode.startIndex;
|
|
2473
|
+
receiverCallEnd = recvNode.endIndex;
|
|
2152
2474
|
} else if (prodFunc?.type === 'member_expression') {
|
|
2153
2475
|
const prodProp = prodFunc.childForFieldName('property');
|
|
2154
2476
|
if (prodProp) {
|
|
@@ -2157,6 +2479,8 @@ function findCallsInCode(code, parser) {
|
|
|
2157
2479
|
// Method records report the property
|
|
2158
2480
|
// node's own line
|
|
2159
2481
|
receiverCallLine = prodProp.startPosition.row + 1;
|
|
2482
|
+
receiverCallStart = recvNode.startIndex;
|
|
2483
|
+
receiverCallEnd = recvNode.endIndex;
|
|
2160
2484
|
}
|
|
2161
2485
|
}
|
|
2162
2486
|
}
|
|
@@ -2164,13 +2488,25 @@ function findCallsInCode(code, parser) {
|
|
|
2164
2488
|
}
|
|
2165
2489
|
// Literal receivers carry their builtin type: [].map() can
|
|
2166
2490
|
// never be a project class method
|
|
2491
|
+
// A freshly constructed receiver has an exact runtime
|
|
2492
|
+
// type as well: new Service().start(). Recording it here
|
|
2493
|
+
// avoids treating the call as an untyped method dispatch.
|
|
2494
|
+
const constructedReceiverType = objNode?.type === 'new_expression'
|
|
2495
|
+
? jsConstructorTypeName(objNode.childForFieldName('constructor'))
|
|
2496
|
+
: undefined;
|
|
2497
|
+
const constructedReceiverQualifier = objNode?.type === 'new_expression'
|
|
2498
|
+
? jsConstructorTypeQualifier(objNode.childForFieldName('constructor'))
|
|
2499
|
+
: undefined;
|
|
2167
2500
|
const receiverType = receiver
|
|
2168
2501
|
? localVarTypes.get(receiver)
|
|
2169
|
-
: (
|
|
2502
|
+
: (constructedReceiverType ||
|
|
2503
|
+
(objNode ? JS_LITERAL_RECEIVER_TYPES[objNode.type] : undefined));
|
|
2170
2504
|
// Module receiver (ns.helper()) — unless locally shadowed
|
|
2171
2505
|
// by a typed instance binding
|
|
2172
|
-
const
|
|
2173
|
-
|
|
2506
|
+
const receiverModuleSpecifier = jsLiteralRequireModule(objNode);
|
|
2507
|
+
const receiverIsModule = !!receiverModuleSpecifier ||
|
|
2508
|
+
(!!receiver && moduleAliases.has(receiver) &&
|
|
2509
|
+
!localVarTypes.has(receiver));
|
|
2174
2510
|
const firstArg = getFirstStringArg(node);
|
|
2175
2511
|
const argCount = getArgCount(node);
|
|
2176
2512
|
const assignedTo = jsAssignmentTargetOf(node);
|
|
@@ -2181,16 +2517,33 @@ function findCallsInCode(code, parser) {
|
|
|
2181
2517
|
// line — the account's ground set is keyed by the
|
|
2182
2518
|
// name's line
|
|
2183
2519
|
line: propNode.startPosition.row + 1,
|
|
2520
|
+
callStart: node.startIndex,
|
|
2521
|
+
callEnd: node.endIndex,
|
|
2184
2522
|
isMethod: true,
|
|
2185
2523
|
receiver,
|
|
2186
2524
|
...(receiverType && { receiverType }),
|
|
2525
|
+
...((constructedReceiverQualifier ||
|
|
2526
|
+
(receiver && localVarTypeQualifiers.get(receiver))) && {
|
|
2527
|
+
receiverTypeQualifier: constructedReceiverQualifier ||
|
|
2528
|
+
localVarTypeQualifiers.get(receiver),
|
|
2529
|
+
}),
|
|
2187
2530
|
...(receiverIsModule && { receiverIsModule: true }),
|
|
2531
|
+
...(receiverModuleSpecifier && { receiverModuleSpecifier }),
|
|
2532
|
+
...(receiver && assignedMembers.has(`${receiver}.${propName}`) && {
|
|
2533
|
+
receiverMemberAssigned: true,
|
|
2534
|
+
}),
|
|
2535
|
+
...(receiverBindingNode &&
|
|
2536
|
+
isShadowedByLocal(receiverBindingNode, receiverBindingNode.text) &&
|
|
2537
|
+
{ receiverLocalBinding: true }),
|
|
2188
2538
|
...(receiverFieldName && { receiverRoot, receiverField: receiverFieldName }),
|
|
2189
2539
|
...(receiverFieldName && receiverRootType && { receiverRootType }),
|
|
2540
|
+
...(receiverDeepPath && { receiverDeepPath: true }),
|
|
2190
2541
|
...(receiverCall && { receiverCall }),
|
|
2191
2542
|
...(receiverCallIsMethod && { receiverCallIsMethod: true }),
|
|
2192
2543
|
...(receiverCallAwaited && { receiverCallAwaited: true }),
|
|
2193
2544
|
...(receiverCallLine && { receiverCallLine }),
|
|
2545
|
+
...(receiverCallStart != null && { receiverCallStart }),
|
|
2546
|
+
...(receiverCallEnd != null && { receiverCallEnd }),
|
|
2194
2547
|
...(assignedTo && { assignedTo }),
|
|
2195
2548
|
enclosingFunction,
|
|
2196
2549
|
uncertain,
|
|
@@ -2315,6 +2668,7 @@ function findCallsInCode(code, parser) {
|
|
|
2315
2668
|
line: node.startPosition.row + 1,
|
|
2316
2669
|
isMethod: false,
|
|
2317
2670
|
isConstructor: true,
|
|
2671
|
+
...(isShadowedByLocal(ctorNode, ctorNode.text) && { localShadow: true }),
|
|
2318
2672
|
enclosingFunction
|
|
2319
2673
|
});
|
|
2320
2674
|
} else if (ctorNode.type === 'member_expression') {
|
|
@@ -2426,6 +2780,11 @@ function findCallsInCode(code, parser) {
|
|
|
2426
2780
|
localVarTypes.clear();
|
|
2427
2781
|
for (const [k, v] of saved) localVarTypes.set(k, v);
|
|
2428
2782
|
}
|
|
2783
|
+
const savedQualifiers = localVarTypeQualifiersStack.pop();
|
|
2784
|
+
if (savedQualifiers) {
|
|
2785
|
+
localVarTypeQualifiers.clear();
|
|
2786
|
+
for (const [k, v] of savedQualifiers) localVarTypeQualifiers.set(k, v);
|
|
2787
|
+
}
|
|
2429
2788
|
const savedDeclared = declaredTypeVarsStack.pop();
|
|
2430
2789
|
if (savedDeclared) {
|
|
2431
2790
|
declaredTypeVars.clear();
|
|
@@ -2733,11 +3092,17 @@ function findImportsInCode(code, parser) {
|
|
|
2733
3092
|
|
|
2734
3093
|
// Check parent for variable name
|
|
2735
3094
|
let parent = node.parent;
|
|
3095
|
+
let defaultLike = false;
|
|
2736
3096
|
if (parent && parent.type === 'variable_declarator') {
|
|
2737
3097
|
const nameNode = parent.childForFieldName('name');
|
|
2738
3098
|
if (nameNode) {
|
|
2739
3099
|
if (nameNode.type === 'identifier') {
|
|
2740
3100
|
names.push(nameNode.text);
|
|
3101
|
+
// `const app = require('./app')` binds the
|
|
3102
|
+
// value assigned to `module.exports`, not a
|
|
3103
|
+
// named property called `app`. Preserve that
|
|
3104
|
+
// distinction for exact import ownership.
|
|
3105
|
+
defaultLike = true;
|
|
2741
3106
|
} else if (nameNode.type === 'object_pattern') {
|
|
2742
3107
|
// Destructuring: const { a, b } = require('x')
|
|
2743
3108
|
for (let i = 0; i < nameNode.namedChildCount; i++) {
|
|
@@ -2762,6 +3127,7 @@ function findImportsInCode(code, parser) {
|
|
|
2762
3127
|
|
|
2763
3128
|
if (modulePath) {
|
|
2764
3129
|
imports.push({ module: modulePath, names, type: 'require', line, dynamic,
|
|
3130
|
+
...(defaultLike && { defaultLike: true }),
|
|
2765
3131
|
// Per-import rename pairing (fix #269): the flat
|
|
2766
3132
|
// importAliases list loses WHICH module a renamed
|
|
2767
3133
|
// name came from — `{ validate: validateSchema }`
|
|
@@ -2985,10 +3351,16 @@ function findExportsInCode(code, parser) {
|
|
|
2985
3351
|
for (let i = 0; i < rightNode.namedChildCount; i++) {
|
|
2986
3352
|
const prop = rightNode.namedChild(i);
|
|
2987
3353
|
if (prop.type === 'shorthand_property_identifier') {
|
|
2988
|
-
exports.push({ name: prop.text, type: 'module.exports', line });
|
|
3354
|
+
exports.push({ name: prop.text, localName: prop.text, type: 'module.exports', line });
|
|
2989
3355
|
} else if (prop.type === 'pair') {
|
|
2990
3356
|
const key = prop.childForFieldName('key');
|
|
2991
|
-
|
|
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
|
+
});
|
|
2992
3364
|
} else if (prop.type === 'method_definition') {
|
|
2993
3365
|
// Shorthand methods are exports too
|
|
2994
3366
|
// (fix #252 — `module.exports =
|
|
@@ -2996,12 +3368,41 @@ function findExportsInCode(code, parser) {
|
|
|
2996
3368
|
// export list, so deadcode audited a
|
|
2997
3369
|
// require()-reachable function).
|
|
2998
3370
|
const mName = prop.childForFieldName('name');
|
|
2999
|
-
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
|
+
}
|
|
3000
3401
|
}
|
|
3001
3402
|
}
|
|
3002
3403
|
} else if (rightNode && rightNode.type === 'identifier') {
|
|
3003
3404
|
// module.exports = something
|
|
3004
|
-
exports.push({ name: rightNode.text, type: 'module.exports', line });
|
|
3405
|
+
exports.push({ name: rightNode.text, localName: rightNode.text, type: 'module.exports', line });
|
|
3005
3406
|
} else {
|
|
3006
3407
|
exports.push({ name: 'default', type: 'module.exports', line });
|
|
3007
3408
|
}
|
|
@@ -3011,7 +3412,13 @@ function findExportsInCode(code, parser) {
|
|
|
3011
3412
|
// exports.name = ...
|
|
3012
3413
|
if (objNode.text === 'exports') {
|
|
3013
3414
|
const line = node.startPosition.row + 1;
|
|
3014
|
-
|
|
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
|
+
});
|
|
3015
3422
|
return true;
|
|
3016
3423
|
}
|
|
3017
3424
|
|
|
@@ -3046,12 +3453,14 @@ function findUsagesInCode(code, name, parser, tree) {
|
|
|
3046
3453
|
|
|
3047
3454
|
visitNameNodes(tree, code, name, (node) => {
|
|
3048
3455
|
// Look for identifier, property_identifier (method names in obj.method() calls),
|
|
3456
|
+
// private_property_identifier (#method definitions and calls),
|
|
3049
3457
|
// type_identifier (TypeScript type annotations), shorthand_property_identifier_pattern
|
|
3050
3458
|
// (destructured names in `const { name } = require(...)`), and
|
|
3051
3459
|
// shorthand_property_identifier (value-position shorthand — CJS export
|
|
3052
3460
|
// objects `module.exports = { helper }` and option objects `f({ helper })`
|
|
3053
3461
|
// reference the symbol but produced no usage record at all, fix #241)
|
|
3054
3462
|
const isIdentifier = node.type === 'identifier' || node.type === 'property_identifier' ||
|
|
3463
|
+
node.type === 'private_property_identifier' ||
|
|
3055
3464
|
node.type === 'type_identifier' || node.type === 'shorthand_property_identifier_pattern' ||
|
|
3056
3465
|
node.type === 'shorthand_property_identifier';
|
|
3057
3466
|
if (!isIdentifier || node.text !== name) {
|
|
@@ -3155,17 +3564,11 @@ function findUsagesInCode(code, name, parser, tree) {
|
|
|
3155
3564
|
// Property access (method call): a.name() - the name after dot
|
|
3156
3565
|
else if (parent.type === 'member_expression' &&
|
|
3157
3566
|
sameNode(parent.childForFieldName('property'), node)) {
|
|
3158
|
-
//
|
|
3567
|
+
// Preserve the receiver and let the project-aware usage layer
|
|
3568
|
+
// decide ownership. A spelling such as `util` or `path` can be
|
|
3569
|
+
// either a standard module or a local project namespace, which
|
|
3570
|
+
// cannot be decided correctly from this file's AST alone.
|
|
3159
3571
|
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
3572
|
// Check if this is a method call
|
|
3170
3573
|
const grandparent = parent.parent;
|
|
3171
3574
|
if (grandparent && grandparent.type === 'call_expression') {
|