ucn 3.8.25 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +44 -18
- package/README.md +95 -28
- package/cli/index.js +28 -5
- package/core/account.js +354 -0
- package/core/analysis.js +335 -15
- package/core/bridge.js +0 -16
- package/core/build-worker.js +21 -1
- package/core/cache.js +52 -3
- package/core/callers.js +3434 -158
- package/core/confidence.js +82 -19
- package/core/deadcode.js +114 -21
- package/core/execute.js +4 -0
- package/core/graph-build.js +44 -2
- package/core/imports.js +118 -1
- package/core/output/analysis.js +345 -83
- package/core/output/reporting.js +8 -2
- package/core/output/shared.js +33 -2
- package/core/output/tracing.js +208 -10
- package/core/project.js +19 -2
- package/core/registry.js +15 -3
- package/core/search.js +0 -42
- package/core/tracing.js +534 -190
- package/languages/go.js +317 -6
- package/languages/index.js +79 -0
- package/languages/java.js +243 -16
- package/languages/javascript.js +357 -24
- package/languages/python.js +423 -28
- package/languages/rust.js +377 -8
- package/languages/utils.js +72 -18
- package/mcp/server.js +3 -3
- package/package.json +9 -3
- package/.github/workflows/ci.yml +0 -45
- package/.github/workflows/publish.yml +0 -79
package/languages/python.js
CHANGED
|
@@ -11,7 +11,8 @@ const {
|
|
|
11
11
|
nodeToLocation,
|
|
12
12
|
parseStructuredParams,
|
|
13
13
|
extractPythonDocstring,
|
|
14
|
-
paramTypesFromStructured
|
|
14
|
+
paramTypesFromStructured,
|
|
15
|
+
visitNameNodes,
|
|
15
16
|
} = require('./utils');
|
|
16
17
|
const { PARSE_OPTIONS, safeParse } = require('./index');
|
|
17
18
|
|
|
@@ -228,6 +229,40 @@ function _processState(node, objects, lines) {
|
|
|
228
229
|
return false;
|
|
229
230
|
}
|
|
230
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Collect module-scope assignment target names (fix #217). A module-level
|
|
234
|
+
* `render = something` (including inside if/try/for blocks — module control
|
|
235
|
+
* flow still binds module attributes) or a `global name` declaration creates
|
|
236
|
+
* a module attribute the import-binding name-chase cannot model, so the
|
|
237
|
+
* chase must treat such names as undetermined rather than provably absent.
|
|
238
|
+
*/
|
|
239
|
+
function _processModuleAssign(node, names) {
|
|
240
|
+
if (node.type === 'global_statement') {
|
|
241
|
+
// `global X` declares that enclosing-function assignments of X bind
|
|
242
|
+
// the MODULE attribute — collect regardless of nesting.
|
|
243
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
244
|
+
const c = node.namedChild(i);
|
|
245
|
+
if (c.type === 'identifier') names.add(c.text);
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (node.type !== 'assignment' && node.type !== 'named_expression') return;
|
|
250
|
+
for (let p = node.parent; p; p = p.parent) {
|
|
251
|
+
// Function scope → local; class body → class attr. Either way, not a
|
|
252
|
+
// module attribute. if/try/for/with blocks at module level still are.
|
|
253
|
+
if (p.type === 'function_definition' || p.type === 'class_definition') return;
|
|
254
|
+
}
|
|
255
|
+
const left = node.childForFieldName('left') || node.childForFieldName('name');
|
|
256
|
+
if (!left) return;
|
|
257
|
+
if (left.type === 'identifier') names.add(left.text);
|
|
258
|
+
else if (left.type === 'tuple' || left.type === 'pattern_list') {
|
|
259
|
+
for (let i = 0; i < left.namedChildCount; i++) {
|
|
260
|
+
const c = left.namedChild(i);
|
|
261
|
+
if (c.type === 'identifier') names.add(c.text);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
231
266
|
// --- End single-pass helpers ---
|
|
232
267
|
|
|
233
268
|
/**
|
|
@@ -419,6 +454,7 @@ function parse(code, parser) {
|
|
|
419
454
|
const functions = [];
|
|
420
455
|
const classes = [];
|
|
421
456
|
const stateObjects = [];
|
|
457
|
+
const moduleAssigned = new Set();
|
|
422
458
|
const processedFn = new Set();
|
|
423
459
|
const processedCls = new Set();
|
|
424
460
|
|
|
@@ -426,6 +462,7 @@ function parse(code, parser) {
|
|
|
426
462
|
_processFunction(node, functions, processedFn, lines, code);
|
|
427
463
|
_processClass(node, classes, processedCls, lines);
|
|
428
464
|
_processState(node, stateObjects, lines);
|
|
465
|
+
_processModuleAssign(node, moduleAssigned);
|
|
429
466
|
return true;
|
|
430
467
|
});
|
|
431
468
|
|
|
@@ -439,6 +476,7 @@ function parse(code, parser) {
|
|
|
439
476
|
functions,
|
|
440
477
|
classes,
|
|
441
478
|
stateObjects,
|
|
479
|
+
...(moduleAssigned.size > 0 && { moduleAssignedNames: [...moduleAssigned].sort() }),
|
|
442
480
|
imports: [],
|
|
443
481
|
exports: []
|
|
444
482
|
};
|
|
@@ -450,6 +488,120 @@ function parse(code, parser) {
|
|
|
450
488
|
* @param {object} parser - Tree-sitter parser instance
|
|
451
489
|
* @returns {Array<{name: string, line: number, isMethod: boolean, receiver?: string}>}
|
|
452
490
|
*/
|
|
491
|
+
// Builtin types for literal method receivers: {'a': 1}.get('a') is dict.get,
|
|
492
|
+
// never a project class method. Keys are tree-sitter node types.
|
|
493
|
+
const PY_LITERAL_RECEIVER_TYPES = {
|
|
494
|
+
dictionary: 'dict',
|
|
495
|
+
dictionary_comprehension: 'dict',
|
|
496
|
+
list: 'list',
|
|
497
|
+
list_comprehension: 'list',
|
|
498
|
+
set: 'set',
|
|
499
|
+
set_comprehension: 'set',
|
|
500
|
+
string: 'str',
|
|
501
|
+
concatenated_string: 'str',
|
|
502
|
+
tuple: 'tuple',
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// typing wrappers whose first argument is the actual value type
|
|
506
|
+
const PY_TYPE_WRAPPERS = new Set(['Optional', 'Annotated', 'Final', 'ClassVar']);
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Extract a single concrete type name from an annotation's `type` node.
|
|
510
|
+
* Conservative by design: a wrong type would exclude true callers downstream
|
|
511
|
+
* (receiver-type-mismatch), so anything ambiguous returns undefined.
|
|
512
|
+
* Handles: Foo · pkg.Foo · Foo | None · Optional[Foo] · "Foo" · dict[str, int]
|
|
513
|
+
*/
|
|
514
|
+
function typeNameFromAnnotation(typeNode) {
|
|
515
|
+
if (!typeNode) return undefined;
|
|
516
|
+
const inner = typeNode.namedChildCount > 0 ? typeNode.namedChild(0) : null;
|
|
517
|
+
return typeNameFromExpr(inner);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function typeNameFromExpr(node) {
|
|
521
|
+
if (!node) return undefined;
|
|
522
|
+
switch (node.type) {
|
|
523
|
+
case 'identifier':
|
|
524
|
+
return node.text;
|
|
525
|
+
case 'attribute': {
|
|
526
|
+
// dotted name: classes match by name in the symbol table → last segment
|
|
527
|
+
const attr = node.childForFieldName('attribute');
|
|
528
|
+
return attr?.text;
|
|
529
|
+
}
|
|
530
|
+
case 'binary_operator': {
|
|
531
|
+
// PEP 604 union: X | None → X; unions of two real types are ambiguous
|
|
532
|
+
const left = node.namedChild(0);
|
|
533
|
+
const right = node.namedChild(1);
|
|
534
|
+
if (left?.type === 'none' && right?.type !== 'none') return typeNameFromExpr(right);
|
|
535
|
+
if (right?.type === 'none' && left?.type !== 'none') return typeNameFromExpr(left);
|
|
536
|
+
return undefined;
|
|
537
|
+
}
|
|
538
|
+
case 'subscript': {
|
|
539
|
+
// typing.Optional[Foo] parses as subscript when base is dotted
|
|
540
|
+
const base = typeNameFromExpr(node.childForFieldName('value'));
|
|
541
|
+
if (PY_TYPE_WRAPPERS.has(base)) {
|
|
542
|
+
return typeNameFromExpr(node.childForFieldName('subscript'));
|
|
543
|
+
}
|
|
544
|
+
return base; // dict[str, int] → the receiver IS a dict
|
|
545
|
+
}
|
|
546
|
+
case 'generic_type': {
|
|
547
|
+
// Optional[Foo] / Mapping[str, int] in annotation position
|
|
548
|
+
const base = typeNameFromExpr(node.namedChild(0));
|
|
549
|
+
if (PY_TYPE_WRAPPERS.has(base)) {
|
|
550
|
+
const params = node.namedChild(1); // type_parameter → type wrappers
|
|
551
|
+
const firstType = params && params.namedChildCount > 0 ? params.namedChild(0) : null;
|
|
552
|
+
return typeNameFromAnnotation(firstType);
|
|
553
|
+
}
|
|
554
|
+
return base;
|
|
555
|
+
}
|
|
556
|
+
case 'string': {
|
|
557
|
+
// forward reference: "Foo" — only accept a bare dotted name
|
|
558
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
559
|
+
const c = node.child(i);
|
|
560
|
+
if (c.type === 'string_content') {
|
|
561
|
+
const txt = c.text.trim();
|
|
562
|
+
if (/^[A-Za-z_][\w.]*$/.test(txt)) return txt.split('.').pop();
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return undefined;
|
|
566
|
+
}
|
|
567
|
+
default:
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Variable receiving this call's result: `x = foo(...)` / `x = await foo(...)`
|
|
574
|
+
* → 'x'. Identifier targets only (no tuples/attributes). Compared by node id —
|
|
575
|
+
* tree-sitter wrapper objects are not identity-stable.
|
|
576
|
+
*/
|
|
577
|
+
function assignmentTargetOf(callNode) {
|
|
578
|
+
let n = callNode;
|
|
579
|
+
let p = n.parent;
|
|
580
|
+
if (p && p.type === 'await') { n = p; p = n.parent; }
|
|
581
|
+
if (p && p.type === 'assignment') {
|
|
582
|
+
const right = p.childForFieldName('right');
|
|
583
|
+
const left = p.childForFieldName('left');
|
|
584
|
+
if (right && right.id === n.id && left?.type === 'identifier') return left.text;
|
|
585
|
+
}
|
|
586
|
+
return undefined;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Type name from a constructor-call callee: ClassName(...) or pkg.ClassName(...).
|
|
591
|
+
* Uppercase-first heuristic (Python class naming convention).
|
|
592
|
+
*/
|
|
593
|
+
function constructorTypeName(funcNode) {
|
|
594
|
+
if (!funcNode) return undefined;
|
|
595
|
+
if (funcNode.type === 'identifier') {
|
|
596
|
+
return /^[A-Z]/.test(funcNode.text) ? funcNode.text : undefined;
|
|
597
|
+
}
|
|
598
|
+
if (funcNode.type === 'attribute') {
|
|
599
|
+
const attr = funcNode.childForFieldName('attribute');
|
|
600
|
+
return attr && /^[A-Z]/.test(attr.text) ? attr.text : undefined;
|
|
601
|
+
}
|
|
602
|
+
return undefined;
|
|
603
|
+
}
|
|
604
|
+
|
|
453
605
|
function findCallsInCode(code, parser) {
|
|
454
606
|
const tree = parseTree(parser, code);
|
|
455
607
|
const calls = [];
|
|
@@ -457,6 +609,16 @@ function findCallsInCode(code, parser) {
|
|
|
457
609
|
const aliases = new Map(); // Track local aliases: aliasName -> originalName
|
|
458
610
|
const nonCallableNames = new Set(); // Track names assigned non-callable values
|
|
459
611
|
const localVarTypes = new Map(); // Track local variable types: varName -> typeName (for receiverType inference)
|
|
612
|
+
// Member-access aliases (fix #218): `append = output.append` makes a later
|
|
613
|
+
// bare `append(part)` a METHOD call on `output` — it must carry the
|
|
614
|
+
// receiver's evidence, never bind by bare name to a same-file def
|
|
615
|
+
// (rich text.py: 7 list.append calls confirmed exact-binding against
|
|
616
|
+
// Text.append). aliasName -> { receiver: string|null, attr: string };
|
|
617
|
+
// receiver is null for chained/deep objects (self._text.append) — the
|
|
618
|
+
// rewritten call is then receiver-blind and routes through dispatch tiering.
|
|
619
|
+
const memberAliases = new Map();
|
|
620
|
+
const memberAliasesStack = []; // function-scoped save/restore, like localVarTypes
|
|
621
|
+
const moduleAliases = new Set(); // Names bound to MODULES (import httpx / import numpy as np)
|
|
460
622
|
const localVarTypesStack = []; // Stack for function-scoped save/restore of localVarTypes
|
|
461
623
|
|
|
462
624
|
// Helper: extract first string-arg literal from a call node.
|
|
@@ -541,7 +703,99 @@ function findCallsInCode(code, parser) {
|
|
|
541
703
|
: null;
|
|
542
704
|
};
|
|
543
705
|
|
|
706
|
+
// fix #203: is a bare-identifier function REFERENCE shadowed by a local of
|
|
707
|
+
// the enclosing function? Python locals are FUNCTION-scoped and an
|
|
708
|
+
// assignment ANYWHERE in the function makes the name local for ALL its
|
|
709
|
+
// references (UnboundLocalError semantics) — so scan the whole enclosing
|
|
710
|
+
// function subtree (excluding nested function bodies, which are separate
|
|
711
|
+
// scopes) for assignment/for/with-as/walrus bindings of the name.
|
|
712
|
+
// Enclosing-function PARAMS are checked at query time in findCallers.
|
|
713
|
+
const _targetBindsName = (left, name) => {
|
|
714
|
+
if (!left) return false;
|
|
715
|
+
if (left.type === 'identifier' && left.text === name) return true;
|
|
716
|
+
if (left.type === 'pattern_list' || left.type === 'tuple_pattern') {
|
|
717
|
+
for (let j = 0; j < left.namedChildCount; j++) {
|
|
718
|
+
if (left.namedChild(j).type === 'identifier' && left.namedChild(j).text === name) return true;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return false;
|
|
722
|
+
};
|
|
723
|
+
const _bindsNameInScope = (scopeNode, name) => {
|
|
724
|
+
for (let i = 0; i < scopeNode.namedChildCount; i++) {
|
|
725
|
+
const c = scopeNode.namedChild(i);
|
|
726
|
+
if (c.type === 'function_definition' || c.type === 'async_function_definition' ||
|
|
727
|
+
c.type === 'class_definition') {
|
|
728
|
+
// The body is a separate scope, but the DEF NAME itself is an
|
|
729
|
+
// assignment in THIS scope (fix #218: a nested `def get_style`
|
|
730
|
+
// shadows the name for sibling references).
|
|
731
|
+
if (c.childForFieldName('name')?.text === name) return true;
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
if (c.type === 'lambda') continue; // separate scope, no name
|
|
735
|
+
if (c.type === 'assignment' || c.type === 'augmented_assignment' || c.type === 'named_expression') {
|
|
736
|
+
if (_targetBindsName(c.childForFieldName('left') || c.childForFieldName('name'), name)) return true;
|
|
737
|
+
} else if (c.type === 'for_statement') {
|
|
738
|
+
if (_targetBindsName(c.childForFieldName('left'), name)) return true;
|
|
739
|
+
} else if (c.type === 'with_statement') {
|
|
740
|
+
// with open(f) as fh: — as-target is inside with_clause/with_item
|
|
741
|
+
const text = c.namedChild(0)?.text || '';
|
|
742
|
+
const m = text.match(/\bas\s+([A-Za-z_][A-Za-z0-9_]*)/);
|
|
743
|
+
if (m && m[1] === name) return true;
|
|
744
|
+
}
|
|
745
|
+
if (_bindsNameInScope(c, name)) return true;
|
|
746
|
+
}
|
|
747
|
+
return false;
|
|
748
|
+
};
|
|
749
|
+
const PY_COMPREHENSIONS = new Set([
|
|
750
|
+
'generator_expression', 'list_comprehension', 'set_comprehension', 'dictionary_comprehension',
|
|
751
|
+
]);
|
|
752
|
+
const isShadowedByLocal = (refNode, name) => {
|
|
753
|
+
for (let p = refNode.parent; p; p = p.parent) {
|
|
754
|
+
// Comprehension for-clause targets are scoped to the comprehension
|
|
755
|
+
// itself (PEP 3110-era scoping): `cell_len(line) for line in lines`
|
|
756
|
+
// binds `line` ONLY inside the comprehension — block-accurate, so
|
|
757
|
+
// check on the way up rather than function-wide (fix #218).
|
|
758
|
+
if (PY_COMPREHENSIONS.has(p.type)) {
|
|
759
|
+
for (let i = 0; i < p.namedChildCount; i++) {
|
|
760
|
+
const c = p.namedChild(i);
|
|
761
|
+
if (c.type === 'for_in_clause' && _targetBindsName(c.childForFieldName('left'), name)) return true;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
// Lambda params shadow their body the same way (fix #218).
|
|
765
|
+
if (p.type === 'lambda') {
|
|
766
|
+
const params = p.childForFieldName('parameters');
|
|
767
|
+
if (params) for (let i = 0; i < params.namedChildCount; i++) {
|
|
768
|
+
const c = params.namedChild(i);
|
|
769
|
+
if (c.type === 'identifier' && c.text === name) return true;
|
|
770
|
+
if (c.type === 'default_parameter' && c.childForFieldName('name')?.text === name) return true;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if (p.type === 'function_definition' || p.type === 'async_function_definition') {
|
|
774
|
+
const body = p.childForFieldName('body');
|
|
775
|
+
return body ? _bindsNameInScope(body, name) : false;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return false; // module level — that's a module binding, not a shadow
|
|
779
|
+
};
|
|
780
|
+
|
|
544
781
|
traverseTree(tree.rootNode, (node) => {
|
|
782
|
+
// Track module-alias bindings: `import httpx` binds 'httpx' (a module),
|
|
783
|
+
// `import numpy as np` binds 'np'. Method calls through these receivers
|
|
784
|
+
// dispatch to module functions, never to class methods. `from x import y`
|
|
785
|
+
// is skipped — y may be a symbol, not a module.
|
|
786
|
+
if (node.type === 'import_statement') {
|
|
787
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
788
|
+
const child = node.namedChild(i);
|
|
789
|
+
if (child.type === 'dotted_name') {
|
|
790
|
+
const first = child.namedChild(0);
|
|
791
|
+
if (first?.type === 'identifier') moduleAliases.add(first.text);
|
|
792
|
+
} else if (child.type === 'aliased_import') {
|
|
793
|
+
const alias = child.childForFieldName('alias');
|
|
794
|
+
if (alias?.type === 'identifier') moduleAliases.add(alias.text);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
545
799
|
// Track function entry
|
|
546
800
|
if (isFunctionNode(node)) {
|
|
547
801
|
// Use decorated_definition start line if present, to match symbol index
|
|
@@ -556,6 +810,7 @@ function findCallsInCode(code, parser) {
|
|
|
556
810
|
});
|
|
557
811
|
// Save localVarTypes so inner declarations don't leak to sibling functions
|
|
558
812
|
localVarTypesStack.push(new Map(localVarTypes));
|
|
813
|
+
memberAliasesStack.push(new Map(memberAliases));
|
|
559
814
|
}
|
|
560
815
|
|
|
561
816
|
// Track parameter type annotations: def foo(x: Foo) → x is Foo
|
|
@@ -564,9 +819,24 @@ function findCallsInCode(code, parser) {
|
|
|
564
819
|
const nameNode = node.childForFieldName('name') || node.namedChild(0);
|
|
565
820
|
const typeNode = node.childForFieldName('type');
|
|
566
821
|
if (nameNode?.type === 'identifier' && typeNode) {
|
|
567
|
-
const
|
|
568
|
-
if (
|
|
569
|
-
localVarTypes.set(nameNode.text,
|
|
822
|
+
const typeName = typeNameFromAnnotation(typeNode);
|
|
823
|
+
if (typeName && !['self', 'cls'].includes(nameNode.text)) {
|
|
824
|
+
localVarTypes.set(nameNode.text, typeName);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Track with-statement bindings: with Client() as c → c is Client
|
|
830
|
+
// (covers async with too — same with_item/as_pattern node shape)
|
|
831
|
+
if (node.type === 'with_item') {
|
|
832
|
+
const value = node.childForFieldName('value') || node.namedChild(0);
|
|
833
|
+
if (value?.type === 'as_pattern') {
|
|
834
|
+
const ctx = value.namedChild(0);
|
|
835
|
+
const target = value.namedChildCount > 1 ? value.namedChild(value.namedChildCount - 1) : null;
|
|
836
|
+
const targetId = target?.type === 'as_pattern_target' ? target.namedChild(0) : null;
|
|
837
|
+
if (ctx?.type === 'call' && targetId?.type === 'identifier') {
|
|
838
|
+
const ctorName = constructorTypeName(ctx.childForFieldName('function'));
|
|
839
|
+
if (ctorName) localVarTypes.set(targetId.text, ctorName);
|
|
570
840
|
}
|
|
571
841
|
}
|
|
572
842
|
}
|
|
@@ -579,15 +849,39 @@ function findCallsInCode(code, parser) {
|
|
|
579
849
|
// Track type annotation: x: Foo = ... → x is Foo
|
|
580
850
|
const typeNode = node.childForFieldName('type');
|
|
581
851
|
if (typeNode) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
localVarTypes.set(left.text, typeId.text);
|
|
852
|
+
const typeName = typeNameFromAnnotation(typeNode);
|
|
853
|
+
if (typeName) {
|
|
854
|
+
localVarTypes.set(left.text, typeName);
|
|
586
855
|
}
|
|
587
856
|
}
|
|
857
|
+
memberAliases.delete(left.text); // any assignment rebinds the name
|
|
858
|
+
// Rebinding without a known type makes any previously inferred
|
|
859
|
+
// type stale — nearest-preceding-assignment semantics (#199's
|
|
860
|
+
// documented rule). Without this, `x = ""; x = render(); x.m()`
|
|
861
|
+
// would carry str and falsely exclude project methods.
|
|
862
|
+
if (!typeNode) localVarTypes.delete(left.text);
|
|
863
|
+
// Literal assignment types the variable (fix #218):
|
|
864
|
+
// ansi_bytes = b"…" → bytes; out = [] → list. Compiler-true,
|
|
865
|
+
// same trust grade as literal receivers ({}.get() → dict).
|
|
866
|
+
if (!typeNode && right && PY_LITERAL_RECEIVER_TYPES[right.type]) {
|
|
867
|
+
let litType = PY_LITERAL_RECEIVER_TYPES[right.type];
|
|
868
|
+
if (litType === 'str' && /^[rRuU]*[bB]/.test(right.text)) litType = 'bytes';
|
|
869
|
+
localVarTypes.set(left.text, litType);
|
|
870
|
+
}
|
|
588
871
|
if (right?.type === 'identifier') {
|
|
589
872
|
aliases.set(left.text, right.text);
|
|
590
873
|
}
|
|
874
|
+
// Member-access alias (fix #218): append = output.append
|
|
875
|
+
else if (right?.type === 'attribute') {
|
|
876
|
+
const attrName = right.childForFieldName('attribute');
|
|
877
|
+
const objNode = right.childForFieldName('object');
|
|
878
|
+
if (attrName?.type === 'identifier') {
|
|
879
|
+
memberAliases.set(left.text, {
|
|
880
|
+
receiver: objNode?.type === 'identifier' ? objNode.text : null,
|
|
881
|
+
attr: attrName.text,
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
}
|
|
591
885
|
// Track partial(fn, ...) aliases: fast_process = partial(process, mode='fast')
|
|
592
886
|
else if (right?.type === 'call') {
|
|
593
887
|
const callFunc = right.childForFieldName('function');
|
|
@@ -626,11 +920,11 @@ function findCallsInCode(code, parser) {
|
|
|
626
920
|
// Exception: partial() already handled above via alias tracking.
|
|
627
921
|
else if (right?.type === 'call' && !aliases.has(left.text)) {
|
|
628
922
|
nonCallableNames.add(left.text);
|
|
629
|
-
// Infer type from constructor call: x = ClassName(...)
|
|
630
|
-
// Python convention: classes start
|
|
631
|
-
const
|
|
632
|
-
if (
|
|
633
|
-
localVarTypes.set(left.text,
|
|
923
|
+
// Infer type from constructor call: x = ClassName(...) or
|
|
924
|
+
// x = pkg.ClassName(...). Python convention: classes start uppercase
|
|
925
|
+
const ctorName = constructorTypeName(right.childForFieldName('function'));
|
|
926
|
+
if (ctorName) {
|
|
927
|
+
localVarTypes.set(left.text, ctorName);
|
|
634
928
|
}
|
|
635
929
|
}
|
|
636
930
|
// Third: subscript/attribute access results are non-callable data
|
|
@@ -651,20 +945,68 @@ function findCallsInCode(code, parser) {
|
|
|
651
945
|
|
|
652
946
|
const enclosingFunction = getCurrentEnclosingFunction();
|
|
653
947
|
let uncertain = false;
|
|
948
|
+
const assignedTo = assignmentTargetOf(node);
|
|
949
|
+
|
|
950
|
+
// Call-site arg count (positional + keyword) for arity pruning.
|
|
951
|
+
// *args/**kwargs splats make the count open-ended — flag them so
|
|
952
|
+
// pruning skips the site.
|
|
953
|
+
const callArgsNode = node.childForFieldName('arguments');
|
|
954
|
+
let argCount = 0;
|
|
955
|
+
let argSpread = false;
|
|
956
|
+
if (callArgsNode) {
|
|
957
|
+
for (let i = 0; i < callArgsNode.namedChildCount; i++) {
|
|
958
|
+
const arg = callArgsNode.namedChild(i);
|
|
959
|
+
if (arg.type === 'comment') continue;
|
|
960
|
+
if (arg.type === 'list_splat' || arg.type === 'dictionary_splat') argSpread = true;
|
|
961
|
+
argCount++;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
654
964
|
|
|
655
965
|
if (funcNode.type === 'identifier') {
|
|
656
|
-
//
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
966
|
+
// Member-alias call (fix #218): `append = output.append` makes
|
|
967
|
+
// this bare call a METHOD call on the alias's receiver — emit
|
|
968
|
+
// it as one so receiver typing/dispatch tiering applies.
|
|
969
|
+
// Restricted to self-named aliases (alias === attr, the local
|
|
970
|
+
// bound-method optimization idiom): a renamed alias's line
|
|
971
|
+
// doesn't contain the method name, so it sits outside the
|
|
972
|
+
// account's text ground set — and never matched the target
|
|
973
|
+
// name before either (no FP to fix there).
|
|
974
|
+
const memberAlias = memberAliases.get(funcNode.text);
|
|
975
|
+
if (memberAlias && memberAlias.attr === funcNode.text) {
|
|
976
|
+
const recvType = memberAlias.receiver ? localVarTypes.get(memberAlias.receiver) : undefined;
|
|
977
|
+
const recvIsModule = !!memberAlias.receiver && moduleAliases.has(memberAlias.receiver) &&
|
|
978
|
+
!localVarTypes.has(memberAlias.receiver);
|
|
979
|
+
calls.push({
|
|
980
|
+
name: memberAlias.attr,
|
|
981
|
+
line: node.startPosition.row + 1,
|
|
982
|
+
isMethod: true,
|
|
983
|
+
aliasCall: true,
|
|
984
|
+
...(memberAlias.receiver && { receiver: memberAlias.receiver }),
|
|
985
|
+
...(recvType && { receiverType: recvType }),
|
|
986
|
+
...(recvIsModule && { receiverIsModule: true }),
|
|
987
|
+
...(assignedTo && { assignedTo }),
|
|
988
|
+
argCount,
|
|
989
|
+
...(argSpread && { argSpread: true }),
|
|
990
|
+
enclosingFunction,
|
|
991
|
+
uncertain,
|
|
992
|
+
});
|
|
993
|
+
} else {
|
|
994
|
+
// Direct call: foo()
|
|
995
|
+
const resolvedName = aliases.get(funcNode.text);
|
|
996
|
+
const firstArg = getFirstStringArg(node);
|
|
997
|
+
calls.push({
|
|
998
|
+
name: funcNode.text,
|
|
999
|
+
...(resolvedName && { resolvedName }),
|
|
1000
|
+
line: node.startPosition.row + 1,
|
|
1001
|
+
isMethod: false,
|
|
1002
|
+
...(assignedTo && { assignedTo }),
|
|
1003
|
+
argCount,
|
|
1004
|
+
...(argSpread && { argSpread: true }),
|
|
1005
|
+
enclosingFunction,
|
|
1006
|
+
uncertain,
|
|
1007
|
+
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
668
1010
|
} else if (funcNode.type === 'attribute') {
|
|
669
1011
|
// Method/attribute call: obj.foo() or self.attr.foo()
|
|
670
1012
|
const attrNode = funcNode.childForFieldName('attribute');
|
|
@@ -673,6 +1015,13 @@ function findCallsInCode(code, parser) {
|
|
|
673
1015
|
if (attrNode) {
|
|
674
1016
|
let receiver = objNode?.type === 'identifier' ? objNode.text : undefined;
|
|
675
1017
|
let selfAttribute = undefined;
|
|
1018
|
+
// Chained receiver (fix #219): the receiver IS a call —
|
|
1019
|
+
// fetch_data().json() — record the producer so findCallers
|
|
1020
|
+
// can type the receiver from its declared return
|
|
1021
|
+
// annotation. `(await f()).m()` unwraps to the call and
|
|
1022
|
+
// marks awaited (an un-awaited async producer's value is a
|
|
1023
|
+
// coroutine, not the annotation's type).
|
|
1024
|
+
let receiverCall, receiverCallIsMethod, receiverCallAwaited;
|
|
676
1025
|
|
|
677
1026
|
// Detect super().method() pattern
|
|
678
1027
|
if (objNode?.type === 'call') {
|
|
@@ -681,6 +1030,27 @@ function findCallsInCode(code, parser) {
|
|
|
681
1030
|
receiver = 'super';
|
|
682
1031
|
}
|
|
683
1032
|
}
|
|
1033
|
+
{
|
|
1034
|
+
let recvNode = objNode;
|
|
1035
|
+
if (recvNode?.type === 'parenthesized_expression' &&
|
|
1036
|
+
recvNode.namedChild(0)?.type === 'await') {
|
|
1037
|
+
receiverCallAwaited = true;
|
|
1038
|
+
recvNode = recvNode.namedChild(0).namedChild(0);
|
|
1039
|
+
}
|
|
1040
|
+
if (recvNode?.type === 'call' && receiver !== 'super') {
|
|
1041
|
+
const prodFunc = recvNode.childForFieldName('function');
|
|
1042
|
+
if (prodFunc?.type === 'identifier') {
|
|
1043
|
+
receiverCall = prodFunc.text;
|
|
1044
|
+
} else if (prodFunc?.type === 'attribute') {
|
|
1045
|
+
const prodAttr = prodFunc.childForFieldName('attribute');
|
|
1046
|
+
if (prodAttr) {
|
|
1047
|
+
receiverCall = prodAttr.text;
|
|
1048
|
+
receiverCallIsMethod = true;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
if (!receiverCall) receiverCallAwaited = undefined;
|
|
1053
|
+
}
|
|
684
1054
|
|
|
685
1055
|
// Detect self.X.method() pattern: objNode is attribute access on self/cls
|
|
686
1056
|
if (objNode?.type === 'attribute') {
|
|
@@ -694,15 +1064,33 @@ function findCallsInCode(code, parser) {
|
|
|
694
1064
|
}
|
|
695
1065
|
}
|
|
696
1066
|
|
|
697
|
-
|
|
1067
|
+
// Literal receivers carry their builtin type: {}.get() can
|
|
1068
|
+
// never be a project class method
|
|
1069
|
+
const receiverType = receiver
|
|
1070
|
+
? localVarTypes.get(receiver)
|
|
1071
|
+
: (objNode ? PY_LITERAL_RECEIVER_TYPES[objNode.type] : undefined);
|
|
1072
|
+
// Module receiver (httpx.get()) — unless locally shadowed
|
|
1073
|
+
// by a typed instance binding
|
|
1074
|
+
const receiverIsModule = !!receiver && moduleAliases.has(receiver) &&
|
|
1075
|
+
!localVarTypes.has(receiver);
|
|
698
1076
|
const firstArg = getFirstStringArg(node);
|
|
699
1077
|
calls.push({
|
|
700
1078
|
name: attrNode.text,
|
|
701
|
-
line
|
|
1079
|
+
// Multi-line chains (obj.x()\n.y()) must report each
|
|
1080
|
+
// method's OWN name line, not the chain-start line —
|
|
1081
|
+
// the account's ground set is keyed by the name's line
|
|
1082
|
+
line: attrNode.startPosition.row + 1,
|
|
702
1083
|
isMethod: true,
|
|
703
1084
|
receiver,
|
|
704
1085
|
...(receiverType && { receiverType }),
|
|
1086
|
+
...(receiverIsModule && { receiverIsModule: true }),
|
|
705
1087
|
...(selfAttribute && { selfAttribute }),
|
|
1088
|
+
...(receiverCall && { receiverCall }),
|
|
1089
|
+
...(receiverCallIsMethod && { receiverCallIsMethod: true }),
|
|
1090
|
+
...(receiverCallAwaited && { receiverCallAwaited: true }),
|
|
1091
|
+
...(assignedTo && { assignedTo }),
|
|
1092
|
+
argCount,
|
|
1093
|
+
...(argSpread && { argSpread: true }),
|
|
706
1094
|
enclosingFunction,
|
|
707
1095
|
uncertain,
|
|
708
1096
|
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
|
|
@@ -730,6 +1118,7 @@ function findCallsInCode(code, parser) {
|
|
|
730
1118
|
isMethod: false,
|
|
731
1119
|
isFunctionReference: true,
|
|
732
1120
|
isPotentialCallback: true,
|
|
1121
|
+
...(isShadowedByLocal(arg, arg.text) && { localShadow: true }),
|
|
733
1122
|
enclosingFunction
|
|
734
1123
|
});
|
|
735
1124
|
}
|
|
@@ -747,6 +1136,7 @@ function findCallsInCode(code, parser) {
|
|
|
747
1136
|
isMethod: false,
|
|
748
1137
|
isFunctionReference: true,
|
|
749
1138
|
isPotentialCallback: true,
|
|
1139
|
+
...(isShadowedByLocal(val, val.text) && { localShadow: true }),
|
|
750
1140
|
enclosingFunction
|
|
751
1141
|
});
|
|
752
1142
|
}
|
|
@@ -770,6 +1160,11 @@ function findCallsInCode(code, parser) {
|
|
|
770
1160
|
localVarTypes.clear();
|
|
771
1161
|
for (const [k, v] of saved) localVarTypes.set(k, v);
|
|
772
1162
|
}
|
|
1163
|
+
const savedAliases = memberAliasesStack.pop();
|
|
1164
|
+
if (savedAliases) {
|
|
1165
|
+
memberAliases.clear();
|
|
1166
|
+
for (const [k, v] of savedAliases) memberAliases.set(k, v);
|
|
1167
|
+
}
|
|
773
1168
|
}
|
|
774
1169
|
}
|
|
775
1170
|
});
|
|
@@ -955,7 +1350,7 @@ function findUsagesInCode(code, name, parser) {
|
|
|
955
1350
|
const tree = parseTree(parser, code);
|
|
956
1351
|
const usages = [];
|
|
957
1352
|
|
|
958
|
-
|
|
1353
|
+
visitNameNodes(tree, code, name, (node) => {
|
|
959
1354
|
// Only look for identifiers with the matching name
|
|
960
1355
|
if (node.type !== 'identifier' || node.text !== name) {
|
|
961
1356
|
return true;
|