ucn 5.2.1 → 5.3.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 +49 -3
- package/.claude/skills/ucn/references/commands.md +3 -1
- package/README.md +158 -533
- package/cli/index.js +74 -10
- package/core/account.js +36 -8
- package/core/cache.js +167 -21
- package/core/callers.js +1229 -162
- package/core/execute.js +24 -6
- package/core/graph.js +167 -35
- package/core/index-ir.js +17 -12
- package/core/ir.js +56 -8
- package/core/output/graph.js +60 -11
- package/core/output/lines.js +259 -0
- package/core/output/public.js +15 -0
- package/core/output/reporting.js +9 -2
- package/core/output-budget.js +7 -4
- package/core/project.js +103 -5
- package/core/registry.js +7 -6
- package/core/reporting.js +159 -14
- package/languages/c-family.js +19 -17
- package/languages/go.js +170 -42
- package/languages/javascript.js +470 -15
- package/languages/python.js +678 -38
- package/languages/rust.js +1 -0
- package/mcp/server.js +3 -1
- package/package.json +2 -2
- package/assets/demo.svg +0 -31
package/languages/javascript.js
CHANGED
|
@@ -463,6 +463,55 @@ function returnsReceiverSelf(node) {
|
|
|
463
463
|
return sawSelf;
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
+
/**
|
|
467
|
+
* Exact call expression returned by an expression-bodied arrow. This is a
|
|
468
|
+
* syntax proof, not return-type inference: query-time flow resolves the call
|
|
469
|
+
* through its ordinary import/receiver ownership rails. Block bodies,
|
|
470
|
+
* conditionals, and other expressions deliberately stay unmarked.
|
|
471
|
+
*/
|
|
472
|
+
function returnedArrowCallSpan(node) {
|
|
473
|
+
if (node.type !== 'arrow_function') return null;
|
|
474
|
+
let body = node.childForFieldName('body');
|
|
475
|
+
while (body?.type === 'parenthesized_expression' && body.namedChildCount === 1) {
|
|
476
|
+
body = body.namedChild(0);
|
|
477
|
+
}
|
|
478
|
+
if (body?.type !== 'call_expression') return null;
|
|
479
|
+
return { start: body.startIndex, end: body.endIndex };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Exact `this.field...` value returned by a one-statement method body. The
|
|
484
|
+
* deliberately narrow shape proves both that the member path is returned and
|
|
485
|
+
* that no fallthrough/alternate return widens the compiler-inferred type.
|
|
486
|
+
*/
|
|
487
|
+
function returnedReceiverFieldPath(node) {
|
|
488
|
+
const body = node.childForFieldName('body');
|
|
489
|
+
if (!body || body.type !== 'statement_block' || body.namedChildCount !== 1) {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
const statement = body.namedChild(0);
|
|
493
|
+
if (statement?.type !== 'return_statement' || statement.namedChildCount !== 1) {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
let value = statement.namedChild(0);
|
|
497
|
+
while (value?.type === 'parenthesized_expression' && value.namedChildCount === 1) {
|
|
498
|
+
value = value.namedChild(0);
|
|
499
|
+
}
|
|
500
|
+
const fields = [];
|
|
501
|
+
while (value?.type === 'member_expression') {
|
|
502
|
+
const property = value.childForFieldName('property');
|
|
503
|
+
const object = value.childForFieldName('object');
|
|
504
|
+
if (!property || !object ||
|
|
505
|
+
!['property_identifier', 'private_property_identifier', 'identifier']
|
|
506
|
+
.includes(property.type)) {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
fields.unshift(property.text);
|
|
510
|
+
value = object;
|
|
511
|
+
}
|
|
512
|
+
return value?.type === 'this' && fields.length > 0 ? fields : null;
|
|
513
|
+
}
|
|
514
|
+
|
|
466
515
|
/**
|
|
467
516
|
* Process a node for function extraction (single-pass helper)
|
|
468
517
|
* Returns true if node was matched, false otherwise
|
|
@@ -634,6 +683,7 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
634
683
|
// declaration text, so we double-check the value node directly.
|
|
635
684
|
const valueIsAsync = valueNode.text.trimStart().startsWith('async ');
|
|
636
685
|
const isAsync = valueIsAsync || modifiers.includes('async');
|
|
686
|
+
const returnedCall = returnedArrowCallSpan(valueNode);
|
|
637
687
|
|
|
638
688
|
functions.push({
|
|
639
689
|
name: nameNode.text,
|
|
@@ -648,6 +698,10 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
648
698
|
modifiers,
|
|
649
699
|
...lexicalOwnerRange(node),
|
|
650
700
|
...typeAnno,
|
|
701
|
+
...(returnedCall && {
|
|
702
|
+
returnedCallStart: returnedCall.start,
|
|
703
|
+
returnedCallEnd: returnedCall.end,
|
|
704
|
+
}),
|
|
651
705
|
...(generics && { generics }),
|
|
652
706
|
...(docstring && { docstring })
|
|
653
707
|
});
|
|
@@ -1301,6 +1355,8 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
1301
1355
|
const isAsync = text.match(/^\s*(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:override\s+)?async\s/) !== null;
|
|
1302
1356
|
const returnType = extractReturnType(child) ||
|
|
1303
1357
|
(returnsReceiverSelf(child) ? 'this' : null);
|
|
1358
|
+
const returnedReceiverPath = !returnType
|
|
1359
|
+
? returnedReceiverFieldPath(child) : null;
|
|
1304
1360
|
const docstring = extractJSDocstring(code, startLine);
|
|
1305
1361
|
const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
|
|
1306
1362
|
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, code, startLine, true);
|
|
@@ -1329,6 +1385,7 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
1329
1385
|
// (fix #230) — pickBestDefinition prefers the implementation.
|
|
1330
1386
|
...(child.type === 'method_signature' && { isSignature: true }),
|
|
1331
1387
|
...typeAnno,
|
|
1388
|
+
...(returnedReceiverPath && { returnedReceiverPath }),
|
|
1332
1389
|
...(docstring && { docstring }),
|
|
1333
1390
|
...(decorators.length > 0 && { decorators }),
|
|
1334
1391
|
...(decoratorsWithArgs.length > 0 && { decoratorsWithArgs })
|
|
@@ -1467,6 +1524,14 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
1467
1524
|
const fieldTypeNode = child.childForFieldName('type');
|
|
1468
1525
|
const fieldType = fieldTypeNode
|
|
1469
1526
|
? fieldTypeNode.text.replace(/^:\s*/, '').trim() : undefined;
|
|
1527
|
+
// A direct identifier initializer on a static field keeps
|
|
1528
|
+
// the lexical callable identity available to the IR:
|
|
1529
|
+
// `static create = createSchema`. Resolution remains
|
|
1530
|
+
// same-file and overload-disciplined in createFileIR;
|
|
1531
|
+
// expressions, member accesses, and instance fields do
|
|
1532
|
+
// not receive this proof marker.
|
|
1533
|
+
const callableTarget = isStatic && valueNode?.type === 'identifier'
|
|
1534
|
+
? valueNode.text : undefined;
|
|
1470
1535
|
members.push({
|
|
1471
1536
|
name,
|
|
1472
1537
|
startLine,
|
|
@@ -1474,6 +1539,7 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
1474
1539
|
memberType: name.startsWith('#') ? 'private field' : 'field',
|
|
1475
1540
|
...(isStatic && { modifiers: ['static'] }),
|
|
1476
1541
|
...(fieldType && { fieldType }),
|
|
1542
|
+
...(callableTarget && { callableTarget }),
|
|
1477
1543
|
...(fieldDecorators.length > 0 && { decorators: fieldDecorators })
|
|
1478
1544
|
// Not a method - regular field
|
|
1479
1545
|
});
|
|
@@ -1920,13 +1986,120 @@ function findCallsInCode(code, parser) {
|
|
|
1920
1986
|
const tree = parseTree(parser, code);
|
|
1921
1987
|
const calls = [];
|
|
1922
1988
|
const assignedMembers = new Set();
|
|
1989
|
+
const mutatedObjectRoots = new Set();
|
|
1990
|
+
const moduleCompositions = new Map();
|
|
1991
|
+
const unsafeModuleCompositions = new Set();
|
|
1992
|
+
const namespaceAliases = new Set();
|
|
1993
|
+
const accessRoot = (node) => {
|
|
1994
|
+
let current = node;
|
|
1995
|
+
while (current && (current.type === 'member_expression' ||
|
|
1996
|
+
current.type === 'subscript_expression')) {
|
|
1997
|
+
current = current.childForFieldName('object');
|
|
1998
|
+
}
|
|
1999
|
+
return current?.type === 'identifier' ? current.text : undefined;
|
|
2000
|
+
};
|
|
1923
2001
|
traverseTreeCached(tree.rootNode, node => {
|
|
1924
|
-
if (node.type
|
|
1925
|
-
node.
|
|
1926
|
-
|
|
1927
|
-
|
|
2002
|
+
if (node.type === 'namespace_import') {
|
|
2003
|
+
const identifier = node.namedChild(0);
|
|
2004
|
+
if (identifier?.type === 'identifier') namespaceAliases.add(identifier.text);
|
|
2005
|
+
}
|
|
2006
|
+
if (node.type === 'variable_declarator') {
|
|
2007
|
+
const declaration = node.parent;
|
|
2008
|
+
const nameNode = node.childForFieldName('name');
|
|
2009
|
+
const valueNode = node.childForFieldName('value');
|
|
2010
|
+
if (nameNode?.type === 'identifier' && valueNode?.type === 'object' &&
|
|
2011
|
+
declaration?.type === 'lexical_declaration' &&
|
|
2012
|
+
declaration.child(0)?.text === 'const' && isModuleScope(declaration)) {
|
|
2013
|
+
const layers = [];
|
|
2014
|
+
let spreadCandidates = 0;
|
|
2015
|
+
for (let i = 0; i < valueNode.namedChildCount; i++) {
|
|
2016
|
+
const item = valueNode.namedChild(i);
|
|
2017
|
+
if (item.type === 'spread_element') {
|
|
2018
|
+
const value = item.namedChild(0);
|
|
2019
|
+
if (value?.type === 'identifier') {
|
|
2020
|
+
// Namespace imports may legally appear later in
|
|
2021
|
+
// the module. Resolve candidates after this pass
|
|
2022
|
+
// has seen the complete import surface.
|
|
2023
|
+
layers.push({ kind: 'spread-candidate', receiver: value.text });
|
|
2024
|
+
spreadCandidates++;
|
|
2025
|
+
} else {
|
|
2026
|
+
layers.push({ kind: 'unknown' });
|
|
2027
|
+
}
|
|
2028
|
+
continue;
|
|
2029
|
+
}
|
|
2030
|
+
if (item.type === 'pair') {
|
|
2031
|
+
const key = item.childForFieldName('key');
|
|
2032
|
+
const staticKey = key && ['property_identifier', 'identifier', 'string']
|
|
2033
|
+
.includes(key.type)
|
|
2034
|
+
? key.text.replace(/^['"]|['"]$/g, '') : null;
|
|
2035
|
+
layers.push(staticKey
|
|
2036
|
+
? { kind: 'property', name: staticKey }
|
|
2037
|
+
: { kind: 'unknown' });
|
|
2038
|
+
continue;
|
|
2039
|
+
}
|
|
2040
|
+
if (item.type === 'shorthand_property_identifier' ||
|
|
2041
|
+
item.type === 'method_definition') {
|
|
2042
|
+
const name = item.type === 'method_definition'
|
|
2043
|
+
? item.childForFieldName('name')?.text : item.text;
|
|
2044
|
+
layers.push(name
|
|
2045
|
+
? { kind: 'property', name }
|
|
2046
|
+
: { kind: 'unknown' });
|
|
2047
|
+
continue;
|
|
2048
|
+
}
|
|
2049
|
+
layers.push({ kind: 'unknown' });
|
|
2050
|
+
}
|
|
2051
|
+
if (spreadCandidates > 0) moduleCompositions.set(nameNode.text, layers);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
if (node.type === 'assignment_expression' ||
|
|
2055
|
+
node.type === 'augmented_assignment_expression') {
|
|
2056
|
+
const left = node.childForFieldName('left');
|
|
2057
|
+
if (left?.type === 'member_expression') assignedMembers.add(left.text);
|
|
2058
|
+
const root = accessRoot(left);
|
|
2059
|
+
if (root) mutatedObjectRoots.add(root);
|
|
2060
|
+
}
|
|
2061
|
+
// A namespace-spread composite is exact only while the ordinary
|
|
2062
|
+
// object remains private and unmodified. Any use of the object value
|
|
2063
|
+
// itself (export, alias, return, argument, spread, etc.) can expose a
|
|
2064
|
+
// mutation; property reads/calls are the sole accepted uses.
|
|
2065
|
+
if (node.type !== 'identifier' || !moduleCompositions.has(node.text)) return true;
|
|
2066
|
+
const parent = node.parent;
|
|
2067
|
+
if (parent?.type === 'variable_declarator' &&
|
|
2068
|
+
parent.childForFieldName('name')?.id === node.id) return true;
|
|
2069
|
+
if ((parent?.type === 'member_expression' ||
|
|
2070
|
+
parent?.type === 'subscript_expression') &&
|
|
2071
|
+
parent.childForFieldName('object')?.id === node.id) {
|
|
2072
|
+
let access = parent;
|
|
2073
|
+
while ((access.parent?.type === 'member_expression' ||
|
|
2074
|
+
access.parent?.type === 'subscript_expression') &&
|
|
2075
|
+
access.parent.childForFieldName('object')?.id === access.id) {
|
|
2076
|
+
access = access.parent;
|
|
2077
|
+
}
|
|
2078
|
+
const container = access.parent;
|
|
2079
|
+
const assigned = (container?.type === 'assignment_expression' ||
|
|
2080
|
+
container?.type === 'augmented_assignment_expression') &&
|
|
2081
|
+
container.childForFieldName('left')?.id === access.id;
|
|
2082
|
+
const updated = container?.type === 'update_expression';
|
|
2083
|
+
const deleted = container?.type === 'unary_expression' &&
|
|
2084
|
+
container.child(0)?.text === 'delete';
|
|
2085
|
+
if (!assigned && !updated && !deleted) return true;
|
|
2086
|
+
}
|
|
2087
|
+
unsafeModuleCompositions.add(node.text);
|
|
1928
2088
|
return true;
|
|
1929
2089
|
});
|
|
2090
|
+
for (const [name, layers] of moduleCompositions) {
|
|
2091
|
+
const normalized = layers.map(layer => layer.kind === 'spread-candidate'
|
|
2092
|
+
? (namespaceAliases.has(layer.receiver)
|
|
2093
|
+
? { kind: 'spread', receiver: layer.receiver }
|
|
2094
|
+
: { kind: 'unknown' })
|
|
2095
|
+
: layer);
|
|
2096
|
+
if (normalized.some(layer => layer.kind === 'spread')) {
|
|
2097
|
+
moduleCompositions.set(name, normalized);
|
|
2098
|
+
} else {
|
|
2099
|
+
moduleCompositions.delete(name);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
for (const name of mutatedObjectRoots) unsafeModuleCompositions.add(name);
|
|
1930
2103
|
const functionStack = []; // Stack of { name, startLine, endLine }
|
|
1931
2104
|
// Local aliases with lexical ownership. A flat aliasName→target map leaks
|
|
1932
2105
|
// block locals into the rest of a module (`let effect = batchedEffect`
|
|
@@ -2162,6 +2335,16 @@ function findCallsInCode(code, parser) {
|
|
|
2162
2335
|
|
|
2163
2336
|
const _patternDeclaresName = (pattern, name) => {
|
|
2164
2337
|
if (!pattern) return false;
|
|
2338
|
+
// TypeScript parameter wrappers contain both the runtime binding
|
|
2339
|
+
// pattern and a type annotation. Only the pattern declares names:
|
|
2340
|
+
// `metadata: registries.GlobalMeta` must not make the namespace
|
|
2341
|
+
// identifier `registries` look like a shadowing parameter.
|
|
2342
|
+
if (pattern.type === 'required_parameter' ||
|
|
2343
|
+
pattern.type === 'optional_parameter') {
|
|
2344
|
+
return _patternDeclaresName(
|
|
2345
|
+
pattern.childForFieldName('pattern') ||
|
|
2346
|
+
pattern.childForFieldName('name'), name);
|
|
2347
|
+
}
|
|
2165
2348
|
if ((pattern.type === 'identifier' ||
|
|
2166
2349
|
pattern.type === 'shorthand_property_identifier_pattern') &&
|
|
2167
2350
|
pattern.text === name) return true;
|
|
@@ -2188,6 +2371,94 @@ function findCallsInCode(code, parser) {
|
|
|
2188
2371
|
}
|
|
2189
2372
|
return false;
|
|
2190
2373
|
};
|
|
2374
|
+
// fix #337: a declarator initialized from require()/import() is an
|
|
2375
|
+
// IMPORT binding — the module's own name reaching this scope — not a
|
|
2376
|
+
// local shadow. `function build() { const { Foo } = require('./lib');
|
|
2377
|
+
// new Foo() }` resolves through import ownership exactly like the
|
|
2378
|
+
// top-level require the walk already exempts as the module binding
|
|
2379
|
+
// itself. Unwraps `await import()`, parens, and `require('./x').Foo`.
|
|
2380
|
+
const _isImportBindingInitializer = (value) => {
|
|
2381
|
+
let v = value;
|
|
2382
|
+
for (;;) {
|
|
2383
|
+
if (!v) return false;
|
|
2384
|
+
if (v.type === 'await_expression' || v.type === 'parenthesized_expression') {
|
|
2385
|
+
v = v.namedChild(0);
|
|
2386
|
+
continue;
|
|
2387
|
+
}
|
|
2388
|
+
if (v.type === 'member_expression' || v.type === 'subscript_expression') {
|
|
2389
|
+
v = v.childForFieldName('object');
|
|
2390
|
+
continue;
|
|
2391
|
+
}
|
|
2392
|
+
break;
|
|
2393
|
+
}
|
|
2394
|
+
if (v.type !== 'call_expression') return false;
|
|
2395
|
+
const fn = v.childForFieldName('function');
|
|
2396
|
+
return !!fn && (fn.type === 'import' || (fn.type === 'identifier' && fn.text === 'require'));
|
|
2397
|
+
};
|
|
2398
|
+
const _declaresLocalShadow = (declNode, name) => {
|
|
2399
|
+
for (let i = 0; i < declNode.namedChildCount; i++) {
|
|
2400
|
+
const d = declNode.namedChild(i);
|
|
2401
|
+
if (d.type !== 'variable_declarator') continue;
|
|
2402
|
+
if (!_patternDeclaresName(d.childForFieldName('name'), name)) continue;
|
|
2403
|
+
if (_isImportBindingInitializer(d.childForFieldName('value'))) continue;
|
|
2404
|
+
return true;
|
|
2405
|
+
}
|
|
2406
|
+
return false;
|
|
2407
|
+
};
|
|
2408
|
+
|
|
2409
|
+
// Bare callback references need to distinguish a module-owned VALUE from
|
|
2410
|
+
// an unbound name. File-level import reachability cannot prove the value's
|
|
2411
|
+
// identity: `const app = express(); use(app)` may live in a file that also
|
|
2412
|
+
// imports the pinned target, but `app` is the factory result rather than a
|
|
2413
|
+
// direct lexical reference to that target. Keep the parser evidence exact
|
|
2414
|
+
// and cheap by collecting only declarations whose lexical owner is the
|
|
2415
|
+
// program/module root (export wrappers included).
|
|
2416
|
+
const moduleValueBindings = new Set();
|
|
2417
|
+
const collectPatternNames = (pattern) => {
|
|
2418
|
+
if (!pattern) return;
|
|
2419
|
+
if (pattern.type === 'identifier' ||
|
|
2420
|
+
pattern.type === 'shorthand_property_identifier_pattern') {
|
|
2421
|
+
moduleValueBindings.add(pattern.text);
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
if (pattern.type === 'pair_pattern' || pattern.type === 'pair') {
|
|
2425
|
+
collectPatternNames(pattern.childForFieldName('value'));
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
if (pattern.type === 'assignment_pattern') {
|
|
2429
|
+
collectPatternNames(
|
|
2430
|
+
pattern.childForFieldName('left') || pattern.childForFieldName('pattern'));
|
|
2431
|
+
return;
|
|
2432
|
+
}
|
|
2433
|
+
for (let i = 0; i < pattern.namedChildCount; i++) {
|
|
2434
|
+
collectPatternNames(pattern.namedChild(i));
|
|
2435
|
+
}
|
|
2436
|
+
};
|
|
2437
|
+
const collectModuleDeclaration = (statement) => {
|
|
2438
|
+
let declaration = statement;
|
|
2439
|
+
if (statement.type === 'export_statement') {
|
|
2440
|
+
declaration = null;
|
|
2441
|
+
for (let i = 0; i < statement.namedChildCount; i++) {
|
|
2442
|
+
const child = statement.namedChild(i);
|
|
2443
|
+
if (child.type === 'lexical_declaration' || child.type === 'variable_declaration') {
|
|
2444
|
+
declaration = child;
|
|
2445
|
+
break;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
if (!declaration ||
|
|
2450
|
+
(declaration.type !== 'lexical_declaration' &&
|
|
2451
|
+
declaration.type !== 'variable_declaration')) return;
|
|
2452
|
+
for (let i = 0; i < declaration.namedChildCount; i++) {
|
|
2453
|
+
const declarator = declaration.namedChild(i);
|
|
2454
|
+
if (declarator.type === 'variable_declarator') {
|
|
2455
|
+
collectPatternNames(declarator.childForFieldName('name'));
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
};
|
|
2459
|
+
for (let i = 0; i < tree.rootNode.namedChildCount; i++) {
|
|
2460
|
+
collectModuleDeclaration(tree.rootNode.namedChild(i));
|
|
2461
|
+
}
|
|
2191
2462
|
|
|
2192
2463
|
// fix #203: is a bare-identifier function REFERENCE shadowed by a
|
|
2193
2464
|
// let/const/var local, for/catch binding, or inner-arrow param in an
|
|
@@ -2210,7 +2481,7 @@ function findCallsInCode(code, parser) {
|
|
|
2210
2481
|
stmt.childForFieldName('name')?.text === name) return true;
|
|
2211
2482
|
if (stmt.startIndex >= refNode.startIndex) continue; // declaration-before-use
|
|
2212
2483
|
if ((stmt.type === 'lexical_declaration' || stmt.type === 'variable_declaration') &&
|
|
2213
|
-
|
|
2484
|
+
_declaresLocalShadow(stmt, name)) return true;
|
|
2214
2485
|
}
|
|
2215
2486
|
} else if (p.type === 'for_statement') {
|
|
2216
2487
|
const init = p.childForFieldName('initializer');
|
|
@@ -2241,6 +2512,11 @@ function findCallsInCode(code, parser) {
|
|
|
2241
2512
|
return false;
|
|
2242
2513
|
};
|
|
2243
2514
|
|
|
2515
|
+
const bareReferenceBindingFields = (refNode) => ({
|
|
2516
|
+
...(isShadowedByLocal(refNode, refNode.text) && { localShadow: true }),
|
|
2517
|
+
...(moduleValueBindings.has(refNode.text) && { moduleLocalBinding: true }),
|
|
2518
|
+
});
|
|
2519
|
+
|
|
2244
2520
|
const isConditionalReassignment = node => {
|
|
2245
2521
|
for (let p = node.parent; p && !isFunctionNode(p); p = p.parent) {
|
|
2246
2522
|
if (p.type === 'if_statement') {
|
|
@@ -2429,7 +2705,7 @@ function findCallsInCode(code, parser) {
|
|
|
2429
2705
|
isMethod: false,
|
|
2430
2706
|
isFunctionReference: true,
|
|
2431
2707
|
isPotentialCallback: true,
|
|
2432
|
-
...(
|
|
2708
|
+
...bareReferenceBindingFields(right),
|
|
2433
2709
|
enclosingFunction: getCurrentEnclosingFunction(),
|
|
2434
2710
|
});
|
|
2435
2711
|
}
|
|
@@ -2696,6 +2972,9 @@ function findCallsInCode(code, parser) {
|
|
|
2696
2972
|
const receiverIsModule = !!receiverModuleSpecifier ||
|
|
2697
2973
|
(!!receiver && moduleAliases.has(receiver) &&
|
|
2698
2974
|
!localVarTypes.has(receiver));
|
|
2975
|
+
const receiverModuleComposition = receiver &&
|
|
2976
|
+
!unsafeModuleCompositions.has(receiver)
|
|
2977
|
+
? moduleCompositions.get(receiver) : undefined;
|
|
2699
2978
|
const firstArg = getFirstStringArg(node);
|
|
2700
2979
|
const argCount = getArgCount(node);
|
|
2701
2980
|
let assignedTo = jsAssignmentTargetOf(node);
|
|
@@ -2730,6 +3009,9 @@ function findCallsInCode(code, parser) {
|
|
|
2730
3009
|
}),
|
|
2731
3010
|
...(receiverIsModule && { receiverIsModule: true }),
|
|
2732
3011
|
...(receiverModuleSpecifier && { receiverModuleSpecifier }),
|
|
3012
|
+
...(receiverModuleComposition && {
|
|
3013
|
+
receiverModuleComposition,
|
|
3014
|
+
}),
|
|
2733
3015
|
...(receiver && assignedMembers.has(`${receiver}.${propName}`) && {
|
|
2734
3016
|
receiverMemberAssigned: true,
|
|
2735
3017
|
}),
|
|
@@ -2787,7 +3069,7 @@ function findCallsInCode(code, parser) {
|
|
|
2787
3069
|
line: arg.startPosition.row + 1,
|
|
2788
3070
|
isMethod: false,
|
|
2789
3071
|
isFunctionReference: true,
|
|
2790
|
-
...(
|
|
3072
|
+
...bareReferenceBindingFields(arg),
|
|
2791
3073
|
enclosingFunction
|
|
2792
3074
|
});
|
|
2793
3075
|
} else if (arg.type === 'member_expression') {
|
|
@@ -2832,7 +3114,7 @@ function findCallsInCode(code, parser) {
|
|
|
2832
3114
|
isMethod: false,
|
|
2833
3115
|
isFunctionReference: true,
|
|
2834
3116
|
isPotentialCallback: true,
|
|
2835
|
-
...(
|
|
3117
|
+
...bareReferenceBindingFields(arg),
|
|
2836
3118
|
enclosingFunction
|
|
2837
3119
|
});
|
|
2838
3120
|
}
|
|
@@ -2873,7 +3155,7 @@ function findCallsInCode(code, parser) {
|
|
|
2873
3155
|
isMethod: false,
|
|
2874
3156
|
isFunctionReference: true,
|
|
2875
3157
|
isPotentialCallback: true,
|
|
2876
|
-
...(
|
|
3158
|
+
...bareReferenceBindingFields(val),
|
|
2877
3159
|
enclosingFunction
|
|
2878
3160
|
});
|
|
2879
3161
|
}
|
|
@@ -2977,7 +3259,7 @@ function findCallsInCode(code, parser) {
|
|
|
2977
3259
|
isMethod: false,
|
|
2978
3260
|
isFunctionReference: true,
|
|
2979
3261
|
isPotentialCallback: true,
|
|
2980
|
-
...(
|
|
3262
|
+
...bareReferenceBindingFields(child),
|
|
2981
3263
|
enclosingFunction
|
|
2982
3264
|
});
|
|
2983
3265
|
} else if (child.type === 'member_expression') {
|
|
@@ -3190,6 +3472,144 @@ function findImportsInCode(code, parser) {
|
|
|
3190
3472
|
const imports = [];
|
|
3191
3473
|
let importAliases = null; // {original, local}[] — tracks renamed imports
|
|
3192
3474
|
|
|
3475
|
+
// fix #338: classify edges that do not execute during module
|
|
3476
|
+
// initialization so dependency-cycle reporting can separate an eager
|
|
3477
|
+
// import-time loop from a deliberate lazy one. `require()`/`import()`
|
|
3478
|
+
// nested in any function body (incl. `() => require('./x')` thunks) runs
|
|
3479
|
+
// only when that function is called; TS `import type` / `export type`
|
|
3480
|
+
// re-exports and all-`type` specifier lists are erased at compile time.
|
|
3481
|
+
const FUNCTION_LIKE = new Set(['function_declaration', 'function_expression', 'arrow_function',
|
|
3482
|
+
'method_definition', 'generator_function_declaration', 'generator_function', 'function']);
|
|
3483
|
+
const importDeferral = (node) => {
|
|
3484
|
+
for (let p = node.parent; p; p = p.parent) {
|
|
3485
|
+
if (FUNCTION_LIKE.has(p.type)) return 'function-local';
|
|
3486
|
+
}
|
|
3487
|
+
return null;
|
|
3488
|
+
};
|
|
3489
|
+
// Static path folding is positive identity evidence. A method merely
|
|
3490
|
+
// named join/resolve need not be Node's path utility. Keep an ambiguous
|
|
3491
|
+
// or shadowed binding dynamic rather than inventing a module edge.
|
|
3492
|
+
let pathBindings = null;
|
|
3493
|
+
const isPathModuleCall = node => {
|
|
3494
|
+
if (node?.type !== 'call_expression') return false;
|
|
3495
|
+
const fn = node.childForFieldName('function');
|
|
3496
|
+
const args = node.childForFieldName('arguments');
|
|
3497
|
+
const arg = args?.namedChild(0);
|
|
3498
|
+
return fn?.type === 'identifier' && fn.text === 'require' &&
|
|
3499
|
+
args.namedChildCount === 1 && arg?.type === 'string' &&
|
|
3500
|
+
['path', 'node:path'].includes(arg.text.slice(1, -1));
|
|
3501
|
+
};
|
|
3502
|
+
const collectPathBindings = () => {
|
|
3503
|
+
if (pathBindings) return;
|
|
3504
|
+
pathBindings = new Map();
|
|
3505
|
+
const add = (pattern, value) => {
|
|
3506
|
+
if (!pattern) return;
|
|
3507
|
+
traverseTree(pattern, id => {
|
|
3508
|
+
if (id.type === 'identifier' || id.type === 'shorthand_property_identifier_pattern') {
|
|
3509
|
+
const entries = pathBindings.get(id.text) || [];
|
|
3510
|
+
entries.push({ pattern, value });
|
|
3511
|
+
pathBindings.set(id.text, entries);
|
|
3512
|
+
}
|
|
3513
|
+
return true;
|
|
3514
|
+
});
|
|
3515
|
+
};
|
|
3516
|
+
// File-wide ambiguity is deliberately conservative, including writes
|
|
3517
|
+
// and parameters in unrelated scopes. This rare syntax needs proof,
|
|
3518
|
+
// while ordinary literal require specifiers keep their existing path.
|
|
3519
|
+
traverseTree(tree.rootNode, n => {
|
|
3520
|
+
if (n.type === 'variable_declarator') add(n.childForFieldName('name'), n.childForFieldName('value'));
|
|
3521
|
+
else if (n.type === 'formal_parameters' || n.type === 'import_clause') add(n, null);
|
|
3522
|
+
else if (n.type === 'assignment_expression' || n.type === 'augmented_assignment_expression') add(n.childForFieldName('left'), null);
|
|
3523
|
+
else if (n.type === 'update_expression') add(n.childForFieldName('argument'), null);
|
|
3524
|
+
else if (n.type === 'class_declaration' || n.type === 'class') add(n.childForFieldName('name'), null);
|
|
3525
|
+
else if (n.type === 'catch_clause') add(n.childForFieldName('parameter'), null);
|
|
3526
|
+
else if (FUNCTION_LIKE.has(n.type)) {
|
|
3527
|
+
add(n.childForFieldName('name'), null);
|
|
3528
|
+
add(n.childForFieldName('parameter'), null); // unparenthesized arrow
|
|
3529
|
+
}
|
|
3530
|
+
return true;
|
|
3531
|
+
});
|
|
3532
|
+
};
|
|
3533
|
+
const isPathUtility = fn => {
|
|
3534
|
+
if (pathBindings.has('require')) return false;
|
|
3535
|
+
if (fn?.type !== 'member_expression' ||
|
|
3536
|
+
!['join', 'resolve'].includes(fn.childForFieldName('property')?.text)) return false;
|
|
3537
|
+
const object = fn.childForFieldName('object');
|
|
3538
|
+
if (isPathModuleCall(object)) return true;
|
|
3539
|
+
if (object?.type !== 'identifier') return false;
|
|
3540
|
+
const bindings = pathBindings.get(object.text) || [];
|
|
3541
|
+
return bindings.length === 1 && bindings[0].pattern.type === 'identifier' &&
|
|
3542
|
+
isPathModuleCall(bindings[0].value);
|
|
3543
|
+
};
|
|
3544
|
+
// Static composition of `__dirname`-rooted require paths (fix #337b).
|
|
3545
|
+
// Returns a relative specifier ('./x' / '../x') or null when any piece is
|
|
3546
|
+
// not a string literal.
|
|
3547
|
+
const unquote = (n) => (n.type === 'string' &&
|
|
3548
|
+
!n.namedChildren.some(child => child.type === 'escape_sequence') ? n.text.slice(1, -1) : null);
|
|
3549
|
+
const staticDirnamePath = (arg) => {
|
|
3550
|
+
collectPathBindings();
|
|
3551
|
+
if (pathBindings.has('__dirname') || pathBindings.has('require')) return null;
|
|
3552
|
+
let parts = null;
|
|
3553
|
+
if (arg.type === 'call_expression') {
|
|
3554
|
+
const fn = arg.childForFieldName('function');
|
|
3555
|
+
if (!isPathUtility(fn)) return null;
|
|
3556
|
+
const args = arg.childForFieldName('arguments');
|
|
3557
|
+
if (!args || args.namedChildCount < 2) return null;
|
|
3558
|
+
if (args.namedChild(0).type !== 'identifier' || args.namedChild(0).text !== '__dirname') return null;
|
|
3559
|
+
parts = [];
|
|
3560
|
+
for (let i = 1; i < args.namedChildCount; i++) {
|
|
3561
|
+
const piece = unquote(args.namedChild(i));
|
|
3562
|
+
if (piece == null || piece.startsWith('/')) return null;
|
|
3563
|
+
parts.push(piece);
|
|
3564
|
+
}
|
|
3565
|
+
} else if (arg.type === 'binary_expression') {
|
|
3566
|
+
const operands = [];
|
|
3567
|
+
const flatten = (n) => {
|
|
3568
|
+
if (n.type === 'binary_expression' && n.childForFieldName('operator')?.text === '+') {
|
|
3569
|
+
flatten(n.childForFieldName('left'));
|
|
3570
|
+
flatten(n.childForFieldName('right'));
|
|
3571
|
+
} else operands.push(n);
|
|
3572
|
+
};
|
|
3573
|
+
flatten(arg);
|
|
3574
|
+
if (operands.length < 2 || operands[0].type !== 'identifier' || operands[0].text !== '__dirname') return null;
|
|
3575
|
+
let tail = '';
|
|
3576
|
+
for (let i = 1; i < operands.length; i++) {
|
|
3577
|
+
const piece = unquote(operands[i]);
|
|
3578
|
+
if (piece == null) return null;
|
|
3579
|
+
tail += piece;
|
|
3580
|
+
}
|
|
3581
|
+
if (!tail.startsWith('/')) return null;
|
|
3582
|
+
parts = [tail.slice(1)];
|
|
3583
|
+
} else if (arg.type === 'template_string') {
|
|
3584
|
+
let tail = '';
|
|
3585
|
+
let sawDirname = false;
|
|
3586
|
+
for (let i = 0; i < arg.childCount; i++) {
|
|
3587
|
+
const c = arg.child(i);
|
|
3588
|
+
if (c.type === 'template_substitution') {
|
|
3589
|
+
if (sawDirname || c.namedChildCount !== 1 || c.namedChild(0).text !== '__dirname') return null;
|
|
3590
|
+
sawDirname = true;
|
|
3591
|
+
} else if (c.type === 'string_fragment') {
|
|
3592
|
+
if (!sawDirname) return null;
|
|
3593
|
+
tail += c.text;
|
|
3594
|
+
} else if (c.type !== '`') return null;
|
|
3595
|
+
}
|
|
3596
|
+
if (!sawDirname || !tail.startsWith('/')) return null;
|
|
3597
|
+
parts = [tail.slice(1)];
|
|
3598
|
+
}
|
|
3599
|
+
if (!parts || parts.length === 0) return null;
|
|
3600
|
+
const joined = parts.join('/').replace(/\\/g, '/');
|
|
3601
|
+
if (!joined || joined.includes('${')) return null;
|
|
3602
|
+
const normalized = require('path').posix.normalize(joined);
|
|
3603
|
+
if (normalized.startsWith('/') || normalized === '.') return null;
|
|
3604
|
+
return normalized.startsWith('.') ? normalized : `./${normalized}`;
|
|
3605
|
+
};
|
|
3606
|
+
const hasTypeKeyword = (node) => {
|
|
3607
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
3608
|
+
if (node.child(i).type === 'type') return true;
|
|
3609
|
+
}
|
|
3610
|
+
return false;
|
|
3611
|
+
};
|
|
3612
|
+
|
|
3193
3613
|
traverseTreeCached(tree.rootNode, (node) => {
|
|
3194
3614
|
// ES6 import statements
|
|
3195
3615
|
if (node.type === 'import_statement') {
|
|
@@ -3198,6 +3618,10 @@ function findImportsInCode(code, parser) {
|
|
|
3198
3618
|
const names = [];
|
|
3199
3619
|
const esmRenames = [];
|
|
3200
3620
|
let importType = 'named';
|
|
3621
|
+
let typeOnly = hasTypeKeyword(node);
|
|
3622
|
+
let specifierCount = 0;
|
|
3623
|
+
let typeSpecifierCount = 0;
|
|
3624
|
+
let hasValueBinding = false;
|
|
3201
3625
|
|
|
3202
3626
|
// Find the module path (string node)
|
|
3203
3627
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -3218,7 +3642,8 @@ function findImportsInCode(code, parser) {
|
|
|
3218
3642
|
if (c.type === 'string') src = c.text.slice(1, -1);
|
|
3219
3643
|
}
|
|
3220
3644
|
if (src) {
|
|
3221
|
-
imports.push({ module: src, names: alias ? [alias] : [], type: 'require', line
|
|
3645
|
+
imports.push({ module: src, names: alias ? [alias] : [], type: 'require', line,
|
|
3646
|
+
...(typeOnly && { deferred: true, deferredReason: 'type-only' }) });
|
|
3222
3647
|
}
|
|
3223
3648
|
return true;
|
|
3224
3649
|
}
|
|
@@ -3230,6 +3655,7 @@ function findImportsInCode(code, parser) {
|
|
|
3230
3655
|
// Default import: import foo from 'x'
|
|
3231
3656
|
names.push(clauseChild.text);
|
|
3232
3657
|
importType = 'default';
|
|
3658
|
+
hasValueBinding = true;
|
|
3233
3659
|
} else if (clauseChild.type === 'named_imports') {
|
|
3234
3660
|
// Named imports: import { a, b } from 'x'
|
|
3235
3661
|
for (let k = 0; k < clauseChild.namedChildCount; k++) {
|
|
@@ -3237,6 +3663,8 @@ function findImportsInCode(code, parser) {
|
|
|
3237
3663
|
if (specifier.type === 'import_specifier') {
|
|
3238
3664
|
const nameNode = specifier.namedChild(0);
|
|
3239
3665
|
const aliasNode = specifier.namedChild(1);
|
|
3666
|
+
specifierCount++;
|
|
3667
|
+
if (hasTypeKeyword(specifier)) typeSpecifierCount++;
|
|
3240
3668
|
if (nameNode) names.push(nameNode.text);
|
|
3241
3669
|
// Track renamed imports: import { X as Y }
|
|
3242
3670
|
if (nameNode && aliasNode && aliasNode.text !== nameNode.text) {
|
|
@@ -3253,6 +3681,7 @@ function findImportsInCode(code, parser) {
|
|
|
3253
3681
|
clauseChild.namedChild(0);
|
|
3254
3682
|
if (nsName) names.push(nsName.text);
|
|
3255
3683
|
importType = 'namespace';
|
|
3684
|
+
hasValueBinding = true;
|
|
3256
3685
|
}
|
|
3257
3686
|
}
|
|
3258
3687
|
}
|
|
@@ -3263,8 +3692,13 @@ function findImportsInCode(code, parser) {
|
|
|
3263
3692
|
// Side-effect import: import 'x'
|
|
3264
3693
|
importType = 'side-effect';
|
|
3265
3694
|
}
|
|
3695
|
+
if (!typeOnly && specifierCount > 0 && typeSpecifierCount === specifierCount &&
|
|
3696
|
+
importType === 'named' && !hasValueBinding) {
|
|
3697
|
+
typeOnly = true;
|
|
3698
|
+
}
|
|
3266
3699
|
imports.push({ module: modulePath, names, type: importType, line,
|
|
3267
|
-
...(esmRenames.length > 0 && { renames: esmRenames })
|
|
3700
|
+
...(esmRenames.length > 0 && { renames: esmRenames }),
|
|
3701
|
+
...(typeOnly && { deferred: true, deferredReason: 'type-only' }) });
|
|
3268
3702
|
}
|
|
3269
3703
|
return true;
|
|
3270
3704
|
}
|
|
@@ -3274,6 +3708,8 @@ function findImportsInCode(code, parser) {
|
|
|
3274
3708
|
if (node.type === 'export_statement') {
|
|
3275
3709
|
let source = null;
|
|
3276
3710
|
const names = [];
|
|
3711
|
+
let specifierCount = 0;
|
|
3712
|
+
let typeSpecifierCount = 0;
|
|
3277
3713
|
|
|
3278
3714
|
// Find the source module (string node with 'from')
|
|
3279
3715
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -3285,6 +3721,8 @@ function findImportsInCode(code, parser) {
|
|
|
3285
3721
|
for (let j = 0; j < child.namedChildCount; j++) {
|
|
3286
3722
|
const specifier = child.namedChild(j);
|
|
3287
3723
|
if (specifier.type === 'export_specifier') {
|
|
3724
|
+
specifierCount++;
|
|
3725
|
+
if (hasTypeKeyword(specifier)) typeSpecifierCount++;
|
|
3288
3726
|
const nameNode = specifier.namedChild(0);
|
|
3289
3727
|
if (nameNode) names.push(nameNode.text);
|
|
3290
3728
|
}
|
|
@@ -3296,7 +3734,9 @@ function findImportsInCode(code, parser) {
|
|
|
3296
3734
|
const line = node.startPosition.row + 1;
|
|
3297
3735
|
const isStarReExport = node.text.includes('export *');
|
|
3298
3736
|
const importType = isStarReExport ? 'namespace' : 'named';
|
|
3299
|
-
imports.push({ module: source, names, type: importType, line, isReExport: true
|
|
3737
|
+
imports.push({ module: source, names, type: importType, line, isReExport: true,
|
|
3738
|
+
...((hasTypeKeyword(node) || (specifierCount > 0 && specifierCount === typeSpecifierCount)) &&
|
|
3739
|
+
{ deferred: true, deferredReason: 'type-only' }) });
|
|
3300
3740
|
}
|
|
3301
3741
|
return true;
|
|
3302
3742
|
}
|
|
@@ -3314,8 +3754,17 @@ function findImportsInCode(code, parser) {
|
|
|
3314
3754
|
let modulePath;
|
|
3315
3755
|
let dynamic = false;
|
|
3316
3756
|
|
|
3757
|
+
const composedPath = firstArg && firstArg.type !== 'string' ? staticDirnamePath(firstArg) : null;
|
|
3317
3758
|
if (firstArg && firstArg.type === 'string') {
|
|
3318
3759
|
modulePath = firstArg.text.slice(1, -1);
|
|
3760
|
+
} else if (composedPath) {
|
|
3761
|
+
// fix #337b: `require(path.join(__dirname, '..', 'x'))`,
|
|
3762
|
+
// `require(__dirname + '/x')`, `require(\`${__dirname}/x\`)`
|
|
3763
|
+
// compose to an exact relative specifier — the CJS
|
|
3764
|
+
// test-suite idiom that used to be an unresolvable
|
|
3765
|
+
// dynamic module (excluding every constructor call it
|
|
3766
|
+
// bound as other-definition-import).
|
|
3767
|
+
modulePath = composedPath;
|
|
3319
3768
|
} else {
|
|
3320
3769
|
dynamic = true;
|
|
3321
3770
|
modulePath = firstArg ? firstArg.text : null;
|
|
@@ -3357,7 +3806,9 @@ function findImportsInCode(code, parser) {
|
|
|
3357
3806
|
}
|
|
3358
3807
|
|
|
3359
3808
|
if (modulePath) {
|
|
3809
|
+
const deferral = importDeferral(node);
|
|
3360
3810
|
imports.push({ module: modulePath, names, type: 'require', line, dynamic,
|
|
3811
|
+
...(deferral && { deferred: true, deferredReason: deferral }),
|
|
3361
3812
|
...(defaultLike && { defaultLike: true }),
|
|
3362
3813
|
// Per-import rename pairing (fix #269): the flat
|
|
3363
3814
|
// importAliases list loses WHICH module a renamed
|
|
@@ -3375,11 +3826,15 @@ function findImportsInCode(code, parser) {
|
|
|
3375
3826
|
if (argsNode && argsNode.namedChildCount > 0) {
|
|
3376
3827
|
const firstArg = argsNode.namedChild(0);
|
|
3377
3828
|
const line = node.startPosition.row + 1;
|
|
3829
|
+
const deferral = importDeferral(node);
|
|
3830
|
+
const deferredFields = deferral ? { deferred: true, deferredReason: deferral } : {};
|
|
3378
3831
|
if (firstArg && firstArg.type === 'string') {
|
|
3379
3832
|
const modulePath = firstArg.text.slice(1, -1);
|
|
3380
|
-
imports.push({ module: modulePath, names: [], type: 'dynamic', line, dynamic: false
|
|
3833
|
+
imports.push({ module: modulePath, names: [], type: 'dynamic', line, dynamic: false,
|
|
3834
|
+
...deferredFields });
|
|
3381
3835
|
} else if (firstArg) {
|
|
3382
|
-
imports.push({ module: firstArg.text, names: [], type: 'dynamic', line, dynamic: true
|
|
3836
|
+
imports.push({ module: firstArg.text, names: [], type: 'dynamic', line, dynamic: true,
|
|
3837
|
+
...deferredFields });
|
|
3383
3838
|
}
|
|
3384
3839
|
}
|
|
3385
3840
|
}
|