ucn 5.6.0 → 5.6.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 +4 -2
- package/.claude/skills/ucn/references/commands.md +1 -1
- package/CHANGELOG.md +11 -0
- package/README.md +5 -8
- package/core/analysis.js +3 -3
- package/core/cache.js +3 -1
- package/core/call-fit.js +130 -0
- package/core/callers.js +1 -1
- package/core/capture.js +40 -8
- package/core/change-kinds.js +221 -2
- package/core/check.js +48 -15
- package/core/command-contracts.js +2 -2
- package/core/deadcode.js +94 -23
- package/core/deletion.js +29 -9
- package/core/entrypoint-toml.js +110 -0
- package/core/index-ir.js +2 -2
- package/core/ir.js +1 -1
- package/core/outgoing-check.js +116 -0
- package/core/output/check.js +5 -2
- package/core/output/reporting.js +4 -0
- package/core/output/search.js +3 -0
- package/core/output/tracing.js +1 -1
- package/core/output-budget.js +29 -11
- package/core/python-binding-identity.js +707 -0
- package/core/reflection.js +99 -10
- package/core/registry.js +2 -2
- package/core/runtime-entrypoints.js +159 -0
- package/core/search.js +61 -38
- package/core/test-units.js +116 -42
- package/core/tracing.js +193 -635
- package/core/verify.js +37 -12
- package/languages/csharp.js +4 -4
- package/languages/go.js +1 -1
- package/languages/index.js +45 -1
- package/languages/java.js +4 -4
- package/languages/javascript.js +40 -6
- package/languages/python.js +49 -15
- package/languages/rust.js +126 -112
- package/languages/static-js.js +88 -0
- package/mcp/server.js +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Resolve only identities written in Python syntax. Multiple writes, parameters,
|
|
4
|
+
// conditional bindings and unknown expressions have no proven identity.
|
|
5
|
+
const { referenceScopeNode, pythonScopeBindingKinds } = require('../languages/lexical-scope');
|
|
6
|
+
const { sameNode } = require('../languages/utils');
|
|
7
|
+
const BUILTINS = new Set(['object', 'property', 'classmethod', 'staticmethod',
|
|
8
|
+
'getattr', 'setattr', 'hasattr', 'delattr', 'eval', 'exec', 'globals', 'locals', 'vars', '__import__']);
|
|
9
|
+
const UNBOUND_BUILTINS = new Set(['object', 'property', 'classmethod', 'staticmethod']);
|
|
10
|
+
const EFFECTS = new WeakMap();
|
|
11
|
+
const SPELLINGS = new WeakMap();
|
|
12
|
+
const OP_NORMALIZED_NAMES = new WeakMap();
|
|
13
|
+
const OP_NAMESPACE_EFFECTS = new WeakMap();
|
|
14
|
+
const SEQUENCES = new Set(['tuple_pattern', 'list_pattern', 'pattern_list', 'tuple', 'list', 'expression_list']);
|
|
15
|
+
const TARGET_WRAPPERS = new Set([...SEQUENCES, 'parenthesized_expression', 'list_splat_pattern', 'as_pattern_target']);
|
|
16
|
+
const LOCAL_NAMESPACE = '<local-namespace>';
|
|
17
|
+
const UNKNOWN_NAMESPACE = '<unknown-namespace>';
|
|
18
|
+
const NAMESPACE_ATTRIBUTES = new Set(['__dict__', '__self__', '__globals__', '__builtins__']);
|
|
19
|
+
const STANDARD_NAMESPACES = new Set(['builtins', 'typing']);
|
|
20
|
+
const NAMESPACE_READS = new Set(['get', 'keys', 'values', 'items', 'copy', '__getitem__', '__contains__', '__iter__', '__len__']);
|
|
21
|
+
const UNKNOWN_ORIGIN = '<unresolved-origin>';
|
|
22
|
+
const POSSIBLE_RUNTIME_APIS = [...BUILTINS].map(name => `builtins.${name}`).concat([
|
|
23
|
+
'unittest.mock.patch', 'unittest.mock.patch.object', 'pytest.MonkeyPatch.instance.setattr',
|
|
24
|
+
'setuptools.setup', 'distutils.core.setup',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
// Python normalizes identifiers, not string values. Keep original spellings
|
|
28
|
+
// on declarations/handles; use normalized identity only at this boundary.
|
|
29
|
+
const pythonSourceName = name => String(name || '').normalize('NFKC');
|
|
30
|
+
|
|
31
|
+
function sourceSpellings(tree, name) {
|
|
32
|
+
let spellings = SPELLINGS.get(tree);
|
|
33
|
+
if (!spellings) {
|
|
34
|
+
spellings = new Map();
|
|
35
|
+
for (const node of tree.rootNode.descendantsOfType('identifier')) {
|
|
36
|
+
const normalized = pythonSourceName(node.text);
|
|
37
|
+
if (normalized === node.text) continue;
|
|
38
|
+
if (!spellings.has(normalized)) spellings.set(normalized, new Set([normalized]));
|
|
39
|
+
spellings.get(normalized).add(node.text);
|
|
40
|
+
}
|
|
41
|
+
SPELLINGS.set(tree, spellings);
|
|
42
|
+
}
|
|
43
|
+
return spellings.get(pythonSourceName(name)) || [name];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Only source identifiers enter this capability inventory, never strings. */
|
|
47
|
+
function pythonNormalizedNames(index) {
|
|
48
|
+
const operation = index._opTreeCache;
|
|
49
|
+
if (operation && OP_NORMALIZED_NAMES.has(operation)) return OP_NORMALIZED_NAMES.get(operation);
|
|
50
|
+
const names = new Set();
|
|
51
|
+
const { langTraits } = require('../languages');
|
|
52
|
+
for (const [file, entry] of index.files) {
|
|
53
|
+
if (!langTraits(entry.language)?.reflectionApi?.pythonBindingIdentity) continue;
|
|
54
|
+
let source, tree;
|
|
55
|
+
try {
|
|
56
|
+
source = index._readFile(file);
|
|
57
|
+
// This is only a parse precheck. Runtime strings are never added.
|
|
58
|
+
if (source.normalize('NFKC') === source) continue;
|
|
59
|
+
tree = index._getParsedTree(file, source, entry.language);
|
|
60
|
+
} catch { continue; }
|
|
61
|
+
if (!tree) continue;
|
|
62
|
+
for (const identifier of tree.rootNode.descendantsOfType('identifier')) {
|
|
63
|
+
const normalized = pythonSourceName(identifier.text);
|
|
64
|
+
if (normalized !== identifier.text) names.add(normalized);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (operation) OP_NORMALIZED_NAMES.set(operation, names);
|
|
68
|
+
return names;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Explicit project source can mutate otherwise-known standard identities. */
|
|
72
|
+
function projectNamespaceEffects(index) {
|
|
73
|
+
const operation = index._opTreeCache;
|
|
74
|
+
if (operation && OP_NAMESPACE_EFFECTS.has(operation)) return OP_NAMESPACE_EFFECTS.get(operation);
|
|
75
|
+
const affected = new Set();
|
|
76
|
+
const { langTraits } = require('../languages');
|
|
77
|
+
for (const [file, entry] of index.files) {
|
|
78
|
+
// The sole consumer treats '*' as affecting every identity. Later
|
|
79
|
+
// files cannot change that answer; this inventory is operation-local.
|
|
80
|
+
if (affected.has('*')) break;
|
|
81
|
+
if (!langTraits(entry.language)?.reflectionApi?.pythonBindingIdentity) continue;
|
|
82
|
+
let source, tree;
|
|
83
|
+
try {
|
|
84
|
+
source = index._readFile(file);
|
|
85
|
+
// A text precheck only. Identities/effects come from the AST.
|
|
86
|
+
if (!/\b(?:builtins|typing|__builtins__|__dict__|__self__|__globals__|__import__|import_module|getattr|vars|globals)\b/.test(source.normalize('NFKC'))) continue;
|
|
87
|
+
tree = index._getParsedTree(file, source, entry.language);
|
|
88
|
+
} catch { affected.add('*'); continue; }
|
|
89
|
+
if (!tree) { affected.add('*'); continue; }
|
|
90
|
+
const node = tree.rootNode;
|
|
91
|
+
const effects = effectsOf(node);
|
|
92
|
+
const memo = new Map();
|
|
93
|
+
for (const effect of namespaceEffects(node, memo)) {
|
|
94
|
+
if (effect.owner === UNKNOWN_NAMESPACE) affected.add('*');
|
|
95
|
+
else if (STANDARD_NAMESPACES.has(effect.owner)) affected.add(`${effect.owner}.${effect.key ?? '*'}`);
|
|
96
|
+
}
|
|
97
|
+
for (const write of effects.writes) {
|
|
98
|
+
if (write.type !== 'attribute') continue;
|
|
99
|
+
for (const origin of resolveIdentity(write, memo, 0, true).origins) {
|
|
100
|
+
if (origin.startsWith(UNKNOWN_ORIGIN)) affected.add('*');
|
|
101
|
+
else if ([...STANDARD_NAMESPACES].some(namespace => origin.startsWith(`${namespace}.`))) affected.add(origin);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (operation) OP_NAMESPACE_EFFECTS.set(operation, affected);
|
|
106
|
+
return affected;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function pythonProjectIdentityAffected(index, identity) {
|
|
110
|
+
const effects = projectNamespaceEffects(index);
|
|
111
|
+
if (effects.has('*')) return true;
|
|
112
|
+
const namespace = identity.split('.')[0];
|
|
113
|
+
if (effects.has(`${namespace}.*`)) return true;
|
|
114
|
+
if (identity.endsWith('.*')) return [...effects].some(effect => effect.startsWith(identity.slice(0, -1)));
|
|
115
|
+
return effects.has(identity) || [...effects].some(effect => effect.startsWith(`${identity}.`));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function bindingSites(scope, name) {
|
|
119
|
+
const found = [];
|
|
120
|
+
for (const spelling of sourceSpellings(scope.tree, name)) {
|
|
121
|
+
for (const binding of pythonScopeBindingKinds(scope, spelling)) {
|
|
122
|
+
if (!found.some(other => sameNode(other.node, binding.node))) found.push(binding);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return found.sort((a, b) => a.node.startIndex - b.node.startIndex);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function sourceScope(node, memo) {
|
|
129
|
+
const candidates = [];
|
|
130
|
+
for (const spelling of sourceSpellings(node.tree, node.text)) {
|
|
131
|
+
const scope = referenceScopeNode(node, spelling, memo);
|
|
132
|
+
if (!scope) return null;
|
|
133
|
+
candidates.push(scope);
|
|
134
|
+
}
|
|
135
|
+
// A normalized spelling can bind in a nearer scope than the written one.
|
|
136
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
137
|
+
if (candidates.some(scope => sameNode(parent, scope))) return parent;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function deferredBoundary(node, scope) {
|
|
143
|
+
let boundary = null;
|
|
144
|
+
for (let child = node, parent = node.parent; parent && !sameNode(parent, scope); child = parent, parent = parent.parent) {
|
|
145
|
+
if (['function_definition', 'lambda'].includes(parent.type) && sameNode(parent.childForFieldName('body'), child)) boundary = parent.startIndex;
|
|
146
|
+
if (parent.type === 'generator_expression') boundary = parent.startIndex;
|
|
147
|
+
}
|
|
148
|
+
return boundary;
|
|
149
|
+
}
|
|
150
|
+
const deferredRead = (node, scope) => deferredBoundary(node, scope) !== null;
|
|
151
|
+
|
|
152
|
+
function inImport(node) {
|
|
153
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
154
|
+
if (['import_statement', 'import_from_statement'].includes(parent.type)) return true;
|
|
155
|
+
if (['expression_statement', 'block', 'module'].includes(parent.type)) break;
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Every assignable leaf, without confusing the receiver/key with a target. */
|
|
161
|
+
function writeTargets(target, out) {
|
|
162
|
+
if (!target) return false;
|
|
163
|
+
if (['identifier', 'attribute', 'subscript'].includes(target.type)) { out.push(target); return true; }
|
|
164
|
+
if (!TARGET_WRAPPERS.has(target.type)) return false;
|
|
165
|
+
let complete = true;
|
|
166
|
+
for (const child of target.namedChildren) complete = writeTargets(child, out) && complete;
|
|
167
|
+
return complete;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function effectsOf(node) {
|
|
171
|
+
const tree = node.tree;
|
|
172
|
+
if (EFFECTS.has(tree)) return EFFECTS.get(tree);
|
|
173
|
+
const escaped = new Set();
|
|
174
|
+
const escapedSites = new Map();
|
|
175
|
+
for (const declaration of tree.rootNode.descendantsOfType(['global_statement', 'nonlocal_statement'])) {
|
|
176
|
+
for (const name of declaration.namedChildren) if (name.type === 'identifier') {
|
|
177
|
+
const identity = pythonSourceName(name.text);
|
|
178
|
+
escaped.add(identity);
|
|
179
|
+
if (!escapedSites.has(identity)) escapedSites.set(identity, []);
|
|
180
|
+
escapedSites.get(identity).push(...bindingSites(declaration, name.text));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const writes = [];
|
|
184
|
+
let unknownWrites = false;
|
|
185
|
+
for (const statement of tree.rootNode.descendantsOfType(['assignment', 'augmented_assignment', 'delete_statement',
|
|
186
|
+
'for_statement', 'for_in_clause', 'as_pattern', 'named_expression'])) {
|
|
187
|
+
const targets = statement.type === 'delete_statement' ? statement.namedChildren
|
|
188
|
+
: [statement.childForFieldName(statement.type === 'as_pattern' ? 'alias'
|
|
189
|
+
: statement.type === 'named_expression' ? 'name' : 'left')].filter(Boolean);
|
|
190
|
+
for (const target of targets) if (!writeTargets(target, writes)) unknownWrites = true;
|
|
191
|
+
}
|
|
192
|
+
const wildcards = tree.rootNode.descendantsOfType('wildcard_import').map(imported => pythonSourceName(imported.parent.childForFieldName('module_name')?.text)).filter(Boolean);
|
|
193
|
+
const identifiers = tree.rootNode.descendantsOfType('identifier');
|
|
194
|
+
const moduleNames = new Set([...BUILTINS, '__builtins__']);
|
|
195
|
+
for (const identifier of identifiers) {
|
|
196
|
+
for (let parent = identifier.parent; parent; parent = parent.parent) {
|
|
197
|
+
if (['import_statement', 'import_from_statement'].includes(parent.type)) {
|
|
198
|
+
const origin = importedIdentity(identifier);
|
|
199
|
+
if ([...STANDARD_NAMESPACES].some(namespace => origin === namespace || origin?.startsWith(`${namespace}.`))) moduleNames.add(pythonSourceName(identifier.text));
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
if (['expression_statement', 'block', 'module'].includes(parent.type)) break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const value = { escaped, escapedSites, writes, unknownWrites, identifiers, moduleNames, calls: tree.rootNode.descendantsOfType('call'),
|
|
206
|
+
attributes: tree.rootNode.descendantsOfType('attribute'),
|
|
207
|
+
wildcard: wildcards.length > 0, wildcards,
|
|
208
|
+
namespaceWrites: new Map(), writtenOrigins: new Map() };
|
|
209
|
+
EFFECTS.set(tree, value);
|
|
210
|
+
return value;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function importedIdentity(site) {
|
|
214
|
+
let part = site;
|
|
215
|
+
while (part.parent && !['import_statement', 'import_from_statement'].includes(part.parent.type)) part = part.parent;
|
|
216
|
+
const statement = part.parent;
|
|
217
|
+
const imported = pythonSourceName(part.type === 'aliased_import' ? part.childForFieldName('name')?.text : part.text);
|
|
218
|
+
if (!imported || !statement) return null;
|
|
219
|
+
if (statement.type === 'import_from_statement') {
|
|
220
|
+
const module = pythonSourceName(statement.childForFieldName('module_name')?.text);
|
|
221
|
+
return module ? `${module}.${imported}` : null;
|
|
222
|
+
}
|
|
223
|
+
return part.type === 'aliased_import' ? imported : imported.split('.')[0];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const UNKNOWN = Object.freeze({ origins: [], certain: false });
|
|
227
|
+
const BUDGET_UNKNOWN = Object.freeze({ origins: [UNKNOWN_ORIGIN], certain: false });
|
|
228
|
+
|
|
229
|
+
function projectValue(pattern, expression, site) {
|
|
230
|
+
while (expression?.type === 'assignment' || expression?.type === 'parenthesized_expression') {
|
|
231
|
+
expression = expression.type === 'assignment' ? expression.childForFieldName('right') : expression.namedChild(0);
|
|
232
|
+
}
|
|
233
|
+
if (sameNode(pattern, site)) return expression;
|
|
234
|
+
if (pattern.type === 'parenthesized_expression') return projectValue(pattern.namedChild(0), expression, site);
|
|
235
|
+
if (!SEQUENCES.has(pattern.type) || !SEQUENCES.has(expression?.type)) return null;
|
|
236
|
+
const children = pattern.namedChildren;
|
|
237
|
+
const position = children.findIndex(child => child.startIndex <= site.startIndex && child.endIndex >= site.endIndex);
|
|
238
|
+
const star = children.findIndex(child => ['list_splat', 'list_splat_pattern'].includes(child.type));
|
|
239
|
+
if (position < 0 || position === star) return null;
|
|
240
|
+
if (star < 0 && children.length !== expression.namedChildCount) return null;
|
|
241
|
+
if (star >= 0 && expression.namedChildCount < children.length - 1) return null;
|
|
242
|
+
const valuePosition = star >= 0 && position > star ? expression.namedChildCount - (children.length - position) : position;
|
|
243
|
+
return projectValue(children[position], expression.namedChild(valuePosition), site);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** One written assignment value, including fixed-size unpacking and chains. */
|
|
247
|
+
function bindingValue(site, before) {
|
|
248
|
+
if (['default_parameter', 'typed_default_parameter'].includes(site.parent?.type)) {
|
|
249
|
+
return site.parent.childForFieldName('value');
|
|
250
|
+
}
|
|
251
|
+
if (site.parent?.type === 'named_expression') {
|
|
252
|
+
return site.parent.endIndex <= before ? site.parent.childForFieldName('value') : null;
|
|
253
|
+
}
|
|
254
|
+
let target = site;
|
|
255
|
+
while (SEQUENCES.has(target.parent?.type) || target.parent?.type === 'parenthesized_expression') target = target.parent;
|
|
256
|
+
const assignment = target.parent;
|
|
257
|
+
if (assignment?.type !== 'assignment' || !sameNode(assignment.childForFieldName('left'), target) || assignment.endIndex > before) return null;
|
|
258
|
+
return projectValue(target, assignment.childForFieldName('right'), site);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Iteration retains possible written values; it never proves a binding exact. */
|
|
262
|
+
function iterationValues(site) {
|
|
263
|
+
let target = site;
|
|
264
|
+
while (TARGET_WRAPPERS.has(target.parent?.type)) target = target.parent;
|
|
265
|
+
const loop = target.parent;
|
|
266
|
+
if (!['for_statement', 'for_in_clause'].includes(loop?.type) || !sameNode(loop.childForFieldName('left'), target)) return [];
|
|
267
|
+
const values = [];
|
|
268
|
+
const pending = [loop.childForFieldName('right')];
|
|
269
|
+
const seen = new Set();
|
|
270
|
+
const memo = new Map();
|
|
271
|
+
let budget = 256;
|
|
272
|
+
while (pending.length) {
|
|
273
|
+
if (--budget < 0) { values.unresolved = true; break; }
|
|
274
|
+
let iterable = pending.pop();
|
|
275
|
+
if (!iterable) continue;
|
|
276
|
+
while (iterable.type === 'parenthesized_expression') iterable = iterable.namedChild(0);
|
|
277
|
+
if (seen.has(iterable.id)) continue;
|
|
278
|
+
seen.add(iterable.id);
|
|
279
|
+
if (['list', 'tuple', 'set'].includes(iterable.type)) {
|
|
280
|
+
for (const value of iterable.namedChildren) {
|
|
281
|
+
if (value.type === 'list_splat') pending.push(value.namedChild(0));
|
|
282
|
+
else {
|
|
283
|
+
const projected = projectValue(target, value, site);
|
|
284
|
+
if (projected) values.push(projected);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} else if (iterable.type === 'identifier') {
|
|
288
|
+
const scope = sourceScope(iterable, memo);
|
|
289
|
+
if (!scope) continue;
|
|
290
|
+
const before = deferredRead(iterable, scope) ? Infinity : iterable.startIndex;
|
|
291
|
+
for (const binding of bindingSites(scope, iterable.text)) {
|
|
292
|
+
const value = bindingValue(binding.node, before);
|
|
293
|
+
if (value) pending.push(value);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return values;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Literal runtime names are not source identifiers. A bound/computed name that
|
|
301
|
+
// cannot be read here stays unknown; it cannot prove a namespace inaccessible.
|
|
302
|
+
function namespaceMember(node, depth = 0) {
|
|
303
|
+
if (!node || depth > 8) return null;
|
|
304
|
+
while (node.type === 'parenthesized_expression') node = node.namedChild(0);
|
|
305
|
+
if (node.type === 'string') return require('./runtime-entrypoints').pythonStringLiteral(node);
|
|
306
|
+
if (node.type === 'concatenated_string') {
|
|
307
|
+
const parts = node.namedChildren.map(child => namespaceMember(child, depth + 1));
|
|
308
|
+
return parts.every(part => part !== null) ? parts.join('') : null;
|
|
309
|
+
}
|
|
310
|
+
if (node.type === 'binary_operator' && node.childForFieldName('operator')?.text === '+') {
|
|
311
|
+
const left = namespaceMember(node.childForFieldName('left'), depth + 1);
|
|
312
|
+
const right = namespaceMember(node.childForFieldName('right'), depth + 1);
|
|
313
|
+
return left !== null && right !== null ? left + right : null;
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function namespaceAttributeOwners(receiver, member, memo, depth) {
|
|
319
|
+
if (member === null) return [UNKNOWN_NAMESPACE];
|
|
320
|
+
if (!NAMESPACE_ATTRIBUTES.has(member)) return [];
|
|
321
|
+
if (member === '__builtins__') return ['builtins'];
|
|
322
|
+
if (member === '__globals__') return [UNKNOWN_NAMESPACE];
|
|
323
|
+
// Unknown bound receivers can be module objects. Losing the callable's
|
|
324
|
+
// identity cannot prove that its explicit namespace carrier is inert.
|
|
325
|
+
const origins = resolveIdentity(receiver, memo, depth + 1, true).origins.map(origin =>
|
|
326
|
+
member === '__self__' ? origin.startsWith('builtins.') ? 'builtins' : UNKNOWN_ORIGIN : origin);
|
|
327
|
+
return standardNamespaceOwners(origins);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function standardNamespaceOwners(origins) {
|
|
331
|
+
// A qualified imported value names a binding, not the object returned by
|
|
332
|
+
// its namespace carrier. Only established standard module identities can
|
|
333
|
+
// retain their owner; every other explicit carrier remains unknown.
|
|
334
|
+
return origins.length && origins.every(origin => STANDARD_NAMESPACES.has(origin)) ? origins : [UNKNOWN_NAMESPACE];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function readonlyNamespaceInput(expression, memo) {
|
|
338
|
+
const argumentsNode = expression.parent;
|
|
339
|
+
const call = argumentsNode?.parent;
|
|
340
|
+
if (argumentsNode?.type !== 'argument_list' || call?.type !== 'call' ||
|
|
341
|
+
!sameNode(argumentsNode.namedChild(0), expression)) return false;
|
|
342
|
+
const fn = resolveIdentity(call.childForFieldName('function'), memo, 0, true);
|
|
343
|
+
return fn.certain && fn.origins.length === 1 && ['builtins.getattr', 'builtins.vars'].includes(fn.origins[0]);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Possible namespace mappings, separate from callable/builtin identity proof. */
|
|
347
|
+
function namespaceOwners(expression, memo, depth = 0) {
|
|
348
|
+
let node = expression;
|
|
349
|
+
if (!node) return [];
|
|
350
|
+
if (depth > 8) return [UNKNOWN_NAMESPACE];
|
|
351
|
+
while (node.type === 'parenthesized_expression') node = node.namedChild(0);
|
|
352
|
+
if (node.type === 'attribute' && NAMESPACE_ATTRIBUTES.has(pythonSourceName(node.childForFieldName('attribute')?.text))) {
|
|
353
|
+
const member = pythonSourceName(node.childForFieldName('attribute')?.text);
|
|
354
|
+
return namespaceAttributeOwners(node.childForFieldName('object'), member, memo, depth);
|
|
355
|
+
}
|
|
356
|
+
if (node.type === 'call') {
|
|
357
|
+
const fn = resolveIdentity(node.childForFieldName('function'), memo, depth + 1, true).origins;
|
|
358
|
+
const args = node.childForFieldName('arguments')?.namedChildren || [];
|
|
359
|
+
if (fn.some(origin => origin.startsWith(UNKNOWN_ORIGIN))) return [UNKNOWN_NAMESPACE];
|
|
360
|
+
if (fn.includes('builtins.getattr') && args.length >= 2) {
|
|
361
|
+
return namespaceAttributeOwners(args[0], namespaceMember(args[1]), memo, depth);
|
|
362
|
+
}
|
|
363
|
+
if (fn.some(origin => ['builtins.__import__', 'importlib.import_module'].includes(origin))) {
|
|
364
|
+
const origins = resolveIdentity(node, memo, depth + 1, true).origins;
|
|
365
|
+
return origins.includes('builtins') ? ['builtins'] : origins.some(origin => origin.startsWith(UNKNOWN_ORIGIN)) ? [UNKNOWN_NAMESPACE] : [];
|
|
366
|
+
}
|
|
367
|
+
if (!args.length && fn.some(origin => ['builtins.globals', 'builtins.locals', 'builtins.vars'].includes(origin))) return [LOCAL_NAMESPACE];
|
|
368
|
+
if (args.length === 1 && fn.includes('builtins.vars')) {
|
|
369
|
+
const origins = resolveIdentity(args[0], memo, depth + 1, true).origins;
|
|
370
|
+
return standardNamespaceOwners(origins);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (node.type === 'identifier') {
|
|
374
|
+
if (pythonSourceName(node.text) === '__builtins__' && resolveIdentity(node, memo, depth + 1, true).origins.includes('builtins')) return ['builtins'];
|
|
375
|
+
const scope = sourceScope(node, memo);
|
|
376
|
+
const key = scope && `identity-sites:${scope.startIndex}:${scope.endIndex}:${pythonSourceName(node.text)}`;
|
|
377
|
+
let bindings = key && memo.get(key);
|
|
378
|
+
if (key && !bindings) { bindings = bindingSites(scope, node.text); memo.set(key, bindings); }
|
|
379
|
+
const sites = bindings ? bindings.map(binding => binding.node)
|
|
380
|
+
: effectsOf(node).writes.filter(target => target.type === 'identifier' && pythonSourceName(target.text) === pythonSourceName(node.text));
|
|
381
|
+
const importedDictionaries = (bindings || []).filter(binding => binding.kind === 'import')
|
|
382
|
+
.map(binding => importedIdentity(binding.node)).filter(origin => origin?.endsWith('.__dict__')).map(origin => origin.slice(0, -9));
|
|
383
|
+
const before = scope && deferredRead(node, scope) ? Infinity : node.startIndex;
|
|
384
|
+
return [...new Set([...importedDictionaries, ...sites.flatMap(site => {
|
|
385
|
+
const value = bindingValue(site, before);
|
|
386
|
+
const values = value ? [value] : iterationValues(site);
|
|
387
|
+
return values.unresolved ? [UNKNOWN_NAMESPACE] : values.flatMap(candidate => namespaceOwners(candidate, memo, depth + 1));
|
|
388
|
+
})])];
|
|
389
|
+
}
|
|
390
|
+
if (['conditional_expression', 'boolean_operator'].includes(node.type)) {
|
|
391
|
+
const arms = node.type === 'boolean_operator' ? [node.childForFieldName('left'), node.childForFieldName('right')]
|
|
392
|
+
: [node.namedChild(0), node.namedChild(node.namedChildCount - 1)];
|
|
393
|
+
return [...new Set(arms.flatMap(arm => namespaceOwners(arm, memo, depth + 1)))];
|
|
394
|
+
}
|
|
395
|
+
return [];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Unknown operations on an exposed namespace cannot preserve an exemption. */
|
|
399
|
+
function namespaceEffects(node, memo) {
|
|
400
|
+
const effects = effectsOf(node);
|
|
401
|
+
if (effects.namespaceEffects) return effects.namespaceEffects;
|
|
402
|
+
const mutations = [];
|
|
403
|
+
// A module object is itself its writable namespace. Capture exposure at
|
|
404
|
+
// the known import/builtin value, before aliases or containers hide it.
|
|
405
|
+
// Builtin function values can carry that module through __self__ too.
|
|
406
|
+
for (const expression of [...effects.identifiers, ...effects.attributes, ...effects.calls]) {
|
|
407
|
+
let root = expression;
|
|
408
|
+
while (root?.type === 'attribute' || root?.type === 'parenthesized_expression') {
|
|
409
|
+
root = root.type === 'attribute' ? root.childForFieldName('object') : root.namedChild(0);
|
|
410
|
+
}
|
|
411
|
+
if (expression.type !== 'call' && (root?.type !== 'identifier' || !effects.moduleNames.has(pythonSourceName(root.text)))) continue;
|
|
412
|
+
if (inImport(expression)) continue;
|
|
413
|
+
const origins = resolveIdentity(expression, memo, 0, true).origins;
|
|
414
|
+
const modules = origins.filter(origin => STANDARD_NAMESPACES.has(origin));
|
|
415
|
+
const boundFunction = origins.some(origin => origin.startsWith('builtins.') && origin !== 'builtins.__dict__' &&
|
|
416
|
+
!UNBOUND_BUILTINS.has(origin.slice(9)));
|
|
417
|
+
if (!modules.length && !boundFunction) continue;
|
|
418
|
+
let value = expression;
|
|
419
|
+
while (value.parent?.type === 'parenthesized_expression') value = value.parent;
|
|
420
|
+
const parent = value.parent;
|
|
421
|
+
if (parent?.type === 'attribute' && sameNode(parent.childForFieldName('object'), value)) continue;
|
|
422
|
+
if (!modules.length && parent?.type === 'call' && sameNode(parent.childForFieldName('function'), value)) continue;
|
|
423
|
+
if (modules.length && readonlyNamespaceInput(value, memo)) continue;
|
|
424
|
+
for (const owner of modules.length ? modules : ['builtins']) mutations.push({ owner, key: null });
|
|
425
|
+
}
|
|
426
|
+
// Record exposure where the namespace is created, before a container,
|
|
427
|
+
// spread or deferred alias can hide it. Only a direct read-only operation
|
|
428
|
+
// on a known builtin/local dictionary proves the mapping unexposed.
|
|
429
|
+
for (const producer of [...effects.calls, ...effects.attributes,
|
|
430
|
+
...effects.identifiers.filter(identifier => effects.moduleNames.has(pythonSourceName(identifier.text)))]) {
|
|
431
|
+
if (inImport(producer)) continue;
|
|
432
|
+
if (producer.type === 'attribute' && !NAMESPACE_ATTRIBUTES.has(pythonSourceName(producer.childForFieldName('attribute')?.text))) continue;
|
|
433
|
+
const owners = namespaceOwners(producer, memo);
|
|
434
|
+
if (!owners.length) continue;
|
|
435
|
+
let expression = producer;
|
|
436
|
+
while (expression.parent?.type === 'parenthesized_expression') expression = expression.parent;
|
|
437
|
+
const parent = expression.parent;
|
|
438
|
+
const readonly = parent?.type === 'attribute' && sameNode(parent.childForFieldName('object'), expression) &&
|
|
439
|
+
NAMESPACE_READS.has(pythonSourceName(parent.childForFieldName('attribute')?.text)) &&
|
|
440
|
+
parent.parent?.type === 'call' && sameNode(parent.parent.childForFieldName('function'), parent) &&
|
|
441
|
+
owners.every(owner => owner === LOCAL_NAMESPACE || STANDARD_NAMESPACES.has(owner));
|
|
442
|
+
if (!readonly) for (const owner of owners) mutations.push({ owner, key: null });
|
|
443
|
+
}
|
|
444
|
+
for (const write of effects.writes) if (write.type === 'subscript') {
|
|
445
|
+
const keyNode = write.childForFieldName('subscript');
|
|
446
|
+
// A bound key may itself need namespace-effect checking. Keeping it
|
|
447
|
+
// unknown here avoids circular proof and conservatively revokes it.
|
|
448
|
+
const key = keyNode?.type === 'string' ? require('./runtime-entrypoints').pythonStringLiteral(keyNode) : null;
|
|
449
|
+
for (const owner of namespaceOwners(write.childForFieldName('value'), memo)) mutations.push({ owner, key });
|
|
450
|
+
}
|
|
451
|
+
// A method value can be called through an alias. Only known read-only dict
|
|
452
|
+
// operations retain exactness; every other operation may write any key.
|
|
453
|
+
for (const attribute of effects.attributes) {
|
|
454
|
+
if (NAMESPACE_READS.has(pythonSourceName(attribute.childForFieldName('attribute')?.text))) continue;
|
|
455
|
+
for (const owner of namespaceOwners(attribute.childForFieldName('object'), memo)) mutations.push({ owner, key: null });
|
|
456
|
+
}
|
|
457
|
+
// Passing the mapping to unknown code exposes it to writes, even when the
|
|
458
|
+
// call's implementation is not read. This does not execute that code.
|
|
459
|
+
for (const call of effects.calls) for (const argument of call.childForFieldName('arguments')?.namedChildren || []) {
|
|
460
|
+
const value = argument.type === 'keyword_argument' ? argument.childForFieldName('value') : argument;
|
|
461
|
+
for (const owner of namespaceOwners(value, memo)) mutations.push({ owner, key: null });
|
|
462
|
+
}
|
|
463
|
+
effects.namespaceEffects = mutations;
|
|
464
|
+
return mutations;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function namespaceWritten(node, memo) {
|
|
468
|
+
const effects = effectsOf(node);
|
|
469
|
+
const name = pythonSourceName(node.text);
|
|
470
|
+
let written = effects.namespaceWrites.get(name);
|
|
471
|
+
if (written !== undefined) return written;
|
|
472
|
+
written = namespaceEffects(node, memo).some(effect => [LOCAL_NAMESPACE, UNKNOWN_NAMESPACE].includes(effect.owner) &&
|
|
473
|
+
(effect.key == null || effect.key === name));
|
|
474
|
+
effects.namespaceWrites.set(name, written);
|
|
475
|
+
return written;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Possible origins survive uncertainty; only a single unwritten origin is an exemption. */
|
|
479
|
+
function resolveIdentity(expression, memo, depth = 0, ignoreEffects = false) {
|
|
480
|
+
let node = expression;
|
|
481
|
+
if (!node) return UNKNOWN;
|
|
482
|
+
if (depth > 8) return BUDGET_UNKNOWN;
|
|
483
|
+
while (node.type === 'parenthesized_expression') node = node.namedChild(0);
|
|
484
|
+
let result;
|
|
485
|
+
if (['named_expression', 'assignment'].includes(node.type)) {
|
|
486
|
+
result = resolveIdentity(node.childForFieldName(node.type === 'assignment' ? 'right' : 'value'), memo, depth + 1, ignoreEffects);
|
|
487
|
+
} else if (['conditional_expression', 'boolean_operator'].includes(node.type)) {
|
|
488
|
+
// The condition is not a candidate value. Both value arms remain
|
|
489
|
+
// possible without interpreting truthiness or executing the program.
|
|
490
|
+
const arms = node.type === 'boolean_operator'
|
|
491
|
+
? [node.childForFieldName('left'), node.childForFieldName('right')]
|
|
492
|
+
: [node.namedChild(0), node.namedChild(node.namedChildCount - 1)];
|
|
493
|
+
result = { origins: [...new Set(arms.flatMap(arm => resolveIdentity(arm, memo, depth + 1, ignoreEffects).origins))], certain: false };
|
|
494
|
+
} else if (node.type === 'attribute') {
|
|
495
|
+
const owner = resolveIdentity(node.childForFieldName('object'), memo, depth + 1, ignoreEffects);
|
|
496
|
+
const member = pythonSourceName(node.childForFieldName('attribute')?.text);
|
|
497
|
+
result = member ? { origins: owner.origins.map(origin => member === '__self__' && origin.startsWith('builtins.')
|
|
498
|
+
? 'builtins' : `${origin}.${member}`), certain: owner.certain && member !== '__self__' } : UNKNOWN;
|
|
499
|
+
} else if (node.type === 'call') {
|
|
500
|
+
const called = resolveIdentity(node.childForFieldName('function'), memo, depth + 1, ignoreEffects);
|
|
501
|
+
const argument = node.childForFieldName('arguments')?.namedChild(0);
|
|
502
|
+
const importsModule = called.origins.some(origin => ['builtins.__import__', 'importlib.import_module'].includes(origin));
|
|
503
|
+
const moduleName = argument?.type === 'string' ? require('./runtime-entrypoints').pythonStringLiteral(argument) : null;
|
|
504
|
+
result = { origins: called.origins.flatMap(origin => origin.startsWith(UNKNOWN_ORIGIN) ? [UNKNOWN_ORIGIN]
|
|
505
|
+
: ['pytest.MonkeyPatch', 'pytest.MonkeyPatch.context'].includes(origin) ? ['pytest.MonkeyPatch.instance'] : []), certain: called.certain };
|
|
506
|
+
if (importsModule && (moduleName === 'builtins' || moduleName == null)) result = {
|
|
507
|
+
origins: [...result.origins, moduleName === 'builtins' ? 'builtins' : UNKNOWN_ORIGIN], certain: false,
|
|
508
|
+
};
|
|
509
|
+
} else if (node.type === 'identifier') {
|
|
510
|
+
const name = pythonSourceName(node.text);
|
|
511
|
+
let scope = sourceScope(node, memo);
|
|
512
|
+
const effects = effectsOf(node);
|
|
513
|
+
// global/nonlocal prevent exact lexical proof, but the original import
|
|
514
|
+
// or stable alias is still a possible reflective origin.
|
|
515
|
+
if (!scope && effects.escaped.has(name)) {
|
|
516
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
517
|
+
if (!['function_definition', 'module'].includes(parent.type)) continue;
|
|
518
|
+
if (parent.type === 'function_definition' && parent.descendantsOfType('global_statement')
|
|
519
|
+
.some(statement => statement.namedChildren.some(identifier => pythonSourceName(identifier.text) === name))) {
|
|
520
|
+
scope = node.tree.rootNode; break;
|
|
521
|
+
}
|
|
522
|
+
if (bindingSites(parent, node.text).length) { scope = parent; break; }
|
|
523
|
+
if (parent.type === 'module') scope = parent;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (!scope) return UNKNOWN;
|
|
527
|
+
const deferred = deferredRead(node, scope);
|
|
528
|
+
const key = `identity-sites:${scope.startIndex}:${scope.endIndex}:${name}`;
|
|
529
|
+
let sites = memo.get(key);
|
|
530
|
+
if (!sites) { sites = bindingSites(scope, node.text); memo.set(key, sites); }
|
|
531
|
+
// The right side reads the old value, before this assignment binds.
|
|
532
|
+
// This also applies to an unpacking target and to a walrus expression.
|
|
533
|
+
sites = sites.filter(binding => {
|
|
534
|
+
let assignment = binding.node.parent;
|
|
535
|
+
while (assignment && (SEQUENCES.has(assignment.type) || assignment.type === 'parenthesized_expression')) assignment = assignment.parent;
|
|
536
|
+
const value = assignment?.type === 'assignment' ? assignment.childForFieldName('right')
|
|
537
|
+
: assignment?.type === 'named_expression' ? assignment.childForFieldName('value') : null;
|
|
538
|
+
return !value || node.startIndex < value.startIndex || node.endIndex > value.endIndex;
|
|
539
|
+
});
|
|
540
|
+
if (effects.escaped.has(name)) {
|
|
541
|
+
const combined = [...sites, ...(effects.escapedSites.get(name) || [])];
|
|
542
|
+
sites = combined.filter((binding, position) => combined.findIndex(other => sameNode(other.node, binding.node)) === position);
|
|
543
|
+
} else if (sites.length > 1) {
|
|
544
|
+
// An unconditional write before the enclosing function was even
|
|
545
|
+
// created replaces earlier values. Writes after its creation are
|
|
546
|
+
// still possible when the deferred body eventually executes.
|
|
547
|
+
const limit = deferred ? deferredBoundary(node, scope) : node.startIndex;
|
|
548
|
+
let lastCertain = -1;
|
|
549
|
+
for (let position = 0; position < sites.length; position++) {
|
|
550
|
+
const binding = sites[position];
|
|
551
|
+
let unconditional = binding.node.startIndex < limit;
|
|
552
|
+
for (let parent = binding.node.parent; parent && !sameNode(parent, scope); parent = parent.parent) {
|
|
553
|
+
if (['if_statement', 'for_statement', 'while_statement', 'try_statement', 'match_statement'].includes(parent.type)) unconditional = false;
|
|
554
|
+
}
|
|
555
|
+
if (unconditional) lastCertain = position;
|
|
556
|
+
}
|
|
557
|
+
if (lastCertain >= 0) sites = sites.slice(lastCertain);
|
|
558
|
+
}
|
|
559
|
+
if (sites.length === 0) result = { origins: [...new Set([
|
|
560
|
+
...(BUILTINS.has(name) ? [`builtins.${name}`] : []),
|
|
561
|
+
...(name === '__builtins__' ? ['builtins'] : []),
|
|
562
|
+
...effects.wildcards.map(module => `${module}.${name}`),
|
|
563
|
+
])], certain: !effects.wildcard && name !== '__builtins__' };
|
|
564
|
+
else {
|
|
565
|
+
let certain = sites.length === 1 && !deferred;
|
|
566
|
+
const origins = [];
|
|
567
|
+
for (const binding of sites) {
|
|
568
|
+
const site = binding.node;
|
|
569
|
+
for (let parent = site.parent; parent && !sameNode(parent, scope); parent = parent.parent) {
|
|
570
|
+
if (['if_statement', 'for_statement', 'while_statement', 'try_statement', 'match_statement'].includes(parent.type)) certain = false;
|
|
571
|
+
}
|
|
572
|
+
if (binding.kind === 'import') {
|
|
573
|
+
const imported = importedIdentity(site);
|
|
574
|
+
if (imported) origins.push(imported); else certain = false;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
const parent = site.parent;
|
|
578
|
+
let value = bindingValue(site, deferred ? Infinity : node.startIndex);
|
|
579
|
+
if (['default_parameter', 'typed_default_parameter'].includes(parent?.type)) certain = false;
|
|
580
|
+
if (parent?.type === 'as_pattern_target' && parent.parent?.type === 'as_pattern') value = parent.parent.namedChild(0);
|
|
581
|
+
else if (parent?.type === 'as_pattern') value = parent.namedChild(0);
|
|
582
|
+
const iterated = value ? [] : iterationValues(site);
|
|
583
|
+
const resolved = value ? resolveIdentity(value, memo, depth + 1, ignoreEffects)
|
|
584
|
+
: { origins: iterated.unresolved ? [UNKNOWN_ORIGIN]
|
|
585
|
+
: [...new Set(iterated.flatMap(candidate => resolveIdentity(candidate, memo, depth + 1, ignoreEffects).origins))], certain: false };
|
|
586
|
+
origins.push(...resolved.origins);
|
|
587
|
+
certain &&= resolved.certain;
|
|
588
|
+
}
|
|
589
|
+
result = { origins: [...new Set(origins)], certain };
|
|
590
|
+
}
|
|
591
|
+
if (!ignoreEffects) {
|
|
592
|
+
if (effects.unknownWrites || effects.escaped.has(name) || effects.wildcard || namespaceWritten(node, memo)) result = { ...result, certain: false };
|
|
593
|
+
}
|
|
594
|
+
} else return UNKNOWN;
|
|
595
|
+
if (!ignoreEffects && result.certain && result.origins.length) {
|
|
596
|
+
const effects = effectsOf(node);
|
|
597
|
+
const effectKey = result.origins.join('\0');
|
|
598
|
+
let written = effects.writtenOrigins.get(effectKey);
|
|
599
|
+
if (written === undefined) written = effects.unknownWrites || effects.writes.some(write => {
|
|
600
|
+
if (write.type === 'attribute') return resolveIdentity(write, memo, 0, true).origins
|
|
601
|
+
.some(origin => origin.startsWith(UNKNOWN_ORIGIN) || result.origins.includes(origin));
|
|
602
|
+
return false;
|
|
603
|
+
}) || namespaceEffects(node, memo).some(effect => effect.owner === UNKNOWN_NAMESPACE || (effect.owner !== LOCAL_NAMESPACE &&
|
|
604
|
+
result.origins.some(origin => effect.key == null ? origin.startsWith(`${effect.owner}.`) : origin === `${effect.owner}.${effect.key}`))) || effects.calls.some(call => {
|
|
605
|
+
const fn = resolveIdentity(call.childForFieldName('function'), memo, 0, true);
|
|
606
|
+
if (!fn.origins.some(origin => origin === 'builtins.setattr' || origin === 'builtins.delattr')) return false;
|
|
607
|
+
const args = call.childForFieldName('arguments')?.namedChildren || [];
|
|
608
|
+
const owner = resolveIdentity(args[0], memo, 0, true);
|
|
609
|
+
const member = require('./runtime-entrypoints').pythonStringLiteral(args[1]);
|
|
610
|
+
return owner.origins.some(origin => member ? result.origins.includes(`${origin}.${member}`)
|
|
611
|
+
: result.origins.some(candidate => candidate.startsWith(`${origin}.`)));
|
|
612
|
+
});
|
|
613
|
+
effects.writtenOrigins.set(effectKey, written);
|
|
614
|
+
if (written) result = { ...result, certain: false };
|
|
615
|
+
}
|
|
616
|
+
return result;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function pythonIdentity(expression, memo = new Map(), depth = 0) {
|
|
620
|
+
const resolved = resolveIdentity(expression, memo, depth);
|
|
621
|
+
return resolved.certain && resolved.origins.length === 1 ? resolved.origins[0] : null;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function pythonPossibleIdentities(expression, memo = new Map()) {
|
|
625
|
+
// This reader consumes only possible origins. Namespace effects can
|
|
626
|
+
// revoke certainty, but cannot erase an origin; exact identity and
|
|
627
|
+
// registration readers still evaluate those effects independently.
|
|
628
|
+
const origins = resolveIdentity(expression, memo, 0, true).origins;
|
|
629
|
+
// Exhaustion means an unresolved runtime API, never absence of runtime
|
|
630
|
+
// use. This conservative vocabulary is consumed only by liveness readers;
|
|
631
|
+
// it cannot grant an exact identity or a confirmed call edge.
|
|
632
|
+
return origins.some(origin => origin.startsWith(UNKNOWN_ORIGIN))
|
|
633
|
+
? [...new Set([...origins.filter(origin => !origin.startsWith(UNKNOWN_ORIGIN)), ...POSSIBLE_RUNTIME_APIS])]
|
|
634
|
+
: origins;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** Written nesting is retained on saved declarations for runtime module paths. */
|
|
638
|
+
function pythonRuntimeOwners(declaration) {
|
|
639
|
+
const names = [];
|
|
640
|
+
for (let parent = declaration.parent; parent; parent = parent.parent) {
|
|
641
|
+
if (parent.type === 'class_definition') {
|
|
642
|
+
const name = parent.childForFieldName('name')?.text;
|
|
643
|
+
if (name) names.push(name);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return names.reverse();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function pythonBoundValue(node, depth = 0) {
|
|
650
|
+
if (!node || depth > 8 || node.type !== 'identifier') return node;
|
|
651
|
+
const scope = sourceScope(node);
|
|
652
|
+
if (!scope) return null;
|
|
653
|
+
const effects = effectsOf(node);
|
|
654
|
+
if (effects.unknownWrites || effects.escaped.has(pythonSourceName(node.text)) || effects.wildcard || deferredRead(node, scope) || namespaceWritten(node, new Map())) return null;
|
|
655
|
+
const sites = bindingSites(scope, node.text);
|
|
656
|
+
if (sites.length !== 1) return null;
|
|
657
|
+
const site = sites[0].node;
|
|
658
|
+
const assignment = site.parent;
|
|
659
|
+
if (assignment?.type !== 'assignment' || !sameNode(assignment.childForFieldName('left'), site) ||
|
|
660
|
+
assignment.endIndex > node.startIndex) return null;
|
|
661
|
+
for (let parent = assignment.parent; parent && !sameNode(parent, scope); parent = parent.parent) {
|
|
662
|
+
if (['if_statement', 'for_statement', 'while_statement', 'try_statement', 'match_statement'].includes(parent.type)) return null;
|
|
663
|
+
}
|
|
664
|
+
const value = pythonBoundValue(assignment.childForFieldName('right'), depth + 1);
|
|
665
|
+
if (['dictionary', 'list', 'set'].includes(value?.type) &&
|
|
666
|
+
scope.descendantsOfType('identifier').some(reference => pythonSourceName(reference.text) === pythonSourceName(node.text) &&
|
|
667
|
+
!sameNode(reference, node) && !sameNode(reference, site))) return null;
|
|
668
|
+
return value;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function registrationUnknown(declaration) {
|
|
672
|
+
const decorated = declaration?.parent;
|
|
673
|
+
if (decorated?.type !== 'decorated_definition') return false;
|
|
674
|
+
const memo = new Map();
|
|
675
|
+
const descriptors = new Set(['builtins.property', 'builtins.classmethod', 'builtins.staticmethod']);
|
|
676
|
+
const accessors = new Set(['getter', 'setter', 'deleter']);
|
|
677
|
+
const propertyBefore = (owner, here) => {
|
|
678
|
+
owner = pythonSourceName(owner);
|
|
679
|
+
let property = false;
|
|
680
|
+
for (const statement of here.parent?.namedChildren || []) {
|
|
681
|
+
if (statement.startIndex >= here.startIndex) break;
|
|
682
|
+
const definition = statement.type === 'decorated_definition'
|
|
683
|
+
? statement.namedChildren.find(n => n.type === 'function_definition') : statement;
|
|
684
|
+
if (definition?.type === 'function_definition' && pythonSourceName(definition.childForFieldName('name')?.text) === owner) {
|
|
685
|
+
const decorators = statement.type === 'decorated_definition'
|
|
686
|
+
? statement.namedChildren.filter(n => n.type === 'decorator').map(n => n.namedChild(0)) : [];
|
|
687
|
+
property = decorators.length === 1 && (pythonIdentity(decorators[0], memo) === 'builtins.property' ||
|
|
688
|
+
(property && decorators[0]?.type === 'attribute' && pythonSourceName(decorators[0].childForFieldName('object')?.text) === owner &&
|
|
689
|
+
accessors.has(pythonSourceName(decorators[0].childForFieldName('attribute')?.text))));
|
|
690
|
+
} else if (statement.descendantsOfType('identifier').some(n => pythonSourceName(n.text) === owner)) {
|
|
691
|
+
// A write, callback, or opaque statement may replace the descriptor.
|
|
692
|
+
property = false;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return property;
|
|
696
|
+
};
|
|
697
|
+
return decorated.namedChildren.filter(node => node.type === 'decorator').some(decorator => {
|
|
698
|
+
const expression = decorator.namedChild(0);
|
|
699
|
+
if (descriptors.has(pythonIdentity(expression, memo))) return false;
|
|
700
|
+
if (expression?.type !== 'attribute' || !accessors.has(pythonSourceName(expression.childForFieldName('attribute')?.text))) return true;
|
|
701
|
+
const owner = expression.childForFieldName('object');
|
|
702
|
+
return owner?.type !== 'identifier' || !propertyBefore(owner.text, decorated);
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
module.exports = { pythonIdentity, pythonPossibleIdentities, pythonRuntimeOwners, pythonBoundValue,
|
|
707
|
+
pythonSourceName, pythonNormalizedNames, pythonProjectIdentityAffected, registrationUnknown };
|