ucn 5.4.2 → 5.5.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 +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +27 -16
- package/cli/index.js +45 -14
- package/core/accessors.js +118 -3
- package/core/account.js +62 -11
- package/core/analysis.js +517 -266
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +529 -31
- package/core/callers.js +12228 -921
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +7 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +305 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +472 -66
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +3026 -0
- package/core/rust-modules.js +203 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1185 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2265 -112
- package/languages/utils.js +298 -4
- package/mcp/server.js +22 -8
- package/package.json +1 -1
package/languages/python.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const { ReceiverTypeMap, typeOrigin } = require('./type-evidence');
|
|
9
|
+
const { referenceScope, scopeFields } = require('./lexical-scope');
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
const {
|
|
@@ -18,9 +19,63 @@ const {
|
|
|
18
19
|
paramTypesFromStructured,
|
|
19
20
|
visitNameNodes,
|
|
20
21
|
sameNode,
|
|
22
|
+
containsOwnNode,
|
|
23
|
+
parseErrorRegions,
|
|
24
|
+
branchClausesExclusive,
|
|
25
|
+
getCachedNodeList,
|
|
21
26
|
} = require('./utils');
|
|
27
|
+
|
|
28
|
+
// A function whose OWN body yields is a generator: calling it returns an
|
|
29
|
+
// iterator (an async iterator for `async def`), never a coroutine.
|
|
30
|
+
const PY_YIELD_TYPES = new Set(['yield']);
|
|
31
|
+
const PY_SCOPE_BOUNDARIES = new Set(['function_definition', 'lambda', 'class_definition']);
|
|
32
|
+
function isPythonGenerator(fnNode) {
|
|
33
|
+
const body = fnNode.childForFieldName('body');
|
|
34
|
+
return !!body && body.text.includes('yield') &&
|
|
35
|
+
containsOwnNode(body, PY_YIELD_TYPES, PY_SCOPE_BOUNDARIES);
|
|
36
|
+
}
|
|
22
37
|
const { PARSE_OPTIONS, safeParse } = require('./index');
|
|
23
38
|
|
|
39
|
+
// Statements that run in the namespace of the scope holding them: a def
|
|
40
|
+
// under `if`/`try`/`with`/`for`/`while`/`match` in a class body executes in
|
|
41
|
+
// the class namespace and binds a class attribute (a method), exactly like
|
|
42
|
+
// one written directly in the body (fix #402).
|
|
43
|
+
const PY_CLASS_BODY_COMPOUNDS = new Set(['block', 'if_statement', 'elif_clause', 'else_clause',
|
|
44
|
+
'try_statement', 'except_clause', 'except_group_clause', 'finally_clause', 'with_statement',
|
|
45
|
+
'for_statement', 'while_statement', 'match_statement', 'case_clause', 'decorated_definition']);
|
|
46
|
+
|
|
47
|
+
/** The class whose body declares `defNode` (a def or class, decorated or
|
|
48
|
+
* not), through compound statements; null for any other scope. */
|
|
49
|
+
function pythonClassBodyOwner(defNode) {
|
|
50
|
+
for (let node = defNode?.parent; node; node = node.parent) {
|
|
51
|
+
if (node.type === 'class_definition') return node;
|
|
52
|
+
if (!PY_CLASS_BODY_COMPOUNDS.has(node.type)) return null;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Function definitions a class body declares (decorated ones as their
|
|
58
|
+
* `decorated_definition`), in source order, through compound statements;
|
|
59
|
+
* nested functions and classes are not entered. */
|
|
60
|
+
function pythonClassBodyDefs(body) {
|
|
61
|
+
const out = [];
|
|
62
|
+
const visit = container => {
|
|
63
|
+
for (let i = 0; i < container.namedChildCount; i++) {
|
|
64
|
+
const child = container.namedChild(i);
|
|
65
|
+
if (child.type === 'function_definition') { out.push(child); continue; }
|
|
66
|
+
if (child.type === 'decorated_definition') {
|
|
67
|
+
const definition = child.childForFieldName('definition');
|
|
68
|
+
if (definition?.type === 'function_definition') out.push(child);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (child.type === 'class_definition') continue;
|
|
72
|
+
if (PY_CLASS_BODY_COMPOUNDS.has(child.type)) visit(child);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
if (body) visit(body);
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
24
79
|
function parseTree(parser, code) {
|
|
25
80
|
return safeParse(parser, code, undefined, PARSE_OPTIONS);
|
|
26
81
|
}
|
|
@@ -147,19 +202,9 @@ function _processFunction(node, functions, processedRanges, lines, code) {
|
|
|
147
202
|
if (processedRanges.has(rangeKey)) return true;
|
|
148
203
|
processedRanges.add(rangeKey);
|
|
149
204
|
|
|
150
|
-
// Skip functions
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (parent && parent.type === 'decorated_definition') {
|
|
154
|
-
parent = parent.parent;
|
|
155
|
-
}
|
|
156
|
-
// Check if parent is a class body (block inside class_definition)
|
|
157
|
-
if (parent && parent.type === 'block') {
|
|
158
|
-
const grandparent = parent.parent;
|
|
159
|
-
if (grandparent && grandparent.type === 'class_definition') {
|
|
160
|
-
return true; // Skip - this is a class method
|
|
161
|
-
}
|
|
162
|
-
}
|
|
205
|
+
// Skip functions a class body declares, directly or under its
|
|
206
|
+
// compound statements: they're extracted as class members.
|
|
207
|
+
if (pythonClassBodyOwner(node)) return true;
|
|
163
208
|
|
|
164
209
|
const nameNode = node.childForFieldName('name');
|
|
165
210
|
const paramsNode = node.childForFieldName('parameters');
|
|
@@ -182,6 +227,7 @@ function _processFunction(node, functions, processedRanges, lines, code) {
|
|
|
182
227
|
|
|
183
228
|
// Check for async
|
|
184
229
|
const isAsync = node.text.trimStart().startsWith('async ');
|
|
230
|
+
const isGenerator = isPythonGenerator(node);
|
|
185
231
|
|
|
186
232
|
// Extract decorators
|
|
187
233
|
const decorators = extractDecorators(node);
|
|
@@ -200,6 +246,7 @@ function _processFunction(node, functions, processedRanges, lines, code) {
|
|
|
200
246
|
endLine,
|
|
201
247
|
indent,
|
|
202
248
|
isAsync,
|
|
249
|
+
...(isGenerator && { isGenerator }),
|
|
203
250
|
modifiers: isAsync ? ['async'] : [],
|
|
204
251
|
...(returnType && { returnType }),
|
|
205
252
|
...(returnedConstructors && { returnedConstructors }),
|
|
@@ -243,6 +290,7 @@ function _processClass(node, classes, processedRanges, lines) {
|
|
|
243
290
|
const decorators = extractDecorators(node);
|
|
244
291
|
const bases = extractBases(node);
|
|
245
292
|
const nameLine = nameNode.startPosition.row + 1;
|
|
293
|
+
const lexicalScope = pythonFunctionLocalScope(node);
|
|
246
294
|
|
|
247
295
|
classes.push({
|
|
248
296
|
name: nameNode.text,
|
|
@@ -253,12 +301,34 @@ function _processClass(node, classes, processedRanges, lines) {
|
|
|
253
301
|
...(docstring && { docstring }),
|
|
254
302
|
...(decorators.length > 0 && { decorators }),
|
|
255
303
|
...(bases.length > 0 && { extends: bases.join(', ') }),
|
|
256
|
-
...(nameLine !== startLine && { nameLine })
|
|
304
|
+
...(nameLine !== startLine && { nameLine }),
|
|
305
|
+
...lexicalScope,
|
|
257
306
|
});
|
|
258
307
|
}
|
|
259
308
|
return true;
|
|
260
309
|
}
|
|
261
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Lexical scope of a class declared inside a function (fix #381): the
|
|
313
|
+
* enclosing function's body. The name is a local of that function, so it
|
|
314
|
+
* is visible only there (including nested functions, which close over it).
|
|
315
|
+
* Module-level and class-body classes are unscoped.
|
|
316
|
+
*/
|
|
317
|
+
function pythonFunctionLocalScope(node) {
|
|
318
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
319
|
+
if (parent.type === 'class_definition') return {};
|
|
320
|
+
if (parent.type === 'function_definition' ||
|
|
321
|
+
parent.type === 'async_function_definition') {
|
|
322
|
+
const body = parent.childForFieldName('body') || parent;
|
|
323
|
+
return {
|
|
324
|
+
lexicalScopeStartLine: body.startPosition.row + 1,
|
|
325
|
+
lexicalScopeEndLine: body.endPosition.row + 1,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return {};
|
|
330
|
+
}
|
|
331
|
+
|
|
262
332
|
// Module-level state detection patterns
|
|
263
333
|
const _STATE_PATTERN = /^(CONFIG|SETTINGS|[A-Z][A-Z0-9_]+|[A-Z][a-zA-Z]*(?:Config|Settings|Options|State|Store|Context))$/;
|
|
264
334
|
// Pattern for UPPER_CASE constants that may have scalar values (string, number, bool, etc.)
|
|
@@ -336,6 +406,475 @@ function _processModuleAssign(node, names) {
|
|
|
336
406
|
}
|
|
337
407
|
}
|
|
338
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Module-level names bound exactly once, to another name (fix #389):
|
|
411
|
+
* `Alias = Box`, `Alias = models.Box`. Query time decides whether the
|
|
412
|
+
* target is a class; such an alias then denotes it in annotations and
|
|
413
|
+
* constructor calls. A name bound again at module scope (assignment,
|
|
414
|
+
* augmented assignment, loop/with/except target, import, def, class, walrus,
|
|
415
|
+
* del) or declared `global` in any function is not an alias.
|
|
416
|
+
*/
|
|
417
|
+
function pythonModuleValueAliases(root) {
|
|
418
|
+
// Cheap first pass: a module-level statement `Name = other.name` at all
|
|
419
|
+
// (directly, or in a module-level if/try/with block).
|
|
420
|
+
const aliasShaped = (expr) => expr?.type === 'assignment' &&
|
|
421
|
+
expr.childForFieldName('left')?.type === 'identifier' &&
|
|
422
|
+
['identifier', 'attribute'].includes(expr.childForFieldName('right')?.type);
|
|
423
|
+
let shaped = false;
|
|
424
|
+
const probe = (node) => {
|
|
425
|
+
for (const statement of node.namedChildren) {
|
|
426
|
+
if (shaped) return;
|
|
427
|
+
if (statement.type === 'expression_statement') {
|
|
428
|
+
if (statement.namedChildren.some(aliasShaped)) shaped = true;
|
|
429
|
+
} else if (/^(if_statement|elif_clause|else_clause|try_statement|except_clause|finally_clause|with_statement|block)$/.test(statement.type)) {
|
|
430
|
+
probe(statement);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
probe(root);
|
|
435
|
+
if (!shaped) return [];
|
|
436
|
+
// Every module-level binding of each name (fix #392): an alias
|
|
437
|
+
// assignment, an import, or anything else.
|
|
438
|
+
const bindings = new Map();
|
|
439
|
+
const bind = (name, entry) => {
|
|
440
|
+
if (!name) return;
|
|
441
|
+
if (!bindings.has(name)) bindings.set(name, []);
|
|
442
|
+
bindings.get(name).push(entry);
|
|
443
|
+
};
|
|
444
|
+
const bindTarget = (node) => {
|
|
445
|
+
if (!node) return;
|
|
446
|
+
if (node.type === 'identifier') { bind(node.text, { kind: 'other' }); return; }
|
|
447
|
+
if (['pattern_list', 'tuple_pattern', 'list_pattern', 'tuple', 'list', 'expression_list',
|
|
448
|
+
'parenthesized_expression', 'list_splat_pattern', 'list_splat', 'as_pattern_target'].includes(node.type)) {
|
|
449
|
+
for (const child of node.namedChildren) bindTarget(child);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
const visit = (node) => {
|
|
453
|
+
for (const child of node.namedChildren) {
|
|
454
|
+
switch (child.type) {
|
|
455
|
+
case 'function_definition':
|
|
456
|
+
case 'class_definition':
|
|
457
|
+
bind(child.childForFieldName('name')?.text, { kind: 'other' });
|
|
458
|
+
continue;
|
|
459
|
+
case 'decorated_definition':
|
|
460
|
+
bind(child.childForFieldName('definition')?.childForFieldName('name')?.text, { kind: 'other' });
|
|
461
|
+
continue;
|
|
462
|
+
case 'import_statement':
|
|
463
|
+
case 'future_import_statement':
|
|
464
|
+
for (const part of child.namedChildren) {
|
|
465
|
+
if (part.type === 'aliased_import') bind(part.childForFieldName('alias')?.text, { kind: 'other' });
|
|
466
|
+
else if (part.type === 'dotted_name') bind(part.namedChild(0)?.text, { kind: 'other' });
|
|
467
|
+
}
|
|
468
|
+
continue;
|
|
469
|
+
case 'import_from_statement': {
|
|
470
|
+
const moduleName = child.childForFieldName('module_name')?.text;
|
|
471
|
+
for (const part of child.namedChildren) {
|
|
472
|
+
if (sameNode(part, child.childForFieldName('module_name'))) continue;
|
|
473
|
+
if (part.type === 'wildcard_import') { bind('*', { kind: 'other' }); continue; }
|
|
474
|
+
const imported = part.type === 'aliased_import'
|
|
475
|
+
? part.childForFieldName('name')?.text : part.type === 'dotted_name' ? part.text : null;
|
|
476
|
+
const local = part.type === 'aliased_import'
|
|
477
|
+
? part.childForFieldName('alias')?.text : part.type === 'dotted_name'
|
|
478
|
+
? part.namedChild(part.namedChildCount - 1)?.text : null;
|
|
479
|
+
if (!local) continue;
|
|
480
|
+
// A from-import binds the imported object: one binding
|
|
481
|
+
// site of a class the name may alias.
|
|
482
|
+
bind(local, moduleName && imported && !imported.includes('.')
|
|
483
|
+
? { kind: 'import', module: moduleName, imported, line: child.startPosition.row + 1 }
|
|
484
|
+
: { kind: 'other' });
|
|
485
|
+
}
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
case 'expression_statement':
|
|
489
|
+
for (let expr of child.namedChildren) {
|
|
490
|
+
while (expr?.type === 'assignment' || expr?.type === 'augmented_assignment') {
|
|
491
|
+
const left = expr.childForFieldName('left');
|
|
492
|
+
const right = expr.childForFieldName('right');
|
|
493
|
+
if (expr.type === 'assignment' && left?.type === 'identifier' &&
|
|
494
|
+
!expr.childForFieldName('type') &&
|
|
495
|
+
(right?.type === 'identifier' || right?.type === 'attribute') &&
|
|
496
|
+
/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(right.text) &&
|
|
497
|
+
right.text !== left.text) {
|
|
498
|
+
bind(left.text, { kind: 'alias', target: right.text,
|
|
499
|
+
line: expr.startPosition.row + 1 });
|
|
500
|
+
} else if (expr.type === 'assignment' && left?.type === 'identifier' &&
|
|
501
|
+
!expr.childForFieldName('type') && right?.type === 'call' &&
|
|
502
|
+
right.childForFieldName('function')?.type === 'identifier') {
|
|
503
|
+
// `registry = Registry()` (fix #398F): a module value
|
|
504
|
+
// built by calling a name, read when an alias names
|
|
505
|
+
// one of its attributes.
|
|
506
|
+
bind(left.text, { kind: 'constructed',
|
|
507
|
+
callee: right.childForFieldName('function').text,
|
|
508
|
+
line: expr.startPosition.row + 1 });
|
|
509
|
+
} else {
|
|
510
|
+
bindTarget(left);
|
|
511
|
+
}
|
|
512
|
+
expr = expr.type === 'assignment' ? right : null;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
continue;
|
|
516
|
+
case 'delete_statement':
|
|
517
|
+
for (const target of child.namedChildren) bindTarget(target);
|
|
518
|
+
continue;
|
|
519
|
+
case 'for_statement':
|
|
520
|
+
bindTarget(child.childForFieldName('left'));
|
|
521
|
+
break;
|
|
522
|
+
case 'with_statement':
|
|
523
|
+
case 'try_statement':
|
|
524
|
+
for (const target of child.descendantsOfType('as_pattern_target')) bindTarget(target);
|
|
525
|
+
break;
|
|
526
|
+
default:
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
if (child.namedChildCount > 0 && child.type !== 'expression_statement') visit(child);
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
visit(root);
|
|
533
|
+
for (const walrus of root.descendantsOfType('named_expression')) {
|
|
534
|
+
let inFunction = false;
|
|
535
|
+
for (let p = walrus.parent; p; p = p.parent) {
|
|
536
|
+
if (p.type === 'function_definition' || p.type === 'class_definition' || p.type === 'lambda') {
|
|
537
|
+
inFunction = true;
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (!inFunction) bind(walrus.childForFieldName('name')?.text, { kind: 'other' });
|
|
542
|
+
}
|
|
543
|
+
const declaredGlobal = new Set();
|
|
544
|
+
for (const statement of root.descendantsOfType('global_statement')) {
|
|
545
|
+
for (const name of statement.namedChildren) declaredGlobal.add(name.text);
|
|
546
|
+
}
|
|
547
|
+
const aliases = [];
|
|
548
|
+
const constructed = new Map();
|
|
549
|
+
for (const [name, sites] of bindings) {
|
|
550
|
+
if (declaredGlobal.has(name) || sites.some(site => site.kind === 'other')) continue;
|
|
551
|
+
if (sites.length === 1 && sites[0].kind === 'constructed') {
|
|
552
|
+
constructed.set(name, { name, callee: sites[0].callee, line: sites[0].line });
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const assignments = sites.filter(site => site.kind === 'alias');
|
|
556
|
+
if (assignments.length === 0 || sites.some(site => site.kind === 'constructed')) continue;
|
|
557
|
+
if (sites.length === 1) {
|
|
558
|
+
// Bound once (also inside a module-level block, fix #392).
|
|
559
|
+
aliases.push({ name, target: assignments[0].target, line: assignments[0].line });
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
// Bound on several paths (if/else, try/except): an alias only when
|
|
563
|
+
// every binding site names the same class, decided where it resolves.
|
|
564
|
+
aliases.push({
|
|
565
|
+
name, target: assignments[0].target, line: assignments[0].line,
|
|
566
|
+
alternatives: sites.map(site => site.kind === 'alias'
|
|
567
|
+
? { target: site.target } : { module: site.module, imported: site.imported }),
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
// Constructed values an alias reads an attribute of (`loads =
|
|
571
|
+
// registry.loads`, fix #398F).
|
|
572
|
+
const roots = new Set(aliases.flatMap(alias => [alias.target, ...(alias.alternatives || [])
|
|
573
|
+
.map(alternative => alternative.target)]).filter(Boolean)
|
|
574
|
+
.map(target => String(target).split('.')).filter(parts => parts.length === 2).map(parts => parts[0]));
|
|
575
|
+
const values = [...roots].filter(root => constructed.has(root)).sort().map(root => constructed.get(root));
|
|
576
|
+
if (values.length > 0) Object.defineProperty(aliases, 'constructed', { value: values });
|
|
577
|
+
return aliases;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const PY_SCOPE_NODES = new Set(['function_definition', 'class_definition', 'lambda', 'module']);
|
|
581
|
+
const PY_BRANCH_CLAUSES = new Set(['elif_clause', 'else_clause', 'except_clause', 'except_group_clause',
|
|
582
|
+
'finally_clause', 'case_clause']);
|
|
583
|
+
|
|
584
|
+
/** Does a statement list (descending compound statements, never into
|
|
585
|
+
* nested scopes) bind `name`? */
|
|
586
|
+
function pythonClauseBindsName(node, name) {
|
|
587
|
+
for (const child of node.namedChildren) {
|
|
588
|
+
switch (child.type) {
|
|
589
|
+
case 'function_definition':
|
|
590
|
+
case 'class_definition':
|
|
591
|
+
if (child.childForFieldName('name')?.text === name) return true;
|
|
592
|
+
continue;
|
|
593
|
+
case 'decorated_definition':
|
|
594
|
+
if (child.childForFieldName('definition')?.childForFieldName('name')?.text === name) return true;
|
|
595
|
+
continue;
|
|
596
|
+
case 'import_statement':
|
|
597
|
+
case 'import_from_statement':
|
|
598
|
+
case 'expression_statement':
|
|
599
|
+
case 'delete_statement':
|
|
600
|
+
case 'for_statement':
|
|
601
|
+
case 'with_statement':
|
|
602
|
+
if (child.descendantsOfType('identifier').some(id => id.text === name)) return true;
|
|
603
|
+
if (child.type === 'for_statement' || child.type === 'with_statement') {
|
|
604
|
+
for (const part of child.namedChildren) {
|
|
605
|
+
if ((part.type === 'block' || part.type === 'else_clause') && pythonClauseBindsName(part, name)) return true;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
continue;
|
|
609
|
+
default:
|
|
610
|
+
if (child.type === 'block' || child.type.endsWith('_statement') || PY_BRANCH_CLAUSES.has(child.type)) {
|
|
611
|
+
if (pythonClauseBindsName(child, name)) return true;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Cheap gate for configuration items: a def, class or import inside a
|
|
620
|
+
* branch whose statement binds the same name in another clause. Files
|
|
621
|
+
* without one (nearly all) skip the scope walk.
|
|
622
|
+
*/
|
|
623
|
+
function pythonConfigurationCandidateScopes(root, bindingNodes) {
|
|
624
|
+
const scopes = new Set();
|
|
625
|
+
const nodes = bindingNodes || getCachedNodeList(root).nodes.filter(node =>
|
|
626
|
+
node.type === 'function_definition' || node.type === 'class_definition' ||
|
|
627
|
+
node.type === 'import_statement' || node.type === 'import_from_statement');
|
|
628
|
+
for (const node of nodes) {
|
|
629
|
+
// Directly in a scope body: no branch can hold it.
|
|
630
|
+
const holder = node.parent?.type === 'decorated_definition' ? node.parent.parent : node.parent;
|
|
631
|
+
if (holder?.type === 'module' || (holder?.type === 'block' && PY_SCOPE_NODES.has(holder.parent?.type))) continue;
|
|
632
|
+
let names = null;
|
|
633
|
+
const start = node.parent?.type === 'decorated_definition' ? node.parent : node;
|
|
634
|
+
for (let parent = start.parent; parent && !PY_SCOPE_NODES.has(parent.type); parent = parent.parent) {
|
|
635
|
+
const statement = parent.type === 'block' ? null
|
|
636
|
+
: ['if_statement', 'try_statement'].includes(parent.type) ? parent
|
|
637
|
+
: PY_BRANCH_CLAUSES.has(parent.type) ? (parent.type === 'case_clause' ? parent.parent?.parent : parent.parent)
|
|
638
|
+
: null;
|
|
639
|
+
if (!statement) continue;
|
|
640
|
+
if (!names) {
|
|
641
|
+
if (node.type === 'function_definition' || node.type === 'class_definition') {
|
|
642
|
+
names = [node.childForFieldName('name')?.text].filter(Boolean);
|
|
643
|
+
} else {
|
|
644
|
+
names = [];
|
|
645
|
+
for (const part of node.namedChildren) {
|
|
646
|
+
if (part.type === 'aliased_import') names.push(part.childForFieldName('alias')?.text);
|
|
647
|
+
else if (part.type === 'dotted_name' && !sameNode(part, node.childForFieldName('module_name'))) {
|
|
648
|
+
names.push(node.type === 'import_statement' ? part.namedChild(0)?.text
|
|
649
|
+
: part.namedChild(part.namedChildCount - 1)?.text);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
names = names.filter(Boolean);
|
|
653
|
+
}
|
|
654
|
+
if (names.length === 0) break;
|
|
655
|
+
}
|
|
656
|
+
// Another clause of this statement (or its match body) binding
|
|
657
|
+
// one of the names.
|
|
658
|
+
const holder = statement.type === 'match_statement' ? statement.childForFieldName('body') : statement;
|
|
659
|
+
let found = false;
|
|
660
|
+
for (const clause of holder?.namedChildren || []) {
|
|
661
|
+
if (clause.startIndex <= node.startIndex && clause.endIndex >= node.endIndex) continue;
|
|
662
|
+
if (clause.type !== 'block' && !PY_BRANCH_CLAUSES.has(clause.type)) continue;
|
|
663
|
+
if (names.some(name => pythonClauseBindsName(clause, name))) { found = true; break; }
|
|
664
|
+
}
|
|
665
|
+
if (found) {
|
|
666
|
+
let scope = statement.parent;
|
|
667
|
+
while (scope && !PY_SCOPE_NODES.has(scope.type)) scope = scope.parent;
|
|
668
|
+
if (scope) scopes.add(scope);
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return scopes;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Configuration items (fix #398F): a name bound in one scope (module, class
|
|
678
|
+
* body or function body) only in mutually exclusive branches of `if` /
|
|
679
|
+
* `try` / `match` statements - `if sys.platform == "win32": def getchar` /
|
|
680
|
+
* `else: def getchar`, `try: from ._speedups import f` / `except
|
|
681
|
+
* ImportError: def f` - is ONE item with an alternative per configuration.
|
|
682
|
+
* Every binding of the name in that scope must sit in a different exclusive
|
|
683
|
+
* branch (a later unconditional rebinding, a loop target or a `global` /
|
|
684
|
+
* `nonlocal` write breaks the item), and one of them must be a def, class or
|
|
685
|
+
* import. Sites carry their branch path (statement line + clause tag, outer
|
|
686
|
+
* first); `clauses` holds the line range of every clause of those
|
|
687
|
+
* statements, so a call inside another branch is placed without the tree.
|
|
688
|
+
*/
|
|
689
|
+
function pythonConfigurationItems(root, bindingNodes = null, reboundNodes = null) {
|
|
690
|
+
const candidateScopes = pythonConfigurationCandidateScopes(root, bindingNodes);
|
|
691
|
+
if (candidateScopes.size === 0) return [];
|
|
692
|
+
const scopes = new Map(); // scope line -> Map(name -> sites)
|
|
693
|
+
const clauseRanges = new Map(); // statement line -> [[tag, start, end]]
|
|
694
|
+
const rebound = new Set(); // `global` / `nonlocal` names
|
|
695
|
+
const lineOf = node => node.startPosition.row + 1;
|
|
696
|
+
const endOf = node => node.endPosition.row + 1;
|
|
697
|
+
const add = (scope, name, kind, line, branch) => {
|
|
698
|
+
if (!name) return;
|
|
699
|
+
let names = scopes.get(scope);
|
|
700
|
+
if (!names) scopes.set(scope, names = new Map());
|
|
701
|
+
if (!names.has(name)) names.set(name, []);
|
|
702
|
+
names.get(name).push({ kind, line, branch });
|
|
703
|
+
};
|
|
704
|
+
const bindTarget = (scope, node, branch) => {
|
|
705
|
+
if (!node) return;
|
|
706
|
+
if (node.type === 'identifier') { add(scope, node.text, 'value', lineOf(node), branch); return; }
|
|
707
|
+
if (['pattern_list', 'tuple_pattern', 'list_pattern', 'tuple', 'list', 'expression_list',
|
|
708
|
+
'parenthesized_expression', 'list_splat_pattern', 'list_splat', 'as_pattern_target'].includes(node.type)) {
|
|
709
|
+
for (const child of node.namedChildren) bindTarget(scope, child, branch);
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
const noteClauses = (statement, clauses) => {
|
|
713
|
+
clauseRanges.set(lineOf(statement), clauses.map(([tag, node]) => [tag, lineOf(node), endOf(node)]));
|
|
714
|
+
};
|
|
715
|
+
const visitBlock = (block, scope, branch) => {
|
|
716
|
+
if (!block) return;
|
|
717
|
+
for (const child of block.namedChildren) visitStatement(child, scope, branch);
|
|
718
|
+
};
|
|
719
|
+
const visitStatement = (node, scope, branch) => {
|
|
720
|
+
switch (node.type) {
|
|
721
|
+
case 'decorated_definition': {
|
|
722
|
+
const definition = node.childForFieldName('definition');
|
|
723
|
+
if (!definition) return;
|
|
724
|
+
add(scope, definition.childForFieldName('name')?.text,
|
|
725
|
+
definition.type === 'class_definition' ? 'class' : 'def', lineOf(node), branch);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
case 'function_definition':
|
|
729
|
+
case 'class_definition':
|
|
730
|
+
add(scope, node.childForFieldName('name')?.text,
|
|
731
|
+
node.type === 'class_definition' ? 'class' : 'def', lineOf(node), branch);
|
|
732
|
+
return;
|
|
733
|
+
case 'import_statement':
|
|
734
|
+
for (const part of node.namedChildren) {
|
|
735
|
+
if (part.type === 'aliased_import') add(scope, part.childForFieldName('alias')?.text, 'import', lineOf(node), branch);
|
|
736
|
+
else if (part.type === 'dotted_name') add(scope, part.namedChild(0)?.text, 'import', lineOf(node), branch);
|
|
737
|
+
}
|
|
738
|
+
return;
|
|
739
|
+
case 'import_from_statement': {
|
|
740
|
+
const moduleName = node.childForFieldName('module_name');
|
|
741
|
+
for (const part of node.namedChildren) {
|
|
742
|
+
if (moduleName && part.id === moduleName.id) continue;
|
|
743
|
+
if (part.type === 'wildcard_import') { add(scope, '*', 'import', lineOf(node), branch); continue; }
|
|
744
|
+
const local = part.type === 'aliased_import' ? part.childForFieldName('alias')?.text
|
|
745
|
+
: part.type === 'dotted_name' ? part.namedChild(part.namedChildCount - 1)?.text : null;
|
|
746
|
+
add(scope, local, 'import', lineOf(node), branch);
|
|
747
|
+
}
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
case 'global_statement':
|
|
751
|
+
case 'nonlocal_statement':
|
|
752
|
+
return;
|
|
753
|
+
case 'expression_statement':
|
|
754
|
+
for (let expr of node.namedChildren) {
|
|
755
|
+
while (expr?.type === 'assignment' || expr?.type === 'augmented_assignment') {
|
|
756
|
+
bindTarget(scope, expr.childForFieldName('left'), branch);
|
|
757
|
+
expr = expr.type === 'assignment' ? expr.childForFieldName('right') : null;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return;
|
|
761
|
+
case 'delete_statement':
|
|
762
|
+
for (const target of node.namedChildren) bindTarget(scope, target, branch);
|
|
763
|
+
return;
|
|
764
|
+
case 'if_statement': {
|
|
765
|
+
const line = lineOf(node);
|
|
766
|
+
const consequence = node.childForFieldName('consequence');
|
|
767
|
+
const clauses = consequence ? [['if', consequence]] : [];
|
|
768
|
+
let elif = 0;
|
|
769
|
+
for (const alternative of node.childrenForFieldName('alternative')) {
|
|
770
|
+
if (alternative.type === 'elif_clause') clauses.push([`elif:${elif++}`, alternative]);
|
|
771
|
+
else if (alternative.type === 'else_clause') clauses.push(['else', alternative]);
|
|
772
|
+
}
|
|
773
|
+
noteClauses(node, clauses);
|
|
774
|
+
// `if TYPE_CHECKING:` branches exist for type checkers, not
|
|
775
|
+
// for one runtime configuration: their names form no item.
|
|
776
|
+
const typeChecking = /^(?:\w+\.)?TYPE_CHECKING$/.test(node.childForFieldName('condition')?.text || '');
|
|
777
|
+
for (const [tag, clause] of clauses) {
|
|
778
|
+
const block = clause.type === 'block' ? clause
|
|
779
|
+
: clause.childForFieldName('consequence') || clause.childForFieldName('body');
|
|
780
|
+
visitBlock(block, scope, [...branch, typeChecking ? [line, tag, 'type-checking'] : [line, tag]]);
|
|
781
|
+
}
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
case 'try_statement': {
|
|
785
|
+
const line = lineOf(node);
|
|
786
|
+
const clauses = [];
|
|
787
|
+
let handler = 0;
|
|
788
|
+
for (const child of node.namedChildren) {
|
|
789
|
+
if (child.type === 'block') clauses.push(['try', child]);
|
|
790
|
+
else if (child.type === 'except_clause' || child.type === 'except_group_clause') {
|
|
791
|
+
clauses.push([`except:${handler++}`, child]);
|
|
792
|
+
} else if (child.type === 'else_clause') clauses.push(['try-else', child]);
|
|
793
|
+
else if (child.type === 'finally_clause') clauses.push(['finally', child]);
|
|
794
|
+
}
|
|
795
|
+
noteClauses(node, clauses);
|
|
796
|
+
for (const [tag, clause] of clauses) {
|
|
797
|
+
const path = [...branch, [line, tag]];
|
|
798
|
+
if (clause.type === 'block') { visitBlock(clause, scope, path); continue; }
|
|
799
|
+
for (const target of clause.descendantsOfType('as_pattern_target')) {
|
|
800
|
+
if (target.parent?.parent && sameNode(target.parent.parent, clause)) bindTarget(scope, target, path);
|
|
801
|
+
}
|
|
802
|
+
for (const child of clause.namedChildren) if (child.type === 'block') visitBlock(child, scope, path);
|
|
803
|
+
}
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
case 'match_statement': {
|
|
807
|
+
const line = lineOf(node);
|
|
808
|
+
const body = node.childForFieldName('body');
|
|
809
|
+
const cases = body ? body.namedChildren.filter(child => child.type === 'case_clause') : [];
|
|
810
|
+
const clauses = cases.map((clause, i) => [`case:${i}`, clause]);
|
|
811
|
+
noteClauses(node, clauses);
|
|
812
|
+
for (const [tag, clause] of clauses) {
|
|
813
|
+
for (const child of clause.namedChildren) if (child.type === 'block') visitBlock(child, scope, [...branch, [line, tag]]);
|
|
814
|
+
}
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
case 'for_statement':
|
|
818
|
+
bindTarget(scope, node.childForFieldName('left'), branch);
|
|
819
|
+
// A loop body may run several times or not at all: its
|
|
820
|
+
// bindings are sequential, never exclusive alternatives.
|
|
821
|
+
for (const child of node.namedChildren) if (child.type === 'block' || child.type === 'else_clause') visitBlock(child.type === 'block' ? child : child.namedChildren.find(c => c.type === 'block'), scope, [...branch, [lineOf(node), 'loop']]);
|
|
822
|
+
return;
|
|
823
|
+
case 'while_statement':
|
|
824
|
+
for (const child of node.namedChildren) if (child.type === 'block' || child.type === 'else_clause') visitBlock(child.type === 'block' ? child : child.namedChildren.find(c => c.type === 'block'), scope, [...branch, [lineOf(node), 'loop']]);
|
|
825
|
+
return;
|
|
826
|
+
case 'with_statement':
|
|
827
|
+
for (const target of node.descendantsOfType('as_pattern_target')) {
|
|
828
|
+
if (target.parent?.parent?.parent && sameNode(target.parent.parent.parent, node.namedChildren[0])) bindTarget(scope, target, branch);
|
|
829
|
+
}
|
|
830
|
+
visitBlock(node.childForFieldName('body'), scope, branch);
|
|
831
|
+
return;
|
|
832
|
+
default:
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
// Only the scopes holding a candidate are walked (their own statements;
|
|
837
|
+
// nested scopes are walked when they are candidates themselves).
|
|
838
|
+
for (const scope of candidateScopes) {
|
|
839
|
+
if (scope.type === 'module') { visitBlock(scope, 0, []); continue; }
|
|
840
|
+
const head = scope.parent?.type === 'decorated_definition' ? scope.parent : scope;
|
|
841
|
+
visitBlock(scope.childForFieldName('body'), lineOf(head), []);
|
|
842
|
+
}
|
|
843
|
+
// A `global` / `nonlocal` write anywhere rebinds the name at run time.
|
|
844
|
+
for (const node of reboundNodes || getCachedNodeList(root).nodes) {
|
|
845
|
+
if (node.type !== 'global_statement' && node.type !== 'nonlocal_statement') continue;
|
|
846
|
+
for (const name of node.namedChildren) if (name.type === 'identifier') rebound.add(name.text);
|
|
847
|
+
}
|
|
848
|
+
const exclusive = (a, b) => {
|
|
849
|
+
for (let k = 0; k < Math.min(a.length, b.length); k++) {
|
|
850
|
+
if (a[k][0] !== b[k][0]) return false;
|
|
851
|
+
if (a[k][1] !== b[k][1]) return branchClausesExclusive(a[k][1], b[k][1]);
|
|
852
|
+
}
|
|
853
|
+
return false;
|
|
854
|
+
};
|
|
855
|
+
const items = [];
|
|
856
|
+
for (const [scope, names] of scopes) {
|
|
857
|
+
for (const [name, sites] of names) {
|
|
858
|
+
if (sites.length < 2 || name === '*' || rebound.has(name)) continue;
|
|
859
|
+
if (!sites.some(site => site.kind !== 'value')) continue;
|
|
860
|
+
if (sites.some(site => site.branch.some(step => step.length > 2))) continue;
|
|
861
|
+
let all = true;
|
|
862
|
+
for (let i = 0; all && i < sites.length; i++) {
|
|
863
|
+
for (let j = i + 1; all && j < sites.length; j++) all = exclusive(sites[i].branch, sites[j].branch);
|
|
864
|
+
}
|
|
865
|
+
if (!all) continue;
|
|
866
|
+
const statements = new Set(sites.flatMap(site => site.branch.map(([line]) => line)));
|
|
867
|
+
items.push({
|
|
868
|
+
name, scope,
|
|
869
|
+
sites: sites.map(site => ({ kind: site.kind, line: site.line, branch: site.branch })),
|
|
870
|
+
clauses: [...statements].sort((a, b) => a - b).map(line => [line, clauseRanges.get(line) || []]),
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
items.sort((a, b) => a.scope - b.scope || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
875
|
+
return items;
|
|
876
|
+
}
|
|
877
|
+
|
|
339
878
|
// --- End single-pass helpers ---
|
|
340
879
|
|
|
341
880
|
/**
|
|
@@ -530,9 +1069,7 @@ function extractClassMembers(classNode, code) {
|
|
|
530
1069
|
const bodyNode = classNode.childForFieldName('body');
|
|
531
1070
|
if (!bodyNode) return members;
|
|
532
1071
|
|
|
533
|
-
for (
|
|
534
|
-
const child = bodyNode.namedChild(i);
|
|
535
|
-
|
|
1072
|
+
for (const child of pythonClassBodyDefs(bodyNode)) {
|
|
536
1073
|
let funcNode = child;
|
|
537
1074
|
let decoratorStart = null;
|
|
538
1075
|
const memberDecorators = [];
|
|
@@ -586,6 +1123,7 @@ function extractClassMembers(classNode, code) {
|
|
|
586
1123
|
}
|
|
587
1124
|
|
|
588
1125
|
const isAsync = funcNode.text.trimStart().startsWith('async ');
|
|
1126
|
+
const isGenerator = isPythonGenerator(funcNode);
|
|
589
1127
|
const returnType = extractReturnType(funcNode);
|
|
590
1128
|
const returnedConstructors = extractReturnedConstructors(funcNode);
|
|
591
1129
|
const defLine = getDefLine(funcNode);
|
|
@@ -603,6 +1141,7 @@ function extractClassMembers(classNode, code) {
|
|
|
603
1141
|
endLine,
|
|
604
1142
|
memberType,
|
|
605
1143
|
isAsync,
|
|
1144
|
+
...(isGenerator && { isGenerator }),
|
|
606
1145
|
isMethod: true, // Mark as method for context() lookups
|
|
607
1146
|
// Match top-level Python functions: `async def` → ['async'] modifiers.
|
|
608
1147
|
modifiers: isAsync ? ['async'] : [],
|
|
@@ -649,18 +1188,28 @@ function parse(code, parser) {
|
|
|
649
1188
|
const processedFn = new Set();
|
|
650
1189
|
const processedCls = new Set();
|
|
651
1190
|
|
|
1191
|
+
// Binding statements and `global`/`nonlocal` declarations seen by the
|
|
1192
|
+
// one traversal feed the configuration-item gate (fix #398F).
|
|
1193
|
+
const bindingNodes = [];
|
|
1194
|
+
const reboundNodes = [];
|
|
652
1195
|
traverseTreeCached(tree.rootNode, (node) => {
|
|
653
1196
|
_processFunction(node, functions, processedFn, lines, code);
|
|
654
1197
|
_processClass(node, classes, processedCls, lines) ||
|
|
655
1198
|
_processTypeAlias(node, classes, processedCls, lines);
|
|
656
1199
|
_processState(node, stateObjects, lines);
|
|
657
1200
|
_processModuleAssign(node, moduleAssigned);
|
|
1201
|
+
const type = node.type;
|
|
1202
|
+
if (type === 'function_definition' || type === 'class_definition' ||
|
|
1203
|
+
type === 'import_statement' || type === 'import_from_statement') bindingNodes.push(node);
|
|
1204
|
+
else if (type === 'global_statement' || type === 'nonlocal_statement') reboundNodes.push(node);
|
|
658
1205
|
return true;
|
|
659
1206
|
});
|
|
660
1207
|
|
|
661
1208
|
functions.sort((a, b) => a.startLine - b.startLine);
|
|
662
1209
|
classes.sort((a, b) => a.startLine - b.startLine);
|
|
663
1210
|
stateObjects.sort((a, b) => a.startLine - b.startLine);
|
|
1211
|
+
const moduleValueAliases = moduleAssigned.size > 0 ? pythonModuleValueAliases(tree.rootNode) : [];
|
|
1212
|
+
const configurationItems = pythonConfigurationItems(tree.rootNode, bindingNodes, reboundNodes);
|
|
664
1213
|
|
|
665
1214
|
return {
|
|
666
1215
|
language: 'python',
|
|
@@ -668,7 +1217,10 @@ function parse(code, parser) {
|
|
|
668
1217
|
functions,
|
|
669
1218
|
classes,
|
|
670
1219
|
stateObjects,
|
|
671
|
-
...(
|
|
1220
|
+
...(moduleValueAliases.length > 0 && { moduleValueAliases: [...moduleValueAliases] }),
|
|
1221
|
+
...(moduleValueAliases.constructed && { moduleConstructedValues: moduleValueAliases.constructed }),
|
|
1222
|
+
...(configurationItems.length > 0 && { configurationItems }),
|
|
1223
|
+
...(tree.rootNode.hasError && { parseRecovery: true, parseErrorRegions: parseErrorRegions(tree.rootNode) }),
|
|
672
1224
|
...(moduleAssigned.size > 0 && { moduleAssignedNames: [...moduleAssigned].sort() }),
|
|
673
1225
|
imports: [],
|
|
674
1226
|
exports: []
|
|
@@ -843,6 +1395,40 @@ function iterableBindingTypes(typeNode) {
|
|
|
843
1395
|
return typeName ? [typeName] : [];
|
|
844
1396
|
}
|
|
845
1397
|
|
|
1398
|
+
// Subscript contracts of builtin/typing containers (fix #359, starlette-
|
|
1399
|
+
// measured): `params: dict[str, Convertor]` makes `params[key]` a Convertor;
|
|
1400
|
+
// `items: list[Conv]` makes `items[0]` a Conv. The element is the declared
|
|
1401
|
+
// value slot of the annotation, never guessed: unions, Any/object, bare
|
|
1402
|
+
// type variables and heterogeneous tuples abstain.
|
|
1403
|
+
const PY_MAPPING_ANNOTATIONS = new Set([
|
|
1404
|
+
'dict', 'Dict', 'Mapping', 'MutableMapping', 'OrderedDict', 'defaultdict',
|
|
1405
|
+
'DefaultDict',
|
|
1406
|
+
]);
|
|
1407
|
+
const PY_SEQUENCE_ANNOTATIONS = new Set([
|
|
1408
|
+
'list', 'List', 'Sequence', 'MutableSequence', 'deque', 'Deque',
|
|
1409
|
+
]);
|
|
1410
|
+
function subscriptElementContract(typeNode) {
|
|
1411
|
+
const outerRaw = typeNode && typeNode.type === 'type' && typeNode.namedChildCount > 0
|
|
1412
|
+
? typeNode.namedChild(0) : typeNode;
|
|
1413
|
+
const outer = genericTypeParts(outerRaw);
|
|
1414
|
+
if (!outer) return null;
|
|
1415
|
+
let element = null;
|
|
1416
|
+
if (PY_MAPPING_ANNOTATIONS.has(outer.base) && outer.args.length === 2) {
|
|
1417
|
+
element = outer.args[1];
|
|
1418
|
+
} else if (PY_SEQUENCE_ANNOTATIONS.has(outer.base) && outer.args.length === 1) {
|
|
1419
|
+
element = outer.args[0];
|
|
1420
|
+
} else if (['tuple', 'Tuple'].includes(outer.base) && outer.args.length === 2 &&
|
|
1421
|
+
outer.args[1]?.type === 'ellipsis') {
|
|
1422
|
+
element = outer.args[0];
|
|
1423
|
+
}
|
|
1424
|
+
if (!element) return null;
|
|
1425
|
+
const type = typeNameFromExpr(unwrapTypeNode(element));
|
|
1426
|
+
if (!type || type === 'Any' || type === 'object' || type === 'None' ||
|
|
1427
|
+
/^[A-Z][A-Z0-9]?$/.test(type)) return null;
|
|
1428
|
+
const qualifier = typeQualifierFromExpr(unwrapTypeNode(element));
|
|
1429
|
+
return { type, ...(qualifier && { qualifier }), node: element };
|
|
1430
|
+
}
|
|
1431
|
+
|
|
846
1432
|
function patternIdentifiers(node, out = []) {
|
|
847
1433
|
if (!node) return out;
|
|
848
1434
|
if (node.type === 'identifier') {
|
|
@@ -864,6 +1450,12 @@ function enclosingPythonClassName(node) {
|
|
|
864
1450
|
return null;
|
|
865
1451
|
}
|
|
866
1452
|
|
|
1453
|
+
const FIELD_CONTRACT_VALUE_NODES = new Set([
|
|
1454
|
+
'call', 'attribute', 'subscript', 'binary_operator', 'boolean_operator',
|
|
1455
|
+
'comparison_operator', 'list', 'dictionary', 'set', 'tuple', 'string',
|
|
1456
|
+
'concatenated_string', 'parameters', 'decorator',
|
|
1457
|
+
]);
|
|
1458
|
+
|
|
867
1459
|
function explicitInstanceFieldContracts(tree, parser) {
|
|
868
1460
|
const result = new Map();
|
|
869
1461
|
const parseCommentType = comment => {
|
|
@@ -877,16 +1469,20 @@ function explicitInstanceFieldContracts(tree, parser) {
|
|
|
877
1469
|
const assignment = statement?.namedChild(0);
|
|
878
1470
|
return assignment?.childForFieldName('type') || null;
|
|
879
1471
|
};
|
|
880
|
-
|
|
1472
|
+
traverseTree(tree.rootNode, node => {
|
|
1473
|
+
// Only statements can declare an annotated instance field. Value
|
|
1474
|
+
// expressions and parameter/default/decorator trees contain no
|
|
1475
|
+
// such statements, so do not materialize all their descendants.
|
|
1476
|
+
if (FIELD_CONTRACT_VALUE_NODES.has(node.type)) return false;
|
|
881
1477
|
if (node.type !== 'expression_statement') return true;
|
|
882
1478
|
const assignment = node.namedChild(0);
|
|
883
|
-
if (assignment?.type !== 'assignment') return
|
|
1479
|
+
if (assignment?.type !== 'assignment') return false;
|
|
884
1480
|
const left = assignment.childForFieldName('left');
|
|
885
1481
|
if (left?.type !== 'attribute' ||
|
|
886
|
-
left.childForFieldName('object')?.text !== 'self') return
|
|
1482
|
+
left.childForFieldName('object')?.text !== 'self') return false;
|
|
887
1483
|
const field = left.childForFieldName('attribute')?.text;
|
|
888
1484
|
const className = enclosingPythonClassName(node);
|
|
889
|
-
if (!field || !className) return
|
|
1485
|
+
if (!field || !className) return false;
|
|
890
1486
|
let typeNode = assignment.childForFieldName('type');
|
|
891
1487
|
if (!typeNode) {
|
|
892
1488
|
const parent = node.parent;
|
|
@@ -899,16 +1495,16 @@ function explicitInstanceFieldContracts(tree, parser) {
|
|
|
899
1495
|
}
|
|
900
1496
|
}
|
|
901
1497
|
}
|
|
902
|
-
if (!typeNode) return
|
|
1498
|
+
if (!typeNode) return false;
|
|
903
1499
|
const type = typeNameFromAnnotation(typeNode);
|
|
904
1500
|
const itemTypes = iterableBindingTypes(typeNode);
|
|
905
|
-
if (!type && itemTypes.length === 0) return
|
|
1501
|
+
if (!type && itemTypes.length === 0) return false;
|
|
906
1502
|
if (!result.has(className)) result.set(className, new Map());
|
|
907
1503
|
result.get(className).set(field, {
|
|
908
1504
|
...(type && { type }),
|
|
909
1505
|
...(itemTypes.length > 0 && { itemTypes }),
|
|
910
1506
|
});
|
|
911
|
-
return
|
|
1507
|
+
return false;
|
|
912
1508
|
});
|
|
913
1509
|
return result;
|
|
914
1510
|
}
|
|
@@ -1050,6 +1646,13 @@ function narrowedReceiverType(refNode, receiverName, declaredUnion) {
|
|
|
1050
1646
|
return undefined;
|
|
1051
1647
|
}
|
|
1052
1648
|
|
|
1649
|
+
/** `Union[Foo, None]` names Foo (fix #402); a union of several types names none. */
|
|
1650
|
+
function unionMemberName(node) {
|
|
1651
|
+
const members = (genericTypeParts(node)?.args || []).filter(arg => arg &&
|
|
1652
|
+
arg.type !== 'none' && !(arg.type === 'identifier' && arg.text === 'None'));
|
|
1653
|
+
return members.length === 1 ? typeNameFromExpr(members[0]) : undefined;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1053
1656
|
function typeNameFromExpr(node) {
|
|
1054
1657
|
if (!node) return undefined;
|
|
1055
1658
|
switch (node.type) {
|
|
@@ -1074,6 +1677,7 @@ function typeNameFromExpr(node) {
|
|
|
1074
1677
|
case 'subscript': {
|
|
1075
1678
|
// typing.Optional[Foo] parses as subscript when base is dotted
|
|
1076
1679
|
const base = typeNameFromExpr(node.childForFieldName('value'));
|
|
1680
|
+
if (base === 'Union') return unionMemberName(node);
|
|
1077
1681
|
if (PY_TYPE_WRAPPERS.has(base)) {
|
|
1078
1682
|
return typeNameFromExpr(node.childForFieldName('subscript'));
|
|
1079
1683
|
}
|
|
@@ -1082,6 +1686,7 @@ function typeNameFromExpr(node) {
|
|
|
1082
1686
|
case 'generic_type': {
|
|
1083
1687
|
// Optional[Foo] / Mapping[str, int] in annotation position
|
|
1084
1688
|
const base = typeNameFromExpr(node.namedChild(0));
|
|
1689
|
+
if (base === 'Union') return unionMemberName(node);
|
|
1085
1690
|
if (PY_TYPE_WRAPPERS.has(base)) {
|
|
1086
1691
|
const params = node.namedChild(1); // type_parameter → type wrappers
|
|
1087
1692
|
const firstType = params && params.namedChildCount > 0 ? params.namedChild(0) : null;
|
|
@@ -1372,6 +1977,108 @@ function pythonScopeBoundNames(scopeNode) {
|
|
|
1372
1977
|
return names;
|
|
1373
1978
|
}
|
|
1374
1979
|
|
|
1980
|
+
/** Type of the innermost scope node (function, lambda, class, module). */
|
|
1981
|
+
function pythonEnclosingScopeType(node) {
|
|
1982
|
+
for (let parent = node?.parent; parent; parent = parent.parent) {
|
|
1983
|
+
if (['function_definition', 'async_function_definition', 'lambda',
|
|
1984
|
+
'class_definition', 'module'].includes(parent.type)) return parent.type;
|
|
1985
|
+
}
|
|
1986
|
+
return 'module';
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
/** Innermost enclosing function of a node; null at module/class/lambda level. */
|
|
1990
|
+
function pythonEnclosingFunctionNode(node) {
|
|
1991
|
+
for (let parent = node?.parent; parent; parent = parent.parent) {
|
|
1992
|
+
if (parent.type === 'function_definition' ||
|
|
1993
|
+
parent.type === 'async_function_definition') return parent;
|
|
1994
|
+
if (parent.type === 'lambda' || parent.type === 'class_definition') return null;
|
|
1995
|
+
}
|
|
1996
|
+
return null;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
const scopeBindingCountsByTree = new WeakMap();
|
|
2000
|
+
|
|
2001
|
+
/**
|
|
2002
|
+
* How many times a function binds each local name (fix #381): parameters,
|
|
2003
|
+
* assignment/augmented/walrus targets, loop and comprehension-free for
|
|
2004
|
+
* targets, with/except `as` targets, imports, nested def/class names; a
|
|
2005
|
+
* `global`/`nonlocal`/`del` of the name counts as two (never a single local
|
|
2006
|
+
* binding). Nested functions, lambdas and classes are their own scopes.
|
|
2007
|
+
*/
|
|
2008
|
+
function pythonBindingCount(fnNode, name) {
|
|
2009
|
+
let byId = scopeBindingCountsByTree.get(fnNode.tree);
|
|
2010
|
+
if (!byId) { byId = new Map(); scopeBindingCountsByTree.set(fnNode.tree, byId); }
|
|
2011
|
+
let counts = byId.get(fnNode.id);
|
|
2012
|
+
if (!counts) {
|
|
2013
|
+
counts = new Map();
|
|
2014
|
+
const add = (id, n = 1) => { if (id) counts.set(id, (counts.get(id) || 0) + n); };
|
|
2015
|
+
// Only names in binding position: `self.x = ...` / `a[i] = ...`
|
|
2016
|
+
// bind no local.
|
|
2017
|
+
const addTarget = (target) => {
|
|
2018
|
+
if (!target) return;
|
|
2019
|
+
if (target.type === 'identifier') { add(target.text); return; }
|
|
2020
|
+
if (['pattern_list', 'tuple_pattern', 'list_pattern', 'tuple', 'list',
|
|
2021
|
+
'parenthesized_expression', 'list_splat_pattern', 'list_splat'].includes(target.type)) {
|
|
2022
|
+
for (const item of target.namedChildren) addTarget(item);
|
|
2023
|
+
}
|
|
2024
|
+
};
|
|
2025
|
+
const params = fnNode.childForFieldName('parameters');
|
|
2026
|
+
if (params) {
|
|
2027
|
+
for (const param of params.namedChildren) {
|
|
2028
|
+
const id = param.type === 'identifier' ? param
|
|
2029
|
+
: (param.childForFieldName('name') || param.namedChild(0));
|
|
2030
|
+
if (id?.type === 'identifier') add(id.text);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
const walk = (node) => {
|
|
2034
|
+
for (const child of node.namedChildren) {
|
|
2035
|
+
if (child.type === 'function_definition' ||
|
|
2036
|
+
child.type === 'async_function_definition' ||
|
|
2037
|
+
child.type === 'class_definition') {
|
|
2038
|
+
add(child.childForFieldName('name')?.text);
|
|
2039
|
+
continue;
|
|
2040
|
+
}
|
|
2041
|
+
if (child.type === 'lambda') continue;
|
|
2042
|
+
if (child.type === 'assignment' || child.type === 'augmented_assignment') {
|
|
2043
|
+
addTarget(child.childForFieldName('left'));
|
|
2044
|
+
} else if (child.type === 'named_expression') {
|
|
2045
|
+
addTarget(child.childForFieldName('name'));
|
|
2046
|
+
} else if (child.type === 'for_statement' || child.type === 'for_in_clause') {
|
|
2047
|
+
addTarget(child.childForFieldName('left'));
|
|
2048
|
+
} else if (child.type === 'as_pattern') {
|
|
2049
|
+
addTarget(child.childForFieldName('alias') || child.namedChild(child.namedChildCount - 1));
|
|
2050
|
+
} else if (child.type === 'except_clause') {
|
|
2051
|
+
for (const part of child.namedChildren) {
|
|
2052
|
+
if (part.type === 'as_pattern') continue;
|
|
2053
|
+
if (part.type === 'identifier' && part.previousSibling?.type === 'as') add(part.text);
|
|
2054
|
+
}
|
|
2055
|
+
} else if (child.type === 'import_statement' || child.type === 'import_from_statement') {
|
|
2056
|
+
for (const part of child.namedChildren) {
|
|
2057
|
+
if (part.type === 'aliased_import') {
|
|
2058
|
+
add(part.childForFieldName('alias')?.text);
|
|
2059
|
+
} else if (part.type === 'dotted_name' &&
|
|
2060
|
+
!(child.type === 'import_from_statement' && part === child.childForFieldName('module_name'))) {
|
|
2061
|
+
add(part.text.split('.')[0]);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
} else if (child.type === 'global_statement' || child.type === 'nonlocal_statement' ||
|
|
2065
|
+
child.type === 'delete_statement') {
|
|
2066
|
+
for (const id of patternIdentifiers(child)) add(id, 2);
|
|
2067
|
+
}
|
|
2068
|
+
walk(child);
|
|
2069
|
+
}
|
|
2070
|
+
};
|
|
2071
|
+
const body = fnNode.childForFieldName('body');
|
|
2072
|
+
if (body) walk(body);
|
|
2073
|
+
byId.set(fnNode.id, counts);
|
|
2074
|
+
}
|
|
2075
|
+
return counts.get(name) || 0;
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
function pythonSingleLocalBinding(fnNode, name) {
|
|
2079
|
+
return pythonBindingCount(fnNode, name) === 1;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
1375
2082
|
function pythonScopeBindsName(scopeNode, name) {
|
|
1376
2083
|
return pythonScopeBoundNames(scopeNode).has(name);
|
|
1377
2084
|
}
|
|
@@ -1481,12 +2188,100 @@ function isPythonNameShadowedAt(refNode, name) {
|
|
|
1481
2188
|
}
|
|
1482
2189
|
}
|
|
1483
2190
|
const body = parent.childForFieldName('body');
|
|
1484
|
-
|
|
2191
|
+
if (body && pythonScopeBindsName(body, name)) {
|
|
2192
|
+
// `global name` makes the function's binding the module's.
|
|
2193
|
+
return !pythonDeclaresScopeName(body, name);
|
|
2194
|
+
}
|
|
2195
|
+
// Unbound here: a nested function reads the enclosing function
|
|
2196
|
+
// scopes (closures) before the module (fix #402); class bodies
|
|
2197
|
+
// are skipped, as Python skips them.
|
|
2198
|
+
if (body && pythonDeclaresScopeName(body, name)) return false;
|
|
1485
2199
|
}
|
|
1486
2200
|
}
|
|
1487
2201
|
return false;
|
|
1488
2202
|
}
|
|
1489
2203
|
|
|
2204
|
+
/**
|
|
2205
|
+
* The start line (decorators included) of the enclosing function a closure
|
|
2206
|
+
* reference binds `name` in (fix #402): null when the reference's own
|
|
2207
|
+
* function (or a comprehension or loop around it) binds it, or no enclosing
|
|
2208
|
+
* function does. A lambda is a function of its own (fix #403): the call
|
|
2209
|
+
* records name it as the reference's enclosing function, so a name its
|
|
2210
|
+
* enclosing def binds is a closure name.
|
|
2211
|
+
*/
|
|
2212
|
+
function pythonClosureScopeLine(refNode, name) {
|
|
2213
|
+
let first = true;
|
|
2214
|
+
for (let parent = refNode.parent; parent; parent = parent.parent) {
|
|
2215
|
+
if (parent.type === 'lambda') {
|
|
2216
|
+
const params = parent.childForFieldName('parameters');
|
|
2217
|
+
if (params?.namedChildren.some(param => (param.type === 'identifier' ? param
|
|
2218
|
+
: param.childForFieldName('name'))?.text === name)) return null;
|
|
2219
|
+
first = false;
|
|
2220
|
+
continue;
|
|
2221
|
+
}
|
|
2222
|
+
if (PY_COMPREHENSIONS.has(parent.type)) {
|
|
2223
|
+
if (parent.namedChildren.some(clause => clause.type === 'for_in_clause' &&
|
|
2224
|
+
pythonTargetBindsName(clause.childForFieldName('left'), name))) return null;
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
if (parent.type !== 'function_definition' && parent.type !== 'async_function_definition') continue;
|
|
2228
|
+
const params = parent.childForFieldName('parameters');
|
|
2229
|
+
const body = parent.childForFieldName('body');
|
|
2230
|
+
const bindsParam = !!params && params.namedChildren.some(param => {
|
|
2231
|
+
const id = param.type === 'identifier' ? param : (param.childForFieldName('name') || param.namedChild(0));
|
|
2232
|
+
return id?.type === 'identifier' && id.text === name;
|
|
2233
|
+
});
|
|
2234
|
+
if (body && pythonDeclaresScopeName(body, name)) return null;
|
|
2235
|
+
if (bindsParam || (body && pythonScopeBindsName(body, name))) {
|
|
2236
|
+
if (first) return null;
|
|
2237
|
+
const start = parent.parent?.type === 'decorated_definition' ? parent.parent : parent;
|
|
2238
|
+
return start.startPosition.row + 1;
|
|
2239
|
+
}
|
|
2240
|
+
first = false;
|
|
2241
|
+
}
|
|
2242
|
+
return null;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
/** Shadow fields of a bare reference: `localShadow`, and the enclosing
|
|
2246
|
+
* function a closure binds it in. */
|
|
2247
|
+
function pythonShadowFields(refNode, name) {
|
|
2248
|
+
if (!isPythonNameShadowedAt(refNode, name)) return null;
|
|
2249
|
+
const scope = pythonClosureScopeLine(refNode, name);
|
|
2250
|
+
return scope ? { localShadow: true, localShadowScope: scope } : { localShadow: true };
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
/** Names a function body declares `global` (not in nested scopes),
|
|
2254
|
+
* memoized per tree and body node (fix #402). */
|
|
2255
|
+
const globalNamesByTree = new WeakMap();
|
|
2256
|
+
function pythonGlobalNames(body) {
|
|
2257
|
+
let byId = globalNamesByTree.get(body.tree);
|
|
2258
|
+
if (!byId) { byId = new Map(); globalNamesByTree.set(body.tree, byId); }
|
|
2259
|
+
let names = byId.get(body.id);
|
|
2260
|
+
if (names) return names;
|
|
2261
|
+
names = new Set();
|
|
2262
|
+
// Prefilter: no `global` keyword in the body, no declaration.
|
|
2263
|
+
if (body.text.includes('global')) {
|
|
2264
|
+
const stack = [body];
|
|
2265
|
+
while (stack.length > 0) {
|
|
2266
|
+
const node = stack.pop();
|
|
2267
|
+
if (node.type === 'global_statement') {
|
|
2268
|
+
for (const child of node.namedChildren) if (child.type === 'identifier') names.add(child.text);
|
|
2269
|
+
continue;
|
|
2270
|
+
}
|
|
2271
|
+
if (node !== body && (node.type === 'function_definition' || node.type === 'class_definition' ||
|
|
2272
|
+
node.type === 'lambda')) continue;
|
|
2273
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) stack.push(node.namedChild(i));
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
byId.set(body.id, names);
|
|
2277
|
+
return names;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
/** Whether a function body declares `global name` (not in nested scopes). */
|
|
2281
|
+
function pythonDeclaresScopeName(body, name) {
|
|
2282
|
+
return pythonGlobalNames(body).has(name);
|
|
2283
|
+
}
|
|
2284
|
+
|
|
1490
2285
|
/**
|
|
1491
2286
|
* Whether an uppercase call target is a local VALUE binding rather than a
|
|
1492
2287
|
* class declaration.
|
|
@@ -1637,8 +2432,145 @@ function pythonModuleAliases(tree) {
|
|
|
1637
2432
|
return aliases;
|
|
1638
2433
|
}
|
|
1639
2434
|
|
|
1640
|
-
|
|
2435
|
+
// Annotation positions whose value Python evaluates as an expression.
|
|
2436
|
+
const PY_ANNOTATION_OWNERS = new Set(['typed_parameter', 'typed_default_parameter', 'function_definition', 'assignment']);
|
|
2437
|
+
|
|
2438
|
+
/**
|
|
2439
|
+
* String annotations holding calls (fix #398F): `def f(v: "Annotated[int,
|
|
2440
|
+
* typer.Argument()]")`. A forward reference is an expression the runtime
|
|
2441
|
+
* evaluates (`typing.get_type_hints`), so the calls it spells are calls.
|
|
2442
|
+
* Plain one-line string literals only (no prefix, escape or
|
|
2443
|
+
* interpolation); the result lists the byte ranges of their quotes.
|
|
2444
|
+
*/
|
|
2445
|
+
function annotationStringQuotes(tree) {
|
|
2446
|
+
const quotes = [];
|
|
2447
|
+
for (const holder of getCachedNodeList(tree.rootNode).nodes) {
|
|
2448
|
+
if (holder.type !== 'type') continue;
|
|
2449
|
+
const string = holder.namedChildCount === 1 ? holder.namedChild(0) : null;
|
|
2450
|
+
if (string?.type !== 'string' || string.startPosition.row !== string.endPosition.row) continue;
|
|
2451
|
+
const owner = holder.parent;
|
|
2452
|
+
if (!owner || !PY_ANNOTATION_OWNERS.has(owner.type)) continue;
|
|
2453
|
+
const field = owner.type === 'function_definition' ? 'return_type' : 'type';
|
|
2454
|
+
if (!sameNode(owner.childForFieldName(field), holder)) continue;
|
|
2455
|
+
const parts = string.children;
|
|
2456
|
+
if (parts.length !== 3 || parts[0].type !== 'string_start' || parts[1].type !== 'string_content' ||
|
|
2457
|
+
parts[2].type !== 'string_end') continue;
|
|
2458
|
+
const start = parts[0].text;
|
|
2459
|
+
if (start !== '"' && start !== "'") continue;
|
|
2460
|
+
const content = parts[1];
|
|
2461
|
+
if (content.namedChildCount > 0 || !content.text.includes('(')) continue;
|
|
2462
|
+
quotes.push({ open: parts[0].startIndex, close: parts[2].startIndex,
|
|
2463
|
+
contentStart: content.startIndex, contentEnd: content.endIndex });
|
|
2464
|
+
}
|
|
2465
|
+
return quotes;
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
/**
|
|
2469
|
+
* References to the file's function-local classes (fix #398F): the row of
|
|
2470
|
+
* the one class declaration the name binds at the call (the receiver's
|
|
2471
|
+
* constructor, a class qualifier, a bare constructor call), or 0 when a
|
|
2472
|
+
* parameter, assignment or second declaration binds it too. Query-time
|
|
2473
|
+
* identity proofs read this instead of parsing the file again.
|
|
2474
|
+
*/
|
|
2475
|
+
function annotateLocalClassReferences(tree, calls, classNodes) {
|
|
2476
|
+
const names = new Set();
|
|
2477
|
+
for (const definition of classNodes) {
|
|
2478
|
+
if (definition.parent?.type === 'module') continue;
|
|
2479
|
+
for (let parent = definition.parent; parent; parent = parent.parent) {
|
|
2480
|
+
if (parent.type === 'function_definition' || parent.type === 'lambda') {
|
|
2481
|
+
const name = definition.childForFieldName('name')?.text;
|
|
2482
|
+
if (name) names.add(name);
|
|
2483
|
+
break;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
if (names.size === 0) return;
|
|
2488
|
+
const memo = new Map();
|
|
2489
|
+
const rowAt = (start, name) => {
|
|
2490
|
+
if (!Number.isInteger(start)) return undefined;
|
|
2491
|
+
let node = tree.rootNode.descendantForIndex(start);
|
|
2492
|
+
if (node?.text !== name) {
|
|
2493
|
+
for (let owner = node?.parent; owner && owner.startIndex === start; owner = owner.parent) {
|
|
2494
|
+
const value = owner.childForFieldName('right') || owner.childForFieldName('value');
|
|
2495
|
+
if (value?.type === 'call') {
|
|
2496
|
+
node = value.childForFieldName('function');
|
|
2497
|
+
break;
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
if (node?.text !== name && node?.parent?.type === 'attribute') {
|
|
2502
|
+
node = node.parent.childForFieldName('object');
|
|
2503
|
+
}
|
|
2504
|
+
if (!node || node.text !== name) return 0;
|
|
2505
|
+
const scope = referenceScope(node, 'python', memo, name, { includeClasses: true });
|
|
2506
|
+
return scope?.defRows?.length === 1 ? scope.defRows[0] + 1 : 0;
|
|
2507
|
+
};
|
|
2508
|
+
for (const call of calls) {
|
|
2509
|
+
if (!call.isMethod && !call.receiver && names.has(call.name)) {
|
|
2510
|
+
const row = rowAt(call.callSite?.start, call.name);
|
|
2511
|
+
if (row !== undefined) call.localClassRow = row;
|
|
2512
|
+
}
|
|
2513
|
+
if (call.isMethod && call.receiver && !call.receiverType && names.has(call.receiver)) {
|
|
2514
|
+
const row = rowAt(call.callSite?.start ?? call.callStart, call.receiver);
|
|
2515
|
+
if (row !== undefined) call.receiverLocalClassRow = row;
|
|
2516
|
+
}
|
|
2517
|
+
// Kept beside the evidence (never inside it: the evidence object is
|
|
2518
|
+
// a provenance fact), tied to the evidence position it was read at.
|
|
2519
|
+
const evidence = call.receiverTypeEvidence;
|
|
2520
|
+
if (call.receiverTypeSource === 'constructor' && evidence && names.has(call.receiverType)) {
|
|
2521
|
+
const row = rowAt(evidence.start, call.receiverType);
|
|
2522
|
+
if (row !== undefined) call.receiverTypeLocalClassRef = { start: evidence.start, row };
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
function findCallsInCode(code, parser, options = {}) {
|
|
1641
2528
|
const tree = parseTree(parser, code);
|
|
2529
|
+
const sink = { classNodes: [] };
|
|
2530
|
+
const calls = findCallsInTree(code, tree, parser, sink);
|
|
2531
|
+
annotateLocalClassReferences(tree, calls, sink.classNodes);
|
|
2532
|
+
if (options.stringAnnotations === false || !/(?::|->)\s*["'][^"'\n]*\(/.test(code)) return calls;
|
|
2533
|
+
const quotes = annotationStringQuotes(tree);
|
|
2534
|
+
if (quotes.length === 0) return calls;
|
|
2535
|
+
// The same source with those quotes blanked parses the annotation as
|
|
2536
|
+
// the expression it is, at the same offsets; only calls inside the
|
|
2537
|
+
// blanked strings are taken from it, and only where it parses clean.
|
|
2538
|
+
const chars = code.split('');
|
|
2539
|
+
for (const quote of quotes) {
|
|
2540
|
+
chars[quote.open] = ' ';
|
|
2541
|
+
chars[quote.close] = ' ';
|
|
2542
|
+
}
|
|
2543
|
+
const masked = chars.join('');
|
|
2544
|
+
const maskedTree = parseTree(parser, masked);
|
|
2545
|
+
const clean = quotes.filter(quote => {
|
|
2546
|
+
const node = maskedTree.rootNode.descendantForIndex(quote.contentStart, quote.contentEnd);
|
|
2547
|
+
return node && !node.hasError && !node.isMissing;
|
|
2548
|
+
});
|
|
2549
|
+
if (clean.length === 0) return calls;
|
|
2550
|
+
const inside = call => {
|
|
2551
|
+
const start = call.callSite?.start;
|
|
2552
|
+
return Number.isInteger(start) &&
|
|
2553
|
+
clean.some(quote => start >= quote.contentStart && start < quote.contentEnd);
|
|
2554
|
+
};
|
|
2555
|
+
const extra = findCallsInTree(masked, maskedTree, parser)
|
|
2556
|
+
.filter(inside).map(call => ({ ...call, stringAnnotation: true }));
|
|
2557
|
+
if (extra.length === 0) return calls;
|
|
2558
|
+
const position = call => [call.line, call.callSite?.column ?? call.column ?? 0];
|
|
2559
|
+
const merged = [...calls];
|
|
2560
|
+
for (const call of extra) {
|
|
2561
|
+
const [line, column] = position(call);
|
|
2562
|
+
let at = merged.length;
|
|
2563
|
+
while (at > 0) {
|
|
2564
|
+
const [l, c] = position(merged[at - 1]);
|
|
2565
|
+
if (l < line || (l === line && c <= column)) break;
|
|
2566
|
+
at--;
|
|
2567
|
+
}
|
|
2568
|
+
merged.splice(at, 0, call);
|
|
2569
|
+
}
|
|
2570
|
+
return merged;
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
function findCallsInTree(code, tree, parser, sink = null) {
|
|
1642
2574
|
const calls = [];
|
|
1643
2575
|
const instanceFieldContracts = explicitInstanceFieldContracts(tree, parser);
|
|
1644
2576
|
// Same-file callable return contracts are a closed type source for loop
|
|
@@ -1646,6 +2578,7 @@ function findCallsInCode(code, parser) {
|
|
|
1646
2578
|
// `parse_items() -> list[Item]` without guessing about external code.
|
|
1647
2579
|
const callableIterableTypes = new Map();
|
|
1648
2580
|
traverseTreeCached(tree.rootNode, node => {
|
|
2581
|
+
if (sink && node.type === 'class_definition') sink.classNodes.push(node);
|
|
1649
2582
|
if (node.type !== 'function_definition') return true;
|
|
1650
2583
|
const name = node.childForFieldName('name')?.text;
|
|
1651
2584
|
const returnType = node.childForFieldName('return_type');
|
|
@@ -1676,6 +2609,8 @@ function findCallsInCode(code, parser) {
|
|
|
1676
2609
|
const localIterationSources = new Map(); // loop variable -> declared iterable path + tuple index
|
|
1677
2610
|
const localDictValueTypes = new Map(); // local dict -> exact string-key value types
|
|
1678
2611
|
const localSubscriptSources = new Map(); // local value -> exact typed container selected by []
|
|
2612
|
+
const localSubscriptElements = new Map(); // container binding -> declared element contract (fix #359)
|
|
2613
|
+
const localSubscriptElementsStack = [];
|
|
1679
2614
|
const localVarStdlibContracts = new Map(); // variable -> stdlib module proving its type flow
|
|
1680
2615
|
const assignmentRhsReceiverTypes = new Map(); // call-node id -> pre-assignment type and origin
|
|
1681
2616
|
const constructedReceiverVars = new Set(); // exact constructor-result bindings
|
|
@@ -1688,6 +2623,10 @@ function findCallsInCode(code, parser) {
|
|
|
1688
2623
|
// receiver is null for chained/deep objects (self._text.append) — the
|
|
1689
2624
|
// rewritten call is then receiver-blind and routes through dispatch tiering.
|
|
1690
2625
|
const memberAliases = new Map();
|
|
2626
|
+
// One-hop local aliases of attribute paths (fix #381):
|
|
2627
|
+
// `config = self.config; config.load()` receives through `self.config`.
|
|
2628
|
+
// name -> { node: the aliased attribute node, scope: function node id }.
|
|
2629
|
+
const localFieldAliases = new Map();
|
|
1691
2630
|
const memberAliasesStack = []; // function-scoped save/restore, like localVarTypes
|
|
1692
2631
|
// Exact class-value aliases: `_Segment = Segment` makes later
|
|
1693
2632
|
// `_Segment.line()` dispatch through the imported/local class object.
|
|
@@ -1719,25 +2658,61 @@ function findCallsInCode(code, parser) {
|
|
|
1719
2658
|
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
1720
2659
|
const arg = argsNode.namedChild(i);
|
|
1721
2660
|
if (arg.type.endsWith('comment')) continue;
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
2661
|
+
return stringValueOf(arg);
|
|
2662
|
+
}
|
|
2663
|
+
return null;
|
|
2664
|
+
};
|
|
2665
|
+
const stringValueOf = (arg) => {
|
|
2666
|
+
// Handle f-string explicitly
|
|
2667
|
+
if (arg.type === 'string') {
|
|
2668
|
+
// f-string detection: tree-sitter-python wraps interpolations as 'interpolation' children.
|
|
2669
|
+
// If any interpolation child exists, this is interpolated; extract literal prefix.
|
|
2670
|
+
let interp = false;
|
|
2671
|
+
let prefix = '';
|
|
2672
|
+
for (let j = 0; j < arg.namedChildCount; j++) {
|
|
2673
|
+
const sc = arg.namedChild(j);
|
|
2674
|
+
if (sc.type === 'interpolation') { interp = true; break; }
|
|
2675
|
+
if (sc.type === 'string_content') prefix += sc.text;
|
|
2676
|
+
}
|
|
2677
|
+
if (interp) {
|
|
2678
|
+
return { value: prefix + (prefix.endsWith('*') ? '' : '*'), interp: true };
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
return _extractStringArg(arg);
|
|
2682
|
+
};
|
|
2683
|
+
|
|
2684
|
+
// fix #366: keyword request configuration — `session.request(
|
|
2685
|
+
// method="POST", url="/api/items")`, `send(url="/x")`. Returns
|
|
2686
|
+
// { url, interp, method? } when a `url=` keyword carries a string.
|
|
2687
|
+
// Whether the callee performs HTTP is decided at query time.
|
|
2688
|
+
const getRequestConfig = (callNode) => {
|
|
2689
|
+
const argsNode = callNode.childForFieldName('arguments');
|
|
2690
|
+
if (!argsNode) return null;
|
|
2691
|
+
let url = null;
|
|
2692
|
+
let method = null;
|
|
2693
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
2694
|
+
const arg = argsNode.namedChild(i);
|
|
2695
|
+
if (arg.type !== 'keyword_argument') continue;
|
|
2696
|
+
const nameNode = arg.childForFieldName('name');
|
|
2697
|
+
const valueNode = arg.childForFieldName('value');
|
|
2698
|
+
if (!nameNode || !valueNode || valueNode.type !== 'string') continue;
|
|
2699
|
+
// `path=` is overwhelmingly a filesystem path in Python APIs;
|
|
2700
|
+
// only `url=` names a request target.
|
|
2701
|
+
if (nameNode.text === 'url' && !url) {
|
|
2702
|
+
const v = stringValueOf(valueNode);
|
|
2703
|
+
if (v && typeof v.value === 'string' && v.value.length > 0) {
|
|
2704
|
+
url = { value: v.value, interp: !!v.interp, key: nameNode.text };
|
|
1732
2705
|
}
|
|
1733
|
-
|
|
1734
|
-
|
|
2706
|
+
} else if (nameNode.text === 'method') {
|
|
2707
|
+
const v = _extractStringArg(valueNode);
|
|
2708
|
+
if (v && !v.interp && typeof v.value === 'string' && v.value.length > 0) {
|
|
2709
|
+
method = v.value.toUpperCase();
|
|
1735
2710
|
}
|
|
1736
|
-
return _extractStringArg(arg);
|
|
1737
2711
|
}
|
|
1738
|
-
return _extractStringArg(arg);
|
|
1739
2712
|
}
|
|
1740
|
-
return null;
|
|
2713
|
+
if (!url) return null;
|
|
2714
|
+
return { url: url.value, key: url.key, ...(url.interp && { interp: true }),
|
|
2715
|
+
...(method && { method }) };
|
|
1741
2716
|
};
|
|
1742
2717
|
|
|
1743
2718
|
// Helper to check if a node is a non-callable literal
|
|
@@ -2075,6 +3050,7 @@ function findCallsInCode(code, parser) {
|
|
|
2075
3050
|
[...localDictValueTypes].map(([name, values]) =>
|
|
2076
3051
|
[name, new Map(values)])));
|
|
2077
3052
|
localSubscriptSourcesStack.push(new Map(localSubscriptSources));
|
|
3053
|
+
localSubscriptElementsStack.push(new Map(localSubscriptElements));
|
|
2078
3054
|
localVarStdlibContractsStack.push(new Map(localVarStdlibContracts));
|
|
2079
3055
|
constructedReceiverVarsStack.push(new Set(constructedReceiverVars));
|
|
2080
3056
|
withBindingVarsStack.push(new Set(withBindingVars));
|
|
@@ -2109,6 +3085,12 @@ function findCallsInCode(code, parser) {
|
|
|
2109
3085
|
localSubscriptSources.delete(name);
|
|
2110
3086
|
}
|
|
2111
3087
|
}
|
|
3088
|
+
for (const name of localSubscriptElements.keys()) {
|
|
3089
|
+
if ((body && pythonScopeBindsName(body, name)) ||
|
|
3090
|
+
functionParameterBindsName(node, name)) {
|
|
3091
|
+
localSubscriptElements.delete(name);
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
2112
3094
|
for (const name of classValueAliases.keys()) {
|
|
2113
3095
|
if ((body && pythonScopeBindsName(body, name)) ||
|
|
2114
3096
|
functionParameterBindsName(node, name)) {
|
|
@@ -2151,6 +3133,10 @@ function findCallsInCode(code, parser) {
|
|
|
2151
3133
|
else localVarUnionTypes.delete(nameNode.text);
|
|
2152
3134
|
if (itemTypes.length > 0) localIterableTypes.set(nameNode.text, itemTypes);
|
|
2153
3135
|
else localIterableTypes.delete(nameNode.text);
|
|
3136
|
+
const elementContract = parameterPattern?.type === 'identifier'
|
|
3137
|
+
? subscriptElementContract(typeNode) : null;
|
|
3138
|
+
if (elementContract) localSubscriptElements.set(nameNode.text, elementContract);
|
|
3139
|
+
else localSubscriptElements.delete(nameNode.text);
|
|
2154
3140
|
}
|
|
2155
3141
|
}
|
|
2156
3142
|
|
|
@@ -2257,6 +3243,9 @@ function findCallsInCode(code, parser) {
|
|
|
2257
3243
|
else localVarUnionTypes.delete(left.text);
|
|
2258
3244
|
if (itemTypes.length > 0) localIterableTypes.set(left.text, itemTypes);
|
|
2259
3245
|
else localIterableTypes.delete(left.text);
|
|
3246
|
+
const elementContract = subscriptElementContract(typeNode);
|
|
3247
|
+
if (elementContract) localSubscriptElements.set(left.text, elementContract);
|
|
3248
|
+
else localSubscriptElements.delete(left.text);
|
|
2260
3249
|
}
|
|
2261
3250
|
memberAliases.delete(left.text); // any assignment rebinds the name
|
|
2262
3251
|
constructedReceiverVars.delete(left.text);
|
|
@@ -2270,6 +3259,7 @@ function findCallsInCode(code, parser) {
|
|
|
2270
3259
|
localVarTypeQualifiers.delete(left.text);
|
|
2271
3260
|
localVarUnionTypes.delete(left.text);
|
|
2272
3261
|
localIterableTypes.delete(left.text);
|
|
3262
|
+
localSubscriptElements.delete(left.text);
|
|
2273
3263
|
localIterationSources.delete(left.text);
|
|
2274
3264
|
localDictValueTypes.delete(left.text);
|
|
2275
3265
|
localVarStdlibContracts.delete(left.text);
|
|
@@ -2357,6 +3347,21 @@ function findCallsInCode(code, parser) {
|
|
|
2357
3347
|
}
|
|
2358
3348
|
if (!typeNode && right?.type === 'subscript') {
|
|
2359
3349
|
const base = right.childForFieldName('value');
|
|
3350
|
+
// Declared container element (fix #359): `c = params[k]`
|
|
3351
|
+
// with `params: dict[str, Convertor]` types c. A slice
|
|
3352
|
+
// (`items[1:]`) is a container, not an element.
|
|
3353
|
+
const element = base?.type === 'identifier' && base.text !== left.text
|
|
3354
|
+
? localSubscriptElements.get(base.text) : null;
|
|
3355
|
+
const subscriptKey = right.childForFieldName('subscript');
|
|
3356
|
+
if (element && subscriptKey?.type !== 'slice' &&
|
|
3357
|
+
!declaredVarTypes.get(left.text)) {
|
|
3358
|
+
localVarTypes.set(left.text, element.type, 'annotation', element.node);
|
|
3359
|
+
if (element.qualifier) {
|
|
3360
|
+
localVarTypeQualifiers.set(left.text, element.qualifier);
|
|
3361
|
+
} else {
|
|
3362
|
+
localVarTypeQualifiers.delete(left.text);
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
2360
3365
|
if (base?.type === 'identifier' && base.text !== left.text) {
|
|
2361
3366
|
const rootType = localVarTypes.get(base.text);
|
|
2362
3367
|
if (rootType) {
|
|
@@ -2376,7 +3381,14 @@ function findCallsInCode(code, parser) {
|
|
|
2376
3381
|
localVarTypes.set(left.text, roundTripType, 'flow', node);
|
|
2377
3382
|
localVarStdlibContracts.set(left.text, 'pickle');
|
|
2378
3383
|
}
|
|
2379
|
-
|
|
3384
|
+
// A class-body assignment binds a class attribute, never a
|
|
3385
|
+
// name of the enclosing function (fix #381): `Connection =
|
|
3386
|
+
// Mock` inside a local class leaves the function's
|
|
3387
|
+
// `Connection(...)` calls alone.
|
|
3388
|
+
const classBodyAssignment = pythonEnclosingScopeType(node) === 'class_definition';
|
|
3389
|
+
if (right?.type === 'identifier' && classBodyAssignment) {
|
|
3390
|
+
// attribute binding only
|
|
3391
|
+
} else if (right?.type === 'identifier') {
|
|
2380
3392
|
aliases.set(left.text, right.text);
|
|
2381
3393
|
const classValue = !typeNode && isDirectScopeAssignment(node)
|
|
2382
3394
|
? exactConstructorInfo(right) : null;
|
|
@@ -2393,6 +3405,49 @@ function findCallsInCode(code, parser) {
|
|
|
2393
3405
|
receiver: objNode?.type === 'identifier' ? objNode.text : null,
|
|
2394
3406
|
attr: attrName.text,
|
|
2395
3407
|
});
|
|
3408
|
+
// A bound method read through a typed receiver
|
|
3409
|
+
// (`loads = registry.loads`, `run = Client().run`,
|
|
3410
|
+
// fix #400) references the method: a rename edits
|
|
3411
|
+
// the attribute, never the variable holding it.
|
|
3412
|
+
let valueType = null;
|
|
3413
|
+
let valueFields = null;
|
|
3414
|
+
if (objNode?.type === 'identifier' && !['self', 'cls'].includes(objNode.text) &&
|
|
3415
|
+
!moduleAliases.has(objNode.text)) {
|
|
3416
|
+
valueType = localVarTypes.get(objNode.text) || null;
|
|
3417
|
+
if (valueType) valueFields = localVarTypes.fields(objNode.text, valueType);
|
|
3418
|
+
} else if (objNode?.type === 'call') {
|
|
3419
|
+
const ctor = exactConstructorInfo(objNode.childForFieldName('function'));
|
|
3420
|
+
if (ctor && !ctor.qualifier) {
|
|
3421
|
+
valueType = ctor.type;
|
|
3422
|
+
valueFields = { receiverTypeSource: 'constructor',
|
|
3423
|
+
receiverTypeEvidence: { ...typeOrigin('constructor', objNode), type: ctor.type } };
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
if (valueType) {
|
|
3427
|
+
// The receiver context a call on it carries.
|
|
3428
|
+
const recv = objNode.type === 'identifier' ? objNode.text : null;
|
|
3429
|
+
calls.push({
|
|
3430
|
+
callSite: typeOrigin('call', attrName),
|
|
3431
|
+
name: attrName.text,
|
|
3432
|
+
line: attrName.startPosition.row + 1,
|
|
3433
|
+
isMethod: true,
|
|
3434
|
+
...(recv && { receiver: recv }),
|
|
3435
|
+
receiverType: valueType,
|
|
3436
|
+
...valueFields,
|
|
3437
|
+
...(recv && localVarTypeQualifiers.get(recv) && {
|
|
3438
|
+
receiverTypeQualifier: localVarTypeQualifiers.get(recv),
|
|
3439
|
+
}),
|
|
3440
|
+
...(recv && localVarStdlibContracts.has(recv) && {
|
|
3441
|
+
receiverTypeStdlibModule: localVarStdlibContracts.get(recv),
|
|
3442
|
+
}),
|
|
3443
|
+
...((!recv || constructedReceiverVars.has(recv)) && { receiverConstructed: true }),
|
|
3444
|
+
...(recv && withBindingVars.has(recv) && { receiverWithBinding: true }),
|
|
3445
|
+
...(recv && isShadowedByLocal(objNode, recv) && { receiverLocalBinding: true }),
|
|
3446
|
+
isFunctionReference: true,
|
|
3447
|
+
memberRead: true,
|
|
3448
|
+
enclosingFunction: getCurrentEnclosingFunction(),
|
|
3449
|
+
});
|
|
3450
|
+
}
|
|
2396
3451
|
}
|
|
2397
3452
|
}
|
|
2398
3453
|
// Track partial(fn, ...) aliases: fast_process = partial(process, mode='fast')
|
|
@@ -2423,6 +3478,37 @@ function findCallsInCode(code, parser) {
|
|
|
2423
3478
|
}
|
|
2424
3479
|
}
|
|
2425
3480
|
}
|
|
3481
|
+
// One-hop local alias (fix #381): a function local bound
|
|
3482
|
+
// exactly once, by `x = y` or `x = self.attr`, receives
|
|
3483
|
+
// like its initializer. A second binding anywhere in the
|
|
3484
|
+
// function (reassignment, loop target, global/nonlocal)
|
|
3485
|
+
// leaves it untyped.
|
|
3486
|
+
localFieldAliases.delete(left.text);
|
|
3487
|
+
if (!typeNode && right && right.text !== left.text &&
|
|
3488
|
+
(right.type === 'identifier' || right.type === 'attribute')) {
|
|
3489
|
+
const scope = pythonEnclosingFunctionNode(node);
|
|
3490
|
+
if (scope && pythonSingleLocalBinding(scope, left.text)) {
|
|
3491
|
+
if (right.type === 'identifier' && localVarTypes.has(right.text) &&
|
|
3492
|
+
!localVarUnionTypes.has(right.text)) {
|
|
3493
|
+
localVarTypes.set(left.text, localVarTypes.get(right.text),
|
|
3494
|
+
localVarTypes.origins.get(right.text) || 'flow');
|
|
3495
|
+
if (localVarTypeQualifiers.has(right.text)) {
|
|
3496
|
+
localVarTypeQualifiers.set(left.text, localVarTypeQualifiers.get(right.text));
|
|
3497
|
+
}
|
|
3498
|
+
if (constructedReceiverVars.has(right.text)) constructedReceiverVars.add(left.text);
|
|
3499
|
+
if (localVarStdlibContracts.has(right.text)) {
|
|
3500
|
+
localVarStdlibContracts.set(left.text, localVarStdlibContracts.get(right.text));
|
|
3501
|
+
}
|
|
3502
|
+
} else if (right.type === 'attribute') {
|
|
3503
|
+
const path = attributeReceiverPath(right);
|
|
3504
|
+
if (path && path.root !== left.text &&
|
|
3505
|
+
(path.root === 'self' || path.root === 'cls' ||
|
|
3506
|
+
pythonBindingCount(scope, path.root) <= 1)) {
|
|
3507
|
+
localFieldAliases.set(left.text, { node: right, scope: scope.id });
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
2426
3512
|
// Track non-callable assignments.
|
|
2427
3513
|
// First: explicit literal check (handles dicts-with-lambdas correctly)
|
|
2428
3514
|
if (right && isNonCallableInit(right)) {
|
|
@@ -2538,6 +3624,7 @@ function findCallsInCode(code, parser) {
|
|
|
2538
3624
|
// Direct call: foo()
|
|
2539
3625
|
const resolvedName = aliases.get(funcNode.text);
|
|
2540
3626
|
const firstArg = getFirstStringArg(node);
|
|
3627
|
+
const requestConfig = argCount > 0 ? getRequestConfig(node) : null;
|
|
2541
3628
|
calls.push({
|
|
2542
3629
|
callSite: typeOrigin('call', funcNode),
|
|
2543
3630
|
name: funcNode.text,
|
|
@@ -2551,14 +3638,22 @@ function findCallsInCode(code, parser) {
|
|
|
2551
3638
|
...(argSpread && { argSpread: true }),
|
|
2552
3639
|
enclosingFunction,
|
|
2553
3640
|
uncertain,
|
|
2554
|
-
...(
|
|
2555
|
-
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
|
|
3641
|
+
...pythonShadowFields(funcNode, funcNode.text),
|
|
3642
|
+
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp }),
|
|
3643
|
+
...(requestConfig && { requestConfig })
|
|
2556
3644
|
});
|
|
2557
3645
|
}
|
|
2558
3646
|
} else if (funcNode.type === 'attribute') {
|
|
2559
3647
|
// Method/attribute call: obj.foo() or self.attr.foo()
|
|
2560
3648
|
const attrNode = funcNode.childForFieldName('attribute');
|
|
2561
|
-
|
|
3649
|
+
let objNode = funcNode.childForFieldName('object');
|
|
3650
|
+
// fix #381: `config.load()` after `config = self.config`
|
|
3651
|
+
// receives exactly like `self.config.load()`.
|
|
3652
|
+
if (objNode?.type === 'identifier' && localFieldAliases.has(objNode.text) &&
|
|
3653
|
+
!localVarTypes.has(objNode.text)) {
|
|
3654
|
+
const alias = localFieldAliases.get(objNode.text);
|
|
3655
|
+
if (alias.scope === pythonEnclosingFunctionNode(objNode)?.id) objNode = alias.node;
|
|
3656
|
+
}
|
|
2562
3657
|
|
|
2563
3658
|
if (attrNode) {
|
|
2564
3659
|
let receiver = objNode?.type === 'identifier' ? objNode.text : undefined;
|
|
@@ -2629,6 +3724,7 @@ function findCallsInCode(code, parser) {
|
|
|
2629
3724
|
// Literal receivers carry their builtin type: {}.get() can
|
|
2630
3725
|
// never be a project class method
|
|
2631
3726
|
let subscriptReceiverType;
|
|
3727
|
+
let subscriptElementReceiver = null;
|
|
2632
3728
|
let receiverSubscriptRoot;
|
|
2633
3729
|
let receiverSubscriptRootType;
|
|
2634
3730
|
let receiverSubscriptRootTypeQualifier;
|
|
@@ -2645,6 +3741,12 @@ function findCallsInCode(code, parser) {
|
|
|
2645
3741
|
subscriptReceiverType =
|
|
2646
3742
|
localDictValueTypes.get(base.text)?.get(key);
|
|
2647
3743
|
}
|
|
3744
|
+
const element = localSubscriptElements.get(base.text);
|
|
3745
|
+
if (!subscriptReceiverType && element &&
|
|
3746
|
+
objNode.childForFieldName('subscript')?.type !== 'slice') {
|
|
3747
|
+
subscriptReceiverType = element.type;
|
|
3748
|
+
subscriptElementReceiver = element;
|
|
3749
|
+
}
|
|
2648
3750
|
}
|
|
2649
3751
|
}
|
|
2650
3752
|
if (!receiverSubscriptRoot && receiver) {
|
|
@@ -2695,7 +3797,7 @@ function findCallsInCode(code, parser) {
|
|
|
2695
3797
|
}
|
|
2696
3798
|
const receiverTypeQualifier = receiver
|
|
2697
3799
|
? localVarTypeQualifiers.get(receiver)
|
|
2698
|
-
:
|
|
3800
|
+
: subscriptElementReceiver?.qualifier;
|
|
2699
3801
|
const receiverRootType = receiverPath
|
|
2700
3802
|
? localVarTypes.get(receiverPath.root) : undefined;
|
|
2701
3803
|
const dottedReceiver = receiverPath
|
|
@@ -2710,8 +3812,11 @@ function findCallsInCode(code, parser) {
|
|
|
2710
3812
|
const receiverIsModule = !!receiver && moduleAliases.has(receiver) &&
|
|
2711
3813
|
!localVarTypes.has(receiver);
|
|
2712
3814
|
const firstArg = getFirstStringArg(node);
|
|
3815
|
+
const requestConfig = argCount > 0 ? getRequestConfig(node) : null;
|
|
2713
3816
|
const capabilityGuard = receiverCapabilityGuard(
|
|
2714
3817
|
node, objNode, attrNode.text);
|
|
3818
|
+
let awaitParent = node.parent;
|
|
3819
|
+
while (awaitParent?.type === 'parenthesized_expression') awaitParent = awaitParent.parent;
|
|
2715
3820
|
calls.push({
|
|
2716
3821
|
callSite: typeOrigin('call', attrNode),
|
|
2717
3822
|
name: attrNode.text,
|
|
@@ -2720,6 +3825,11 @@ function findCallsInCode(code, parser) {
|
|
|
2720
3825
|
// the account's ground set is keyed by the name's line
|
|
2721
3826
|
line: attrNode.startPosition.row + 1,
|
|
2722
3827
|
isMethod: true,
|
|
3828
|
+
// `await x.m()` and `f(x.m())` (fix #402): the value
|
|
3829
|
+
// is awaited or passed on; audit-async skips them.
|
|
3830
|
+
...(awaitParent?.type === 'await' && { awaited: true }),
|
|
3831
|
+
...((awaitParent?.type === 'argument_list' || awaitParent?.type === 'keyword_argument') &&
|
|
3832
|
+
{ passedAsArgument: true }),
|
|
2723
3833
|
receiver,
|
|
2724
3834
|
...(receiverType && { receiverType,
|
|
2725
3835
|
...(narrowedType || comprehensionType ? {
|
|
@@ -2733,7 +3843,11 @@ function findCallsInCode(code, parser) {
|
|
|
2733
3843
|
receiver && !localVarTypes.has(receiver) && rhsReceiver?.type === receiverType
|
|
2734
3844
|
? { receiverTypeSource: rhsReceiver.receiverTypeSource,
|
|
2735
3845
|
receiverTypeEvidence: rhsReceiver.receiverTypeEvidence } :
|
|
2736
|
-
receiver ? localVarTypes.fields(receiver, receiverType) :
|
|
3846
|
+
receiver ? localVarTypes.fields(receiver, receiverType) :
|
|
3847
|
+
subscriptElementReceiver ? {
|
|
3848
|
+
receiverTypeSource: 'annotation',
|
|
3849
|
+
receiverTypeEvidence: typeOrigin('annotation', subscriptElementReceiver.node),
|
|
3850
|
+
} : {
|
|
2737
3851
|
receiverTypeSource: 'literal', receiverTypeEvidence: typeOrigin('literal', objNode),
|
|
2738
3852
|
}),
|
|
2739
3853
|
}),
|
|
@@ -2788,7 +3902,8 @@ function findCallsInCode(code, parser) {
|
|
|
2788
3902
|
...(argSpread && { argSpread: true }),
|
|
2789
3903
|
enclosingFunction,
|
|
2790
3904
|
uncertain,
|
|
2791
|
-
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
|
|
3905
|
+
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp }),
|
|
3906
|
+
...(requestConfig && { requestConfig })
|
|
2792
3907
|
});
|
|
2793
3908
|
}
|
|
2794
3909
|
}
|
|
@@ -2820,7 +3935,7 @@ function findCallsInCode(code, parser) {
|
|
|
2820
3935
|
isMethod: false,
|
|
2821
3936
|
isFunctionReference: true,
|
|
2822
3937
|
isPotentialCallback: true,
|
|
2823
|
-
...(
|
|
3938
|
+
...pythonShadowFields(arg, arg.text),
|
|
2824
3939
|
enclosingFunction
|
|
2825
3940
|
});
|
|
2826
3941
|
}
|
|
@@ -2868,7 +3983,7 @@ function findCallsInCode(code, parser) {
|
|
|
2868
3983
|
isMethod: false,
|
|
2869
3984
|
isFunctionReference: true,
|
|
2870
3985
|
isPotentialCallback: true,
|
|
2871
|
-
...(
|
|
3986
|
+
...pythonShadowFields(val, val.text),
|
|
2872
3987
|
enclosingFunction
|
|
2873
3988
|
});
|
|
2874
3989
|
}
|
|
@@ -2925,6 +4040,13 @@ function findCallsInCode(code, parser) {
|
|
|
2925
4040
|
localDictValueTypes.set(name, values);
|
|
2926
4041
|
}
|
|
2927
4042
|
}
|
|
4043
|
+
const savedSubscriptElements = localSubscriptElementsStack.pop();
|
|
4044
|
+
if (savedSubscriptElements) {
|
|
4045
|
+
localSubscriptElements.clear();
|
|
4046
|
+
for (const [name, element] of savedSubscriptElements) {
|
|
4047
|
+
localSubscriptElements.set(name, element);
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
2928
4050
|
const savedSubscriptSources = localSubscriptSourcesStack.pop();
|
|
2929
4051
|
if (savedSubscriptSources) {
|
|
2930
4052
|
localSubscriptSources.clear();
|
|
@@ -3326,10 +4448,43 @@ function findExportsInCode(code, parser) {
|
|
|
3326
4448
|
* @param {object} [tree] - Pre-parsed tree (per-operation cache); parsed here when absent
|
|
3327
4449
|
* @returns {Array<{line: number, column: number, usageType: string}>}
|
|
3328
4450
|
*/
|
|
3329
|
-
function findUsagesInCode(code, name, parser, tree) {
|
|
4451
|
+
function findUsagesInCode(code, name, parser, tree, options = {}) {
|
|
3330
4452
|
tree = tree || parseTree(parser, code);
|
|
4453
|
+
const usages = findUsagesInTree(code, name, parser, tree, options);
|
|
4454
|
+
// Names inside string annotations that spell calls (fix #398F) are code:
|
|
4455
|
+
// the annotation's expression, parsed from the same source with the
|
|
4456
|
+
// quotes blanked (offsets unchanged).
|
|
4457
|
+
if (!/(?::|->)\s*["'][^"'\n]*\(/.test(code)) return usages;
|
|
4458
|
+
const quotes = annotationStringQuotes(tree).filter(quote =>
|
|
4459
|
+
code.slice(quote.contentStart, quote.contentEnd).includes(name));
|
|
4460
|
+
if (quotes.length === 0) return usages;
|
|
4461
|
+
const chars = code.split('');
|
|
4462
|
+
for (const quote of quotes) {
|
|
4463
|
+
chars[quote.open] = ' ';
|
|
4464
|
+
chars[quote.close] = ' ';
|
|
4465
|
+
}
|
|
4466
|
+
const masked = chars.join('');
|
|
4467
|
+
const maskedTree = parseTree(parser, masked);
|
|
4468
|
+
const ranges = [];
|
|
4469
|
+
for (const quote of quotes) {
|
|
4470
|
+
const node = maskedTree.rootNode.descendantForIndex(quote.contentStart, quote.contentEnd);
|
|
4471
|
+
if (!node || node.hasError || node.isMissing) continue;
|
|
4472
|
+
const start = maskedTree.rootNode.descendantForIndex(quote.contentStart).startPosition;
|
|
4473
|
+
ranges.push({ row: start.row, from: start.column, to: start.column + (quote.contentEnd - quote.contentStart) });
|
|
4474
|
+
}
|
|
4475
|
+
if (ranges.length === 0) return usages;
|
|
4476
|
+
const extra = findUsagesInTree(masked, name, parser, maskedTree, options).filter(usage =>
|
|
4477
|
+
ranges.some(range => usage.line === range.row + 1 && usage.column >= range.from && usage.column < range.to));
|
|
4478
|
+
if (extra.length === 0) return usages;
|
|
4479
|
+
return [...usages, ...extra.map(usage => ({ ...usage, stringAnnotation: true }))]
|
|
4480
|
+
.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
4481
|
+
}
|
|
4482
|
+
|
|
4483
|
+
function findUsagesInTree(code, name, parser, tree, options = {}) {
|
|
3331
4484
|
const usages = [];
|
|
3332
4485
|
const moduleAliases = pythonModuleAliases(tree);
|
|
4486
|
+
// Lexical scope verdicts (fix #392) only for refactoring internals.
|
|
4487
|
+
const scopeMemo = options.lexicalScopes ? new Map() : null;
|
|
3333
4488
|
|
|
3334
4489
|
visitNameNodes(tree, code, name, (node) => {
|
|
3335
4490
|
// Only look for identifiers with the matching name
|
|
@@ -3372,11 +4527,19 @@ function findUsagesInCode(code, name, parser, tree) {
|
|
|
3372
4527
|
sameNode(parent.childForFieldName('name'), node)) {
|
|
3373
4528
|
usageType = 'definition';
|
|
3374
4529
|
}
|
|
3375
|
-
// Definition: parameter
|
|
4530
|
+
// Definition: parameter name. A default VALUE is a reference
|
|
4531
|
+
// evaluated in the enclosing scope (fix #392).
|
|
3376
4532
|
else if (parent.type === 'parameter' ||
|
|
3377
|
-
parent.type === '
|
|
3378
|
-
parent.type === '
|
|
3379
|
-
parent.type === '
|
|
4533
|
+
parent.type === 'parameters' ||
|
|
4534
|
+
parent.type === 'lambda_parameters' ||
|
|
4535
|
+
((parent.type === 'default_parameter' ||
|
|
4536
|
+
parent.type === 'typed_default_parameter') &&
|
|
4537
|
+
sameNode(parent.childForFieldName('name'), node)) ||
|
|
4538
|
+
(parent.type === 'typed_parameter' &&
|
|
4539
|
+
sameNode(parent.namedChild(0), node)) ||
|
|
4540
|
+
((parent.type === 'list_splat_pattern' ||
|
|
4541
|
+
parent.type === 'dictionary_splat_pattern') &&
|
|
4542
|
+
/^(parameters|lambda_parameters|typed_parameter)$/.test(parent.parent?.type || ''))) {
|
|
3380
4543
|
usageType = 'definition';
|
|
3381
4544
|
}
|
|
3382
4545
|
// Definition: assignment target (x = ...)
|
|
@@ -3439,7 +4602,11 @@ function findUsagesInCode(code, name, parser, tree) {
|
|
|
3439
4602
|
}
|
|
3440
4603
|
}
|
|
3441
4604
|
|
|
3442
|
-
|
|
4605
|
+
// Where a bare reference resolves (fix #392): plan edits it only
|
|
4606
|
+
// when the language's scoping proves it names the renamed binding.
|
|
4607
|
+
const scope = scopeMemo && usageType === 'reference'
|
|
4608
|
+
? scopeFields(referenceScope(node, 'python', scopeMemo)) : null;
|
|
4609
|
+
usages.push({ line, column, usageType, ...scope });
|
|
3443
4610
|
return true;
|
|
3444
4611
|
});
|
|
3445
4612
|
|
|
@@ -3454,7 +4621,7 @@ function findUsagesInCode(code, name, parser, tree) {
|
|
|
3454
4621
|
* @returns {Map<string, Map<string, string>>} className -> (attrName -> typeName)
|
|
3455
4622
|
*/
|
|
3456
4623
|
function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
3457
|
-
const tree = parseTree(parser, code);
|
|
4624
|
+
const tree = options.tree || parseTree(parser, code);
|
|
3458
4625
|
const result = new Map(); // className -> Map(attrName -> typeName)
|
|
3459
4626
|
const explicitContracts = explicitInstanceFieldContracts(tree, parser);
|
|
3460
4627
|
|
|
@@ -3504,8 +4671,10 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
3504
4671
|
return valueType === narrowedTypes[0] ? valueType : null;
|
|
3505
4672
|
};
|
|
3506
4673
|
|
|
3507
|
-
|
|
3508
|
-
if (node.type !== 'class_definition')
|
|
4674
|
+
traverseTree(tree.rootNode, (node) => {
|
|
4675
|
+
if (node.type !== 'class_definition') {
|
|
4676
|
+
return node.type !== 'expression_statement' && !FIELD_CONTRACT_VALUE_NODES.has(node.type);
|
|
4677
|
+
}
|
|
3509
4678
|
|
|
3510
4679
|
const classNameNode = node.childForFieldName('name');
|
|
3511
4680
|
if (!classNameNode) return true;
|
|
@@ -3519,12 +4688,22 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
3519
4688
|
Object.defineProperty(attrTypes, 'origins', { value: attrOrigins });
|
|
3520
4689
|
const externalFlows = new Map();
|
|
3521
4690
|
Object.defineProperty(attrTypes, 'externalFlows', { value: externalFlows });
|
|
4691
|
+
// Lines of the writes that typed each field (fix #381): a class name
|
|
4692
|
+
// resolves where it is written, so a function-local class assigned
|
|
4693
|
+
// to self.x names that local class, not a same-name module class.
|
|
4694
|
+
const typeWrites = new Map();
|
|
4695
|
+
const writeLines = new Map();
|
|
4696
|
+
Object.defineProperty(attrTypes, 'writeLines', { value: writeLines });
|
|
4697
|
+
const noteTypeWrite = (field, type, node) => {
|
|
4698
|
+
if (!typeWrites.has(field)) typeWrites.set(field, []);
|
|
4699
|
+
typeWrites.get(field).push({ type, line: node.startPosition.row + 1 });
|
|
4700
|
+
};
|
|
3522
4701
|
for (const [field, contract] of explicitContracts.get(className) || []) {
|
|
3523
4702
|
if (contract.type) attrTypes.set(field, contract.type);
|
|
3524
4703
|
}
|
|
3525
4704
|
const methodReturns = new Map();
|
|
3526
|
-
|
|
3527
|
-
|
|
4705
|
+
const memberDefs = pythonClassBodyDefs(body);
|
|
4706
|
+
for (let member of memberDefs) {
|
|
3528
4707
|
if (member.type === 'decorated_definition') {
|
|
3529
4708
|
member = Array.from({ length: member.namedChildCount },
|
|
3530
4709
|
(_, index) => member.namedChild(index))
|
|
@@ -3581,8 +4760,7 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
3581
4760
|
}
|
|
3582
4761
|
|
|
3583
4762
|
// Scan __init__ for self.X = ClassName(...) assignments
|
|
3584
|
-
for (let
|
|
3585
|
-
let child = body.child(i);
|
|
4763
|
+
for (let child of memberDefs) {
|
|
3586
4764
|
// Handle decorated_definition wrapper
|
|
3587
4765
|
if (child.type === 'decorated_definition') {
|
|
3588
4766
|
for (let j = 0; j < child.childCount; j++) {
|
|
@@ -3662,6 +4840,7 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
3662
4840
|
extractConstructorName(rhs);
|
|
3663
4841
|
if (typeName) {
|
|
3664
4842
|
attrTypes.set(attrName, typeName);
|
|
4843
|
+
noteTypeWrite(attrName, typeName, assign);
|
|
3665
4844
|
} else if (rhs.type === 'call') {
|
|
3666
4845
|
const fn = rhs.childForFieldName('function');
|
|
3667
4846
|
if (fn?.type === 'attribute') {
|
|
@@ -3678,6 +4857,7 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
3678
4857
|
} else if (rhs.type === 'identifier' && paramTypes.has(rhs.text)) {
|
|
3679
4858
|
// self.X = param where param has type annotation
|
|
3680
4859
|
attrTypes.set(attrName, paramTypes.get(rhs.text));
|
|
4860
|
+
noteTypeWrite(attrName, paramTypes.get(rhs.text), assign);
|
|
3681
4861
|
}
|
|
3682
4862
|
|
|
3683
4863
|
return true;
|
|
@@ -3694,8 +4874,7 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
3694
4874
|
const runtimeFieldWrites = new Map();
|
|
3695
4875
|
const externalFieldWrites = new Map();
|
|
3696
4876
|
const invalidRuntimeFields = new Set();
|
|
3697
|
-
for (let
|
|
3698
|
-
let member = body.namedChild(i);
|
|
4877
|
+
for (let member of memberDefs) {
|
|
3699
4878
|
if (member.type === 'decorated_definition') {
|
|
3700
4879
|
member = Array.from({ length: member.namedChildCount },
|
|
3701
4880
|
(_, index) => member.namedChild(index))
|
|
@@ -3715,7 +4894,7 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
3715
4894
|
if (!field) return true;
|
|
3716
4895
|
let enclosing = assignment.parent;
|
|
3717
4896
|
while (enclosing && !['function_definition', 'lambda', 'class_definition'].includes(enclosing.type)) enclosing = enclosing.parent;
|
|
3718
|
-
if (assignment.type !== 'assignment' || enclosing
|
|
4897
|
+
if (assignment.type !== 'assignment' || !sameNode(enclosing, member)) {
|
|
3719
4898
|
invalidRuntimeFields.add(field);
|
|
3720
4899
|
return true;
|
|
3721
4900
|
}
|
|
@@ -3772,6 +4951,9 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
3772
4951
|
}
|
|
3773
4952
|
if (!runtimeFieldTypes.has(field)) runtimeFieldTypes.set(field, new Set());
|
|
3774
4953
|
runtimeFieldTypes.get(field).add(runtimeType);
|
|
4954
|
+
if (directConstructor && runtimeType === directConstructor) {
|
|
4955
|
+
noteTypeWrite(field, runtimeType, assignment);
|
|
4956
|
+
}
|
|
3775
4957
|
if (!runtimeFieldWrites.has(field)) runtimeFieldWrites.set(field, []);
|
|
3776
4958
|
runtimeFieldWrites.get(field).push({ type: runtimeType,
|
|
3777
4959
|
assignment: typeOrigin('field', assignment), expression: typeOrigin('field', right),
|
|
@@ -3797,6 +4979,11 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
3797
4979
|
}
|
|
3798
4980
|
}
|
|
3799
4981
|
|
|
4982
|
+
for (const [field, writes] of typeWrites) {
|
|
4983
|
+
const lines = writes.filter(write => write.type === attrTypes.get(field))
|
|
4984
|
+
.map(write => write.line);
|
|
4985
|
+
if (lines.length > 0) writeLines.set(field, [...new Set(lines)].sort((a, b) => a - b));
|
|
4986
|
+
}
|
|
3800
4987
|
if (attrTypes.size > 0 || externalFlows.size > 0) {
|
|
3801
4988
|
result.set(className, attrTypes);
|
|
3802
4989
|
}
|
|
@@ -3807,6 +4994,119 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
3807
4994
|
return result;
|
|
3808
4995
|
}
|
|
3809
4996
|
|
|
4997
|
+
const PY_STORE_CONTAINERS = new Set(['pattern_list', 'tuple_pattern', 'list_pattern', 'tuple', 'list',
|
|
4998
|
+
'parenthesized_expression', 'list_splat_pattern', 'list_splat', 'expression_list']);
|
|
4999
|
+
|
|
5000
|
+
/** Whether an attribute node is written (assigned, deleted, a loop or
|
|
5001
|
+
* `with ... as` target), not read. */
|
|
5002
|
+
function isPythonStoreTarget(node) {
|
|
5003
|
+
let target = node;
|
|
5004
|
+
while (target.parent && PY_STORE_CONTAINERS.has(target.parent.type)) target = target.parent;
|
|
5005
|
+
const parent = target.parent;
|
|
5006
|
+
if (!parent) return false;
|
|
5007
|
+
switch (parent.type) {
|
|
5008
|
+
case 'assignment':
|
|
5009
|
+
case 'augmented_assignment':
|
|
5010
|
+
case 'for_statement':
|
|
5011
|
+
case 'for_in_clause':
|
|
5012
|
+
return sameNode(parent.childForFieldName('left'), target);
|
|
5013
|
+
case 'as_pattern_target':
|
|
5014
|
+
case 'delete_statement':
|
|
5015
|
+
return true;
|
|
5016
|
+
default:
|
|
5017
|
+
return false;
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
5020
|
+
|
|
5021
|
+
/**
|
|
5022
|
+
* What a class body does to its instance fields (fix #402): the class
|
|
5023
|
+
* members it declares (defs, class-level assignments, nested classes,
|
|
5024
|
+
* through compound statements), the fields its methods write on the
|
|
5025
|
+
* instance or class (`self.x = ..`, `cls.x = ..`, augmented, deleted, loop
|
|
5026
|
+
* and `with ... as` targets, nested functions included), and whether it
|
|
5027
|
+
* writes fields dynamically (`setattr(self, ..)`, `self.__dict__`,
|
|
5028
|
+
* `vars(self)`). Null when no class is declared at `line`.
|
|
5029
|
+
*/
|
|
5030
|
+
function findClassFieldFacts(tree, line, name) {
|
|
5031
|
+
if (!tree) return null;
|
|
5032
|
+
const row = line - 1;
|
|
5033
|
+
let classNode = null;
|
|
5034
|
+
const stack = [tree.rootNode];
|
|
5035
|
+
while (stack.length > 0 && !classNode) {
|
|
5036
|
+
const node = stack.pop();
|
|
5037
|
+
if (node.startPosition.row > row || node.endPosition.row < row) continue;
|
|
5038
|
+
if (node.type === 'class_definition') {
|
|
5039
|
+
const nameNode = node.childForFieldName('name');
|
|
5040
|
+
const start = node.parent?.type === 'decorated_definition' ? node.parent : node;
|
|
5041
|
+
if (nameNode?.text === name && (start.startPosition.row === row || node.startPosition.row === row ||
|
|
5042
|
+
nameNode.startPosition.row === row)) { classNode = node; break; }
|
|
5043
|
+
}
|
|
5044
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) stack.push(node.namedChild(i));
|
|
5045
|
+
}
|
|
5046
|
+
const body = classNode?.childForFieldName('body');
|
|
5047
|
+
if (!body) return null;
|
|
5048
|
+
const members = new Set();
|
|
5049
|
+
const receivers = new Set(['self', 'cls']);
|
|
5050
|
+
const addTargets = target => {
|
|
5051
|
+
if (!target) return;
|
|
5052
|
+
if (target.type === 'identifier') { members.add(target.text); return; }
|
|
5053
|
+
if (PY_STORE_CONTAINERS.has(target.type)) {
|
|
5054
|
+
for (let i = 0; i < target.namedChildCount; i++) addTargets(target.namedChild(i));
|
|
5055
|
+
}
|
|
5056
|
+
};
|
|
5057
|
+
const visitBody = container => {
|
|
5058
|
+
for (let i = 0; i < container.namedChildCount; i++) {
|
|
5059
|
+
const child = container.namedChild(i);
|
|
5060
|
+
const definition = child.type === 'decorated_definition' ? child.childForFieldName('definition') : child;
|
|
5061
|
+
if (definition?.type === 'function_definition' || definition?.type === 'class_definition') {
|
|
5062
|
+
const declared = definition.childForFieldName('name')?.text;
|
|
5063
|
+
if (declared) members.add(declared);
|
|
5064
|
+
if (definition.type === 'function_definition') {
|
|
5065
|
+
const params = definition.childForFieldName('parameters');
|
|
5066
|
+
const first = params?.namedChild(0);
|
|
5067
|
+
const firstName = first?.type === 'identifier' ? first.text
|
|
5068
|
+
: (first?.childForFieldName?.('name') || first?.namedChild(0))?.text;
|
|
5069
|
+
if (firstName) receivers.add(firstName);
|
|
5070
|
+
}
|
|
5071
|
+
continue;
|
|
5072
|
+
}
|
|
5073
|
+
if (child.type === 'expression_statement') {
|
|
5074
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
5075
|
+
const expression = child.namedChild(j);
|
|
5076
|
+
if (expression.type === 'assignment' || expression.type === 'augmented_assignment') {
|
|
5077
|
+
addTargets(expression.childForFieldName('left'));
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
5080
|
+
continue;
|
|
5081
|
+
}
|
|
5082
|
+
if (PY_CLASS_BODY_COMPOUNDS.has(child.type)) visitBody(child);
|
|
5083
|
+
}
|
|
5084
|
+
};
|
|
5085
|
+
visitBody(body);
|
|
5086
|
+
const written = new Set();
|
|
5087
|
+
let dynamic = false;
|
|
5088
|
+
const pending = [body];
|
|
5089
|
+
while (pending.length > 0) {
|
|
5090
|
+
const node = pending.pop();
|
|
5091
|
+
if (node.type === 'class_definition') continue;
|
|
5092
|
+
if (node.type === 'attribute') {
|
|
5093
|
+
const object = node.childForFieldName('object');
|
|
5094
|
+
const attribute = node.childForFieldName('attribute');
|
|
5095
|
+
if (object?.type === 'identifier' && receivers.has(object.text) && attribute) {
|
|
5096
|
+
if (attribute.text === '__dict__') dynamic = true;
|
|
5097
|
+
else if (isPythonStoreTarget(node)) written.add(attribute.text);
|
|
5098
|
+
}
|
|
5099
|
+
} else if (node.type === 'call') {
|
|
5100
|
+
const fn = node.childForFieldName('function');
|
|
5101
|
+
const first = node.childForFieldName('arguments')?.namedChild(0);
|
|
5102
|
+
if (fn?.type === 'identifier' && (fn.text === 'setattr' || fn.text === 'vars' ||
|
|
5103
|
+
fn.text === 'delattr') && first?.type === 'identifier' && receivers.has(first.text)) dynamic = true;
|
|
5104
|
+
}
|
|
5105
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) pending.push(node.namedChild(i));
|
|
5106
|
+
}
|
|
5107
|
+
return { members, written, dynamic };
|
|
5108
|
+
}
|
|
5109
|
+
|
|
3810
5110
|
/**
|
|
3811
5111
|
* Extract constructor class name from an expression node.
|
|
3812
5112
|
* Handles: ClassName(...), param or ClassName(...), (param or ClassName(...)),
|
|
@@ -3899,8 +5199,8 @@ function isEntryPoint(symbol) {
|
|
|
3899
5199
|
/** Module-level pytest declarations. Decorator ownership is resolved by the
|
|
3900
5200
|
* index; a decorator merely spelled `fixture` is not sufficient evidence.
|
|
3901
5201
|
*/
|
|
3902
|
-
function findPythonModuleEvidence(code, parser) {
|
|
3903
|
-
const tree = parseTree(parser, code), references = [], wildcards = [];
|
|
5202
|
+
function findPythonModuleEvidence(code, parser, parsedTree) {
|
|
5203
|
+
const tree = parsedTree || parseTree(parser, code), references = [], wildcards = [];
|
|
3904
5204
|
traverseTree(tree.rootNode, node => {
|
|
3905
5205
|
if (node.type === 'identifier' && node.text === '__all__') references.push(node);
|
|
3906
5206
|
if (node.type === 'import_from_statement' && node.namedChildren.some(child => child.type === 'wildcard_import')) {
|
|
@@ -3913,7 +5213,7 @@ function findPythonModuleEvidence(code, parser) {
|
|
|
3913
5213
|
let exports = null;
|
|
3914
5214
|
if (references.length === 1) {
|
|
3915
5215
|
const name = references[0], assignment = name.parent, value = assignment.childForFieldName('right');
|
|
3916
|
-
if (assignment.type === 'assignment' && assignment.childForFieldName('left')
|
|
5216
|
+
if (assignment.type === 'assignment' && sameNode(assignment.childForFieldName('left'), name) &&
|
|
3917
5217
|
assignment.parent?.parent?.type === 'module' && ['list', 'tuple'].includes(value?.type)) {
|
|
3918
5218
|
const literals = value.namedChildren.map(item => {
|
|
3919
5219
|
const content = item.type === 'string' && item.namedChildren.find(child => child.type === 'string_content');
|
|
@@ -3927,8 +5227,8 @@ function findPythonModuleEvidence(code, parser) {
|
|
|
3927
5227
|
return { exports, wildcards };
|
|
3928
5228
|
}
|
|
3929
5229
|
|
|
3930
|
-
function findPytestFunctions(code, parser) {
|
|
3931
|
-
const tree = parseTree(parser, code);
|
|
5230
|
+
function findPytestFunctions(code, parser, parsedTree) {
|
|
5231
|
+
const tree = parsedTree || parseTree(parser, code);
|
|
3932
5232
|
const pathOf = node => {
|
|
3933
5233
|
if (node?.type === 'identifier') return [node.text];
|
|
3934
5234
|
if (node?.type !== 'attribute') return null;
|
|
@@ -4015,7 +5315,191 @@ function getBuiltinFieldType(moduleName, fieldName) {
|
|
|
4015
5315
|
return PY_BUILTIN_FIELD_TYPES[`${moduleName}.${fieldName}`] || null;
|
|
4016
5316
|
}
|
|
4017
5317
|
|
|
5318
|
+
/**
|
|
5319
|
+
* Other bindings of `name` in the scope that holds the definition on
|
|
5320
|
+
* `defLine` (fix #376). Python binds names at run time: every `def`,
|
|
5321
|
+
* `class`, assignment target and import of the name in one scope (module,
|
|
5322
|
+
* class body or function body, through if/elif/else, try/except/finally,
|
|
5323
|
+
* with and loop blocks) is the SAME variable, so version-conditional
|
|
5324
|
+
* definitions (`if sys.version_info >= ...: f = lib.f else: def f(...)`),
|
|
5325
|
+
* try/except import fallbacks and class-body rebindings are alternatives a
|
|
5326
|
+
* rename must change together. In a class body, bare reads of the name
|
|
5327
|
+
* (`close = release`, `@prop.setter`) bind to the class-scope variable and
|
|
5328
|
+
* are references to the member.
|
|
5329
|
+
* @returns {{line, column, kind: 'definition'|'assignment'|'import'|'import-name'|'reference'}[]}
|
|
5330
|
+
* column is the byte column of the name token; 'import-name' is an
|
|
5331
|
+
* unaliased `from m import name` (its alias must be added, not renamed).
|
|
5332
|
+
*/
|
|
5333
|
+
function findScopeAlternativeBindings(tree, defLine, name) {
|
|
5334
|
+
if (!tree || !name) return [];
|
|
5335
|
+
const row = defLine - 1;
|
|
5336
|
+
let defNode = null;
|
|
5337
|
+
const stack = [tree.rootNode];
|
|
5338
|
+
while (stack.length > 0 && !defNode) {
|
|
5339
|
+
const node = stack.pop();
|
|
5340
|
+
if (node.startPosition.row > row || node.endPosition.row < row) continue;
|
|
5341
|
+
if ((node.type === 'function_definition' || node.type === 'class_definition')) {
|
|
5342
|
+
const nameNode = node.childForFieldName('name');
|
|
5343
|
+
if (nameNode && nameNode.text === name && nameNode.startPosition.row === row) {
|
|
5344
|
+
defNode = node;
|
|
5345
|
+
break;
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5348
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) stack.push(node.namedChild(i));
|
|
5349
|
+
}
|
|
5350
|
+
if (!defNode) return [];
|
|
5351
|
+
let container = null;
|
|
5352
|
+
for (let cur = defNode; cur.parent; cur = cur.parent) {
|
|
5353
|
+
const parent = cur.parent;
|
|
5354
|
+
if (parent.type === 'module') { container = parent; break; }
|
|
5355
|
+
if (parent.type === 'block' && parent.parent &&
|
|
5356
|
+
(parent.parent.type === 'class_definition' ||
|
|
5357
|
+
parent.parent.type === 'function_definition')) {
|
|
5358
|
+
container = parent;
|
|
5359
|
+
break;
|
|
5360
|
+
}
|
|
5361
|
+
}
|
|
5362
|
+
if (!container) return [];
|
|
5363
|
+
const classBody = container.parent?.type === 'class_definition';
|
|
5364
|
+
const out = [];
|
|
5365
|
+
const push = (node, kind) => out.push({
|
|
5366
|
+
line: node.startPosition.row + 1, column: node.startPosition.column, kind,
|
|
5367
|
+
});
|
|
5368
|
+
const references = expr => {
|
|
5369
|
+
if (!classBody || !expr) return;
|
|
5370
|
+
const refStack = [expr];
|
|
5371
|
+
while (refStack.length > 0) {
|
|
5372
|
+
const node = refStack.pop();
|
|
5373
|
+
if (node.type === 'lambda' || node.type === 'function_definition' ||
|
|
5374
|
+
node.type === 'class_definition' || node.type.endsWith('comprehension') ||
|
|
5375
|
+
node.type === 'generator_expression') continue;
|
|
5376
|
+
if (node.type === 'identifier') {
|
|
5377
|
+
if (node.text === name) push(node, 'reference');
|
|
5378
|
+
continue;
|
|
5379
|
+
}
|
|
5380
|
+
if (node.type === 'attribute') {
|
|
5381
|
+
refStack.push(node.childForFieldName('object'));
|
|
5382
|
+
continue;
|
|
5383
|
+
}
|
|
5384
|
+
if (node.type === 'keyword_argument') {
|
|
5385
|
+
refStack.push(node.childForFieldName('value'));
|
|
5386
|
+
continue;
|
|
5387
|
+
}
|
|
5388
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
5389
|
+
const child = node.namedChild(i);
|
|
5390
|
+
if (child) refStack.push(child);
|
|
5391
|
+
}
|
|
5392
|
+
}
|
|
5393
|
+
};
|
|
5394
|
+
const targets = (target, kind) => {
|
|
5395
|
+
if (!target) return;
|
|
5396
|
+
if (target.type === 'identifier') {
|
|
5397
|
+
if (target.text === name) push(target, kind);
|
|
5398
|
+
return;
|
|
5399
|
+
}
|
|
5400
|
+
if (['pattern_list', 'tuple_pattern', 'list_pattern', 'expression_list', 'tuple',
|
|
5401
|
+
'list', 'parenthesized_expression'].includes(target.type)) {
|
|
5402
|
+
for (let i = 0; i < target.namedChildCount; i++) targets(target.namedChild(i), kind);
|
|
5403
|
+
return;
|
|
5404
|
+
}
|
|
5405
|
+
references(target);
|
|
5406
|
+
};
|
|
5407
|
+
const visit = node => {
|
|
5408
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
5409
|
+
const child = node.namedChild(i);
|
|
5410
|
+
if (!child) continue;
|
|
5411
|
+
switch (child.type) {
|
|
5412
|
+
case 'function_definition':
|
|
5413
|
+
case 'class_definition': {
|
|
5414
|
+
const nameNode = child.childForFieldName('name');
|
|
5415
|
+
if (nameNode && nameNode.text === name && child.startIndex !== defNode.startIndex) {
|
|
5416
|
+
push(nameNode, 'definition');
|
|
5417
|
+
}
|
|
5418
|
+
// Evaluated in this scope: base classes, return and
|
|
5419
|
+
// parameter annotations, parameter defaults - never the
|
|
5420
|
+
// parameter names themselves.
|
|
5421
|
+
references(child.childForFieldName('superclasses'));
|
|
5422
|
+
references(child.childForFieldName('return_type'));
|
|
5423
|
+
const params = child.childForFieldName('parameters');
|
|
5424
|
+
for (let j = 0; j < (params?.namedChildCount || 0); j++) {
|
|
5425
|
+
const param = params.namedChild(j);
|
|
5426
|
+
references(param.childForFieldName?.('type'));
|
|
5427
|
+
references(param.childForFieldName?.('value'));
|
|
5428
|
+
}
|
|
5429
|
+
break;
|
|
5430
|
+
}
|
|
5431
|
+
case 'decorated_definition':
|
|
5432
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
5433
|
+
const part = child.namedChild(j);
|
|
5434
|
+
if (part.type === 'decorator') references(part);
|
|
5435
|
+
}
|
|
5436
|
+
visit({ namedChildCount: 1, namedChild: () => child.childForFieldName('definition') });
|
|
5437
|
+
break;
|
|
5438
|
+
case 'expression_statement':
|
|
5439
|
+
visit(child);
|
|
5440
|
+
break;
|
|
5441
|
+
case 'assignment':
|
|
5442
|
+
case 'augmented_assignment': {
|
|
5443
|
+
targets(child.childForFieldName('left'), 'assignment');
|
|
5444
|
+
references(child.childForFieldName('type'));
|
|
5445
|
+
const right = child.childForFieldName('right');
|
|
5446
|
+
if (right && right.type === 'assignment') visit({ namedChildCount: 1, namedChild: () => right });
|
|
5447
|
+
else references(right);
|
|
5448
|
+
break;
|
|
5449
|
+
}
|
|
5450
|
+
case 'import_from_statement':
|
|
5451
|
+
case 'import_statement':
|
|
5452
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
5453
|
+
const item = child.namedChild(j);
|
|
5454
|
+
if (item.type === 'aliased_import') {
|
|
5455
|
+
const alias = item.childForFieldName('alias');
|
|
5456
|
+
if (alias && alias.text === name) push(alias, 'import');
|
|
5457
|
+
} else if (item.type === 'dotted_name' &&
|
|
5458
|
+
(child.type === 'import_statement' ||
|
|
5459
|
+
!sameNode(item, child.childForFieldName('module_name')))) {
|
|
5460
|
+
const parts = item.namedChildren || [];
|
|
5461
|
+
if (parts.length === 1 && parts[0].text === name) {
|
|
5462
|
+
push(parts[0], 'import-name');
|
|
5463
|
+
}
|
|
5464
|
+
}
|
|
5465
|
+
}
|
|
5466
|
+
break;
|
|
5467
|
+
case 'if_statement':
|
|
5468
|
+
case 'elif_clause':
|
|
5469
|
+
case 'else_clause':
|
|
5470
|
+
case 'try_statement':
|
|
5471
|
+
case 'except_clause':
|
|
5472
|
+
case 'except_group_clause':
|
|
5473
|
+
case 'finally_clause':
|
|
5474
|
+
case 'with_statement':
|
|
5475
|
+
case 'for_statement':
|
|
5476
|
+
case 'while_statement':
|
|
5477
|
+
case 'block':
|
|
5478
|
+
case 'match_statement':
|
|
5479
|
+
case 'case_clause':
|
|
5480
|
+
if (child.type === 'for_statement') {
|
|
5481
|
+
targets(child.childForFieldName('left'), 'assignment');
|
|
5482
|
+
references(child.childForFieldName('right'));
|
|
5483
|
+
for (const field of ['body', 'alternative']) {
|
|
5484
|
+
const part = child.childForFieldName(field);
|
|
5485
|
+
if (part) visit({ namedChildCount: 1, namedChild: () => part });
|
|
5486
|
+
}
|
|
5487
|
+
break;
|
|
5488
|
+
}
|
|
5489
|
+
visit(child);
|
|
5490
|
+
break;
|
|
5491
|
+
default:
|
|
5492
|
+
if (child.type !== 'comment' && child.type !== 'pass_statement') references(child);
|
|
5493
|
+
}
|
|
5494
|
+
}
|
|
5495
|
+
};
|
|
5496
|
+
visit(container);
|
|
5497
|
+
out.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
5498
|
+
return out;
|
|
5499
|
+
}
|
|
5500
|
+
|
|
4018
5501
|
module.exports = {
|
|
5502
|
+
findScopeAlternativeBindings,
|
|
4019
5503
|
findPythonModuleEvidence,
|
|
4020
5504
|
findFunctions,
|
|
4021
5505
|
findClasses,
|
|
@@ -4025,6 +5509,7 @@ module.exports = {
|
|
|
4025
5509
|
findExportsInCode,
|
|
4026
5510
|
findUsagesInCode,
|
|
4027
5511
|
findInstanceAttributeTypes,
|
|
5512
|
+
findClassFieldFacts,
|
|
4028
5513
|
getBuiltinCallReturnType,
|
|
4029
5514
|
getBuiltinFieldType,
|
|
4030
5515
|
findPytestFunctions,
|