ucn 4.0.2 → 4.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 +31 -7
- package/README.md +90 -51
- package/cli/index.js +199 -94
- package/core/account.js +3 -1
- package/core/analysis.js +322 -304
- package/core/bridge.js +13 -8
- package/core/cache.js +109 -19
- package/core/callers.js +969 -77
- package/core/check.js +19 -8
- package/core/deadcode.js +368 -40
- package/core/discovery.js +31 -11
- package/core/entrypoints.js +149 -17
- package/core/execute.js +330 -43
- package/core/graph-build.js +61 -10
- package/core/graph.js +282 -61
- package/core/imports.js +72 -3
- package/core/output/analysis-ext.js +70 -10
- package/core/output/analysis.js +52 -33
- package/core/output/check.js +4 -1
- package/core/output/endpoints.js +8 -3
- package/core/output/extraction.js +12 -1
- package/core/output/find.js +23 -9
- package/core/output/graph.js +32 -9
- package/core/output/refactoring.js +147 -49
- package/core/output/reporting.js +104 -5
- package/core/output/search.js +30 -3
- package/core/output/shared.js +31 -4
- package/core/output/tracing.js +22 -11
- package/core/parser.js +8 -6
- package/core/project.js +167 -7
- package/core/registry.js +20 -16
- package/core/reporting.js +131 -13
- package/core/search.js +270 -55
- package/core/shared.js +240 -1
- package/core/stacktrace.js +23 -1
- package/core/tracing.js +278 -36
- package/core/verify.js +352 -349
- package/languages/go.js +29 -17
- package/languages/index.js +56 -0
- package/languages/java.js +133 -16
- package/languages/javascript.js +215 -27
- package/languages/python.js +113 -47
- package/languages/rust.js +89 -26
- package/languages/utils.js +35 -7
- package/mcp/server.js +69 -30
- package/package.json +5 -1
package/languages/javascript.js
CHANGED
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
extractJSDocstring,
|
|
15
15
|
buildTypeAnnotations,
|
|
16
16
|
visitNameNodes,
|
|
17
|
+
sameNode,
|
|
17
18
|
} = require('./utils');
|
|
18
19
|
const { PARSE_OPTIONS, safeParse } = require('./index');
|
|
19
20
|
|
|
@@ -99,14 +100,37 @@ function getAssignmentName(leftNode) {
|
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
/**
|
|
102
|
-
* Extract modifiers from
|
|
103
|
+
* Extract modifiers from a declaration NODE — AST tokens, never text
|
|
104
|
+
* (fix #249: the first-line regex fabricated export/async/default from
|
|
105
|
+
* string literals, comments, and `m?.default?.()` on one-line functions,
|
|
106
|
+
* leaking fake exports into api/fileExports and corrupting isAsync).
|
|
107
|
+
* Accepts the declaration or its export_statement wrapper.
|
|
103
108
|
*/
|
|
104
|
-
function extractModifiers(
|
|
109
|
+
function extractModifiers(node) {
|
|
105
110
|
const mods = [];
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
111
|
+
if (!node) return mods;
|
|
112
|
+
let decl = node;
|
|
113
|
+
if (node.type === 'export_statement') {
|
|
114
|
+
mods.push('export');
|
|
115
|
+
decl = node.childForFieldName('declaration') || node;
|
|
116
|
+
}
|
|
117
|
+
// Resolve to the actual function-shaped node: `const x = async () => {}`
|
|
118
|
+
// keeps its async token on the arrow, not the declaration.
|
|
119
|
+
if (decl.type === 'lexical_declaration' || decl.type === 'variable_declaration') {
|
|
120
|
+
const declarator = decl.namedChildren.find(c => c.type === 'variable_declarator');
|
|
121
|
+
const value = declarator && declarator.childForFieldName('value');
|
|
122
|
+
if (value) decl = value;
|
|
123
|
+
}
|
|
124
|
+
let isAsync = false;
|
|
125
|
+
for (let i = 0; i < decl.childCount; i++) {
|
|
126
|
+
if (decl.child(i).type === 'async') { isAsync = true; break; }
|
|
127
|
+
}
|
|
128
|
+
if (isAsync) mods.push('async');
|
|
129
|
+
if (node.type === 'export_statement') {
|
|
130
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
131
|
+
if (node.child(i).type === 'default') { mods.push('default'); break; }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
110
134
|
return mods;
|
|
111
135
|
}
|
|
112
136
|
|
|
@@ -277,8 +301,8 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
277
301
|
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
|
|
278
302
|
// Check parent for export status (function_declaration inside export_statement)
|
|
279
303
|
const modifiers = node.parent && node.parent.type === 'export_statement'
|
|
280
|
-
? extractModifiers(node.parent
|
|
281
|
-
: extractModifiers(node
|
|
304
|
+
? extractModifiers(node.parent)
|
|
305
|
+
: extractModifiers(node);
|
|
282
306
|
// Feature B: explicit isAsync flag (auditAsync needs to know whether
|
|
283
307
|
// the fn was declared `async function`).
|
|
284
308
|
const isAsync = modifiers.includes('async');
|
|
@@ -364,8 +388,8 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
364
388
|
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
|
|
365
389
|
// Check parent for export status (lexical_declaration inside export_statement)
|
|
366
390
|
const modifiers = node.parent && node.parent.type === 'export_statement'
|
|
367
|
-
? extractModifiers(node.parent
|
|
368
|
-
: extractModifiers(node
|
|
391
|
+
? extractModifiers(node.parent)
|
|
392
|
+
: extractModifiers(node);
|
|
369
393
|
// Feature B: detect async — for arrow/fn-expressions the `async`
|
|
370
394
|
// keyword precedes the parameter list on the value node, NOT on
|
|
371
395
|
// the lexical_declaration text. extractModifiers walked the full
|
|
@@ -417,8 +441,8 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
417
441
|
const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
|
|
418
442
|
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
|
|
419
443
|
const modifiers = node.parent && node.parent.type === 'export_statement'
|
|
420
|
-
? extractModifiers(node.parent
|
|
421
|
-
: extractModifiers(node
|
|
444
|
+
? extractModifiers(node.parent)
|
|
445
|
+
: extractModifiers(node);
|
|
422
446
|
|
|
423
447
|
functions.push({
|
|
424
448
|
name: nameNode.text,
|
|
@@ -509,6 +533,48 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
509
533
|
...(docstring && { docstring })
|
|
510
534
|
});
|
|
511
535
|
}
|
|
536
|
+
} else if (rightNode.type === 'object') {
|
|
537
|
+
// CJS export object maps (fix #252): the functions in
|
|
538
|
+
// `module.exports = { doThing(x) {...}, h: function() {...} }`
|
|
539
|
+
// are the module's public API — prototype and exports.h
|
|
540
|
+
// assignments were indexed while this shape was invisible
|
|
541
|
+
// to fn/find/toc.
|
|
542
|
+
const lhsText = leftNode.text;
|
|
543
|
+
if (lhsText === 'module.exports' || lhsText === 'exports' ||
|
|
544
|
+
lhsText.startsWith('module.exports.') || lhsText.startsWith('exports.')) {
|
|
545
|
+
processedRanges.add(rangeKey);
|
|
546
|
+
for (let pi = 0; pi < rightNode.namedChildCount; pi++) {
|
|
547
|
+
const prop = rightNode.namedChild(pi);
|
|
548
|
+
let nameNode = null;
|
|
549
|
+
let fnNode = null;
|
|
550
|
+
if (prop.type === 'method_definition') {
|
|
551
|
+
nameNode = prop.childForFieldName('name');
|
|
552
|
+
fnNode = prop;
|
|
553
|
+
} else if (prop.type === 'pair') {
|
|
554
|
+
const v = prop.childForFieldName('value');
|
|
555
|
+
if (v && (v.type === 'function_expression' ||
|
|
556
|
+
v.type === 'arrow_function' || v.type === 'generator_function')) {
|
|
557
|
+
nameNode = prop.childForFieldName('key');
|
|
558
|
+
fnNode = v;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (!nameNode || !fnNode) continue;
|
|
562
|
+
const paramsNode = fnNode.childForFieldName('parameters');
|
|
563
|
+
const { startLine, endLine, indent } = nodeToLocation(prop, lines);
|
|
564
|
+
const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
|
|
565
|
+
functions.push({
|
|
566
|
+
name: nameNode.text,
|
|
567
|
+
params: extractParams(paramsNode),
|
|
568
|
+
paramsStructured,
|
|
569
|
+
startLine,
|
|
570
|
+
endLine,
|
|
571
|
+
indent,
|
|
572
|
+
isArrow: fnNode.type === 'arrow_function',
|
|
573
|
+
isGenerator: isGenerator(fnNode),
|
|
574
|
+
modifiers: [],
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
}
|
|
512
578
|
}
|
|
513
579
|
}
|
|
514
580
|
return true;
|
|
@@ -959,6 +1025,13 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
959
1025
|
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, code, startLine, true);
|
|
960
1026
|
|
|
961
1027
|
const decoratorsWithArgs = extractDecoratorsWithArgs(child);
|
|
1028
|
+
// TS accessibility keywords (fix #247): `private`/`protected`
|
|
1029
|
+
// members are not public API — without this, deadcode's
|
|
1030
|
+
// exported-member check treated them as implicitly public and
|
|
1031
|
+
// hid them from the default audit. `public` is the default
|
|
1032
|
+
// and stays unrecorded (recording it would read as an export
|
|
1033
|
+
// marker in symbolIsExported).
|
|
1034
|
+
const accessMatch = text.match(/^\s*(private|protected)\s/);
|
|
962
1035
|
members.push({
|
|
963
1036
|
name,
|
|
964
1037
|
params: extractParams(paramsNode),
|
|
@@ -966,9 +1039,14 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
966
1039
|
startLine,
|
|
967
1040
|
endLine,
|
|
968
1041
|
memberType,
|
|
1042
|
+
...(accessMatch && { modifiers: [accessMatch[1]] }),
|
|
969
1043
|
isAsync,
|
|
970
1044
|
isGenerator: isGen,
|
|
971
1045
|
isMethod: true, // Mark as method for context() lookups
|
|
1046
|
+
// TS method OVERLOAD signatures (body-less method_signature
|
|
1047
|
+
// in a class body) mirror the standalone-function marker
|
|
1048
|
+
// (fix #230) — pickBestDefinition prefers the implementation.
|
|
1049
|
+
...(child.type === 'method_signature' && { isSignature: true }),
|
|
972
1050
|
...typeAnno,
|
|
973
1051
|
...(docstring && { docstring }),
|
|
974
1052
|
...(decorators.length > 0 && { decorators }),
|
|
@@ -1657,6 +1735,25 @@ function findCallsInCode(code, parser) {
|
|
|
1657
1735
|
localVarTypes.set(left.text, ctorName);
|
|
1658
1736
|
}
|
|
1659
1737
|
}
|
|
1738
|
+
// Handler-registration references (fix #252, the #221 family's
|
|
1739
|
+
// missing shape): `window.onload = secondPageInit` /
|
|
1740
|
+
// `element.onclick = handler` establish the call relationship
|
|
1741
|
+
// through property assignment — plain assignment RHS and
|
|
1742
|
+
// argument-position references were captured, the
|
|
1743
|
+
// member-expression LHS shape recorded nothing, so search
|
|
1744
|
+
// --unused claimed live handlers dead.
|
|
1745
|
+
if (left?.type === 'member_expression' && right?.type === 'identifier' &&
|
|
1746
|
+
!SKIP_IDENTS.has(right.text) && !nonCallableNames.has(right.text)) {
|
|
1747
|
+
calls.push({
|
|
1748
|
+
name: right.text,
|
|
1749
|
+
line: right.startPosition.row + 1,
|
|
1750
|
+
isMethod: false,
|
|
1751
|
+
isFunctionReference: true,
|
|
1752
|
+
isPotentialCallback: true,
|
|
1753
|
+
...(isShadowedByLocal(right, right.text) && { localShadow: true }),
|
|
1754
|
+
enclosingFunction: getCurrentEnclosingFunction(),
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1660
1757
|
}
|
|
1661
1758
|
|
|
1662
1759
|
// Handle regular function calls: foo(), obj.foo(), foo.call()
|
|
@@ -1694,6 +1791,21 @@ function findCallsInCode(code, parser) {
|
|
|
1694
1791
|
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp }),
|
|
1695
1792
|
...(optionsMethod && { optionsMethod })
|
|
1696
1793
|
});
|
|
1794
|
+
} else if (funcNode.type === 'super') {
|
|
1795
|
+
// super(config) — the subclass constructor invoking the
|
|
1796
|
+
// parent class's constructor (fix #238; these sites were
|
|
1797
|
+
// invisible to every command). Recorded as a super-received
|
|
1798
|
+
// 'constructor' method call so the super walk resolves it to
|
|
1799
|
+
// the parent class's constructor definition.
|
|
1800
|
+
calls.push({
|
|
1801
|
+
name: 'constructor',
|
|
1802
|
+
line: node.startPosition.row + 1,
|
|
1803
|
+
isMethod: true,
|
|
1804
|
+
receiver: 'super',
|
|
1805
|
+
argCount: node.childForFieldName('arguments')?.namedChildCount ?? 0,
|
|
1806
|
+
enclosingFunction,
|
|
1807
|
+
uncertain: false,
|
|
1808
|
+
});
|
|
1697
1809
|
} else if (funcNode.type === 'member_expression') {
|
|
1698
1810
|
// Method call: obj.foo() or foo.call/apply/bind()
|
|
1699
1811
|
const propNode = funcNode.childForFieldName('property');
|
|
@@ -2238,6 +2350,21 @@ function findImportsInCode(code, parser) {
|
|
|
2238
2350
|
const text = child.text;
|
|
2239
2351
|
modulePath = text.slice(1, -1);
|
|
2240
2352
|
}
|
|
2353
|
+
// TS import-equals: `import x = require('./y')` — the
|
|
2354
|
+
// dependency edge was invisible to imports/exporters/graph/
|
|
2355
|
+
// circularDeps (fix #245; `export = fn` was already captured).
|
|
2356
|
+
if (child.type === 'import_require_clause') {
|
|
2357
|
+
let alias = null, src = null;
|
|
2358
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
2359
|
+
const c = child.namedChild(j);
|
|
2360
|
+
if (c.type === 'identifier') alias = c.text;
|
|
2361
|
+
if (c.type === 'string') src = c.text.slice(1, -1);
|
|
2362
|
+
}
|
|
2363
|
+
if (src) {
|
|
2364
|
+
imports.push({ module: src, names: alias ? [alias] : [], type: 'require', line });
|
|
2365
|
+
}
|
|
2366
|
+
return true;
|
|
2367
|
+
}
|
|
2241
2368
|
if (child.type === 'import_clause') {
|
|
2242
2369
|
// Process import clause
|
|
2243
2370
|
for (let j = 0; j < child.namedChildCount; j++) {
|
|
@@ -2475,11 +2602,42 @@ function findExportsInCode(code, parser) {
|
|
|
2475
2602
|
if (nameNode) {
|
|
2476
2603
|
exports.push({ name: nameNode.text, type: isDefaultExport ? 'default' : 'named', line });
|
|
2477
2604
|
}
|
|
2478
|
-
} else if (child.type === 'class_declaration') {
|
|
2605
|
+
} else if (child.type === 'class_declaration' || child.type === 'abstract_class_declaration') {
|
|
2606
|
+
// tree-sitter-typescript emits abstract_class_declaration
|
|
2607
|
+
// for `export abstract class X` — the symbol extractor
|
|
2608
|
+
// knew the node type, the export scanner did not (fix
|
|
2609
|
+
// #245: the class was never recorded as an export).
|
|
2479
2610
|
const nameNode = child.childForFieldName('name');
|
|
2480
2611
|
if (nameNode) {
|
|
2481
2612
|
exports.push({ name: nameNode.text, type: isDefaultExport ? 'default' : 'named', line });
|
|
2482
2613
|
}
|
|
2614
|
+
} else if (child.type === 'ambient_declaration') {
|
|
2615
|
+
// export declare function/class/const X — the ambient
|
|
2616
|
+
// wrapper holds the real declaration (fix #245).
|
|
2617
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
2618
|
+
const inner = child.namedChild(j);
|
|
2619
|
+
const nameNode = inner.childForFieldName?.('name');
|
|
2620
|
+
if (nameNode) {
|
|
2621
|
+
exports.push({ name: nameNode.text, type: 'named', line });
|
|
2622
|
+
} else if (inner.type === 'lexical_declaration' || inner.type === 'variable_declaration') {
|
|
2623
|
+
for (let k = 0; k < inner.namedChildCount; k++) {
|
|
2624
|
+
const d = inner.namedChild(k);
|
|
2625
|
+
const n = d.type === 'variable_declarator' && d.childForFieldName('name');
|
|
2626
|
+
if (n && n.type === 'identifier') {
|
|
2627
|
+
exports.push({ name: n.text, type: 'named', line, isVariable: true, declKind: 'declare' });
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
} else if (child.type === 'internal_module' || child.type === 'module') {
|
|
2633
|
+
// export namespace Geo { ... } — the NAMESPACE is the
|
|
2634
|
+
// importable name; its inner members are reached as
|
|
2635
|
+
// Geo.member (fix #245: only inner names were listed,
|
|
2636
|
+
// none of them importable).
|
|
2637
|
+
const nameNode = child.childForFieldName('name');
|
|
2638
|
+
if (nameNode) {
|
|
2639
|
+
exports.push({ name: nameNode.text, type: 'named', line });
|
|
2640
|
+
}
|
|
2483
2641
|
} else if (child.type === 'type_alias_declaration') {
|
|
2484
2642
|
// export type X = ...
|
|
2485
2643
|
const nameNode = child.childForFieldName('name');
|
|
@@ -2554,6 +2712,14 @@ function findExportsInCode(code, parser) {
|
|
|
2554
2712
|
} else if (prop.type === 'pair') {
|
|
2555
2713
|
const key = prop.childForFieldName('key');
|
|
2556
2714
|
if (key) exports.push({ name: key.text, type: 'module.exports', line });
|
|
2715
|
+
} else if (prop.type === 'method_definition') {
|
|
2716
|
+
// Shorthand methods are exports too
|
|
2717
|
+
// (fix #252 — `module.exports =
|
|
2718
|
+
// { doThing(x) {} }` was invisible to the
|
|
2719
|
+
// export list, so deadcode audited a
|
|
2720
|
+
// require()-reachable function).
|
|
2721
|
+
const mName = prop.childForFieldName('name');
|
|
2722
|
+
if (mName) exports.push({ name: mName.text, type: 'module.exports', line });
|
|
2557
2723
|
}
|
|
2558
2724
|
}
|
|
2559
2725
|
} else if (rightNode && rightNode.type === 'identifier') {
|
|
@@ -2594,18 +2760,23 @@ function findExportsInCode(code, parser) {
|
|
|
2594
2760
|
* @param {string} code - Source code
|
|
2595
2761
|
* @param {string} name - Symbol name to find
|
|
2596
2762
|
* @param {object} parser - Tree-sitter parser instance
|
|
2763
|
+
* @param {object} [tree] - Pre-parsed tree (per-operation cache); parsed here when absent
|
|
2597
2764
|
* @returns {Array<{line: number, column: number, usageType: string}>}
|
|
2598
2765
|
*/
|
|
2599
|
-
function findUsagesInCode(code, name, parser) {
|
|
2600
|
-
|
|
2766
|
+
function findUsagesInCode(code, name, parser, tree) {
|
|
2767
|
+
tree = tree || parseTree(parser, code);
|
|
2601
2768
|
const usages = [];
|
|
2602
2769
|
|
|
2603
2770
|
visitNameNodes(tree, code, name, (node) => {
|
|
2604
2771
|
// Look for identifier, property_identifier (method names in obj.method() calls),
|
|
2605
|
-
// type_identifier (TypeScript type annotations),
|
|
2606
|
-
// (destructured names in `const { name } = require(...)`)
|
|
2772
|
+
// type_identifier (TypeScript type annotations), shorthand_property_identifier_pattern
|
|
2773
|
+
// (destructured names in `const { name } = require(...)`), and
|
|
2774
|
+
// shorthand_property_identifier (value-position shorthand — CJS export
|
|
2775
|
+
// objects `module.exports = { helper }` and option objects `f({ helper })`
|
|
2776
|
+
// reference the symbol but produced no usage record at all, fix #241)
|
|
2607
2777
|
const isIdentifier = node.type === 'identifier' || node.type === 'property_identifier' ||
|
|
2608
|
-
node.type === 'type_identifier' || node.type === 'shorthand_property_identifier_pattern'
|
|
2778
|
+
node.type === 'type_identifier' || node.type === 'shorthand_property_identifier_pattern' ||
|
|
2779
|
+
node.type === 'shorthand_property_identifier';
|
|
2609
2780
|
if (!isIdentifier || node.text !== name) {
|
|
2610
2781
|
return true;
|
|
2611
2782
|
}
|
|
@@ -2626,38 +2797,55 @@ function findUsagesInCode(code, name, parser) {
|
|
|
2626
2797
|
}
|
|
2627
2798
|
// Call: identifier is function in call_expression
|
|
2628
2799
|
else if (parent.type === 'call_expression' &&
|
|
2629
|
-
parent.childForFieldName('function')
|
|
2800
|
+
sameNode(parent.childForFieldName('function'), node)) {
|
|
2630
2801
|
usageType = 'call';
|
|
2631
2802
|
}
|
|
2632
2803
|
// New expression: identifier is constructor
|
|
2633
2804
|
else if (parent.type === 'new_expression' &&
|
|
2634
|
-
parent.childForFieldName('constructor')
|
|
2805
|
+
sameNode(parent.childForFieldName('constructor'), node)) {
|
|
2635
2806
|
usageType = 'call';
|
|
2636
2807
|
}
|
|
2637
2808
|
// Definition: function name in declaration
|
|
2638
2809
|
else if ((parent.type === 'function_declaration' ||
|
|
2639
2810
|
parent.type === 'generator_function_declaration') &&
|
|
2640
|
-
parent.childForFieldName('name')
|
|
2811
|
+
sameNode(parent.childForFieldName('name'), node)) {
|
|
2641
2812
|
usageType = 'definition';
|
|
2642
2813
|
}
|
|
2643
|
-
// Definition: variable name in declarator (left side of =)
|
|
2814
|
+
// Definition: variable name in declarator (left side of =).
|
|
2815
|
+
// When the right side is require()/import() the line IS the import
|
|
2816
|
+
// of the symbol — `const Service = require('./service')` classified
|
|
2817
|
+
// as 'definition' made the project's only import invisible (fix #241).
|
|
2644
2818
|
else if (parent.type === 'variable_declarator' &&
|
|
2645
|
-
parent.childForFieldName('name')
|
|
2819
|
+
sameNode(parent.childForFieldName('name'), node)) {
|
|
2646
2820
|
usageType = 'definition';
|
|
2821
|
+
let value = parent.childForFieldName('value');
|
|
2822
|
+
if (value && value.type === 'await_expression') {
|
|
2823
|
+
value = value.namedChild(0);
|
|
2824
|
+
}
|
|
2825
|
+
// Unwrap require('./x').member — still an import binding
|
|
2826
|
+
if (value && value.type === 'member_expression') {
|
|
2827
|
+
value = value.childForFieldName('object');
|
|
2828
|
+
}
|
|
2829
|
+
if (value && value.type === 'call_expression') {
|
|
2830
|
+
const func = value.childForFieldName('function');
|
|
2831
|
+
if (func && (func.text === 'require' || func.type === 'import')) {
|
|
2832
|
+
usageType = 'import';
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2647
2835
|
}
|
|
2648
2836
|
// Definition: class name
|
|
2649
2837
|
else if (parent.type === 'class_declaration' &&
|
|
2650
|
-
parent.childForFieldName('name')
|
|
2838
|
+
sameNode(parent.childForFieldName('name'), node)) {
|
|
2651
2839
|
usageType = 'definition';
|
|
2652
2840
|
}
|
|
2653
2841
|
// Definition: method name
|
|
2654
2842
|
else if (parent.type === 'method_definition' &&
|
|
2655
|
-
parent.childForFieldName('name')
|
|
2843
|
+
sameNode(parent.childForFieldName('name'), node)) {
|
|
2656
2844
|
usageType = 'definition';
|
|
2657
2845
|
}
|
|
2658
2846
|
// Definition: function expression name (named function expressions)
|
|
2659
2847
|
else if (parent.type === 'function' &&
|
|
2660
|
-
parent.childForFieldName('name')
|
|
2848
|
+
sameNode(parent.childForFieldName('name'), node)) {
|
|
2661
2849
|
usageType = 'definition';
|
|
2662
2850
|
}
|
|
2663
2851
|
// Require: identifier is the name in require('...')
|
|
@@ -2689,7 +2877,7 @@ function findUsagesInCode(code, name, parser) {
|
|
|
2689
2877
|
}
|
|
2690
2878
|
// Property access (method call): a.name() - the name after dot
|
|
2691
2879
|
else if (parent.type === 'member_expression' &&
|
|
2692
|
-
parent.childForFieldName('property')
|
|
2880
|
+
sameNode(parent.childForFieldName('property'), node)) {
|
|
2693
2881
|
// Skip built-in objects and common module names (JSON.parse, path.parse, etc.)
|
|
2694
2882
|
const object = parent.childForFieldName('object');
|
|
2695
2883
|
const builtins = [
|
package/languages/python.js
CHANGED
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
extractPythonDocstring,
|
|
14
14
|
paramTypesFromStructured,
|
|
15
15
|
visitNameNodes,
|
|
16
|
+
sameNode,
|
|
16
17
|
} = require('./utils');
|
|
17
18
|
const { PARSE_OPTIONS, safeParse } = require('./index');
|
|
18
19
|
|
|
@@ -58,10 +59,13 @@ function getIndent(node, code) {
|
|
|
58
59
|
* Extract Python parameters
|
|
59
60
|
*/
|
|
60
61
|
function extractPythonParams(paramsNode) {
|
|
62
|
+
// Distinguish "we have no node" (genuinely unknown) from "node is empty".
|
|
63
|
+
// Returning '...' for empty parens conflated zero-param functions with
|
|
64
|
+
// unknown signatures in JSON output (fix #241; go/rust got this in #238,
|
|
65
|
+
// the shared utils.extractParams already had it).
|
|
61
66
|
if (!paramsNode) return '...';
|
|
62
67
|
const text = paramsNode.text;
|
|
63
68
|
let params = text.replace(/^\(|\)$/g, '').trim();
|
|
64
|
-
if (!params) return '...';
|
|
65
69
|
return params;
|
|
66
70
|
}
|
|
67
71
|
|
|
@@ -297,6 +301,52 @@ function extractDecorators(node) {
|
|
|
297
301
|
return decorators;
|
|
298
302
|
}
|
|
299
303
|
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Type aliases (fix #251 — rule 7: TS/Rust/Go aliases are indexed, Python's
|
|
307
|
+
* were invisible to typedef/find): PEP 695 `type X = int` and annotated
|
|
308
|
+
* `X: TypeAlias = ...` assignments become 'type' symbols with aliasOf.
|
|
309
|
+
*/
|
|
310
|
+
function _processTypeAlias(node, classes, processedRanges, lines) {
|
|
311
|
+
let name = null;
|
|
312
|
+
let valueText = null;
|
|
313
|
+
if (node.type === 'type_alias_statement') {
|
|
314
|
+
// grammar shape: type <left> = <right>
|
|
315
|
+
const left = node.namedChild(0);
|
|
316
|
+
const right = node.namedChild(1);
|
|
317
|
+
if (!left) return false;
|
|
318
|
+
name = left.text.replace(/\[.*\]$/, ''); // strip PEP 695 type params
|
|
319
|
+
valueText = right ? right.text : null;
|
|
320
|
+
} else if (node.type === 'expression_statement') {
|
|
321
|
+
const child = node.namedChild(0);
|
|
322
|
+
if (!child || child.type !== 'assignment') return false;
|
|
323
|
+
const typeNode = child.childForFieldName('type');
|
|
324
|
+
if (!typeNode || !/\bTypeAlias\b/.test(typeNode.text)) return false;
|
|
325
|
+
const leftNode = child.childForFieldName('left');
|
|
326
|
+
const rightNode = child.childForFieldName('right');
|
|
327
|
+
if (!leftNode || leftNode.type !== 'identifier') return false;
|
|
328
|
+
name = leftNode.text;
|
|
329
|
+
valueText = rightNode ? rightNode.text : null;
|
|
330
|
+
} else {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
if (!name) return false;
|
|
334
|
+
const rangeKey = `${node.startIndex}-${node.endIndex}`;
|
|
335
|
+
if (processedRanges.has(rangeKey)) return true;
|
|
336
|
+
processedRanges.add(rangeKey);
|
|
337
|
+
const { startLine, endLine } = nodeToLocation(node, lines);
|
|
338
|
+
classes.push({
|
|
339
|
+
name,
|
|
340
|
+
type: 'type',
|
|
341
|
+
startLine,
|
|
342
|
+
endLine,
|
|
343
|
+
methods: [],
|
|
344
|
+
members: [],
|
|
345
|
+
...(valueText && { aliasOf: valueText.trim() }),
|
|
346
|
+
});
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
|
|
300
350
|
/**
|
|
301
351
|
* Find all classes in Python code using tree-sitter
|
|
302
352
|
*/
|
|
@@ -306,7 +356,8 @@ function findClasses(code, parser) {
|
|
|
306
356
|
const classes = [];
|
|
307
357
|
const processedRanges = new Set();
|
|
308
358
|
traverseTreeCached(tree.rootNode, (node) => {
|
|
309
|
-
_processClass(node, classes, processedRanges, lines)
|
|
359
|
+
_processClass(node, classes, processedRanges, lines) ||
|
|
360
|
+
_processTypeAlias(node, classes, processedRanges, lines);
|
|
310
361
|
return true;
|
|
311
362
|
});
|
|
312
363
|
classes.sort((a, b) => a.startLine - b.startLine);
|
|
@@ -460,7 +511,8 @@ function parse(code, parser) {
|
|
|
460
511
|
|
|
461
512
|
traverseTreeCached(tree.rootNode, (node) => {
|
|
462
513
|
_processFunction(node, functions, processedFn, lines, code);
|
|
463
|
-
_processClass(node, classes, processedCls, lines)
|
|
514
|
+
_processClass(node, classes, processedCls, lines) ||
|
|
515
|
+
_processTypeAlias(node, classes, processedCls, lines);
|
|
464
516
|
_processState(node, stateObjects, lines);
|
|
465
517
|
_processModuleAssign(node, moduleAssigned);
|
|
466
518
|
return true;
|
|
@@ -1344,10 +1396,11 @@ function findExportsInCode(code, parser) {
|
|
|
1344
1396
|
* @param {string} code - Source code
|
|
1345
1397
|
* @param {string} name - Symbol name to find
|
|
1346
1398
|
* @param {object} parser - Tree-sitter parser instance
|
|
1399
|
+
* @param {object} [tree] - Pre-parsed tree (per-operation cache); parsed here when absent
|
|
1347
1400
|
* @returns {Array<{line: number, column: number, usageType: string}>}
|
|
1348
1401
|
*/
|
|
1349
|
-
function findUsagesInCode(code, name, parser) {
|
|
1350
|
-
|
|
1402
|
+
function findUsagesInCode(code, name, parser, tree) {
|
|
1403
|
+
tree = tree || parseTree(parser, code);
|
|
1351
1404
|
const usages = [];
|
|
1352
1405
|
|
|
1353
1406
|
visitNameNodes(tree, code, name, (node) => {
|
|
@@ -1378,17 +1431,17 @@ function findUsagesInCode(code, name, parser) {
|
|
|
1378
1431
|
}
|
|
1379
1432
|
// Call: name()
|
|
1380
1433
|
else if (parent.type === 'call' &&
|
|
1381
|
-
parent.childForFieldName('function')
|
|
1434
|
+
sameNode(parent.childForFieldName('function'), node)) {
|
|
1382
1435
|
usageType = 'call';
|
|
1383
1436
|
}
|
|
1384
1437
|
// Definition: def name(...):
|
|
1385
1438
|
else if (parent.type === 'function_definition' &&
|
|
1386
|
-
parent.childForFieldName('name')
|
|
1439
|
+
sameNode(parent.childForFieldName('name'), node)) {
|
|
1387
1440
|
usageType = 'definition';
|
|
1388
1441
|
}
|
|
1389
1442
|
// Definition: class name:
|
|
1390
1443
|
else if (parent.type === 'class_definition' &&
|
|
1391
|
-
parent.childForFieldName('name')
|
|
1444
|
+
sameNode(parent.childForFieldName('name'), node)) {
|
|
1392
1445
|
usageType = 'definition';
|
|
1393
1446
|
}
|
|
1394
1447
|
// Definition: parameter
|
|
@@ -1400,17 +1453,17 @@ function findUsagesInCode(code, name, parser) {
|
|
|
1400
1453
|
}
|
|
1401
1454
|
// Definition: assignment target (x = ...)
|
|
1402
1455
|
else if (parent.type === 'assignment' &&
|
|
1403
|
-
parent.childForFieldName('left')
|
|
1456
|
+
sameNode(parent.childForFieldName('left'), node)) {
|
|
1404
1457
|
usageType = 'definition';
|
|
1405
1458
|
}
|
|
1406
1459
|
// Definition: for loop variable
|
|
1407
1460
|
else if (parent.type === 'for_statement' &&
|
|
1408
|
-
parent.childForFieldName('left')
|
|
1461
|
+
sameNode(parent.childForFieldName('left'), node)) {
|
|
1409
1462
|
usageType = 'definition';
|
|
1410
1463
|
}
|
|
1411
1464
|
// Method call: obj.name()
|
|
1412
1465
|
else if (parent.type === 'attribute' &&
|
|
1413
|
-
parent.childForFieldName('attribute')
|
|
1466
|
+
sameNode(parent.childForFieldName('attribute'), node)) {
|
|
1414
1467
|
const grandparent = parent.parent;
|
|
1415
1468
|
if (grandparent && grandparent.type === 'call') {
|
|
1416
1469
|
usageType = 'call';
|
|
@@ -1423,6 +1476,19 @@ function findUsagesInCode(code, name, parser) {
|
|
|
1423
1476
|
usages.push({ line, column, usageType, receiver: object.text });
|
|
1424
1477
|
return true;
|
|
1425
1478
|
}
|
|
1479
|
+
// self.attr receiver (unittest setUp idiom: self.w = Widget(3);
|
|
1480
|
+
// self.w.render()) — record the ATTR name so the instance-type
|
|
1481
|
+
// map built from the assignment line ('w' → Widget) matches
|
|
1482
|
+
// (fix #244: class-scoped tests dropped these calls entirely).
|
|
1483
|
+
if (object && object.type === 'attribute') {
|
|
1484
|
+
const innerObj = object.childForFieldName('object');
|
|
1485
|
+
const innerAttr = object.childForFieldName('attribute');
|
|
1486
|
+
if (innerObj && innerObj.type === 'identifier' &&
|
|
1487
|
+
['self', 'cls'].includes(innerObj.text) && innerAttr) {
|
|
1488
|
+
usages.push({ line, column, usageType, receiver: innerAttr.text });
|
|
1489
|
+
return true;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1426
1492
|
}
|
|
1427
1493
|
}
|
|
1428
1494
|
|
|
@@ -1458,43 +1524,43 @@ function findInstanceAttributeTypes(code, parser) {
|
|
|
1458
1524
|
|
|
1459
1525
|
const attrTypes = new Map();
|
|
1460
1526
|
|
|
1461
|
-
//
|
|
1527
|
+
// Scan annotated class-level fields: name: Type [= ...]. Originally
|
|
1528
|
+
// @dataclass-only (fix #28); a BARE class-body annotation is the
|
|
1529
|
+
// same compiler-visible typing evidence (`engine: Engine` inside
|
|
1530
|
+
// class Car types self.engine — pyright enforces it), so those scan
|
|
1531
|
+
// for every class (fix #234). Annotations WITH a value stay
|
|
1532
|
+
// dataclass-only: `tracker: BracketTracker = None` in a plain class
|
|
1533
|
+
// is routinely rebound to something else per-instance, while a
|
|
1534
|
+
// dataclass field default is constructor-managed.
|
|
1462
1535
|
const parentNode = node.parent;
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
const lhs = assign.childForFieldName('left');
|
|
1492
|
-
if (!lhs || lhs.type !== 'identifier') continue;
|
|
1493
|
-
attrTypes.set(lhs.text, typeName);
|
|
1494
|
-
}
|
|
1495
|
-
break;
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1536
|
+
const isDataclass = parentNode?.type === 'decorated_definition' &&
|
|
1537
|
+
Array.from({ length: parentNode.childCount }, (_, d) => parentNode.child(d))
|
|
1538
|
+
.some(dec => dec.type === 'decorator' &&
|
|
1539
|
+
(dec.text.startsWith('@dataclass') || dec.text.includes('.dataclass')));
|
|
1540
|
+
for (let i = 0; i < body.childCount; i++) {
|
|
1541
|
+
const stmt = body.child(i);
|
|
1542
|
+
if (stmt.type !== 'expression_statement') continue;
|
|
1543
|
+
const assign = stmt.firstChild;
|
|
1544
|
+
if (!assign || assign.type !== 'assignment') continue;
|
|
1545
|
+
|
|
1546
|
+
// Must have a type annotation
|
|
1547
|
+
const typeNode = assign.childForFieldName('type');
|
|
1548
|
+
if (!typeNode) continue;
|
|
1549
|
+
if (!isDataclass && assign.childForFieldName('right')) continue;
|
|
1550
|
+
|
|
1551
|
+
// Extract type name from annotation
|
|
1552
|
+
const typeIdent = typeNode.type === 'type' ? typeNode.firstChild : typeNode;
|
|
1553
|
+
if (!typeIdent || typeIdent.type !== 'identifier') continue;
|
|
1554
|
+
const typeName = typeIdent.text;
|
|
1555
|
+
|
|
1556
|
+
// Skip primitives and lowercase types
|
|
1557
|
+
if (PRIMITIVE_TYPES.has(typeName)) continue;
|
|
1558
|
+
if (typeName[0] < 'A' || typeName[0] > 'Z') continue;
|
|
1559
|
+
|
|
1560
|
+
// Field name from LHS
|
|
1561
|
+
const lhs = assign.childForFieldName('left');
|
|
1562
|
+
if (!lhs || lhs.type !== 'identifier') continue;
|
|
1563
|
+
attrTypes.set(lhs.text, typeName);
|
|
1498
1564
|
}
|
|
1499
1565
|
|
|
1500
1566
|
// Scan __init__ for self.X = ClassName(...) assignments
|