ucn 5.0.6 → 5.1.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 +1 -1
- package/.claude/skills/ucn/references/trust-contract.md +3 -2
- package/README.md +3 -1
- package/core/analysis.js +66 -1
- package/core/bridge.js +2 -1
- package/core/cache.js +37 -8
- package/core/callers.js +484 -37
- package/core/index-ir.js +14 -4
- package/core/ir.js +48 -2
- package/core/output/analysis.js +25 -0
- package/core/output/shared.js +2 -2
- package/languages/javascript.js +203 -18
- package/languages/python.js +79 -2
- package/mcp/server.js +1 -1
- package/package.json +1 -1
package/core/index-ir.js
CHANGED
|
@@ -16,6 +16,7 @@ function createImportBindings(imports) {
|
|
|
16
16
|
return {
|
|
17
17
|
name,
|
|
18
18
|
module: item.module,
|
|
19
|
+
...(item.type && { kind: item.type }),
|
|
19
20
|
...(item.line != null && { line: item.line }),
|
|
20
21
|
...(rename && { alias: rename.local }),
|
|
21
22
|
...(item.defaultLike && { defaultLike: true }),
|
|
@@ -69,13 +70,14 @@ const OPTIONAL_SYMBOL_FIELDS = Object.freeze([
|
|
|
69
70
|
'enclosingType', 'isMethod', 'receiver', 'memberType', 'fieldType',
|
|
70
71
|
'aliasOf', 'derefTarget', 'decorators', 'decoratorsWithArgs',
|
|
71
72
|
'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
|
|
72
|
-
'traitName', 'isSignature', 'memberAssigned', 'bodyScopedName',
|
|
73
|
+
'traitName', 'isSignature', 'memberAssigned', 'assignedReceiver', 'bodyScopedName',
|
|
73
74
|
'registryMember', 'registryContainer', 'namespace',
|
|
74
75
|
'isExtensionMethod', 'extensionReceiver', 'explicitInterface',
|
|
75
76
|
'lexicalScopeStartLine', 'lexicalScopeEndLine',
|
|
76
77
|
'returnTypeQualifier', 'macroNeverReturns', 'callbackParamTypes', 'iteratorItemType',
|
|
77
78
|
'returnedConcreteType', 'returnedConstructors', 'templateDependent',
|
|
78
|
-
'linkage', 'functionLike',
|
|
79
|
+
'linkage', 'functionLike', 'callableAlias', 'exportedAlias',
|
|
80
|
+
'aliasOwner', 'aliasMember',
|
|
79
81
|
]);
|
|
80
82
|
|
|
81
83
|
function materializeSymbol(fileEntry, item) {
|
|
@@ -91,7 +93,9 @@ function materializeSymbol(fileEntry, item) {
|
|
|
91
93
|
returnType: item.returnType,
|
|
92
94
|
modifiers: item.modifiers,
|
|
93
95
|
docstring: item.docstring,
|
|
94
|
-
bindingId:
|
|
96
|
+
bindingId: item.id
|
|
97
|
+
? `${fileEntry.relativePath}:${item.id}`
|
|
98
|
+
: `${fileEntry.relativePath}:${item.kind}:${item.startLine}`,
|
|
95
99
|
...(item.owner && { className: item.owner }),
|
|
96
100
|
};
|
|
97
101
|
for (const field of OPTIONAL_SYMBOL_FIELDS) {
|
|
@@ -109,7 +113,13 @@ function materializeSymbol(fileEntry, item) {
|
|
|
109
113
|
function addIRSymbol(fileEntry, item, symbolTable = null) {
|
|
110
114
|
const symbol = materializeSymbol(fileEntry, item);
|
|
111
115
|
fileEntry.symbols.push(symbol);
|
|
112
|
-
|
|
116
|
+
// A Rust `impl X`/`impl Trait for X` block introduces NO name into any
|
|
117
|
+
// scope (fix #286b, cursive-measured: the impl symbol stole the bare-name
|
|
118
|
+
// binding of ColorPair from the cross-file struct, excluding a compiler-
|
|
119
|
+
// true composite-literal caller as other-definition). The struct/enum
|
|
120
|
+
// claim covers the impl — same discipline as deadcode's CLASS_AUDIT_KINDS.
|
|
121
|
+
if (!item.memberAssigned && !item.bodyScopedName && !item.exportedAlias &&
|
|
122
|
+
item.kind !== 'impl') {
|
|
113
123
|
fileEntry.bindings.push({
|
|
114
124
|
id: symbol.bindingId,
|
|
115
125
|
name: symbol.name,
|
package/core/ir.js
CHANGED
|
@@ -47,13 +47,14 @@ function normalizeSymbol(symbol, family, language, kind, owner = null) {
|
|
|
47
47
|
'isNested', 'enclosingType', 'isMethod', 'memberType', 'fieldType',
|
|
48
48
|
'aliasOf', 'derefTarget', 'decorators', 'decoratorsWithArgs',
|
|
49
49
|
'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
|
|
50
|
-
'traitName', 'isSignature', 'memberAssigned', 'bodyScopedName',
|
|
50
|
+
'traitName', 'isSignature', 'memberAssigned', 'assignedReceiver', 'bodyScopedName',
|
|
51
51
|
'registryMember', 'registryContainer', 'isConstructor',
|
|
52
52
|
'isExtensionMethod', 'extensionReceiver', 'explicitInterface',
|
|
53
53
|
'namespace', 'lexicalScopeStartLine', 'lexicalScopeEndLine',
|
|
54
54
|
'returnTypeQualifier', 'macroNeverReturns', 'callbackParamTypes', 'iteratorItemType',
|
|
55
55
|
'returnedConcreteType', 'returnedConstructors', 'templateDependent',
|
|
56
|
-
'linkage', 'functionLike',
|
|
56
|
+
'linkage', 'functionLike', 'callableAlias', 'exportedAlias',
|
|
57
|
+
'aliasOwner', 'aliasMember',
|
|
57
58
|
];
|
|
58
59
|
for (const field of passthrough) {
|
|
59
60
|
if (symbol[field] !== undefined && symbol[field] !== null) {
|
|
@@ -114,6 +115,51 @@ function createFileIR({
|
|
|
114
115
|
for (const symbol of stateObjects) append(symbol, 'state', 'state');
|
|
115
116
|
for (const symbol of macros) append(symbol,
|
|
116
117
|
symbol.functionLike ? 'callable' : 'state', 'macro');
|
|
118
|
+
// An immutable module-scope member alias has the callable signature of
|
|
119
|
+
// the class member it captures: `const make = Widget.create`. Materialize
|
|
120
|
+
// the local value and each explicit export alias as real function symbols
|
|
121
|
+
// only when its member is static and every declared return type agrees.
|
|
122
|
+
// This is compiler-visible identity; mutable aliases and ambiguous
|
|
123
|
+
// overload returns were rejected by the parser/agreement gate above.
|
|
124
|
+
for (const alias of (parsed.callableAliases || [])) {
|
|
125
|
+
const sources = normalizedSymbols.filter(symbol =>
|
|
126
|
+
symbol.name === alias.member && symbol.owner === alias.owner &&
|
|
127
|
+
(symbol.params !== undefined || symbol.paramsStructured) &&
|
|
128
|
+
(symbol.modifiers?.includes('static') ||
|
|
129
|
+
String(symbol.memberType || symbol.kind).startsWith('static')) &&
|
|
130
|
+
symbol.returnType);
|
|
131
|
+
if (sources.length === 0) continue;
|
|
132
|
+
const sourceReturns = new Set(sources.map(source => source.returnType));
|
|
133
|
+
if (sourceReturns.size !== 1) continue;
|
|
134
|
+
const source = sources[0];
|
|
135
|
+
const exported = (parsed.exports || []).filter(item =>
|
|
136
|
+
!item.source && item.name === alias.name);
|
|
137
|
+
const exposed = exported.map(item => ({
|
|
138
|
+
name: item.type === 'default' ? 'default' : (item.alias || item.name),
|
|
139
|
+
line: item.line || alias.startLine,
|
|
140
|
+
}));
|
|
141
|
+
const localIsExported = exposed.some(item => item.name === alias.name);
|
|
142
|
+
const makeAlias = (name, startLine, isExported, exportedAlias = false) => ({
|
|
143
|
+
bindingId: `callable-alias:${alias.owner}.${alias.member}:${name}:${startLine}`,
|
|
144
|
+
name,
|
|
145
|
+
startLine,
|
|
146
|
+
endLine: startLine,
|
|
147
|
+
params: source.params,
|
|
148
|
+
...(source.paramsStructured && { paramsStructured: source.paramsStructured }),
|
|
149
|
+
returnType: source.returnType,
|
|
150
|
+
modifiers: isExported ? ['export'] : [],
|
|
151
|
+
callableAlias: true,
|
|
152
|
+
...(exportedAlias && { exportedAlias: true }),
|
|
153
|
+
aliasOwner: alias.owner,
|
|
154
|
+
aliasMember: alias.member,
|
|
155
|
+
});
|
|
156
|
+
append(makeAlias(alias.name, alias.startLine, localIsExported),
|
|
157
|
+
'callable', 'function');
|
|
158
|
+
for (const item of exposed) {
|
|
159
|
+
if (item.name === alias.name) continue;
|
|
160
|
+
append(makeAlias(item.name, item.line, true, true), 'callable', 'function');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
117
163
|
const imports = [...(parsed.imports || [])];
|
|
118
164
|
return {
|
|
119
165
|
schemaVersion: IR_SCHEMA_VERSION,
|
package/core/output/analysis.js
CHANGED
|
@@ -74,6 +74,23 @@ function isTestEntry(entry) {
|
|
|
74
74
|
return isTestPath(entry.relativePath || entry.file || '');
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
function formatAmbiguityCandidates(lines, ambiguity) {
|
|
78
|
+
if (!ambiguity?.items?.length) return;
|
|
79
|
+
const owners = ambiguity.dispatchOwners
|
|
80
|
+
? `; ${ambiguity.dispatchOwners} dispatch owners` : '';
|
|
81
|
+
lines.push(` competing definitions (${ambiguity.totalDefinitions}${owners}):`);
|
|
82
|
+
for (const candidate of ambiguity.items) {
|
|
83
|
+
const owner = candidate.owner
|
|
84
|
+
? ` on ${candidate.owner}${candidate.memberAssignment ? ' (member assignment)' : ''}` : '';
|
|
85
|
+
const selected = candidate.selected ? ' [selected target]' : '';
|
|
86
|
+
lines.push(` - ${candidate.handle} — ${candidate.type}${owner}${selected}`);
|
|
87
|
+
}
|
|
88
|
+
if (ambiguity.truncated) {
|
|
89
|
+
lines.push(` (+${ambiguity.totalDefinitions - ambiguity.items.length} more — ` +
|
|
90
|
+
`use find ${ambiguity.name})`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
77
94
|
/**
|
|
78
95
|
* Render the conservation contract lines: ACCOUNT and CONTRACT (always),
|
|
79
96
|
* WARNING (unparsed files containing the symbol), FILTERED (display-filter
|
|
@@ -273,6 +290,9 @@ function formatContextJson(context) {
|
|
|
273
290
|
? 'runtime-dispatch' : 'actionable-ambiguity'),
|
|
274
291
|
...(c.dispatchFamily && { dispatchFamily: c.dispatchFamily }),
|
|
275
292
|
})),
|
|
293
|
+
...(context.ambiguityCandidates && {
|
|
294
|
+
ambiguityCandidates: context.ambiguityCandidates,
|
|
295
|
+
}),
|
|
276
296
|
...(context.warnings && { warnings: context.warnings })
|
|
277
297
|
}
|
|
278
298
|
});
|
|
@@ -327,6 +347,9 @@ function formatContextJson(context) {
|
|
|
327
347
|
? 'runtime-dispatch' : 'actionable-ambiguity'),
|
|
328
348
|
...(c.dispatchFamily && { dispatchFamily: c.dispatchFamily }),
|
|
329
349
|
})),
|
|
350
|
+
...(context.ambiguityCandidates && {
|
|
351
|
+
ambiguityCandidates: context.ambiguityCandidates,
|
|
352
|
+
}),
|
|
330
353
|
callees: callees.map(c => ({
|
|
331
354
|
name: c.name,
|
|
332
355
|
type: c.type,
|
|
@@ -427,6 +450,7 @@ function formatContext(ctx, options = {}) {
|
|
|
427
450
|
const typeUnverified = ctx.unverifiedCallers || [];
|
|
428
451
|
if (typeUnverified.length > 0) {
|
|
429
452
|
lines.push(`\nCALLERS — UNVERIFIED (${typeUnverified.length}) — call syntax, no binding/receiver evidence:`);
|
|
453
|
+
formatAmbiguityCandidates(lines, ctx.ambiguityCandidates);
|
|
430
454
|
const cap = 10;
|
|
431
455
|
let shown = 0;
|
|
432
456
|
for (const u of typeUnverified) {
|
|
@@ -653,6 +677,7 @@ function formatContext(ctx, options = {}) {
|
|
|
653
677
|
// Always visible and capped at 10 one-liners unless --all.
|
|
654
678
|
if (actionableUnverified.length > 0) {
|
|
655
679
|
lines.push(`${compact ? '' : '\n'}CALLERS — UNVERIFIED (${actionableUnverified.length}) — call syntax, no binding/receiver evidence:`);
|
|
680
|
+
formatAmbiguityCandidates(lines, ctx.ambiguityCandidates);
|
|
656
681
|
const cap = (ctx.meta && ctx.meta.all) ? Infinity : 10;
|
|
657
682
|
let shown = 0;
|
|
658
683
|
for (const u of actionableUnverified) {
|
package/core/output/shared.js
CHANGED
|
@@ -338,7 +338,7 @@ function formatGitLine(git) {
|
|
|
338
338
|
* Display label for an unverified-tier entry's reason. Dispatch-tiered
|
|
339
339
|
* entries (nominal languages) carry attribution metadata: the declared
|
|
340
340
|
* supertype the call dispatches through (dispatchVia) and how many
|
|
341
|
-
* same-name
|
|
341
|
+
* distinct same-name owners the dispatch could land on (dispatchCandidates).
|
|
342
342
|
*/
|
|
343
343
|
function unverifiedReasonLabel(entry) {
|
|
344
344
|
if (!entry || !entry.reason) return '';
|
|
@@ -356,7 +356,7 @@ function unverifiedReasonLabel(entry) {
|
|
|
356
356
|
: `possible-dispatch via ${entry.dispatchVia}`;
|
|
357
357
|
}
|
|
358
358
|
if (entry.reason === 'method-ambiguous' && entry.dispatchCandidates > 1) {
|
|
359
|
-
return `method-ambiguous — ${entry.dispatchCandidates}
|
|
359
|
+
return `method-ambiguous — ${entry.dispatchCandidates} dispatch owners`;
|
|
360
360
|
}
|
|
361
361
|
if (entry.reason === 'overload-ambiguous' && entry.dispatchCandidates > 1) {
|
|
362
362
|
return `overload-ambiguous — 1 of ${entry.dispatchCandidates} applicable overloads`;
|
package/languages/javascript.js
CHANGED
|
@@ -435,6 +435,34 @@ function extractReturnedConcreteType(node) {
|
|
|
435
435
|
? types[0] : null;
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
+
/**
|
|
439
|
+
* Whether every explicit value-producing return yields the current receiver.
|
|
440
|
+
* A fallthrough/empty return cannot feed a subsequent chained call, so it does
|
|
441
|
+
* not compete with `this`; any other returned value makes the result unknown.
|
|
442
|
+
*/
|
|
443
|
+
function returnsReceiverSelf(node) {
|
|
444
|
+
const body = node.childForFieldName('body');
|
|
445
|
+
if (!body) return false;
|
|
446
|
+
let sawSelf = false;
|
|
447
|
+
const stack = [body];
|
|
448
|
+
while (stack.length > 0) {
|
|
449
|
+
const current = stack.pop();
|
|
450
|
+
if (current !== body && FUNCTION_SCOPE_NODES.has(current.type)) continue;
|
|
451
|
+
if (current.type === 'class_declaration' || current.type === 'class') continue;
|
|
452
|
+
if (current.type === 'return_statement') {
|
|
453
|
+
const value = current.namedChild(0);
|
|
454
|
+
if (!value) continue;
|
|
455
|
+
if (value.type !== 'this') return false;
|
|
456
|
+
sawSelf = true;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
for (let i = current.namedChildCount - 1; i >= 0; i--) {
|
|
460
|
+
stack.push(current.namedChild(i));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return sawSelf;
|
|
464
|
+
}
|
|
465
|
+
|
|
438
466
|
/**
|
|
439
467
|
* Process a node for function extraction (single-pass helper)
|
|
440
468
|
* Returns true if node was matched, false otherwise
|
|
@@ -618,6 +646,7 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
618
646
|
isGenerator: isGen,
|
|
619
647
|
isAsync,
|
|
620
648
|
modifiers,
|
|
649
|
+
...lexicalOwnerRange(node),
|
|
621
650
|
...typeAnno,
|
|
622
651
|
...(generics && { generics }),
|
|
623
652
|
...(docstring && { docstring })
|
|
@@ -699,8 +728,14 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
699
728
|
if (processedRanges.has(rangeKey)) return false;
|
|
700
729
|
|
|
701
730
|
const leftNode = node.childForFieldName('left');
|
|
702
|
-
const
|
|
703
|
-
leftNode.
|
|
731
|
+
const assignedObject = leftNode?.type === 'member_expression'
|
|
732
|
+
? leftNode.childForFieldName('object') : null;
|
|
733
|
+
const prototypeBase = assignedObject?.type === 'member_expression' &&
|
|
734
|
+
assignedObject.childForFieldName('property')?.text === 'prototype'
|
|
735
|
+
? assignedObject.childForFieldName('object') : null;
|
|
736
|
+
const prototypeOwner = prototypeBase?.type === 'identifier'
|
|
737
|
+
? prototypeBase.text : null;
|
|
738
|
+
const isPrototypeAssignment = !!prototypeOwner;
|
|
704
739
|
|
|
705
740
|
// For non-prototype assignments, check if nested
|
|
706
741
|
if (!isPrototypeAssignment) {
|
|
@@ -748,7 +783,8 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
748
783
|
processedRanges.add(rangeKey);
|
|
749
784
|
const paramsNode = rightNode.childForFieldName('parameters');
|
|
750
785
|
const { startLine, endLine, indent } = nodeToLocation(node, lines);
|
|
751
|
-
const returnType = extractReturnType(rightNode)
|
|
786
|
+
const returnType = extractReturnType(rightNode) ||
|
|
787
|
+
(prototypeOwner && returnsReceiverSelf(rightNode) ? 'this' : null);
|
|
752
788
|
const generics = extractGenerics(rightNode);
|
|
753
789
|
const docstring = extractJSDocstring(lines, startLine);
|
|
754
790
|
const isGen = isGenerator(rightNode);
|
|
@@ -774,9 +810,14 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
774
810
|
// assignments carry their class so typed-receiver
|
|
775
811
|
// method resolution reaches them.
|
|
776
812
|
...(leftNode.type === 'member_expression' && { memberAssigned: true }),
|
|
813
|
+
// One-hop member assignments record the object they
|
|
814
|
+
// patch (fix #286a: `console.log = () => {}` — the
|
|
815
|
+
// builtin-global exclusion must see cross-file that
|
|
816
|
+
// the project rebinds this global's member).
|
|
777
817
|
...(leftNode.type === 'member_expression' &&
|
|
778
|
-
|
|
779
|
-
{
|
|
818
|
+
leftNode.childForFieldName('object')?.type === 'identifier' &&
|
|
819
|
+
{ assignedReceiver: leftNode.childForFieldName('object').text }),
|
|
820
|
+
...(prototypeOwner && { className: prototypeOwner, isMethod: true }),
|
|
780
821
|
...typeAnno,
|
|
781
822
|
...(generics && { generics }),
|
|
782
823
|
...(docstring && { docstring })
|
|
@@ -1258,7 +1299,8 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
1258
1299
|
}
|
|
1259
1300
|
|
|
1260
1301
|
const isAsync = text.match(/^\s*(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:override\s+)?async\s/) !== null;
|
|
1261
|
-
const returnType = extractReturnType(child)
|
|
1302
|
+
const returnType = extractReturnType(child) ||
|
|
1303
|
+
(returnsReceiverSelf(child) ? 'this' : null);
|
|
1262
1304
|
const docstring = extractJSDocstring(code, startLine);
|
|
1263
1305
|
const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
|
|
1264
1306
|
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, code, startLine, true);
|
|
@@ -1372,6 +1414,8 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
1372
1414
|
const name = nameNode.text;
|
|
1373
1415
|
const valueNode = child.childForFieldName('value');
|
|
1374
1416
|
const isArrow = valueNode && valueNode.type === 'arrow_function';
|
|
1417
|
+
const isStatic = Array.from({ length: child.childCount }, (_, ci) => child.child(ci))
|
|
1418
|
+
.some(part => part.type === 'static');
|
|
1375
1419
|
|
|
1376
1420
|
// Collect decorators — children of the field node (TS) or preceding siblings (JS)
|
|
1377
1421
|
const fieldDecorators = [];
|
|
@@ -1410,6 +1454,7 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
1410
1454
|
startLine,
|
|
1411
1455
|
endLine,
|
|
1412
1456
|
memberType: name.startsWith('#') ? 'private' : 'field',
|
|
1457
|
+
...(isStatic && { modifiers: ['static'] }),
|
|
1413
1458
|
isArrow: true,
|
|
1414
1459
|
isMethod: true, // Arrow fields are callable like methods
|
|
1415
1460
|
...typeAnno,
|
|
@@ -1427,6 +1472,7 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
1427
1472
|
startLine,
|
|
1428
1473
|
endLine,
|
|
1429
1474
|
memberType: name.startsWith('#') ? 'private field' : 'field',
|
|
1475
|
+
...(isStatic && { modifiers: ['static'] }),
|
|
1430
1476
|
...(fieldType && { fieldType }),
|
|
1431
1477
|
...(fieldDecorators.length > 0 && { decorators: fieldDecorators })
|
|
1432
1478
|
// Not a method - regular field
|
|
@@ -1484,6 +1530,48 @@ function _processState(node, objects, lines) {
|
|
|
1484
1530
|
return false;
|
|
1485
1531
|
}
|
|
1486
1532
|
|
|
1533
|
+
/**
|
|
1534
|
+
* Record immutable module-scope aliases of a statically named class member.
|
|
1535
|
+
*
|
|
1536
|
+
* `const make = Widget.create` preserves the member's compiler-visible
|
|
1537
|
+
* callable signature. The normalized IR can therefore expose both the local
|
|
1538
|
+
* callable value and any `export { make as widget }` surface without guessing
|
|
1539
|
+
* from a later call spelling. Mutable/local/object aliases deliberately stay
|
|
1540
|
+
* out: they need data-flow evidence, not a declaration-shape shortcut.
|
|
1541
|
+
*/
|
|
1542
|
+
function _processCallableAlias(node, aliases) {
|
|
1543
|
+
if (node.type !== 'lexical_declaration' || !isModuleScope(node)) return false;
|
|
1544
|
+
const declarationKind = node.child(0)?.text;
|
|
1545
|
+
if (declarationKind !== 'const') return false;
|
|
1546
|
+
|
|
1547
|
+
let matched = false;
|
|
1548
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1549
|
+
const declarator = node.namedChild(i);
|
|
1550
|
+
if (declarator.type !== 'variable_declarator') continue;
|
|
1551
|
+
const nameNode = declarator.childForFieldName('name');
|
|
1552
|
+
let valueNode = declarator.childForFieldName('value');
|
|
1553
|
+
if (nameNode?.type !== 'identifier' || !valueNode) continue;
|
|
1554
|
+
while (valueNode && ['parenthesized_expression', 'as_expression',
|
|
1555
|
+
'satisfies_expression', 'type_assertion'].includes(valueNode.type)) {
|
|
1556
|
+
valueNode = valueNode.namedChild(0);
|
|
1557
|
+
}
|
|
1558
|
+
if (valueNode?.type !== 'member_expression') continue;
|
|
1559
|
+
const owner = valueNode.childForFieldName('object');
|
|
1560
|
+
const member = valueNode.childForFieldName('property');
|
|
1561
|
+
if (owner?.type !== 'identifier' ||
|
|
1562
|
+
!['identifier', 'property_identifier'].includes(member?.type)) continue;
|
|
1563
|
+
aliases.push({
|
|
1564
|
+
name: nameNode.text,
|
|
1565
|
+
owner: owner.text,
|
|
1566
|
+
member: member.text,
|
|
1567
|
+
startLine: declarator.startPosition.row + 1,
|
|
1568
|
+
endLine: declarator.endPosition.row + 1,
|
|
1569
|
+
});
|
|
1570
|
+
matched = true;
|
|
1571
|
+
}
|
|
1572
|
+
return matched;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1487
1575
|
/**
|
|
1488
1576
|
* Find state objects (CONFIG, constants, etc.)
|
|
1489
1577
|
*/
|
|
@@ -1508,13 +1596,14 @@ function findStateObjects(code, parser) {
|
|
|
1508
1596
|
function parse(code, parser) {
|
|
1509
1597
|
const tree = parseTree(parser, code);
|
|
1510
1598
|
const lines = code.split('\n');
|
|
1511
|
-
const functions = [], classes = [], stateObjects = [];
|
|
1599
|
+
const functions = [], classes = [], stateObjects = [], callableAliases = [];
|
|
1512
1600
|
const processedFn = new Set(), processedCls = new Set();
|
|
1513
1601
|
|
|
1514
1602
|
traverseTreeCached(tree.rootNode, (node) => {
|
|
1515
1603
|
_processFunction(node, functions, processedFn, lines);
|
|
1516
1604
|
_processClass(node, classes, processedCls, lines);
|
|
1517
1605
|
_processState(node, stateObjects, lines);
|
|
1606
|
+
_processCallableAlias(node, callableAliases);
|
|
1518
1607
|
return true; // always continue, never skip subtrees
|
|
1519
1608
|
});
|
|
1520
1609
|
|
|
@@ -1554,6 +1643,7 @@ function parse(code, parser) {
|
|
|
1554
1643
|
declarationTokens.sort((a, b) => a.startIndex - b.startIndex);
|
|
1555
1644
|
|
|
1556
1645
|
const recoveredFunctions = [], recoveredClasses = [], recoveredState = [];
|
|
1646
|
+
const recoveredCallableAliases = [];
|
|
1557
1647
|
for (let i = 0; i < declarationTokens.length; i++) {
|
|
1558
1648
|
const token = declarationTokens[i];
|
|
1559
1649
|
const next = declarationTokens[i + 1];
|
|
@@ -1562,12 +1652,13 @@ function parse(code, parser) {
|
|
|
1562
1652
|
if (!fragment.trim()) continue;
|
|
1563
1653
|
const fragmentTree = parseTree(parser, fragment);
|
|
1564
1654
|
const fragmentLines = fragment.split('\n');
|
|
1565
|
-
const ff = [], fc = [], fs = [];
|
|
1655
|
+
const ff = [], fc = [], fs = [], fa = [];
|
|
1566
1656
|
const pf = new Set(), pc = new Set();
|
|
1567
1657
|
traverseTreeCached(fragmentTree.rootNode, (node) => {
|
|
1568
1658
|
_processFunction(node, ff, pf, fragmentLines);
|
|
1569
1659
|
_processClass(node, fc, pc, fragmentLines);
|
|
1570
1660
|
_processState(node, fs, fragmentLines);
|
|
1661
|
+
_processCallableAlias(node, fa);
|
|
1571
1662
|
return true;
|
|
1572
1663
|
});
|
|
1573
1664
|
const lineOffset = token.startPosition.row;
|
|
@@ -1585,6 +1676,7 @@ function parse(code, parser) {
|
|
|
1585
1676
|
for (const item of ff) { shiftLines(item); recoveredFunctions.push(item); }
|
|
1586
1677
|
for (const item of fc) { shiftLines(item); recoveredClasses.push(item); }
|
|
1587
1678
|
for (const item of fs) { shiftLines(item); recoveredState.push(item); }
|
|
1679
|
+
for (const item of fa) { shiftLines(item); recoveredCallableAliases.push(item); }
|
|
1588
1680
|
}
|
|
1589
1681
|
|
|
1590
1682
|
const mergeUnique = (target, additions, kind) => {
|
|
@@ -1597,11 +1689,21 @@ function parse(code, parser) {
|
|
|
1597
1689
|
mergeUnique(functions, recoveredFunctions, 'function');
|
|
1598
1690
|
mergeUnique(classes, recoveredClasses, 'class');
|
|
1599
1691
|
mergeUnique(stateObjects, recoveredState, 'state');
|
|
1692
|
+
const aliasKeys = new Set(callableAliases.map(alias =>
|
|
1693
|
+
`${alias.name}\0${alias.owner}\0${alias.member}\0${alias.startLine}`));
|
|
1694
|
+
for (const alias of recoveredCallableAliases) {
|
|
1695
|
+
const key = `${alias.name}\0${alias.owner}\0${alias.member}\0${alias.startLine}`;
|
|
1696
|
+
if (!aliasKeys.has(key)) {
|
|
1697
|
+
aliasKeys.add(key);
|
|
1698
|
+
callableAliases.push(alias);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1600
1701
|
}
|
|
1601
1702
|
|
|
1602
1703
|
functions.sort((a, b) => a.startLine - b.startLine);
|
|
1603
1704
|
classes.sort((a, b) => a.startLine - b.startLine);
|
|
1604
1705
|
stateObjects.sort((a, b) => a.startLine - b.startLine);
|
|
1706
|
+
callableAliases.sort((a, b) => a.startLine - b.startLine);
|
|
1605
1707
|
|
|
1606
1708
|
return {
|
|
1607
1709
|
language: 'javascript',
|
|
@@ -1609,6 +1711,7 @@ function parse(code, parser) {
|
|
|
1609
1711
|
functions,
|
|
1610
1712
|
classes,
|
|
1611
1713
|
stateObjects,
|
|
1714
|
+
callableAliases,
|
|
1612
1715
|
...(tree.rootNode.hasError && { parseRecovery: true }),
|
|
1613
1716
|
imports: [], // Handled by core/imports.js
|
|
1614
1717
|
exports: [] // Handled by core/imports.js
|
|
@@ -1653,6 +1756,40 @@ const JS_LITERAL_ASSIGN_TYPES = {
|
|
|
1653
1756
|
// Predefined TS types that pin a receiver; any/unknown/object say nothing.
|
|
1654
1757
|
const TS_PREDEFINED_RECEIVER_TYPES = new Set(['string', 'number', 'boolean', 'bigint', 'symbol']);
|
|
1655
1758
|
|
|
1759
|
+
/**
|
|
1760
|
+
* Companion to tsTypeName: the namespace qualifier that owns the annotated
|
|
1761
|
+
* name (fix #286e — `app: ns.Flask` must carry 'ns' so two same-name classes
|
|
1762
|
+
* resolve by declaration origin, not directory proximity).
|
|
1763
|
+
*/
|
|
1764
|
+
function tsTypeQualifier(node) {
|
|
1765
|
+
if (!node) return undefined;
|
|
1766
|
+
switch (node.type) {
|
|
1767
|
+
case 'nested_type_identifier': {
|
|
1768
|
+
const last = node.namedChild(node.namedChildCount - 1);
|
|
1769
|
+
const start = node.startIndex;
|
|
1770
|
+
return last && last.startIndex > start
|
|
1771
|
+
? node.text.slice(0, last.startIndex - start).replace(/\.$/, '') || undefined
|
|
1772
|
+
: undefined;
|
|
1773
|
+
}
|
|
1774
|
+
case 'generic_type':
|
|
1775
|
+
return tsTypeQualifier(node.namedChild(0));
|
|
1776
|
+
case 'union_type': {
|
|
1777
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1778
|
+
const c = node.namedChild(i);
|
|
1779
|
+
if (c.type === 'nested_type_identifier' || c.type === 'generic_type') {
|
|
1780
|
+
const q = tsTypeQualifier(c);
|
|
1781
|
+
if (q) return q;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
return undefined;
|
|
1785
|
+
}
|
|
1786
|
+
case 'parenthesized_type':
|
|
1787
|
+
return tsTypeQualifier(node.namedChild(0));
|
|
1788
|
+
default:
|
|
1789
|
+
return undefined;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1656
1793
|
/**
|
|
1657
1794
|
* Extract a single concrete type name from a TS type node. Conservative by
|
|
1658
1795
|
* design: a wrong type would exclude true callers downstream
|
|
@@ -2180,6 +2317,12 @@ function findCallsInCode(code, parser) {
|
|
|
2180
2317
|
if (typeName) {
|
|
2181
2318
|
localVarTypes.set(nameNode.text, typeName);
|
|
2182
2319
|
declaredTypeVars.add(nameNode.text);
|
|
2320
|
+
const annotationQualifier = tsTypeQualifier(typeId);
|
|
2321
|
+
if (annotationQualifier) {
|
|
2322
|
+
localVarTypeQualifiers.set(nameNode.text, annotationQualifier);
|
|
2323
|
+
} else {
|
|
2324
|
+
localVarTypeQualifiers.delete(nameNode.text);
|
|
2325
|
+
}
|
|
2183
2326
|
}
|
|
2184
2327
|
} else if (initNode && JS_LITERAL_ASSIGN_TYPES[initNode.type]) {
|
|
2185
2328
|
// Literal declaration types the variable (fix #262):
|
|
@@ -2200,6 +2343,12 @@ function findCallsInCode(code, parser) {
|
|
|
2200
2343
|
if (typeName) {
|
|
2201
2344
|
localVarTypes.set(pat.text, typeName);
|
|
2202
2345
|
declaredTypeVars.add(pat.text);
|
|
2346
|
+
const annotationQualifier = tsTypeQualifier(inner);
|
|
2347
|
+
if (annotationQualifier) {
|
|
2348
|
+
localVarTypeQualifiers.set(pat.text, annotationQualifier);
|
|
2349
|
+
} else {
|
|
2350
|
+
localVarTypeQualifiers.delete(pat.text);
|
|
2351
|
+
}
|
|
2203
2352
|
}
|
|
2204
2353
|
}
|
|
2205
2354
|
}
|
|
@@ -3163,6 +3312,22 @@ function findImportsInCode(code, parser) {
|
|
|
3163
3312
|
return imports;
|
|
3164
3313
|
}
|
|
3165
3314
|
|
|
3315
|
+
/**
|
|
3316
|
+
* Return the local symbol created or referenced by a statically-owned
|
|
3317
|
+
* CommonJS property assignment. Dynamic expressions deliberately return
|
|
3318
|
+
* undefined: `exports.x = factory()` and `exports.x = other.y` may be
|
|
3319
|
+
* re-exports or arbitrary values, so they are not exclusion-grade identity.
|
|
3320
|
+
*/
|
|
3321
|
+
function cjsAssignedLocalName(valueNode, exposedName) {
|
|
3322
|
+
if (!valueNode) return undefined;
|
|
3323
|
+
if (valueNode.type === 'identifier') return valueNode.text;
|
|
3324
|
+
if (['function_expression', 'generator_function', 'arrow_function', 'class']
|
|
3325
|
+
.includes(valueNode.type)) {
|
|
3326
|
+
return valueNode.childForFieldName('name')?.text || exposedName;
|
|
3327
|
+
}
|
|
3328
|
+
return undefined;
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3166
3331
|
/**
|
|
3167
3332
|
* Find all exports in JavaScript/TypeScript code using tree-sitter AST
|
|
3168
3333
|
* @param {string} code - Source code to analyze
|
|
@@ -3355,12 +3520,15 @@ function findExportsInCode(code, parser) {
|
|
|
3355
3520
|
} else if (prop.type === 'pair') {
|
|
3356
3521
|
const key = prop.childForFieldName('key');
|
|
3357
3522
|
const value = prop.childForFieldName('value');
|
|
3358
|
-
if (key)
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3523
|
+
if (key) {
|
|
3524
|
+
const localName = cjsAssignedLocalName(value, key.text);
|
|
3525
|
+
exports.push({
|
|
3526
|
+
name: key.text,
|
|
3527
|
+
...(localName && { localName }),
|
|
3528
|
+
type: 'module.exports',
|
|
3529
|
+
line,
|
|
3530
|
+
});
|
|
3531
|
+
}
|
|
3364
3532
|
} else if (prop.type === 'method_definition') {
|
|
3365
3533
|
// Shorthand methods are exports too
|
|
3366
3534
|
// (fix #252 — `module.exports =
|
|
@@ -3402,9 +3570,21 @@ function findExportsInCode(code, parser) {
|
|
|
3402
3570
|
}
|
|
3403
3571
|
} else if (rightNode && rightNode.type === 'identifier') {
|
|
3404
3572
|
// module.exports = something
|
|
3405
|
-
exports.push({ name: rightNode.text, localName: rightNode.text,
|
|
3573
|
+
exports.push({ name: rightNode.text, localName: rightNode.text,
|
|
3574
|
+
type: 'module.exports', defaultLike: true, line });
|
|
3406
3575
|
} else {
|
|
3407
|
-
|
|
3576
|
+
// A named function/class expression still exports
|
|
3577
|
+
// one callable default. Preserve its LOCAL symbol
|
|
3578
|
+
// identity so `require('./mod')()` resolves back
|
|
3579
|
+
// to the declaration; anonymous expressions use
|
|
3580
|
+
// the parser's synthetic `default` symbol.
|
|
3581
|
+
const localName = rightNode &&
|
|
3582
|
+
['function_expression', 'generator_function', 'class'].includes(rightNode.type)
|
|
3583
|
+
? rightNode.childForFieldName('name')?.text
|
|
3584
|
+
: undefined;
|
|
3585
|
+
exports.push({ name: 'default',
|
|
3586
|
+
...(localName && { localName }),
|
|
3587
|
+
type: 'module.exports', defaultLike: true, line });
|
|
3408
3588
|
}
|
|
3409
3589
|
return true;
|
|
3410
3590
|
}
|
|
@@ -3413,9 +3593,10 @@ function findExportsInCode(code, parser) {
|
|
|
3413
3593
|
if (objNode.text === 'exports') {
|
|
3414
3594
|
const line = node.startPosition.row + 1;
|
|
3415
3595
|
const rightNode = node.childForFieldName('right');
|
|
3596
|
+
const localName = cjsAssignedLocalName(rightNode, propNode.text);
|
|
3416
3597
|
exports.push({
|
|
3417
3598
|
name: propNode.text,
|
|
3418
|
-
...(
|
|
3599
|
+
...(localName && { localName }),
|
|
3419
3600
|
type: 'exports',
|
|
3420
3601
|
line,
|
|
3421
3602
|
});
|
|
@@ -3425,7 +3606,11 @@ function findExportsInCode(code, parser) {
|
|
|
3425
3606
|
// module.exports.name = ...
|
|
3426
3607
|
if (objNode.type === 'member_expression' && objNode.text === 'module.exports') {
|
|
3427
3608
|
const line = node.startPosition.row + 1;
|
|
3428
|
-
|
|
3609
|
+
const rightNode = node.childForFieldName('right');
|
|
3610
|
+
const localName = cjsAssignedLocalName(rightNode, propNode.text);
|
|
3611
|
+
exports.push({ name: propNode.text,
|
|
3612
|
+
...(localName && { localName }),
|
|
3613
|
+
type: 'module.exports', line });
|
|
3429
3614
|
return true;
|
|
3430
3615
|
}
|
|
3431
3616
|
}
|