ucn 4.1.2 → 4.2.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 +80 -353
- package/.claude/skills/ucn/references/commands.md +93 -0
- package/.claude/skills/ucn/references/trust-contract.md +63 -0
- package/README.md +67 -44
- package/cli/index.js +24 -19
- package/core/account.js +18 -0
- package/core/analysis.js +27 -7
- package/core/bridge.js +0 -1
- package/core/brief.js +2 -3
- package/core/build-worker.js +5 -0
- package/core/cache.js +55 -6
- package/core/callers.js +1709 -162
- package/core/check.js +107 -19
- package/core/confidence.js +29 -16
- package/core/deadcode.js +84 -24
- package/core/entrypoints.js +32 -27
- package/core/execute.js +19 -3
- package/core/graph-build.js +33 -0
- package/core/output/analysis.js +53 -14
- package/core/output/check.js +11 -0
- package/core/output/doctor.js +40 -19
- package/core/output/endpoints.js +0 -2
- package/core/output/search.js +26 -1
- package/core/parser.js +1 -1
- package/core/project.js +25 -4
- package/core/registry.js +2 -2
- package/core/reporting.js +166 -108
- package/core/search.js +271 -49
- package/core/tracing.js +2 -2
- package/core/trust-matrix.js +158 -0
- package/core/verify.js +0 -1
- package/eslint.config.js +2 -2
- package/languages/go.js +143 -13
- package/languages/html.js +5 -0
- package/languages/index.js +5 -0
- package/languages/java.js +79 -12
- package/languages/javascript.js +233 -42
- package/languages/python.js +54 -15
- package/languages/rust.js +180 -22
- package/languages/utils.js +0 -1
- package/mcp/server.js +118 -38
- package/package.json +11 -4
package/languages/javascript.js
CHANGED
|
@@ -99,6 +99,109 @@ function getAssignmentName(leftNode) {
|
|
|
99
99
|
return null;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/** True when a declaration is outside every function/class body. */
|
|
103
|
+
function isModuleScope(node) {
|
|
104
|
+
let current = node && node.parent;
|
|
105
|
+
while (current) {
|
|
106
|
+
if (current.type === 'function_declaration' || current.type === 'arrow_function' ||
|
|
107
|
+
current.type === 'function_expression' || current.type === 'method_definition' ||
|
|
108
|
+
current.type === 'generator_function_declaration' || current.type === 'generator_function' ||
|
|
109
|
+
current.type === 'class_body') {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (current.type === 'program' || current.type === 'module') return true;
|
|
113
|
+
current = current.parent;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Unwrap common type/runtime wrappers around an object-literal registry. */
|
|
119
|
+
function unwrapObjectRegistry(node) {
|
|
120
|
+
let current = node;
|
|
121
|
+
while (current && (current.type === 'parenthesized_expression' ||
|
|
122
|
+
current.type === 'as_expression' || current.type === 'satisfies_expression' ||
|
|
123
|
+
current.type === 'type_assertion')) {
|
|
124
|
+
current = current.namedChild(0);
|
|
125
|
+
}
|
|
126
|
+
if (current && current.type === 'object') return current;
|
|
127
|
+
if (current && current.type === 'call_expression') {
|
|
128
|
+
const callee = current.childForFieldName('function');
|
|
129
|
+
if (callee && (callee.text === 'Object.freeze' || callee.text === 'Object.seal')) {
|
|
130
|
+
const args = current.childForFieldName('arguments');
|
|
131
|
+
const first = args && args.namedChild(0);
|
|
132
|
+
return unwrapObjectRegistry(first);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function objectPropertyName(nameNode) {
|
|
139
|
+
if (!nameNode) return null;
|
|
140
|
+
if (nameNode.type === 'identifier' || nameNode.type === 'property_identifier') return nameNode.text;
|
|
141
|
+
if (nameNode.type === 'string') {
|
|
142
|
+
const raw = nameNode.text;
|
|
143
|
+
if (raw.length >= 2 && raw[0] === raw[raw.length - 1]) return raw.slice(1, -1);
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Index function-valued object properties. Module-scope objects are dynamic
|
|
150
|
+
* dispatch surfaces (`HANDLERS[command](...)`); without symbols for their
|
|
151
|
+
* members, calls inside them are orphaned and reachability/deadcode lie.
|
|
152
|
+
*/
|
|
153
|
+
function appendObjectFunctionMembers(objectNode, functions, lines, extraFields = {}) {
|
|
154
|
+
if (!objectNode) return 0;
|
|
155
|
+
let added = 0;
|
|
156
|
+
for (let i = 0; i < objectNode.namedChildCount; i++) {
|
|
157
|
+
const prop = objectNode.namedChild(i);
|
|
158
|
+
let nameNode = null;
|
|
159
|
+
let fnNode = null;
|
|
160
|
+
if (prop.type === 'method_definition') {
|
|
161
|
+
nameNode = prop.childForFieldName('name');
|
|
162
|
+
fnNode = prop;
|
|
163
|
+
} else if (prop.type === 'pair') {
|
|
164
|
+
const value = prop.childForFieldName('value');
|
|
165
|
+
if (value && (value.type === 'function_expression' || value.type === 'arrow_function' ||
|
|
166
|
+
value.type === 'generator_function')) {
|
|
167
|
+
nameNode = prop.childForFieldName('key');
|
|
168
|
+
fnNode = value;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const name = objectPropertyName(nameNode);
|
|
172
|
+
if (!name || !fnNode) continue;
|
|
173
|
+
|
|
174
|
+
const paramsNode = fnNode.childForFieldName('parameters');
|
|
175
|
+
const { startLine, endLine, indent } = nodeToLocation(prop, lines);
|
|
176
|
+
const returnType = extractReturnType(fnNode);
|
|
177
|
+
const generics = extractGenerics(fnNode);
|
|
178
|
+
const docstring = extractJSDocstring(lines, startLine);
|
|
179
|
+
const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
|
|
180
|
+
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
|
|
181
|
+
const modifiers = extractModifiers(fnNode);
|
|
182
|
+
functions.push({
|
|
183
|
+
name,
|
|
184
|
+
params: extractParams(paramsNode),
|
|
185
|
+
paramsStructured,
|
|
186
|
+
startLine,
|
|
187
|
+
endLine,
|
|
188
|
+
indent,
|
|
189
|
+
isArrow: fnNode.type === 'arrow_function',
|
|
190
|
+
isGenerator: isGenerator(fnNode),
|
|
191
|
+
isAsync: modifiers.includes('async'),
|
|
192
|
+
modifiers,
|
|
193
|
+
memberAssigned: true,
|
|
194
|
+
registryMember: true,
|
|
195
|
+
...extraFields,
|
|
196
|
+
...typeAnno,
|
|
197
|
+
...(generics && { generics }),
|
|
198
|
+
...(docstring && { docstring }),
|
|
199
|
+
});
|
|
200
|
+
added++;
|
|
201
|
+
}
|
|
202
|
+
return added;
|
|
203
|
+
}
|
|
204
|
+
|
|
102
205
|
/**
|
|
103
206
|
* Extract modifiers from a declaration NODE — AST tokens, never text
|
|
104
207
|
* (fix #249: the first-line regex fabricated export/async/default from
|
|
@@ -215,7 +318,7 @@ function extractDecoratorsWithArgs(node) {
|
|
|
215
318
|
let inner = null;
|
|
216
319
|
for (let i = 0; i < n.namedChildCount; i++) {
|
|
217
320
|
const c = n.namedChild(i);
|
|
218
|
-
if (c.type
|
|
321
|
+
if (!c.type.endsWith('comment')) { inner = c; break; }
|
|
219
322
|
}
|
|
220
323
|
if (!inner) return;
|
|
221
324
|
|
|
@@ -230,7 +333,7 @@ function extractDecoratorsWithArgs(node) {
|
|
|
230
333
|
let firstStringArg = null;
|
|
231
334
|
for (let j = 0; j < argsNode.namedChildCount; j++) {
|
|
232
335
|
const arg = argsNode.namedChild(j);
|
|
233
|
-
if (arg.type
|
|
336
|
+
if (arg.type.endsWith('comment')) continue;
|
|
234
337
|
const s = extractStringArg(arg);
|
|
235
338
|
if (s && !s.interp) { firstStringArg = s.value; break; }
|
|
236
339
|
if (s) { firstStringArg = s.value; break; }
|
|
@@ -463,6 +566,21 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
463
566
|
}
|
|
464
567
|
}
|
|
465
568
|
}
|
|
569
|
+
|
|
570
|
+
// Module-scope dispatch tables (including Object.freeze /
|
|
571
|
+
// Object.seal wrappers). These handlers are invoked through
|
|
572
|
+
// computed property access, so a plain name-based call graph
|
|
573
|
+
// cannot discover their incoming edge. Index them explicitly
|
|
574
|
+
// and let reachability treat them as conservative roots.
|
|
575
|
+
if (!isArrow && !isFnExpr && isModuleScope(node)) {
|
|
576
|
+
const registryObject = unwrapObjectRegistry(valueNode);
|
|
577
|
+
if (registryObject) {
|
|
578
|
+
const added = appendObjectFunctionMembers(registryObject, functions, lines, {
|
|
579
|
+
registryContainer: nameNode.text,
|
|
580
|
+
});
|
|
581
|
+
if (added > 0) processedRanges.add(rangeKey);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
466
584
|
}
|
|
467
585
|
}
|
|
468
586
|
}
|
|
@@ -555,37 +673,9 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
555
673
|
if (lhsText === 'module.exports' || lhsText === 'exports' ||
|
|
556
674
|
lhsText.startsWith('module.exports.') || lhsText.startsWith('exports.')) {
|
|
557
675
|
processedRanges.add(rangeKey);
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
let fnNode = null;
|
|
562
|
-
if (prop.type === 'method_definition') {
|
|
563
|
-
nameNode = prop.childForFieldName('name');
|
|
564
|
-
fnNode = prop;
|
|
565
|
-
} else if (prop.type === 'pair') {
|
|
566
|
-
const v = prop.childForFieldName('value');
|
|
567
|
-
if (v && (v.type === 'function_expression' ||
|
|
568
|
-
v.type === 'arrow_function' || v.type === 'generator_function')) {
|
|
569
|
-
nameNode = prop.childForFieldName('key');
|
|
570
|
-
fnNode = v;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
if (!nameNode || !fnNode) continue;
|
|
574
|
-
const paramsNode = fnNode.childForFieldName('parameters');
|
|
575
|
-
const { startLine, endLine, indent } = nodeToLocation(prop, lines);
|
|
576
|
-
const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
|
|
577
|
-
functions.push({
|
|
578
|
-
name: nameNode.text,
|
|
579
|
-
params: extractParams(paramsNode),
|
|
580
|
-
paramsStructured,
|
|
581
|
-
startLine,
|
|
582
|
-
endLine,
|
|
583
|
-
indent,
|
|
584
|
-
isArrow: fnNode.type === 'arrow_function',
|
|
585
|
-
isGenerator: isGenerator(fnNode),
|
|
586
|
-
modifiers: [],
|
|
587
|
-
});
|
|
588
|
-
}
|
|
676
|
+
appendObjectFunctionMembers(rightNode, functions, lines, {
|
|
677
|
+
registryContainer: lhsText,
|
|
678
|
+
});
|
|
589
679
|
}
|
|
590
680
|
}
|
|
591
681
|
}
|
|
@@ -1071,6 +1161,40 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
1071
1161
|
...(decorators.length > 0 && { decorators }),
|
|
1072
1162
|
...(decoratorsWithArgs.length > 0 && { decoratorsWithArgs })
|
|
1073
1163
|
});
|
|
1164
|
+
|
|
1165
|
+
// TypeScript constructor parameter-properties are declared
|
|
1166
|
+
// fields, not ordinary parameters. Index them from the AST so
|
|
1167
|
+
// `constructor(private repo: Repository)` gives
|
|
1168
|
+
// `this.repo.save()` compiler-visible receiver evidence.
|
|
1169
|
+
// Accessibility and `readonly` are syntax tokens on the
|
|
1170
|
+
// parameter node; no text-pattern fallback is needed.
|
|
1171
|
+
if (name === 'constructor' && paramsNode) {
|
|
1172
|
+
for (let pi = 0; pi < paramsNode.namedChildCount; pi++) {
|
|
1173
|
+
const param = paramsNode.namedChild(pi);
|
|
1174
|
+
if (!['required_parameter', 'optional_parameter'].includes(param.type)) continue;
|
|
1175
|
+
const access = Array.from({ length: param.namedChildCount }, (_, ci) => param.namedChild(ci))
|
|
1176
|
+
.find(n => n.type === 'accessibility_modifier');
|
|
1177
|
+
const readonly = Array.from({ length: param.childCount }, (_, ci) => param.child(ci))
|
|
1178
|
+
.some(n => n.type === 'readonly');
|
|
1179
|
+
if (!access && !readonly) continue;
|
|
1180
|
+
const pattern = param.childForFieldName('pattern');
|
|
1181
|
+
if (!pattern || pattern.type !== 'identifier') continue;
|
|
1182
|
+
const typeNode = param.childForFieldName('type');
|
|
1183
|
+
const fieldType = typeNode
|
|
1184
|
+
? typeNode.text.replace(/^:\s*/, '').trim() : undefined;
|
|
1185
|
+
const loc = nodeToLocation(param, code);
|
|
1186
|
+
members.push({
|
|
1187
|
+
name: pattern.text,
|
|
1188
|
+
startLine: loc.startLine,
|
|
1189
|
+
endLine: loc.endLine,
|
|
1190
|
+
memberType: 'field',
|
|
1191
|
+
...(fieldType && { fieldType }),
|
|
1192
|
+
...(access && ['private', 'protected'].includes(access.text) && {
|
|
1193
|
+
modifiers: [access.text],
|
|
1194
|
+
}),
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1074
1198
|
}
|
|
1075
1199
|
}
|
|
1076
1200
|
|
|
@@ -1274,6 +1398,7 @@ function parse(code, parser) {
|
|
|
1274
1398
|
functions,
|
|
1275
1399
|
classes,
|
|
1276
1400
|
stateObjects,
|
|
1401
|
+
...(tree.rootNode.hasError && { parseRecovery: true }),
|
|
1277
1402
|
imports: [], // Handled by core/imports.js
|
|
1278
1403
|
exports: [] // Handled by core/imports.js
|
|
1279
1404
|
};
|
|
@@ -1398,7 +1523,11 @@ function findCallsInCode(code, parser) {
|
|
|
1398
1523
|
const tree = parseTree(parser, code);
|
|
1399
1524
|
const calls = [];
|
|
1400
1525
|
const functionStack = []; // Stack of { name, startLine, endLine }
|
|
1401
|
-
|
|
1526
|
+
// Local aliases with lexical ownership. A flat aliasName→target map leaks
|
|
1527
|
+
// block locals into the rest of a module (`let effect = batchedEffect`
|
|
1528
|
+
// inside a loop rewrote a later module-level `effect()` call), producing
|
|
1529
|
+
// false external edges and caller/callee disagreement.
|
|
1530
|
+
const aliases = new Map(); // aliasName -> [{ target, declarationIndex, scopeStart, scopeEnd }]
|
|
1402
1531
|
const nonCallableNames = new Set(); // Track names assigned non-callable values
|
|
1403
1532
|
const localVarTypes = new Map(); // Track local variable types: varName -> typeName (for receiverType inference)
|
|
1404
1533
|
// Names whose type came from a DECLARED annotation (TS `x: Foo` / typed
|
|
@@ -1418,7 +1547,7 @@ function findCallsInCode(code, parser) {
|
|
|
1418
1547
|
if (!argsNode) return null;
|
|
1419
1548
|
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
1420
1549
|
const arg = argsNode.namedChild(i);
|
|
1421
|
-
if (arg.type
|
|
1550
|
+
if (arg.type.endsWith('comment')) continue;
|
|
1422
1551
|
return _extractStringArg(arg);
|
|
1423
1552
|
}
|
|
1424
1553
|
return null;
|
|
@@ -1434,7 +1563,7 @@ function findCallsInCode(code, parser) {
|
|
|
1434
1563
|
let count = 0;
|
|
1435
1564
|
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
1436
1565
|
const arg = argsNode.namedChild(i);
|
|
1437
|
-
if (arg.type
|
|
1566
|
+
if (arg.type.endsWith('comment')) continue;
|
|
1438
1567
|
count++;
|
|
1439
1568
|
}
|
|
1440
1569
|
return count;
|
|
@@ -1452,7 +1581,7 @@ function findCallsInCode(code, parser) {
|
|
|
1452
1581
|
let target = null;
|
|
1453
1582
|
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
1454
1583
|
const arg = argsNode.namedChild(i);
|
|
1455
|
-
if (arg.type
|
|
1584
|
+
if (arg.type.endsWith('comment')) continue;
|
|
1456
1585
|
if (idx === argIdx) { target = arg; break; }
|
|
1457
1586
|
idx++;
|
|
1458
1587
|
}
|
|
@@ -1589,6 +1718,38 @@ function findCallsInCode(code, parser) {
|
|
|
1589
1718
|
: null;
|
|
1590
1719
|
};
|
|
1591
1720
|
|
|
1721
|
+
const aliasScope = (declarator) => {
|
|
1722
|
+
const declaration = declarator.parent;
|
|
1723
|
+
const lexical = declaration?.type === 'lexical_declaration';
|
|
1724
|
+
for (let p = declaration?.parent; p; p = p.parent) {
|
|
1725
|
+
if (lexical && (p.type === 'statement_block' || p.type === 'switch_body' ||
|
|
1726
|
+
p.type === 'for_statement' || p.type === 'for_in_statement')) return p;
|
|
1727
|
+
if (isFunctionNode(p) || p.type === 'program' || p.type === 'module') return p;
|
|
1728
|
+
}
|
|
1729
|
+
return tree.rootNode;
|
|
1730
|
+
};
|
|
1731
|
+
const recordAlias = (name, target, declarator) => {
|
|
1732
|
+
const scope = aliasScope(declarator);
|
|
1733
|
+
if (!aliases.has(name)) aliases.set(name, []);
|
|
1734
|
+
aliases.get(name).push({
|
|
1735
|
+
target,
|
|
1736
|
+
declarationIndex: declarator.startIndex,
|
|
1737
|
+
scopeStart: scope.startIndex,
|
|
1738
|
+
scopeEnd: scope.endIndex,
|
|
1739
|
+
});
|
|
1740
|
+
};
|
|
1741
|
+
const resolveAlias = (name, callNode) => {
|
|
1742
|
+
const records = aliases.get(name);
|
|
1743
|
+
if (!records) return undefined;
|
|
1744
|
+
let best;
|
|
1745
|
+
for (const record of records) {
|
|
1746
|
+
if (record.declarationIndex > callNode.startIndex ||
|
|
1747
|
+
callNode.startIndex < record.scopeStart || callNode.endIndex > record.scopeEnd) continue;
|
|
1748
|
+
if (!best || record.declarationIndex > best.declarationIndex) best = record;
|
|
1749
|
+
}
|
|
1750
|
+
return best && best.target;
|
|
1751
|
+
};
|
|
1752
|
+
|
|
1592
1753
|
// fix #203: does a declaration node declare `name` (incl. shallow destructuring)?
|
|
1593
1754
|
const _declaresName = (declNode, name) => {
|
|
1594
1755
|
for (let i = 0; i < declNode.namedChildCount; i++) {
|
|
@@ -1698,7 +1859,7 @@ function findCallsInCode(code, parser) {
|
|
|
1698
1859
|
}
|
|
1699
1860
|
if (nameNode?.type === 'identifier' && initNode?.type === 'identifier') {
|
|
1700
1861
|
// Simple alias: const p = parse
|
|
1701
|
-
|
|
1862
|
+
recordAlias(nameNode.text, initNode.text, node);
|
|
1702
1863
|
}
|
|
1703
1864
|
// Ternary alias: const fn = cond ? parseCSV : parseJSON → both targets
|
|
1704
1865
|
if (nameNode?.type === 'identifier' && initNode?.type === 'ternary_expression') {
|
|
@@ -1707,7 +1868,7 @@ function findCallsInCode(code, parser) {
|
|
|
1707
1868
|
const targets = [];
|
|
1708
1869
|
if (consequence?.type === 'identifier') targets.push(consequence.text);
|
|
1709
1870
|
if (alternative?.type === 'identifier') targets.push(alternative.text);
|
|
1710
|
-
if (targets.length > 0)
|
|
1871
|
+
if (targets.length > 0) recordAlias(nameNode.text, targets, node);
|
|
1711
1872
|
}
|
|
1712
1873
|
// Destructured rename: const { parse: csvParse } = require(...)
|
|
1713
1874
|
if (nameNode?.type === 'object_pattern') {
|
|
@@ -1718,7 +1879,7 @@ function findCallsInCode(code, parser) {
|
|
|
1718
1879
|
const value = prop.childForFieldName('value');
|
|
1719
1880
|
if ((key?.type === 'identifier' || key?.type === 'property_identifier') &&
|
|
1720
1881
|
value?.type === 'identifier') {
|
|
1721
|
-
|
|
1882
|
+
recordAlias(value.text, key.text, node);
|
|
1722
1883
|
}
|
|
1723
1884
|
}
|
|
1724
1885
|
}
|
|
@@ -1819,6 +1980,36 @@ function findCallsInCode(code, parser) {
|
|
|
1819
1980
|
}
|
|
1820
1981
|
}
|
|
1821
1982
|
|
|
1983
|
+
// Tree-sitter recovery can flatten a valid constructor into an ERROR
|
|
1984
|
+
// node when an earlier unsupported TypeScript construct destabilizes
|
|
1985
|
+
// the surrounding declaration. Keep this AST-first: inspect recovery
|
|
1986
|
+
// tokens/named children only—never source regex. Example (Hono's
|
|
1987
|
+
// overload-heavy factory file): ERROR children `app`, `=`, `new`,
|
|
1988
|
+
// `Hono` for `const app = new Hono<E>(...)`. Without this, the class
|
|
1989
|
+
// had a false zero-caller answer even though the identifier and `new`
|
|
1990
|
+
// token survived in the syntax tree.
|
|
1991
|
+
if (node.type === 'ERROR') {
|
|
1992
|
+
for (let i = 0; i < node.childCount - 1; i++) {
|
|
1993
|
+
const token = node.child(i);
|
|
1994
|
+
if (token.type !== 'new' || token.isNamed) continue;
|
|
1995
|
+
let ctorNode = null;
|
|
1996
|
+
for (let j = i + 1; j < node.childCount; j++) {
|
|
1997
|
+
const candidate = node.child(j);
|
|
1998
|
+
if (candidate.isNamed) { ctorNode = candidate; break; }
|
|
1999
|
+
}
|
|
2000
|
+
const ctorName = jsConstructorTypeName(ctorNode);
|
|
2001
|
+
if (!ctorName) continue;
|
|
2002
|
+
calls.push({
|
|
2003
|
+
name: ctorName,
|
|
2004
|
+
line: ctorNode.startPosition.row + 1,
|
|
2005
|
+
isMethod: ctorNode.type === 'member_expression',
|
|
2006
|
+
isConstructor: true,
|
|
2007
|
+
parseRecovery: true,
|
|
2008
|
+
enclosingFunction: getCurrentEnclosingFunction(),
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
|
|
1822
2013
|
// Handle regular function calls: foo(), obj.foo(), foo.call()
|
|
1823
2014
|
if (node.type === 'call_expression') {
|
|
1824
2015
|
const funcNode = node.childForFieldName('function');
|
|
@@ -1833,7 +2024,7 @@ function findCallsInCode(code, parser) {
|
|
|
1833
2024
|
|
|
1834
2025
|
if (funcNode.type === 'identifier') {
|
|
1835
2026
|
// Direct call: foo()
|
|
1836
|
-
const alias =
|
|
2027
|
+
const alias = resolveAlias(funcNode.text, node);
|
|
1837
2028
|
const resolvedName = typeof alias === 'string' ? alias : undefined;
|
|
1838
2029
|
const resolvedNames = Array.isArray(alias) ? alias : undefined;
|
|
1839
2030
|
const firstArg = getFirstStringArg(node);
|
|
@@ -2031,7 +2222,7 @@ function findCallsInCode(code, parser) {
|
|
|
2031
2222
|
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
2032
2223
|
const arg = argsNode.namedChild(i);
|
|
2033
2224
|
// Skip non-argument nodes (e.g. commas)
|
|
2034
|
-
if (arg.type
|
|
2225
|
+
if (arg.type.endsWith('comment')) continue;
|
|
2035
2226
|
// Only check args at callback positions (null = all positions)
|
|
2036
2227
|
const isCallbackPos = callbackIndices === null || callbackIndices === undefined || callbackIndices.has(argIdx);
|
|
2037
2228
|
if (isCallbackPos) {
|
package/languages/python.js
CHANGED
|
@@ -322,8 +322,8 @@ function extractDecorators(node) {
|
|
|
322
322
|
* `X: TypeAlias = ...` assignments become 'type' symbols with aliasOf.
|
|
323
323
|
*/
|
|
324
324
|
function _processTypeAlias(node, classes, processedRanges, lines) {
|
|
325
|
-
let name
|
|
326
|
-
let valueText
|
|
325
|
+
let name;
|
|
326
|
+
let valueText;
|
|
327
327
|
if (node.type === 'type_alias_statement') {
|
|
328
328
|
// grammar shape: type <left> = <right>
|
|
329
329
|
const left = node.namedChild(0);
|
|
@@ -543,6 +543,7 @@ function parse(code, parser) {
|
|
|
543
543
|
functions,
|
|
544
544
|
classes,
|
|
545
545
|
stateObjects,
|
|
546
|
+
...(tree.rootNode.hasError && { parseRecovery: true }),
|
|
546
547
|
...(moduleAssigned.size > 0 && { moduleAssignedNames: [...moduleAssigned].sort() }),
|
|
547
548
|
imports: [],
|
|
548
549
|
exports: []
|
|
@@ -654,17 +655,24 @@ function assignmentTargetOf(callNode) {
|
|
|
654
655
|
}
|
|
655
656
|
|
|
656
657
|
/**
|
|
657
|
-
* Type
|
|
658
|
-
*
|
|
658
|
+
* Type identity hint from a constructor-call callee: ClassName(...) or
|
|
659
|
+
* pkg.ClassName(...). Preserve the qualifier: dropping `threading` from
|
|
660
|
+
* `threading.Thread()` turns an external class into an unqualified project
|
|
661
|
+
* type name and can falsely confirm `thread.join()` against Project.join.
|
|
662
|
+
* Uppercase-first remains a Python class-naming heuristic, so callers use the
|
|
663
|
+
* qualifier as routing/provenance evidence rather than a hidden hard claim.
|
|
659
664
|
*/
|
|
660
|
-
function
|
|
665
|
+
function constructorTypeInfo(funcNode) {
|
|
661
666
|
if (!funcNode) return undefined;
|
|
662
667
|
if (funcNode.type === 'identifier') {
|
|
663
|
-
return /^[A-Z]/.test(funcNode.text) ? funcNode.text : undefined;
|
|
668
|
+
return /^[A-Z]/.test(funcNode.text) ? { type: funcNode.text } : undefined;
|
|
664
669
|
}
|
|
665
670
|
if (funcNode.type === 'attribute') {
|
|
666
671
|
const attr = funcNode.childForFieldName('attribute');
|
|
667
|
-
|
|
672
|
+
const object = funcNode.childForFieldName('object');
|
|
673
|
+
return attr && /^[A-Z]/.test(attr.text)
|
|
674
|
+
? { type: attr.text, qualifier: object?.type === 'identifier' ? object.text : undefined }
|
|
675
|
+
: undefined;
|
|
668
676
|
}
|
|
669
677
|
return undefined;
|
|
670
678
|
}
|
|
@@ -676,6 +684,7 @@ function findCallsInCode(code, parser) {
|
|
|
676
684
|
const aliases = new Map(); // Track local aliases: aliasName -> originalName
|
|
677
685
|
const nonCallableNames = new Set(); // Track names assigned non-callable values
|
|
678
686
|
const localVarTypes = new Map(); // Track local variable types: varName -> typeName (for receiverType inference)
|
|
687
|
+
const localVarTypeQualifiers = new Map(); // varName -> imported module alias that owns the inferred type
|
|
679
688
|
// Member-access aliases (fix #218): `append = output.append` makes a later
|
|
680
689
|
// bare `append(part)` a METHOD call on `output` — it must carry the
|
|
681
690
|
// receiver's evidence, never bind by bare name to a same-file def
|
|
@@ -687,6 +696,7 @@ function findCallsInCode(code, parser) {
|
|
|
687
696
|
const memberAliasesStack = []; // function-scoped save/restore, like localVarTypes
|
|
688
697
|
const moduleAliases = new Set(); // Names bound to MODULES (import httpx / import numpy as np)
|
|
689
698
|
const localVarTypesStack = []; // Stack for function-scoped save/restore of localVarTypes
|
|
699
|
+
const localVarTypeQualifiersStack = [];
|
|
690
700
|
|
|
691
701
|
// Helper: extract first string-arg literal from a call node.
|
|
692
702
|
// Used by route extraction to capture path arg of requests.get('/users'), httpx.get('/users') etc.
|
|
@@ -697,7 +707,7 @@ function findCallsInCode(code, parser) {
|
|
|
697
707
|
if (!argsNode) return null;
|
|
698
708
|
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
699
709
|
const arg = argsNode.namedChild(i);
|
|
700
|
-
if (arg.type
|
|
710
|
+
if (arg.type.endsWith('comment')) continue;
|
|
701
711
|
// Handle f-string explicitly
|
|
702
712
|
if (arg.type === 'string') {
|
|
703
713
|
// f-string detection: tree-sitter-python wraps interpolations as 'interpolation' children.
|
|
@@ -877,6 +887,7 @@ function findCallsInCode(code, parser) {
|
|
|
877
887
|
});
|
|
878
888
|
// Save localVarTypes so inner declarations don't leak to sibling functions
|
|
879
889
|
localVarTypesStack.push(new Map(localVarTypes));
|
|
890
|
+
localVarTypeQualifiersStack.push(new Map(localVarTypeQualifiers));
|
|
880
891
|
memberAliasesStack.push(new Map(memberAliases));
|
|
881
892
|
}
|
|
882
893
|
|
|
@@ -902,8 +913,15 @@ function findCallsInCode(code, parser) {
|
|
|
902
913
|
const target = value.namedChildCount > 1 ? value.namedChild(value.namedChildCount - 1) : null;
|
|
903
914
|
const targetId = target?.type === 'as_pattern_target' ? target.namedChild(0) : null;
|
|
904
915
|
if (ctx?.type === 'call' && targetId?.type === 'identifier') {
|
|
905
|
-
const
|
|
906
|
-
if (
|
|
916
|
+
const ctor = constructorTypeInfo(ctx.childForFieldName('function'));
|
|
917
|
+
if (ctor) {
|
|
918
|
+
localVarTypes.set(targetId.text, ctor.type);
|
|
919
|
+
if (ctor.qualifier && moduleAliases.has(ctor.qualifier)) {
|
|
920
|
+
localVarTypeQualifiers.set(targetId.text, ctor.qualifier);
|
|
921
|
+
} else {
|
|
922
|
+
localVarTypeQualifiers.delete(targetId.text);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
907
925
|
}
|
|
908
926
|
}
|
|
909
927
|
}
|
|
@@ -926,7 +944,14 @@ function findCallsInCode(code, parser) {
|
|
|
926
944
|
// type stale — nearest-preceding-assignment semantics (#199's
|
|
927
945
|
// documented rule). Without this, `x = ""; x = render(); x.m()`
|
|
928
946
|
// would carry str and falsely exclude project methods.
|
|
929
|
-
if (!typeNode)
|
|
947
|
+
if (!typeNode) {
|
|
948
|
+
localVarTypes.delete(left.text);
|
|
949
|
+
localVarTypeQualifiers.delete(left.text);
|
|
950
|
+
} else {
|
|
951
|
+
// An annotation is the authoritative type source; a
|
|
952
|
+
// previous constructor qualifier must not survive it.
|
|
953
|
+
localVarTypeQualifiers.delete(left.text);
|
|
954
|
+
}
|
|
930
955
|
// Literal assignment types the variable (fix #218):
|
|
931
956
|
// ansi_bytes = b"…" → bytes; out = [] → list. Compiler-true,
|
|
932
957
|
// same trust grade as literal receivers ({}.get() → dict).
|
|
@@ -989,9 +1014,14 @@ function findCallsInCode(code, parser) {
|
|
|
989
1014
|
nonCallableNames.add(left.text);
|
|
990
1015
|
// Infer type from constructor call: x = ClassName(...) or
|
|
991
1016
|
// x = pkg.ClassName(...). Python convention: classes start uppercase
|
|
992
|
-
const
|
|
993
|
-
if (
|
|
994
|
-
localVarTypes.set(left.text,
|
|
1017
|
+
const ctor = constructorTypeInfo(right.childForFieldName('function'));
|
|
1018
|
+
if (ctor) {
|
|
1019
|
+
localVarTypes.set(left.text, ctor.type);
|
|
1020
|
+
if (ctor.qualifier && moduleAliases.has(ctor.qualifier)) {
|
|
1021
|
+
localVarTypeQualifiers.set(left.text, ctor.qualifier);
|
|
1022
|
+
} else {
|
|
1023
|
+
localVarTypeQualifiers.delete(left.text);
|
|
1024
|
+
}
|
|
995
1025
|
}
|
|
996
1026
|
}
|
|
997
1027
|
// Third: subscript/attribute access results are non-callable data
|
|
@@ -1023,7 +1053,7 @@ function findCallsInCode(code, parser) {
|
|
|
1023
1053
|
if (callArgsNode) {
|
|
1024
1054
|
for (let i = 0; i < callArgsNode.namedChildCount; i++) {
|
|
1025
1055
|
const arg = callArgsNode.namedChild(i);
|
|
1026
|
-
if (arg.type
|
|
1056
|
+
if (arg.type.endsWith('comment')) continue;
|
|
1027
1057
|
if (arg.type === 'list_splat' || arg.type === 'dictionary_splat') argSpread = true;
|
|
1028
1058
|
argCount++;
|
|
1029
1059
|
}
|
|
@@ -1142,6 +1172,9 @@ function findCallsInCode(code, parser) {
|
|
|
1142
1172
|
const receiverType = receiver
|
|
1143
1173
|
? localVarTypes.get(receiver)
|
|
1144
1174
|
: (objNode ? PY_LITERAL_RECEIVER_TYPES[objNode.type] : undefined);
|
|
1175
|
+
const receiverTypeQualifier = receiver
|
|
1176
|
+
? localVarTypeQualifiers.get(receiver)
|
|
1177
|
+
: undefined;
|
|
1145
1178
|
// Module receiver (httpx.get()) — unless locally shadowed
|
|
1146
1179
|
// by a typed instance binding
|
|
1147
1180
|
const receiverIsModule = !!receiver && moduleAliases.has(receiver) &&
|
|
@@ -1156,6 +1189,7 @@ function findCallsInCode(code, parser) {
|
|
|
1156
1189
|
isMethod: true,
|
|
1157
1190
|
receiver,
|
|
1158
1191
|
...(receiverType && { receiverType }),
|
|
1192
|
+
...(receiverTypeQualifier && { receiverTypeQualifier }),
|
|
1159
1193
|
...(receiverIsModule && { receiverIsModule: true }),
|
|
1160
1194
|
...(selfAttribute && { selfAttribute }),
|
|
1161
1195
|
...(receiverCall && { receiverCall }),
|
|
@@ -1234,6 +1268,11 @@ function findCallsInCode(code, parser) {
|
|
|
1234
1268
|
localVarTypes.clear();
|
|
1235
1269
|
for (const [k, v] of saved) localVarTypes.set(k, v);
|
|
1236
1270
|
}
|
|
1271
|
+
const savedQualifiers = localVarTypeQualifiersStack.pop();
|
|
1272
|
+
if (savedQualifiers) {
|
|
1273
|
+
localVarTypeQualifiers.clear();
|
|
1274
|
+
for (const [k, v] of savedQualifiers) localVarTypeQualifiers.set(k, v);
|
|
1275
|
+
}
|
|
1237
1276
|
const savedAliases = memberAliasesStack.pop();
|
|
1238
1277
|
if (savedAliases) {
|
|
1239
1278
|
memberAliases.clear();
|