ucn 5.5.1 → 5.5.2
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 +35 -9
- package/.claude/skills/ucn/references/commands.md +38 -16
- package/.claude/skills/ucn/references/resolution-rules.md +126 -20
- package/.claude/skills/ucn/references/trust-contract.md +5 -3
- package/README.md +30 -21
- package/cli/index.js +1 -1
- package/core/accessors.js +341 -54
- package/core/account.js +74 -20
- package/core/analysis.js +257 -125
- package/core/ast-analysis.js +12 -3
- package/core/brief.js +8 -4
- package/core/build-worker.js +2 -1
- package/core/cache.js +137 -6
- package/core/call-name-cache.js +134 -0
- package/core/callable-scope.js +21 -0
- package/core/callers.js +4726 -940
- package/core/check.js +22 -5
- package/core/class-identity.js +399 -12
- package/core/constructor-calls.js +831 -0
- package/core/constructor-hierarchy.js +147 -0
- package/core/constructor-protocol.js +196 -0
- package/core/contract-membership.js +346 -140
- package/core/cpp-conversion-constraints.js +123 -0
- package/core/cpp-conversion-syntax.js +216 -0
- package/core/cpp-conversions.js +414 -0
- package/core/cpp-scope.js +245 -44
- package/core/deadcode.js +35 -15
- package/core/decorator-binding.js +137 -3
- package/core/enum-reference-syntax.js +134 -0
- package/core/execute.js +17 -1
- package/core/external-macros.js +42 -10
- package/core/go-packages.js +171 -0
- package/core/graph-build.js +2 -0
- package/core/graph.js +75 -59
- package/core/ignore-rules.js +2 -1
- package/core/imports.js +145 -19
- package/core/index-ir.js +41 -4
- package/core/ir.js +27 -6
- package/core/macro-expansion.js +38 -2
- package/core/out-of-class-members.js +182 -0
- package/core/output/analysis-ext.js +4 -2
- package/core/output/analysis.js +89 -20
- package/core/output/graph.js +12 -3
- package/core/output/lines.js +4 -3
- package/core/output/public.js +1 -0
- package/core/output/reporting.js +6 -1
- package/core/output/shared.js +2 -0
- package/core/output/tracing.js +28 -22
- package/core/output-budget.js +2 -2
- package/core/parallel-build.js +52 -1
- package/core/project.js +217 -112
- package/core/provenance-constructor-base.js +161 -0
- package/core/provenance-facts.js +257 -27
- package/core/provenance-overload.js +458 -54
- package/core/provenance-type.js +487 -0
- package/core/provenance.js +234 -15
- package/core/public-command.js +17 -2
- package/core/query-bodies.js +159 -0
- package/core/reflection.js +255 -2
- package/core/rename-token-syntax.js +111 -0
- package/core/reporting.js +13 -7
- package/core/rust-macro-expansion.js +151 -19
- package/core/rust-modules.js +1 -1
- package/core/rust-names.js +607 -0
- package/core/search.js +70 -17
- package/core/shared.js +98 -1
- package/core/source-memo.js +201 -0
- package/core/text-usage-syntax.js +38 -0
- package/core/tracing.js +7 -1
- package/core/type-denotation.js +124 -37
- package/core/type-references.js +615 -246
- package/core/value-references.js +2777 -0
- package/core/verify.js +1097 -281
- package/languages/adapter.js +28 -4
- package/languages/c-family.js +1118 -164
- package/languages/constructor-bases.js +105 -0
- package/languages/cpp-platform.js +26 -0
- package/languages/cpp-site-syntax.js +83 -0
- package/languages/csharp-scope.js +439 -0
- package/languages/csharp.js +428 -68
- package/languages/go.js +385 -207
- package/languages/html.js +4 -2
- package/languages/index.js +397 -2
- package/languages/java-platform.js +226 -0
- package/languages/java-unicode.js +269 -0
- package/languages/java.js +461 -117
- package/languages/javascript.js +418 -66
- package/languages/lexical-scope.js +190 -23
- package/languages/python.js +404 -54
- package/languages/rust.js +510 -82
- package/languages/utils.js +67 -8
- package/mcp/server.js +8 -2
- package/package.json +2 -2
package/languages/python.js
CHANGED
|
@@ -76,6 +76,94 @@ function pythonClassBodyDefs(body) {
|
|
|
76
76
|
return out;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/** The nodes an assignment target binds, through tuple and list targets:
|
|
80
|
+
* identifiers (`kind` 'identifier') or attributes ('attribute'). */
|
|
81
|
+
function pythonTargetNames(target, kind) {
|
|
82
|
+
const out = [];
|
|
83
|
+
const visit = node => {
|
|
84
|
+
if (!node) return;
|
|
85
|
+
if (node.type === kind) { out.push(node); return; }
|
|
86
|
+
if (['pattern_list', 'tuple_pattern', 'list_pattern', 'expression_list', 'tuple', 'list',
|
|
87
|
+
'parenthesized_expression'].includes(node.type)) {
|
|
88
|
+
for (let i = 0; i < node.namedChildCount; i++) visit(node.namedChild(i));
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
visit(target);
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The first positional parameter's name of a function definition. */
|
|
96
|
+
function pythonReceiverName(fn) {
|
|
97
|
+
const params = fn.childForFieldName('parameters');
|
|
98
|
+
for (let i = 0; i < (params?.namedChildCount || 0); i++) {
|
|
99
|
+
const param = params.namedChild(i);
|
|
100
|
+
if (param.type === 'identifier') return param.text;
|
|
101
|
+
if (['typed_parameter', 'default_parameter', 'typed_default_parameter'].includes(param.type)) {
|
|
102
|
+
const name = param.childForFieldName('name') || param.namedChild(0);
|
|
103
|
+
return name?.type === 'identifier' ? name.text : null;
|
|
104
|
+
}
|
|
105
|
+
if (param.type !== 'positional_separator') return null;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Whether a nested function or lambda binds `name` as a parameter. */
|
|
111
|
+
function pythonParamsBind(fn, name) {
|
|
112
|
+
const params = fn.childForFieldName('parameters');
|
|
113
|
+
for (let i = 0; i < (params?.namedChildCount || 0); i++) {
|
|
114
|
+
let param = params.namedChild(i);
|
|
115
|
+
if (param.type === 'default_parameter' || param.type === 'typed_default_parameter') {
|
|
116
|
+
param = param.childForFieldName('name');
|
|
117
|
+
} else if (param.type === 'typed_parameter') {
|
|
118
|
+
param = param.namedChild(0);
|
|
119
|
+
}
|
|
120
|
+
if (param?.type === 'list_splat_pattern' || param?.type === 'dictionary_splat_pattern') {
|
|
121
|
+
param = param.namedChild(0);
|
|
122
|
+
}
|
|
123
|
+
if (param?.type === 'identifier' && param.text === name) return true;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Instance attributes the methods of a class body assign on their receiver,
|
|
129
|
+
* one per name in source order, names in `declared` excluded (fix #430).
|
|
130
|
+
* A static method has no receiver; a nested class has its own; a nested
|
|
131
|
+
* function or lambda that rebinds the receiver's name assigns another value. */
|
|
132
|
+
function pythonInstanceAttributes(bodyNode, declared) {
|
|
133
|
+
const out = [];
|
|
134
|
+
const seen = new Set(declared);
|
|
135
|
+
for (const def of pythonClassBodyDefs(bodyNode)) {
|
|
136
|
+
const fn = def.type === 'decorated_definition' ? def.childForFieldName('definition') : def;
|
|
137
|
+
if (def.type === 'decorated_definition' && def.namedChildren.some(child =>
|
|
138
|
+
child.type === 'decorator' && /^@\s*staticmethod\b/.test(child.text))) continue;
|
|
139
|
+
const receiver = fn && pythonReceiverName(fn);
|
|
140
|
+
const body = fn?.childForFieldName('body');
|
|
141
|
+
if (!receiver || !body) continue;
|
|
142
|
+
traverseTree(body, node => {
|
|
143
|
+
if (node.type === 'class_definition') return false;
|
|
144
|
+
if ((node.type === 'function_definition' || node.type === 'lambda') &&
|
|
145
|
+
pythonParamsBind(node, receiver)) return false;
|
|
146
|
+
if (node.type !== 'assignment' && node.type !== 'augmented_assignment') return true;
|
|
147
|
+
const type = node.type === 'assignment' ? node.childForFieldName('type') : null;
|
|
148
|
+
for (const target of pythonTargetNames(node.childForFieldName('left'), 'attribute')) {
|
|
149
|
+
const object = target.childForFieldName('object');
|
|
150
|
+
const attr = target.childForFieldName('attribute');
|
|
151
|
+
if (object?.type !== 'identifier' || object.text !== receiver || !attr) continue;
|
|
152
|
+
if (seen.has(attr.text)) continue;
|
|
153
|
+
// Reading the previous member on the RHS does not change
|
|
154
|
+
// the assignment's storage: `self.m = wrap(self.m)` creates
|
|
155
|
+
// an instance attribute even if the class inherits `m`.
|
|
156
|
+
seen.add(attr.text);
|
|
157
|
+
out.push({ name: attr.text, startLine: attr.startPosition.row + 1,
|
|
158
|
+
endLine: node.endPosition.row + 1, memberType: 'field', instanceAttribute: true,
|
|
159
|
+
...(type && { returnType: type.text, fieldType: type.text }) });
|
|
160
|
+
}
|
|
161
|
+
return true;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
|
|
79
167
|
function parseTree(parser, code) {
|
|
80
168
|
return safeParse(parser, code, undefined, PARSE_OPTIONS);
|
|
81
169
|
}
|
|
@@ -243,6 +331,7 @@ function _processFunction(node, functions, processedRanges, lines, code) {
|
|
|
243
331
|
params: extractPythonParams(paramsNode),
|
|
244
332
|
paramsStructured,
|
|
245
333
|
startLine: decoratorStartLine,
|
|
334
|
+
sourceSpan: nodeToLocation(node.parent?.type === 'decorated_definition' ? node.parent : node, lines).sourceSpan,
|
|
246
335
|
endLine,
|
|
247
336
|
indent,
|
|
248
337
|
isAsync,
|
|
@@ -329,47 +418,33 @@ function pythonFunctionLocalScope(node) {
|
|
|
329
418
|
return {};
|
|
330
419
|
}
|
|
331
420
|
|
|
332
|
-
// Module-level state detection patterns
|
|
333
|
-
const _STATE_PATTERN = /^(CONFIG|SETTINGS|[A-Z][A-Z0-9_]+|[A-Z][a-zA-Z]*(?:Config|Settings|Options|State|Store|Context))$/;
|
|
334
|
-
// Pattern for UPPER_CASE constants that may have scalar values (string, number, bool, etc.)
|
|
335
|
-
const _CONSTANT_PATTERN = /^[A-Z][A-Z0-9_]{1,}$/;
|
|
336
|
-
// RHS types that are scalar/simple values (not dict/list which are handled separately)
|
|
337
|
-
const _SCALAR_TYPES = new Set([
|
|
338
|
-
'string', 'concatenated_string', 'integer', 'float', 'true', 'false', 'none',
|
|
339
|
-
'unary_operator', 'binary_operator', 'tuple', 'set', 'parenthesized_expression',
|
|
340
|
-
'call', 'attribute', 'identifier', 'subscript',
|
|
341
|
-
]);
|
|
342
|
-
|
|
343
421
|
/**
|
|
344
422
|
* Process a node for state object extraction (single-pass helper)
|
|
345
423
|
* Returns true if node was matched, false otherwise
|
|
346
424
|
*/
|
|
347
425
|
function _processState(node, objects, lines) {
|
|
348
|
-
if (node.type
|
|
349
|
-
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
return true;
|
|
363
|
-
} else if (_CONSTANT_PATTERN.test(name) && _SCALAR_TYPES.has(rightNode.type)) {
|
|
364
|
-
// Module-level UPPER_CASE constants with scalar values
|
|
365
|
-
const { startLine, endLine } = nodeToLocation(node, lines);
|
|
366
|
-
objects.push({ name, startLine, endLine, isConstant: true });
|
|
367
|
-
return true;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
426
|
+
if (node.type !== 'assignment') return false;
|
|
427
|
+
for (let scope = node.parent; scope; scope = scope.parent) {
|
|
428
|
+
if (scope.type === 'function_definition' || scope.type === 'lambda' ||
|
|
429
|
+
scope.type === 'class_definition') return false;
|
|
430
|
+
}
|
|
431
|
+
if (node.parent?.type === 'expression_statement' &&
|
|
432
|
+
_processTypeAlias(node.parent, [], new Set(), lines)) return false;
|
|
433
|
+
const target = node.childForFieldName('left');
|
|
434
|
+
if (!target) return false;
|
|
435
|
+
const bindings = [];
|
|
436
|
+
const collect = pattern => {
|
|
437
|
+
if (pattern.type === 'identifier') bindings.push(pattern);
|
|
438
|
+
else if (['pattern_list', 'tuple_pattern', 'list_pattern', 'list_splat_pattern'].includes(pattern.type)) {
|
|
439
|
+
for (const child of pattern.namedChildren) collect(child);
|
|
370
440
|
}
|
|
441
|
+
};
|
|
442
|
+
collect(target);
|
|
443
|
+
for (const binding of bindings) {
|
|
444
|
+
objects.push({ name: binding.text, startLine: binding.startPosition.row + 1,
|
|
445
|
+
endLine: nodeToLocation(node, lines).endLine });
|
|
371
446
|
}
|
|
372
|
-
return
|
|
447
|
+
return bindings.length > 0;
|
|
373
448
|
}
|
|
374
449
|
|
|
375
450
|
/**
|
|
@@ -694,12 +769,12 @@ function pythonConfigurationItems(root, bindingNodes = null, reboundNodes = null
|
|
|
694
769
|
const rebound = new Set(); // `global` / `nonlocal` names
|
|
695
770
|
const lineOf = node => node.startPosition.row + 1;
|
|
696
771
|
const endOf = node => node.endPosition.row + 1;
|
|
697
|
-
const add = (scope, name, kind, line, branch) => {
|
|
772
|
+
const add = (scope, name, kind, line, branch, target = null) => {
|
|
698
773
|
if (!name) return;
|
|
699
774
|
let names = scopes.get(scope);
|
|
700
775
|
if (!names) scopes.set(scope, names = new Map());
|
|
701
776
|
if (!names.has(name)) names.set(name, []);
|
|
702
|
-
names.get(name).push({ kind, line, branch });
|
|
777
|
+
names.get(name).push({ kind, line, branch, ...(target && { target }) });
|
|
703
778
|
};
|
|
704
779
|
const bindTarget = (scope, node, branch) => {
|
|
705
780
|
if (!node) return;
|
|
@@ -753,8 +828,20 @@ function pythonConfigurationItems(root, bindingNodes = null, reboundNodes = null
|
|
|
753
828
|
case 'expression_statement':
|
|
754
829
|
for (let expr of node.namedChildren) {
|
|
755
830
|
while (expr?.type === 'assignment' || expr?.type === 'augmented_assignment') {
|
|
756
|
-
|
|
757
|
-
|
|
831
|
+
const left = expr.childForFieldName('left');
|
|
832
|
+
const right = expr.childForFieldName('right');
|
|
833
|
+
// An alias value (`f = other`, `f = mod.other`)
|
|
834
|
+
// names what it binds (fix #424): the chase follows it.
|
|
835
|
+
if (expr.type === 'assignment' && left?.type === 'identifier' &&
|
|
836
|
+
!expr.childForFieldName('type') &&
|
|
837
|
+
(right?.type === 'identifier' || right?.type === 'attribute') &&
|
|
838
|
+
/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(right.text) &&
|
|
839
|
+
right.text !== left.text) {
|
|
840
|
+
add(scope, left.text, 'value', lineOf(left), branch, right.text);
|
|
841
|
+
} else {
|
|
842
|
+
bindTarget(scope, left, branch);
|
|
843
|
+
}
|
|
844
|
+
expr = expr.type === 'assignment' ? right : null;
|
|
758
845
|
}
|
|
759
846
|
}
|
|
760
847
|
return;
|
|
@@ -866,7 +953,8 @@ function pythonConfigurationItems(root, bindingNodes = null, reboundNodes = null
|
|
|
866
953
|
const statements = new Set(sites.flatMap(site => site.branch.map(([line]) => line)));
|
|
867
954
|
items.push({
|
|
868
955
|
name, scope,
|
|
869
|
-
sites: sites.map(site => ({ kind: site.kind, line: site.line, branch: site.branch
|
|
956
|
+
sites: sites.map(site => ({ kind: site.kind, line: site.line, branch: site.branch,
|
|
957
|
+
...(site.target && { target: site.target }) })),
|
|
870
958
|
clauses: [...statements].sort((a, b) => a - b).map(line => [line, clauseRanges.get(line) || []]),
|
|
871
959
|
});
|
|
872
960
|
}
|
|
@@ -1007,7 +1095,7 @@ function _processTypeAlias(node, classes, processedRanges, lines) {
|
|
|
1007
1095
|
const rangeKey = `${node.startIndex}-${node.endIndex}`;
|
|
1008
1096
|
if (processedRanges.has(rangeKey)) return true;
|
|
1009
1097
|
processedRanges.add(rangeKey);
|
|
1010
|
-
const { startLine, endLine } = nodeToLocation(node, lines);
|
|
1098
|
+
const { startLine, endLine, sourceSpan } = nodeToLocation(node, lines);
|
|
1011
1099
|
const aliasOf = pythonAliasBase(valueNode);
|
|
1012
1100
|
const aliasMembers = pythonAliasMembers(valueNode);
|
|
1013
1101
|
classes.push({
|
|
@@ -1015,6 +1103,7 @@ function _processTypeAlias(node, classes, processedRanges, lines) {
|
|
|
1015
1103
|
type: 'type',
|
|
1016
1104
|
startLine,
|
|
1017
1105
|
endLine,
|
|
1106
|
+
sourceSpan,
|
|
1018
1107
|
methods: [],
|
|
1019
1108
|
members: [],
|
|
1020
1109
|
...(aliasOf && { aliasOf }),
|
|
@@ -1069,6 +1158,36 @@ function extractClassMembers(classNode, code) {
|
|
|
1069
1158
|
const bodyNode = classNode.childForFieldName('body');
|
|
1070
1159
|
if (!bodyNode) return members;
|
|
1071
1160
|
|
|
1161
|
+
// A class-body assignment declares an attribute, including an annotated
|
|
1162
|
+
// dataclass field and each name of a tuple target. Function and
|
|
1163
|
+
// nested-class bodies have their own owners.
|
|
1164
|
+
const declared = new Set();
|
|
1165
|
+
traverseTree(bodyNode, node => {
|
|
1166
|
+
if (node.type === 'function_definition' || node.type === 'class_definition') return false;
|
|
1167
|
+
if (node.type !== 'assignment') return true;
|
|
1168
|
+
const type = node.childForFieldName('type');
|
|
1169
|
+
for (const name of pythonTargetNames(node.childForFieldName('left'), 'identifier')) {
|
|
1170
|
+
declared.add(name.text);
|
|
1171
|
+
members.push({ name: name.text, startLine: name.startPosition.row + 1,
|
|
1172
|
+
endLine: node.endPosition.row + 1, memberType: 'field',
|
|
1173
|
+
...(type && { returnType: type.text, fieldType: type.text }) });
|
|
1174
|
+
}
|
|
1175
|
+
return true;
|
|
1176
|
+
});
|
|
1177
|
+
// An attribute a method assigns on its receiver (`self.x = v`, `self.x:
|
|
1178
|
+
// T = v`, `self.x += v`) is an instance attribute of the class (fix
|
|
1179
|
+
// #430): one declaration per name, the first in source order, unless
|
|
1180
|
+
// the class body declares the name.
|
|
1181
|
+
// A property the class body declares is a data descriptor: assigning
|
|
1182
|
+
// it on the receiver runs its setter, never declares an attribute.
|
|
1183
|
+
for (const def of pythonClassBodyDefs(bodyNode)) {
|
|
1184
|
+
if (def.type !== 'decorated_definition') continue;
|
|
1185
|
+
const name = def.childForFieldName('definition')?.childForFieldName('name')?.text;
|
|
1186
|
+
if (name && def.namedChildren.some(child => child.type === 'decorator' &&
|
|
1187
|
+
/^@\s*(?:property|[A-Za-z_]\w*\.(?:setter|deleter))\s*$/.test(child.text))) declared.add(name);
|
|
1188
|
+
}
|
|
1189
|
+
for (const attribute of pythonInstanceAttributes(bodyNode, declared)) members.push(attribute);
|
|
1190
|
+
|
|
1072
1191
|
for (const child of pythonClassBodyDefs(bodyNode)) {
|
|
1073
1192
|
let funcNode = child;
|
|
1074
1193
|
let decoratorStart = null;
|
|
@@ -1138,6 +1257,7 @@ function extractClassMembers(classNode, code) {
|
|
|
1138
1257
|
params: extractPythonParams(paramsNode),
|
|
1139
1258
|
paramsStructured,
|
|
1140
1259
|
startLine,
|
|
1260
|
+
sourceSpan: nodeToLocation(child, code).sourceSpan,
|
|
1141
1261
|
endLine,
|
|
1142
1262
|
memberType,
|
|
1143
1263
|
isAsync,
|
|
@@ -1625,6 +1745,30 @@ function narrowedReceiverType(refNode, receiverName, declaredUnion) {
|
|
|
1625
1745
|
if (remaining.length === 1) return remaining[0];
|
|
1626
1746
|
}
|
|
1627
1747
|
}
|
|
1748
|
+
// A class pattern (fix #425, the Go type-switch rule): in `case
|
|
1749
|
+
// Sq() as s:` the capture s is a Sq instance, and `match x: case
|
|
1750
|
+
// Sq():` narrows x to Sq, in the clause's body. A union of class
|
|
1751
|
+
// patterns names no single type.
|
|
1752
|
+
if (parent.type === 'case_clause' && sameNode(parent.childForFieldName('consequence'), current)) {
|
|
1753
|
+
const casePatterns = parent.namedChildren.filter(child => child.type === 'case_pattern');
|
|
1754
|
+
const pattern = casePatterns.length === 1 ? casePatterns[0].namedChild(0) : null;
|
|
1755
|
+
let classPattern = null;
|
|
1756
|
+
if (pattern?.type === 'as_pattern') {
|
|
1757
|
+
const target = pattern.namedChild(pattern.namedChildCount - 1);
|
|
1758
|
+
const targetName = target?.type === 'as_pattern_target' ? target.namedChild(0)?.text : target?.text;
|
|
1759
|
+
const inner = pattern.namedChild(0);
|
|
1760
|
+
if (targetName === receiverName && inner?.type === 'case_pattern' &&
|
|
1761
|
+
inner.namedChild(0)?.type === 'class_pattern') classPattern = inner.namedChild(0);
|
|
1762
|
+
} else if (pattern?.type === 'class_pattern') {
|
|
1763
|
+
const subject = parent.parent?.parent?.type === 'match_statement'
|
|
1764
|
+
? parent.parent.parent.childForFieldName('subject') : null;
|
|
1765
|
+
if (subject?.type === 'identifier' && subject.text === receiverName) classPattern = pattern;
|
|
1766
|
+
}
|
|
1767
|
+
const dotted = classPattern?.namedChild(0);
|
|
1768
|
+
if (dotted?.type === 'dotted_name' && dotted.namedChildCount > 0) {
|
|
1769
|
+
return dotted.namedChild(dotted.namedChildCount - 1).text;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1628
1772
|
if (parent.type === 'conditional_expression' &&
|
|
1629
1773
|
parent.namedChildCount >= 3) {
|
|
1630
1774
|
const consequence = parent.namedChild(0);
|
|
@@ -2242,6 +2386,102 @@ function pythonClosureScopeLine(refNode, name) {
|
|
|
2242
2386
|
return null;
|
|
2243
2387
|
}
|
|
2244
2388
|
|
|
2389
|
+
// Methods Python calls with the class as their first argument without a
|
|
2390
|
+
// decorator (data model: implicit classmethods).
|
|
2391
|
+
const PY_IMPLICIT_CLASS_METHODS = new Set(['__init_subclass__', '__class_getitem__']);
|
|
2392
|
+
|
|
2393
|
+
/**
|
|
2394
|
+
* A construction through a method's own receiver (fix #434): `cls(..)` in a
|
|
2395
|
+
* classmethod, `type(self)(..)` and `self.__class__(..)` in an instance
|
|
2396
|
+
* method build the method's class or a subclass of it. The receiver must be
|
|
2397
|
+
* the method's first parameter, never rebound, and read from the method
|
|
2398
|
+
* itself or a closure inside it. Returns the class (by its name line), the
|
|
2399
|
+
* method, the receiver binding ('class' or 'instance') and the spelling of
|
|
2400
|
+
* the callee, or null.
|
|
2401
|
+
*/
|
|
2402
|
+
function pythonOwnClassConstruction(callNode) {
|
|
2403
|
+
const funcNode = callNode.childForFieldName('function');
|
|
2404
|
+
let receiver = null, binding = null, spelling = null;
|
|
2405
|
+
if (funcNode?.type === 'identifier') {
|
|
2406
|
+
receiver = funcNode; binding = 'class'; spelling = funcNode.text;
|
|
2407
|
+
} else if (funcNode?.type === 'call') {
|
|
2408
|
+
const inner = funcNode.childForFieldName('function');
|
|
2409
|
+
const args = funcNode.childForFieldName('arguments');
|
|
2410
|
+
const named = args?.type === 'argument_list'
|
|
2411
|
+
? args.namedChildren.filter(child => !child.type.endsWith('comment')) : [];
|
|
2412
|
+
if (inner?.type === 'identifier' && inner.text === 'type' && named.length === 1 &&
|
|
2413
|
+
named[0].type === 'identifier') {
|
|
2414
|
+
receiver = named[0]; binding = 'instance'; spelling = funcNode.text;
|
|
2415
|
+
}
|
|
2416
|
+
} else if (funcNode?.type === 'attribute' &&
|
|
2417
|
+
funcNode.childForFieldName('attribute')?.text === '__class__' &&
|
|
2418
|
+
funcNode.childForFieldName('object')?.type === 'identifier') {
|
|
2419
|
+
receiver = funcNode.childForFieldName('object'); binding = 'instance'; spelling = '__class__';
|
|
2420
|
+
}
|
|
2421
|
+
if (!receiver) return null;
|
|
2422
|
+
const name = receiver.text;
|
|
2423
|
+
let method = null;
|
|
2424
|
+
for (let parent = callNode.parent; parent && !method; parent = parent.parent) {
|
|
2425
|
+
if (parent.type === 'class_definition' || parent.type === 'module') return null;
|
|
2426
|
+
if (parent.type === 'lambda') {
|
|
2427
|
+
const params = parent.childForFieldName('parameters');
|
|
2428
|
+
if (params && pythonParametersBindName(params, name)) return null;
|
|
2429
|
+
} else if (PY_COMPREHENSIONS.has(parent.type)) {
|
|
2430
|
+
for (const clause of parent.namedChildren) {
|
|
2431
|
+
if (clause.type === 'for_in_clause' &&
|
|
2432
|
+
pythonTargetBindsName(clause.childForFieldName('left'), name)) return null;
|
|
2433
|
+
}
|
|
2434
|
+
} else if (parent.type === 'function_definition' || parent.type === 'async_function_definition') {
|
|
2435
|
+
const params = parent.childForFieldName('parameters');
|
|
2436
|
+
const body = parent.childForFieldName('body');
|
|
2437
|
+
if (params && pythonParametersBindName(params, name)) method = parent;
|
|
2438
|
+
else if (body && pythonScopeBindsName(body, name)) return null;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
if (!method) return null;
|
|
2442
|
+
const params = method.childForFieldName('parameters');
|
|
2443
|
+
const first = params?.namedChildren.find(child => !child.type.endsWith('comment'));
|
|
2444
|
+
const body = method.childForFieldName('body');
|
|
2445
|
+
if (first?.type !== 'identifier' || first.text !== name || (body && pythonScopeBindsName(body, name)) ||
|
|
2446
|
+
isPythonIterationBindingAt(receiver, name)) return null;
|
|
2447
|
+
const holder = method.parent?.type === 'decorated_definition' ? method.parent : method;
|
|
2448
|
+
const classNode = holder.parent?.type === 'block' && holder.parent.parent?.type === 'class_definition'
|
|
2449
|
+
? holder.parent.parent : null;
|
|
2450
|
+
const className = classNode?.childForFieldName('name');
|
|
2451
|
+
const methodName = method.childForFieldName('name');
|
|
2452
|
+
if (!className || !methodName) return null;
|
|
2453
|
+
const decorators = holder.type === 'decorated_definition'
|
|
2454
|
+
? holder.namedChildren.filter(child => child.type === 'decorator').map(child => child.namedChild(0)?.text)
|
|
2455
|
+
: [];
|
|
2456
|
+
const classMethod = decorators.includes('classmethod') || PY_IMPLICIT_CLASS_METHODS.has(methodName.text);
|
|
2457
|
+
if (binding === 'class' ? !classMethod
|
|
2458
|
+
: classMethod || decorators.includes('staticmethod') || methodName.text === '__new__') return null;
|
|
2459
|
+
const own = { name: className.text, line: className.startPosition.row + 1, method: methodName.text,
|
|
2460
|
+
methodLine: methodName.startPosition.row + 1, receiver: name, binding, spelling };
|
|
2461
|
+
// The receiver token names the class at the site (`self` of `type(self)`).
|
|
2462
|
+
Object.defineProperty(own, 'receiverNode', { value: receiver, enumerable: false });
|
|
2463
|
+
return own;
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
/** The `ownClass` field of a call record (fix #434). */
|
|
2467
|
+
function ownClassFields(callNode) {
|
|
2468
|
+
const ownClass = pythonOwnClassConstruction(callNode);
|
|
2469
|
+
return ownClass ? { ownClass } : null;
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
/** Whether a parameter list binds `name`. */
|
|
2473
|
+
function pythonParametersBindName(params, name) {
|
|
2474
|
+
for (const param of params.namedChildren) {
|
|
2475
|
+
let paramName = param.type === 'identifier' ? param
|
|
2476
|
+
: (param.childForFieldName('name') || param.namedChild(0));
|
|
2477
|
+
if (paramName && ['dictionary_splat_pattern', 'list_splat_pattern'].includes(paramName.type)) {
|
|
2478
|
+
paramName = paramName.namedChild(0);
|
|
2479
|
+
}
|
|
2480
|
+
if (paramName?.type === 'identifier' && paramName.text === name) return true;
|
|
2481
|
+
}
|
|
2482
|
+
return false;
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2245
2485
|
/** Shadow fields of a bare reference: `localShadow`, and the enclosing
|
|
2246
2486
|
* function a closure binds it in. */
|
|
2247
2487
|
function pythonShadowFields(refNode, name) {
|
|
@@ -2413,10 +2653,14 @@ function isPythonConstructorValueShadowedAt(refNode, name) {
|
|
|
2413
2653
|
return classDeclarations > 1;
|
|
2414
2654
|
}
|
|
2415
2655
|
|
|
2656
|
+
const moduleAliasesByTree = new WeakMap();
|
|
2416
2657
|
function pythonModuleAliases(tree) {
|
|
2658
|
+
const cached = moduleAliasesByTree.get(tree);
|
|
2659
|
+
if (cached) return cached;
|
|
2417
2660
|
const aliases = new Set();
|
|
2418
|
-
|
|
2419
|
-
|
|
2661
|
+
// A name query needs import statements, not a wrapper for every node
|
|
2662
|
+
// in the file. Keep this source-only answer with its immutable tree.
|
|
2663
|
+
for (const node of tree.rootNode.descendantsOfType('import_statement')) {
|
|
2420
2664
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2421
2665
|
const child = node.namedChild(i);
|
|
2422
2666
|
if (child.type === 'dotted_name') {
|
|
@@ -2427,8 +2671,8 @@ function pythonModuleAliases(tree) {
|
|
|
2427
2671
|
if (alias?.type === 'identifier') aliases.add(alias.text);
|
|
2428
2672
|
}
|
|
2429
2673
|
}
|
|
2430
|
-
|
|
2431
|
-
|
|
2674
|
+
}
|
|
2675
|
+
moduleAliasesByTree.set(tree, aliases);
|
|
2432
2676
|
return aliases;
|
|
2433
2677
|
}
|
|
2434
2678
|
|
|
@@ -3423,7 +3667,9 @@ function findCallsInTree(code, tree, parser, sink = null) {
|
|
|
3423
3667
|
receiverTypeEvidence: { ...typeOrigin('constructor', objNode), type: ctor.type } };
|
|
3424
3668
|
}
|
|
3425
3669
|
}
|
|
3426
|
-
|
|
3670
|
+
const valuePath = !valueType && objNode?.type === 'attribute'
|
|
3671
|
+
? attributeReceiverPath(objNode) : null;
|
|
3672
|
+
if (valueType || valuePath) {
|
|
3427
3673
|
// The receiver context a call on it carries.
|
|
3428
3674
|
const recv = objNode.type === 'identifier' ? objNode.text : null;
|
|
3429
3675
|
calls.push({
|
|
@@ -3432,19 +3678,27 @@ function findCallsInTree(code, tree, parser, sink = null) {
|
|
|
3432
3678
|
line: attrName.startPosition.row + 1,
|
|
3433
3679
|
isMethod: true,
|
|
3434
3680
|
...(recv && { receiver: recv }),
|
|
3435
|
-
receiverType: valueType,
|
|
3681
|
+
...(valueType && { receiverType: valueType }),
|
|
3436
3682
|
...valueFields,
|
|
3683
|
+
...(valuePath && {
|
|
3684
|
+
receiverRoot: valuePath.root,
|
|
3685
|
+
receiverField: valuePath.fields[valuePath.fields.length - 1],
|
|
3686
|
+
receiverFields: valuePath.fields,
|
|
3687
|
+
...(localVarTypes.has(valuePath.root) && {
|
|
3688
|
+
receiverRootType: localVarTypes.get(valuePath.root),
|
|
3689
|
+
}),
|
|
3690
|
+
}),
|
|
3437
3691
|
...(recv && localVarTypeQualifiers.get(recv) && {
|
|
3438
3692
|
receiverTypeQualifier: localVarTypeQualifiers.get(recv),
|
|
3439
3693
|
}),
|
|
3440
3694
|
...(recv && localVarStdlibContracts.has(recv) && {
|
|
3441
3695
|
receiverTypeStdlibModule: localVarStdlibContracts.get(recv),
|
|
3442
3696
|
}),
|
|
3443
|
-
...((!recv || constructedReceiverVars.has(recv)) && { receiverConstructed: true }),
|
|
3697
|
+
...(((!recv && !valuePath) || constructedReceiverVars.has(recv)) && { receiverConstructed: true }),
|
|
3444
3698
|
...(recv && withBindingVars.has(recv) && { receiverWithBinding: true }),
|
|
3445
3699
|
...(recv && isShadowedByLocal(objNode, recv) && { receiverLocalBinding: true }),
|
|
3446
3700
|
isFunctionReference: true,
|
|
3447
|
-
memberRead: true,
|
|
3701
|
+
...(!valuePath && { memberRead: true }),
|
|
3448
3702
|
enclosingFunction: getCurrentEnclosingFunction(),
|
|
3449
3703
|
});
|
|
3450
3704
|
}
|
|
@@ -3639,10 +3893,28 @@ function findCallsInTree(code, tree, parser, sink = null) {
|
|
|
3639
3893
|
enclosingFunction,
|
|
3640
3894
|
uncertain,
|
|
3641
3895
|
...pythonShadowFields(funcNode, funcNode.text),
|
|
3896
|
+
...ownClassFields(node),
|
|
3642
3897
|
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp }),
|
|
3643
3898
|
...(requestConfig && { requestConfig })
|
|
3644
3899
|
});
|
|
3645
3900
|
}
|
|
3901
|
+
} else if (funcNode.type === 'call') {
|
|
3902
|
+
// `type(self)(..)` constructs the method's class (fix #434).
|
|
3903
|
+
const ownClass = pythonOwnClassConstruction(node);
|
|
3904
|
+
if (ownClass) {
|
|
3905
|
+
calls.push({
|
|
3906
|
+
callSite: typeOrigin('call', ownClass.receiverNode),
|
|
3907
|
+
column: ownClass.receiverNode.startPosition.column,
|
|
3908
|
+
name: ownClass.name,
|
|
3909
|
+
line: node.startPosition.row + 1,
|
|
3910
|
+
isMethod: false,
|
|
3911
|
+
argCount,
|
|
3912
|
+
...(argSpread && { argSpread: true }),
|
|
3913
|
+
enclosingFunction,
|
|
3914
|
+
uncertain,
|
|
3915
|
+
ownClass,
|
|
3916
|
+
});
|
|
3917
|
+
}
|
|
3646
3918
|
} else if (funcNode.type === 'attribute') {
|
|
3647
3919
|
// Method/attribute call: obj.foo() or self.attr.foo()
|
|
3648
3920
|
const attrNode = funcNode.childForFieldName('attribute');
|
|
@@ -3670,7 +3942,8 @@ function findCallsInTree(code, tree, parser, sink = null) {
|
|
|
3670
3942
|
// Detect super().method() pattern
|
|
3671
3943
|
if (objNode?.type === 'call') {
|
|
3672
3944
|
const superFunc = objNode.childForFieldName('function');
|
|
3673
|
-
if (superFunc?.type === 'identifier' && superFunc.text === 'super'
|
|
3945
|
+
if (superFunc?.type === 'identifier' && superFunc.text === 'super' &&
|
|
3946
|
+
objNode.childForFieldName('arguments')?.namedChildCount === 0) {
|
|
3674
3947
|
receiver = 'super';
|
|
3675
3948
|
}
|
|
3676
3949
|
}
|
|
@@ -3905,6 +4178,22 @@ function findCallsInTree(code, tree, parser, sink = null) {
|
|
|
3905
4178
|
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp }),
|
|
3906
4179
|
...(requestConfig && { requestConfig })
|
|
3907
4180
|
});
|
|
4181
|
+
// `self.__class__(..)` constructs the method's class (fix #434).
|
|
4182
|
+
const ownClass = attrNode.text === '__class__' ? pythonOwnClassConstruction(node) : null;
|
|
4183
|
+
if (ownClass) {
|
|
4184
|
+
calls.push({
|
|
4185
|
+
callSite: typeOrigin('call', ownClass.receiverNode),
|
|
4186
|
+
column: ownClass.receiverNode.startPosition.column,
|
|
4187
|
+
name: ownClass.name,
|
|
4188
|
+
line: node.startPosition.row + 1,
|
|
4189
|
+
isMethod: false,
|
|
4190
|
+
argCount,
|
|
4191
|
+
...(argSpread && { argSpread: true }),
|
|
4192
|
+
enclosingFunction,
|
|
4193
|
+
uncertain,
|
|
4194
|
+
ownClass,
|
|
4195
|
+
});
|
|
4196
|
+
}
|
|
3908
4197
|
}
|
|
3909
4198
|
}
|
|
3910
4199
|
|
|
@@ -4602,6 +4891,16 @@ function findUsagesInTree(code, name, parser, tree, options = {}) {
|
|
|
4602
4891
|
}
|
|
4603
4892
|
}
|
|
4604
4893
|
|
|
4894
|
+
// A module path segment (`from a.b import c`, `import a.b [as x]`)
|
|
4895
|
+
// names a module, never a function, class or variable; the name of
|
|
4896
|
+
// `from m import name as alias` is m's attribute, decided by m (fix
|
|
4897
|
+
// #424). Neither is a reference resolved in this file's scopes.
|
|
4898
|
+
const importRole = pythonImportTokenRole(node);
|
|
4899
|
+
if (importRole) {
|
|
4900
|
+
usages.push({ line, column, usageType, ...(importRole === 'module-path'
|
|
4901
|
+
? { modulePath: true } : { importSource: true }) });
|
|
4902
|
+
return true;
|
|
4903
|
+
}
|
|
4605
4904
|
// Where a bare reference resolves (fix #392): plan edits it only
|
|
4606
4905
|
// when the language's scoping proves it names the renamed binding.
|
|
4607
4906
|
const scope = scopeMemo && usageType === 'reference'
|
|
@@ -4613,6 +4912,29 @@ function findUsagesInTree(code, name, parser, tree, options = {}) {
|
|
|
4613
4912
|
return usages;
|
|
4614
4913
|
}
|
|
4615
4914
|
|
|
4915
|
+
/**
|
|
4916
|
+
* The role of an identifier inside an import statement (fix #424):
|
|
4917
|
+
* 'module-path' for a segment of the imported module's dotted path,
|
|
4918
|
+
* 'import-source' for the imported name of `from m import name as alias`,
|
|
4919
|
+
* null otherwise (bound names and aliases).
|
|
4920
|
+
*/
|
|
4921
|
+
function pythonImportTokenRole(node) {
|
|
4922
|
+
const dotted = node.parent;
|
|
4923
|
+
if (dotted?.type !== 'dotted_name') return null;
|
|
4924
|
+
const holder = dotted.parent;
|
|
4925
|
+
if (!holder) return null;
|
|
4926
|
+
if (holder.type === 'relative_import' || holder.type === 'import_statement') return 'module-path';
|
|
4927
|
+
if (holder.type === 'import_from_statement') {
|
|
4928
|
+
return sameNode(holder.childForFieldName('module_name'), dotted) ? 'module-path' : null;
|
|
4929
|
+
}
|
|
4930
|
+
if (holder.type === 'aliased_import' && sameNode(holder.childForFieldName('name'), dotted)) {
|
|
4931
|
+
const statement = holder.parent?.type;
|
|
4932
|
+
if (statement === 'import_statement') return 'module-path';
|
|
4933
|
+
if (statement === 'import_from_statement') return 'import-source';
|
|
4934
|
+
}
|
|
4935
|
+
return null;
|
|
4936
|
+
}
|
|
4937
|
+
|
|
4616
4938
|
/**
|
|
4617
4939
|
* Find instance attribute types from __init__ constructor assignments.
|
|
4618
4940
|
* Parses self.X = ClassName(...) patterns in __init__ methods.
|
|
@@ -5085,10 +5407,13 @@ function findClassFieldFacts(tree, line, name) {
|
|
|
5085
5407
|
visitBody(body);
|
|
5086
5408
|
const written = new Set();
|
|
5087
5409
|
let dynamic = false;
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
if (node.type === 'class_definition')
|
|
5410
|
+
let nestedClassEnd = -1;
|
|
5411
|
+
for (const node of body.descendantsOfType(['class_definition', 'attribute', 'call'])) {
|
|
5412
|
+
if (node.startIndex < nestedClassEnd) continue;
|
|
5413
|
+
if (node.type === 'class_definition') {
|
|
5414
|
+
nestedClassEnd = node.endIndex;
|
|
5415
|
+
continue;
|
|
5416
|
+
}
|
|
5092
5417
|
if (node.type === 'attribute') {
|
|
5093
5418
|
const object = node.childForFieldName('object');
|
|
5094
5419
|
const attribute = node.childForFieldName('attribute');
|
|
@@ -5102,7 +5427,6 @@ function findClassFieldFacts(tree, line, name) {
|
|
|
5102
5427
|
if (fn?.type === 'identifier' && (fn.text === 'setattr' || fn.text === 'vars' ||
|
|
5103
5428
|
fn.text === 'delattr') && first?.type === 'identifier' && receivers.has(first.text)) dynamic = true;
|
|
5104
5429
|
}
|
|
5105
|
-
for (let i = node.namedChildCount - 1; i >= 0; i--) pending.push(node.namedChild(i));
|
|
5106
5430
|
}
|
|
5107
5431
|
return { members, written, dynamic };
|
|
5108
5432
|
}
|
|
@@ -5498,7 +5822,33 @@ function findScopeAlternativeBindings(tree, defLine, name) {
|
|
|
5498
5822
|
return out;
|
|
5499
5823
|
}
|
|
5500
5824
|
|
|
5825
|
+
/**
|
|
5826
|
+
* The annotated type of a plain receiver of an attribute reference (fix
|
|
5827
|
+
* #424): `env.config` inside `def f(env: Environment)` reads a member of
|
|
5828
|
+
* Environment when the parameter is the only binding of `env` in the scope
|
|
5829
|
+
* the reference resolves to. `column` is the attribute token's column.
|
|
5830
|
+
* Undefined when the receiver is rebound, unannotated or not a parameter.
|
|
5831
|
+
*/
|
|
5832
|
+
function annotatedReceiverType(tree, line, column, receiver) {
|
|
5833
|
+
if (!tree || !receiver || receiver === 'self' || receiver === 'cls') return undefined;
|
|
5834
|
+
const token = tree.rootNode.descendantForPosition({ row: line - 1, column });
|
|
5835
|
+
const attribute = token?.parent;
|
|
5836
|
+
if (attribute?.type !== 'attribute' || !sameNode(attribute.childForFieldName('attribute'), token)) return undefined;
|
|
5837
|
+
const object = attribute.childForFieldName('object');
|
|
5838
|
+
if (object?.type !== 'identifier' || object.text !== receiver) return undefined;
|
|
5839
|
+
const { referenceScopeNode, pythonBindingSites } = require('./lexical-scope');
|
|
5840
|
+
const scope = referenceScopeNode(object, receiver);
|
|
5841
|
+
if (scope?.type !== 'function_definition') return undefined;
|
|
5842
|
+
const sites = pythonBindingSites(scope, receiver);
|
|
5843
|
+
if (sites.length !== 1) return undefined;
|
|
5844
|
+
const parameter = sites[0].parent;
|
|
5845
|
+
if (!parameter || !['typed_parameter', 'typed_default_parameter'].includes(parameter.type) ||
|
|
5846
|
+
parameter.parent?.type !== 'parameters') return undefined;
|
|
5847
|
+
return typeNameFromAnnotation(parameter.childForFieldName('type')) || undefined;
|
|
5848
|
+
}
|
|
5849
|
+
|
|
5501
5850
|
module.exports = {
|
|
5851
|
+
annotatedReceiverType,
|
|
5502
5852
|
findScopeAlternativeBindings,
|
|
5503
5853
|
findPythonModuleEvidence,
|
|
5504
5854
|
findFunctions,
|