ucn 5.5.2 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +35 -12
- package/.claude/skills/ucn/references/commands.md +21 -17
- package/.claude/skills/ucn/references/resolution-rules.md +4 -0
- package/.claude/skills/ucn/references/trust-contract.md +4 -4
- package/CHANGELOG.md +474 -0
- package/README.md +71 -29
- package/cli/flags.js +184 -0
- package/cli/index.js +138 -324
- package/core/account.js +44 -87
- package/core/analysis.js +273 -107
- package/core/ast-analysis.js +2 -0
- package/core/bridge.js +29 -65
- package/core/cache.js +101 -16
- package/core/callers.js +352 -70
- package/core/capture.js +331 -0
- package/core/change-kinds.js +888 -0
- package/core/check.js +1117 -139
- package/core/client-receivers.js +189 -0
- package/core/command-contracts.js +7 -6
- package/core/constructed-class.js +1195 -0
- package/core/contract-membership.js +142 -0
- package/core/deadcode.js +388 -85
- package/core/deletion.js +401 -0
- package/core/discovery.js +104 -9
- package/core/entrypoints.js +12 -6
- package/core/evidence-bands.js +68 -0
- package/core/evidence-bands.json +44 -0
- package/core/execute.js +172 -161
- package/core/hot-summary.js +241 -0
- package/core/index-ir.js +3 -1
- package/core/inprocess-clients.js +41 -15
- package/core/ir.js +5 -1
- package/core/member-signatures.js +789 -0
- package/core/output/analysis-ext.js +20 -10
- package/core/output/analysis.js +98 -60
- package/core/output/check.js +98 -37
- package/core/output/doctor.js +34 -1
- package/core/output/find.js +2 -1
- package/core/output/lines.js +63 -3
- package/core/output/public.js +30 -11
- package/core/output/refactoring.js +32 -0
- package/core/output/reporting.js +21 -2
- package/core/output/search.js +11 -2
- package/core/output/tracing.js +35 -7
- package/core/output-budget.js +124 -57
- package/core/parallel-build.js +3 -3
- package/core/project.js +106 -23
- package/core/provenance-type.js +12 -5
- package/core/public-command.js +35 -9
- package/core/python-modules.js +6 -0
- package/core/reflection.js +166 -23
- package/core/registry.js +623 -72
- package/core/reporting.js +69 -34
- package/core/request-helpers.js +63 -7
- package/core/review-items.js +75 -0
- package/core/search.js +89 -128
- package/core/shared.js +0 -45
- package/core/snapshot.js +350 -0
- package/core/source-universe.js +707 -0
- package/core/test-units.js +3624 -0
- package/core/tracing.js +150 -96
- package/core/verify.js +27 -11
- package/languages/adapter.js +8 -3
- package/languages/c-family.js +12 -0
- package/languages/csharp.js +634 -0
- package/languages/go.js +120 -0
- package/languages/index.js +422 -4
- package/languages/java.js +712 -0
- package/languages/javascript.js +1460 -2
- package/languages/lexical-scope.js +68 -0
- package/languages/python.js +1672 -1
- package/languages/rust.js +91 -0
- package/mcp/server.js +44 -110
- package/package.json +11 -6
|
@@ -0,0 +1,1195 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The class a constructor expression makes an instance of.
|
|
5
|
+
*
|
|
6
|
+
* `Name(...)` where classes are called and `new Name(...)` construct the
|
|
7
|
+
* class the name denotes where it is written. The lookup is the one behind
|
|
8
|
+
* constructor callers (core/provenance-type.js): the scopes around the site,
|
|
9
|
+
* the module's own bindings, then what the module imports, by local name.
|
|
10
|
+
* A receiver made by such an expression has that class, whichever same-named
|
|
11
|
+
* classes the project declares elsewhere.
|
|
12
|
+
*
|
|
13
|
+
* Answers of `constructedClass`:
|
|
14
|
+
* { definition } the one class definition the name denotes
|
|
15
|
+
* { unread: true } nothing this lookup reads: the file binds the name
|
|
16
|
+
* nowhere (a global of another script, a builtin), no
|
|
17
|
+
* project class is spelled that way, or the name is a
|
|
18
|
+
* function that owns prototype members. The caller's
|
|
19
|
+
* own rules decide.
|
|
20
|
+
* null the file binds the name and the lookup does not settle
|
|
21
|
+
* on one class: a parameter, a function, a rebound or
|
|
22
|
+
* conditionally bound name
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const { langTraits } = require('../languages');
|
|
26
|
+
|
|
27
|
+
const CLASS_KINDS = new Set(['class', 'struct', 'record', 'enum']);
|
|
28
|
+
|
|
29
|
+
const familyOf = (index, def) => {
|
|
30
|
+
const language = index.files.get(def.file)?.language;
|
|
31
|
+
return langTraits(language)?.nameReferenceFamily || language;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Import bindings whose local name is `name`. */
|
|
35
|
+
function localImportBindings(entry, name) {
|
|
36
|
+
return (entry?.importBindings || []).filter(binding =>
|
|
37
|
+
(binding.alias || binding.name) === name && binding.name !== '*');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The file binds `name` at module scope by some statement of its own. */
|
|
41
|
+
function boundInModule(entry, name) {
|
|
42
|
+
if (!entry) return false;
|
|
43
|
+
if (localImportBindings(entry, name).length > 0) return true;
|
|
44
|
+
if ((entry.moduleAssignedNames || []).includes(name)) return true;
|
|
45
|
+
if ((entry.moduleValueAliases || []).some(alias => alias.name === name)) return true;
|
|
46
|
+
if ((entry.configurationItems || []).some(item => item.scope === 0 && item.name === name)) return true;
|
|
47
|
+
return (entry.symbols || []).some(def => def.name === name && !def.className &&
|
|
48
|
+
def.lexicalScopeStartLine == null);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The construction record a constructor-typed receiver was made by: the
|
|
53
|
+
* call of the receiver's type name inside the span its type was read from
|
|
54
|
+
* (`u = User(..)`, `new User(..).m()`). Null when no single record is there.
|
|
55
|
+
*/
|
|
56
|
+
function constructionRecordOf(index, file, call) {
|
|
57
|
+
const evidence = call.receiverTypeEvidence;
|
|
58
|
+
if (!evidence || !Number.isInteger(evidence.start) || !Number.isInteger(evidence.end)) return null;
|
|
59
|
+
// The name as written: an alias already followed keeps its spelling.
|
|
60
|
+
const spelled = call.receiverTypeAlias || evidence.type || call.receiverType;
|
|
61
|
+
const found = [];
|
|
62
|
+
for (const record of index.getCachedCalls(file) || []) {
|
|
63
|
+
if (record.line < evidence.line) continue;
|
|
64
|
+
const start = record.callSite?.start;
|
|
65
|
+
if (!Number.isInteger(start) || start < evidence.start || start >= evidence.end) continue;
|
|
66
|
+
if (record.name !== spelled) continue;
|
|
67
|
+
found.push(record);
|
|
68
|
+
}
|
|
69
|
+
return found.length === 1 ? found[0] : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function constructedClass(index, file, record) {
|
|
73
|
+
if (!record || typeof record !== 'object') return null;
|
|
74
|
+
const memo = index._opMemo?.('constructedClass', () => new WeakMap());
|
|
75
|
+
if (memo?.has(record)) return memo.get(record);
|
|
76
|
+
const result = constructedClassUncached(index, file, record);
|
|
77
|
+
memo?.set(record, result);
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A function that owns prototype members (`function Gadget() {}` with
|
|
83
|
+
* `Gadget.prototype.render = ..`) is a class in all but syntax. Whether the
|
|
84
|
+
* file's binding of `name` is such a function: declared here, or the one
|
|
85
|
+
* import of the name resolves to the file declaring it.
|
|
86
|
+
*/
|
|
87
|
+
function bindsConstructorFunction(index, entry, file, name) {
|
|
88
|
+
const declaredIn = (target, declared) => (index.symbols.get(declared) || []).some(def =>
|
|
89
|
+
def.type === 'function' && def.file === target && !def.className &&
|
|
90
|
+
(index.files.get(target)?.symbols || []).some(member => member.className === declared));
|
|
91
|
+
if (declaredIn(file, name)) return true;
|
|
92
|
+
const bindings = localImportBindings(entry, name);
|
|
93
|
+
if (bindings.length !== 1) return false;
|
|
94
|
+
const relative = entry.moduleResolved?.[bindings[0].module];
|
|
95
|
+
return !!relative && declaredIn(require('path').resolve(index.root, relative), bindings[0].name);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The one import that binds a name for the scope a reference reads it in,
|
|
100
|
+
* where a file imports the name more than once in different scopes (`from
|
|
101
|
+
* a import Name` at module level, `from b import Name` inside a function):
|
|
102
|
+
* the function's own import when the nearest function scope binding the
|
|
103
|
+
* name binds it by that statement alone, the module's one import when the
|
|
104
|
+
* reference resolves at module scope. Null when the scope's binding is not
|
|
105
|
+
* one import statement.
|
|
106
|
+
*/
|
|
107
|
+
function scopeImportTarget(index, file, entry, record) {
|
|
108
|
+
const start = record.callSite?.start;
|
|
109
|
+
if (!Number.isInteger(start)) return null;
|
|
110
|
+
const scopes = require('../languages/lexical-scope');
|
|
111
|
+
if (scopes.familyOf(entry.language) !== 'python') return null;
|
|
112
|
+
return withTree(index, file, tree => {
|
|
113
|
+
const node = tree.rootNode.descendantForIndex(start);
|
|
114
|
+
if (!node || node.text !== record.name) return null;
|
|
115
|
+
let statement = scopes.localImportStatement(node, entry.language, record.name);
|
|
116
|
+
if (!statement && scopes.referenceScope(node, entry.language, new Map(), record.name,
|
|
117
|
+
{ includeClasses: true }) === 'module') {
|
|
118
|
+
const sites = scopes.pythonScopeBindingKinds(tree.rootNode, record.name);
|
|
119
|
+
if (sites.length !== 1 || sites[0].kind !== 'import') return null;
|
|
120
|
+
statement = sites[0].node;
|
|
121
|
+
while (statement && statement.type !== 'import_statement' &&
|
|
122
|
+
statement.type !== 'import_from_statement') statement = statement.parent;
|
|
123
|
+
}
|
|
124
|
+
if (!statement) return null;
|
|
125
|
+
const first = statement.startPosition.row + 1, last = statement.endPosition.row + 1;
|
|
126
|
+
const bindings = localImportBindings(entry, record.name).filter(binding =>
|
|
127
|
+
binding.line >= first && binding.line <= last && binding.kind !== 'import');
|
|
128
|
+
if (bindings.length !== 1) return null;
|
|
129
|
+
const relative = entry.moduleResolved?.[bindings[0].module];
|
|
130
|
+
return relative ? { file: require('path').resolve(index.root, relative), name: bindings[0].name } : null;
|
|
131
|
+
}) || null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function constructedClassUncached(index, file, record) {
|
|
135
|
+
const entry = index.files.get(file);
|
|
136
|
+
const language = entry?.language;
|
|
137
|
+
const traits = langTraits(language);
|
|
138
|
+
if (!traits?.hasConstructors) return null;
|
|
139
|
+
const { isConstruction, constructionTypeCandidates } = require('./constructor-calls');
|
|
140
|
+
if (!isConstruction(record, null, language)) return null;
|
|
141
|
+
// `self.Widget()` and `factory.Widget()` read an attribute of a value;
|
|
142
|
+
// only a module's attribute is a name the module binds.
|
|
143
|
+
if (record.isMethod && !record.receiverIsModule) return null;
|
|
144
|
+
const qualifier = record.isMethod ? record.receiver || null : null;
|
|
145
|
+
// `m.User()` where a parameter or local is named `m`: no module's attribute.
|
|
146
|
+
if (qualifier && record.receiverLocalBinding) return null;
|
|
147
|
+
if (qualifier && qualifiedBindingChanged(index, file, record, qualifier)) return null;
|
|
148
|
+
const name = record.name;
|
|
149
|
+
const family = traits.nameReferenceFamily || language;
|
|
150
|
+
const candidates = constructionTypeCandidates(index, name).filter(def =>
|
|
151
|
+
CLASS_KINDS.has(def.type) && def.file && familyOf(index, def) === family);
|
|
152
|
+
if (candidates.length === 0) return { unread: true };
|
|
153
|
+
const bound = qualifier ? true : !!record.localShadow || boundInModule(entry, name);
|
|
154
|
+
if (!bound) return { unread: true };
|
|
155
|
+
const { occurrenceIdentity } = require('./provenance-facts');
|
|
156
|
+
const { captureTypeLookup } = require('./provenance-type');
|
|
157
|
+
const site = occurrenceIdentity(entry.relativePath, record);
|
|
158
|
+
const query = { file, relativePath: site.file, line: site.line, column: site.column,
|
|
159
|
+
name, qualifier, localShadow: !!record.localShadow, construction: true, functional: false, identity: true };
|
|
160
|
+
const found = [];
|
|
161
|
+
for (const owner of candidates) {
|
|
162
|
+
if (captureTypeLookup(index, query, owner)) found.push(owner);
|
|
163
|
+
}
|
|
164
|
+
// A Python package may expose its classes through a star export. The
|
|
165
|
+
// module-name reader retains that closure as evidence; it still has to
|
|
166
|
+
// select one class and pass the same construction-result proof.
|
|
167
|
+
if (found.length === 0 && qualifier && traits.nameReferenceFamily === 'python') {
|
|
168
|
+
const { pythonModulePath, pythonModuleScope, pythonModuleTypes } = require('./python-modules');
|
|
169
|
+
const modules = pythonModulePath(index, file, qualifier);
|
|
170
|
+
if (modules && !modules.unknown && pythonModuleScope(index, file, qualifier, record.callSite)) {
|
|
171
|
+
for (const module of modules.files) {
|
|
172
|
+
for (const def of pythonModuleTypes(index, module, name, candidates)) {
|
|
173
|
+
if (!found.includes(def)) found.push(def);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (found.length === 1) return constructionKeepsClass(index, found[0]) ? { definition: found[0] } : null;
|
|
179
|
+
// Several imports of the name in different scopes: the one of the scope
|
|
180
|
+
// the reference reads it in decides.
|
|
181
|
+
if (!qualifier && !record.localShadow && localImportBindings(entry, name).length > 1 &&
|
|
182
|
+
!(entry.moduleAssignedNames || []).includes(name)) {
|
|
183
|
+
const imported = scopeImportTarget(index, file, entry, record);
|
|
184
|
+
if (imported) {
|
|
185
|
+
const reached = constructionTypeCandidates(index, imported.name).filter(def =>
|
|
186
|
+
CLASS_KINDS.has(def.type) && def.file && familyOf(index, def) === family &&
|
|
187
|
+
captureTypeLookup(index, { file: imported.file, line: 1, name: imported.name,
|
|
188
|
+
construction: true, identity: true }, def));
|
|
189
|
+
return reached.length === 1 && constructionKeepsClass(index, reached[0])
|
|
190
|
+
? { definition: reached[0] } : null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (found.length === 0 && !qualifier && !record.localShadow &&
|
|
194
|
+
bindsConstructorFunction(index, entry, file, name)) return { unread: true };
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── What calling the class returns ──────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
const FUNCTION_NODES = new Set(['function_definition', 'function_declaration', 'function_expression',
|
|
201
|
+
'function', 'arrow_function', 'generator_function', 'generator_function_declaration', 'method_definition',
|
|
202
|
+
'lambda', 'class_definition', 'class_declaration', 'class', 'class_body']);
|
|
203
|
+
|
|
204
|
+
/** The parsed tree of a file for one read; `done()` frees a tree parsed here. */
|
|
205
|
+
function withTree(index, file, read) {
|
|
206
|
+
const entry = index.files.get(file);
|
|
207
|
+
if (!entry) return undefined;
|
|
208
|
+
const { getParser, safeParse } = require('../languages');
|
|
209
|
+
let owned;
|
|
210
|
+
try {
|
|
211
|
+
const content = index._readFile(file);
|
|
212
|
+
const tree = index._getParsedTree?.(file, content, entry.language) ||
|
|
213
|
+
(owned = safeParse(getParser(entry.language), content));
|
|
214
|
+
return tree ? read(tree, content) : undefined;
|
|
215
|
+
} catch {
|
|
216
|
+
return undefined;
|
|
217
|
+
} finally {
|
|
218
|
+
owned?.delete?.();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Pure syntax facts are shared only within an operation and one actual
|
|
223
|
+
* tree. A fallback parse may be deleted at withTree's exit; a later parse
|
|
224
|
+
* has a different tree key and can never return its nodes. */
|
|
225
|
+
function syntaxFact(index, node, kind, read) {
|
|
226
|
+
const memo = node?.tree && index?._opMemo?.('constructedSyntaxFacts', () => new WeakMap());
|
|
227
|
+
if (!memo) return read();
|
|
228
|
+
let facts = memo.get(node.tree);
|
|
229
|
+
if (!facts) { facts = new Map(); memo.set(node.tree, facts); }
|
|
230
|
+
const key = `${node.id}\0${kind}`;
|
|
231
|
+
if (facts.has(key)) return facts.get(key);
|
|
232
|
+
const value = read();
|
|
233
|
+
facts.set(key, value);
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** `return` statements of a function's own body (not of functions and classes nested in it). */
|
|
238
|
+
function ownReturns(body, index = null) {
|
|
239
|
+
return syntaxFact(index, body, 'returns', () => ownReturnsUncached(body));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function ownReturnsUncached(body) {
|
|
243
|
+
const out = [];
|
|
244
|
+
const stack = body ? [...body.namedChildren] : [];
|
|
245
|
+
while (stack.length > 0) {
|
|
246
|
+
const node = stack.pop();
|
|
247
|
+
if (FUNCTION_NODES.has(node.type)) continue;
|
|
248
|
+
if (node.type === 'return_statement') { out.push(node); continue; }
|
|
249
|
+
for (const child of node.namedChildren) stack.push(child);
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** The function node whose declaration starts at a symbol's source span. */
|
|
255
|
+
function functionNodeOf(tree, def) {
|
|
256
|
+
const start = def.sourceSpan?.start;
|
|
257
|
+
if (!Number.isInteger(start)) return null;
|
|
258
|
+
let node = tree.rootNode.descendantForIndex(start);
|
|
259
|
+
while (node && !['function_definition', 'function_declaration', 'generator_function_declaration',
|
|
260
|
+
'method_definition', 'decorated_definition'].includes(node.type)) node = node.parent;
|
|
261
|
+
if (node?.type === 'decorated_definition') node = node.childForFieldName('definition');
|
|
262
|
+
if (!node || node.startPosition.row + 1 > def.endLine || node.endPosition.row + 1 < def.startLine) return null;
|
|
263
|
+
return node;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function firstParameterName(fn) {
|
|
267
|
+
const parameters = fn.childForFieldName('parameters');
|
|
268
|
+
let first = parameters?.namedChild(0);
|
|
269
|
+
while (first && first.type !== 'identifier') {
|
|
270
|
+
first = first.childForFieldName('pattern') || first.childForFieldName('name') ||
|
|
271
|
+
first.namedChildren.find(child => child.type === 'identifier') || null;
|
|
272
|
+
}
|
|
273
|
+
return first?.text || null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const FUNCTION_DEFINITIONS = new Set(['function_definition', 'function_declaration',
|
|
277
|
+
'generator_function_declaration']);
|
|
278
|
+
const FUNCTION_VALUES = new Set(['arrow_function', 'function_expression', 'function', 'lambda']);
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The function a name denotes for code in `scope`: declared in its body,
|
|
282
|
+
* else at the top of the file. The last declaration of the name is the one
|
|
283
|
+
* bound when the code runs (typing overload stubs come before the body).
|
|
284
|
+
*/
|
|
285
|
+
function functionNamed(tree, scope, name) {
|
|
286
|
+
const declaredIn = statements => {
|
|
287
|
+
let found = null;
|
|
288
|
+
for (let statement of statements || []) {
|
|
289
|
+
if (statement.type === 'decorated_definition') statement = statement.childForFieldName('definition');
|
|
290
|
+
if (statement.type === 'export_statement') statement = statement.childForFieldName('declaration') || statement;
|
|
291
|
+
if (FUNCTION_DEFINITIONS.has(statement?.type) && statement.childForFieldName('name')?.text === name) found = statement;
|
|
292
|
+
if (statement?.type === 'lexical_declaration' || statement?.type === 'variable_declaration') {
|
|
293
|
+
for (const declarator of statement.namedChildren) {
|
|
294
|
+
const value = declarator.childForFieldName?.('value');
|
|
295
|
+
if (declarator.childForFieldName?.('name')?.text === name && FUNCTION_VALUES.has(value?.type)) found = value;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return found;
|
|
300
|
+
};
|
|
301
|
+
if (scope?.type === 'function_definition') {
|
|
302
|
+
const bindings = require('../languages/lexical-scope').pythonScopeBindingKinds(scope, name);
|
|
303
|
+
if (bindings.some(binding => binding.kind !== 'def')) return null;
|
|
304
|
+
}
|
|
305
|
+
const local = declaredIn(scope?.childForFieldName('body')?.namedChildren);
|
|
306
|
+
if (local) return local;
|
|
307
|
+
if (tree.rootNode.type === 'module') {
|
|
308
|
+
const bindings = require('../languages/lexical-scope').pythonScopeBindingKinds(tree.rootNode, name);
|
|
309
|
+
if (bindings.some(binding => binding.kind !== 'def')) return null;
|
|
310
|
+
}
|
|
311
|
+
return declaredIn(tree.rootNode.namedChildren);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Whether a decorator function hands back the class it is given. Every
|
|
316
|
+
* `return` of its own body has to be one of: its first parameter; a call
|
|
317
|
+
* of a keeping function that passes that parameter first, or passes no
|
|
318
|
+
* positional argument at all (a keeping decorator asked for with options);
|
|
319
|
+
* a function that keeps its own first parameter, by name, written in place
|
|
320
|
+
* or wrapped in `partial(..)` (the decorator used with arguments). A
|
|
321
|
+
* function without a `return` keeps the class only where a decorator may
|
|
322
|
+
* return nothing.
|
|
323
|
+
*/
|
|
324
|
+
function ownNodes(body, index = null) {
|
|
325
|
+
return syntaxFact(index, body, 'nodes', () => ownNodesUncached(body));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function ownNodesUncached(body) {
|
|
329
|
+
if (!body) return [];
|
|
330
|
+
// Only these node kinds can affect the identity proofs below. Ask the
|
|
331
|
+
// native tree for them in one traversal rather than materializing every
|
|
332
|
+
// literal, argument list and identifier through namedChildren.
|
|
333
|
+
const kinds = ['assignment', 'assignment_expression', 'variable_declarator', 'augmented_assignment',
|
|
334
|
+
'augmented_assignment_expression', 'named_expression', 'for_in_clause', 'delete_statement',
|
|
335
|
+
'call', 'call_expression', 'attribute', ...FUNCTION_NODES];
|
|
336
|
+
const roots = [], open = [];
|
|
337
|
+
let skippedUntil = -1;
|
|
338
|
+
for (const node of body.descendantsOfType(kinds)) {
|
|
339
|
+
if (node.id === body.id) continue; // the old walk starts at body's children
|
|
340
|
+
const start = node.startIndex, end = node.endIndex;
|
|
341
|
+
if (start < skippedUntil) continue;
|
|
342
|
+
if (FUNCTION_NODES.has(node.type)) { skippedUntil = end; continue; }
|
|
343
|
+
while (open.length && !(start >= open.at(-1).start && end <= open.at(-1).end)) open.pop();
|
|
344
|
+
const item = { node, start, end, children: [] };
|
|
345
|
+
(open.at(-1)?.children || roots).push(item);
|
|
346
|
+
open.push(item);
|
|
347
|
+
}
|
|
348
|
+
// Retain the old reverse-sibling, parent-before-child order, including
|
|
349
|
+
// when irrelevant container nodes lay between two selected nodes.
|
|
350
|
+
const result = [], stack = [...roots];
|
|
351
|
+
while (stack.length) {
|
|
352
|
+
const item = stack.pop();
|
|
353
|
+
result.push(item.node);
|
|
354
|
+
stack.push(...item.children);
|
|
355
|
+
}
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function qualifiedBindingChanged(index, file, record, qualifier) {
|
|
360
|
+
const memo = index._opMemo?.('constructedNamespaceWrites', () => new Map());
|
|
361
|
+
const key = `${file}\0${qualifier}\0${record.name}`;
|
|
362
|
+
if (memo?.has(key)) return memo.get(key);
|
|
363
|
+
const root = qualifier.split('.')[0];
|
|
364
|
+
const entry = index.files.get(file);
|
|
365
|
+
if ((entry.moduleAssignedNames || []).includes(root)) return true;
|
|
366
|
+
const result = withTree(index, file, (tree, content) => {
|
|
367
|
+
const scopes = require('../languages/lexical-scope');
|
|
368
|
+
const scopeMemo = new Map();
|
|
369
|
+
const writtenPath = node => {
|
|
370
|
+
if (node?.type === 'identifier') return { text: node.text, root: node };
|
|
371
|
+
if (!['attribute', 'member_expression', 'subscript', 'subscript_expression'].includes(node?.type)) return null;
|
|
372
|
+
const object = writtenPath(node.childForFieldName('object') || node.childForFieldName('value'));
|
|
373
|
+
const member = node.childForFieldName('attribute') || node.childForFieldName('property');
|
|
374
|
+
const key = member?.text || literalMember(node.childForFieldName('subscript') || node.childForFieldName('index'));
|
|
375
|
+
return object ? { text: key ? `${object.text}.${key}` : object.text, root: object.root } : null;
|
|
376
|
+
};
|
|
377
|
+
const wanted = `${qualifier}.${record.name}`;
|
|
378
|
+
// Only these two Python calls can write a named attribute. Text is
|
|
379
|
+
// a prefilter; the same AST call/name/argument checks still decide.
|
|
380
|
+
const kinds = ['assignment', 'augmented_assignment', 'assignment_expression',
|
|
381
|
+
'augmented_assignment_expression', 'delete_statement', 'unary_expression'];
|
|
382
|
+
if (content.includes('setattr') || content.includes('delattr')) kinds.push('call');
|
|
383
|
+
for (const node of tree.rootNode.descendantsOfType(kinds)) {
|
|
384
|
+
const target = node.childForFieldName('left') || node.childForFieldName('argument') ||
|
|
385
|
+
(node.type === 'delete_statement' ? node.namedChild(0) : null);
|
|
386
|
+
let written = writtenPath(target);
|
|
387
|
+
if (node.type === 'call') {
|
|
388
|
+
const fn = node.childForFieldName('function');
|
|
389
|
+
const args = node.childForFieldName('arguments')?.namedChildren || [];
|
|
390
|
+
if (fn?.type === 'identifier' && ['setattr', 'delattr'].includes(fn.text)) {
|
|
391
|
+
written = writtenPath(args[0]);
|
|
392
|
+
const member = literalMember(args[1]);
|
|
393
|
+
if (written && member) written = { ...written, text: `${written.text}.${member}` };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (!written || !(written.text === wanted || wanted.startsWith(`${written.text}.`))) continue;
|
|
397
|
+
const scope = scopes.referenceScope(written.root, entry.language, scopeMemo, root);
|
|
398
|
+
if (scope === 'module' || !(scope && typeof scope === 'object' && scope.local)) return true;
|
|
399
|
+
}
|
|
400
|
+
return false;
|
|
401
|
+
}) !== false;
|
|
402
|
+
memo?.set(key, result);
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function literalMember(node) {
|
|
407
|
+
if (node?.type !== 'string') return null;
|
|
408
|
+
const value = require('../languages/utils').extractStringArg(node);
|
|
409
|
+
return value && !value.interp && !value.value.includes('\\') ? value.value : null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** The last own method is the method a Python class binds under its name. */
|
|
413
|
+
function ownHelper(fn, name) {
|
|
414
|
+
let owner = fn.parent;
|
|
415
|
+
while (owner && owner.type !== 'class_definition') owner = owner.parent;
|
|
416
|
+
return owner?.childForFieldName('body')?.namedChildren.map(node => node.type === 'decorated_definition'
|
|
417
|
+
? node.childForFieldName('definition') : node).filter(node => node?.childForFieldName('name')?.text === name).at(-1);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Read only Python's proven builtin property descriptor composition. A
|
|
421
|
+
* custom decorator, rebound property name, or non-adjacent replacement is
|
|
422
|
+
* not a body we can invoke as an ordinary getter or setter. */
|
|
423
|
+
function propertyDescriptor(index, file, fn, depth = 0) {
|
|
424
|
+
return syntaxFact(index, fn, `property:${file}:${depth}`, () => propertyDescriptorUncached(index, file, fn, depth));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function propertyDescriptorUncached(index, file, fn, depth) {
|
|
428
|
+
if (!fn || depth > 4 || fn.parent?.type !== 'decorated_definition') return null;
|
|
429
|
+
const wrapper = fn.parent;
|
|
430
|
+
const decorators = wrapper.namedChildren.filter(node => node.type === 'decorator');
|
|
431
|
+
if (decorators.length !== 1) return null;
|
|
432
|
+
const head = decorators[0].namedChild(0);
|
|
433
|
+
const entry = index.files.get(file);
|
|
434
|
+
if (head?.type === 'identifier') {
|
|
435
|
+
const bindings = localImportBindings(entry, head.text);
|
|
436
|
+
const builtin = head.text === 'property' && !boundInModule(entry, head.text);
|
|
437
|
+
const imported = bindings.length === 1 && bindings[0].module === 'builtins' &&
|
|
438
|
+
bindings[0].name === 'property' && !entry.moduleResolved?.builtins &&
|
|
439
|
+
!(entry.moduleAssignedNames || []).includes(head.text);
|
|
440
|
+
const scope = require('../languages/lexical-scope').referenceScope(head, entry.language,
|
|
441
|
+
new Map(), head.text, { includeClasses: true });
|
|
442
|
+
return (builtin || imported) && scope === 'module' ? { get: fn } : null;
|
|
443
|
+
}
|
|
444
|
+
if (head?.type !== 'attribute') return null;
|
|
445
|
+
const object = head.childForFieldName('object');
|
|
446
|
+
const slot = head.childForFieldName('attribute')?.text;
|
|
447
|
+
const name = fn.childForFieldName('name')?.text;
|
|
448
|
+
if (object?.type !== 'identifier' || object.text !== name || !['getter', 'setter', 'deleter'].includes(slot)) return null;
|
|
449
|
+
const siblings = wrapper.parent?.namedChildren.filter(node => node.type !== 'comment') || [];
|
|
450
|
+
const { sameNode } = require('../languages/utils');
|
|
451
|
+
const position = siblings.findIndex(node => sameNode(node, wrapper));
|
|
452
|
+
const previous = position > 0 ? siblings[position - 1] : null;
|
|
453
|
+
const priorFunction = previous?.type === 'decorated_definition' ? previous.childForFieldName('definition') : null;
|
|
454
|
+
if (priorFunction?.childForFieldName('name')?.text !== name) return null;
|
|
455
|
+
const prior = propertyDescriptor(index, file, priorFunction, depth + 1);
|
|
456
|
+
return prior ? { ...prior, [slot === 'getter' ? 'get' : slot === 'setter' ? 'set' : 'delete']: fn } : null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Resolve every self use from the constructed class, including a base
|
|
460
|
+
* initializer's call of a descendant override. The callback consumes AST
|
|
461
|
+
* nodes before a separately parsed declaring file is released. */
|
|
462
|
+
function instanceMember(context, fn, name, read) {
|
|
463
|
+
if (!context?.definition) {
|
|
464
|
+
const helper = fn && ownHelper(fn, name);
|
|
465
|
+
return read(helper && helper.parent?.type !== 'decorated_definition' ? { method: helper } :
|
|
466
|
+
helper ? { unknown: true } : { data: true }, null, context);
|
|
467
|
+
}
|
|
468
|
+
const { index, definition } = context;
|
|
469
|
+
const selected = instanceMemberSelection(index, definition, name);
|
|
470
|
+
if (!selected.member) return read(selected, null, context);
|
|
471
|
+
const { member, instanceShadow } = selected;
|
|
472
|
+
return withTree(index, member.file, tree => {
|
|
473
|
+
const node = functionNodeOf(tree, member);
|
|
474
|
+
const next = { ...context, file: member.file };
|
|
475
|
+
if (!node) return read({ unknown: true }, tree, next);
|
|
476
|
+
const property = propertyDescriptor(index, member.file, node);
|
|
477
|
+
if (property) return read(property, tree, next);
|
|
478
|
+
if (node.parent?.type === 'decorated_definition') {
|
|
479
|
+
return read({ unknown: true }, tree, next);
|
|
480
|
+
}
|
|
481
|
+
return read({ method: node, instanceShadow }, tree, next);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** Symbol-only lookup facts live for this operation. In particular, do not
|
|
486
|
+
* rebuild a filtered definitions array for every self read: member lookup
|
|
487
|
+
* memoizes by that array's identity. No parsed nodes are retained here. */
|
|
488
|
+
function instanceMemberSelection(index, definition, name) {
|
|
489
|
+
const memo = index._opMemo?.('constructedInstanceMembers', () => new WeakMap());
|
|
490
|
+
let byName = memo?.get(definition);
|
|
491
|
+
if (byName?.has(name)) return byName.get(name);
|
|
492
|
+
const result = instanceMemberSelectionUncached(index, definition, name);
|
|
493
|
+
if (memo) {
|
|
494
|
+
if (!byName) { byName = new Map(); memo.set(definition, byName); }
|
|
495
|
+
byName.set(name, result);
|
|
496
|
+
}
|
|
497
|
+
return result;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function instanceMemberSelectionUncached(index, definition, name) {
|
|
501
|
+
const { classDefsNamed, findDeclaringClass, ownedBy } = require('./class-identity');
|
|
502
|
+
const key = classDefsNamed(index, definition.name).entries.find(entry => entry.def === definition)?.key;
|
|
503
|
+
const candidates = index.symbols.get(name) || [];
|
|
504
|
+
const ancestry = constructionAncestry(index, definition);
|
|
505
|
+
// Instance storage cannot hide a data descriptor inherited from a base.
|
|
506
|
+
// A bare annotation also creates no class binding at runtime.
|
|
507
|
+
// Read syntax only for possible owners; the same identity lookup would
|
|
508
|
+
// discard definite nonowners, including module values of this name.
|
|
509
|
+
const found = findDeclaringClass(index, { name: definition.name, def: definition, key }, candidates.filter(member =>
|
|
510
|
+
!member.instanceAttribute && (!ancestry?.length || ancestry.some(ref => ownedBy(index, member, ref) !== 'no')) &&
|
|
511
|
+
classMemberKind(index, member) !== 'annotation'));
|
|
512
|
+
if (!found?.members.length) return { data: true };
|
|
513
|
+
if (found.uncertain) return { unknown: true };
|
|
514
|
+
const declared = found.members.filter(member => !member.instanceAttribute)
|
|
515
|
+
.sort((a, b) => a.startLine - b.startLine || a.endLine - b.endLine);
|
|
516
|
+
const member = declared.at(-1);
|
|
517
|
+
if (!member) return { data: true };
|
|
518
|
+
if (!dispatchMember(member)) return classMemberKind(index, member) === 'data' ? { data: true } : { unknown: true };
|
|
519
|
+
const instanceShadow = (ancestry || []).some(ref => candidates.some(item =>
|
|
520
|
+
item.instanceAttribute && item.file === ref.def.file && item.className === ref.def.name &&
|
|
521
|
+
item.startLine >= ref.def.startLine && item.endLine <= ref.def.endLine));
|
|
522
|
+
return { member, instanceShadow };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function classMemberKind(index, member) {
|
|
526
|
+
if (dispatchMember(member)) return 'method';
|
|
527
|
+
const memo = index._opMemo?.('constructedClassFields', () => new WeakMap());
|
|
528
|
+
if (memo?.has(member)) return memo.get(member);
|
|
529
|
+
const kind = withTree(index, member.file, tree => {
|
|
530
|
+
const assignments = tree.rootNode.descendantsOfType('assignment').filter(node =>
|
|
531
|
+
node.startPosition.row + 1 === member.startLine && node.childForFieldName('left')?.text === member.name);
|
|
532
|
+
// Field records do not yet carry a byte span. Several assignments
|
|
533
|
+
// of this name on one line cannot be selected by the line alone.
|
|
534
|
+
if (assignments.length !== 1) return 'unknown';
|
|
535
|
+
const value = assignments[0].childForFieldName('right');
|
|
536
|
+
if (!value) return 'annotation';
|
|
537
|
+
return ['integer', 'float', 'string', 'true', 'false', 'none', 'list', 'dictionary', 'set', 'tuple',
|
|
538
|
+
'list_comprehension', 'dictionary_comprehension', 'set_comprehension'].includes(value.type) ? 'data' : 'unknown';
|
|
539
|
+
}) || 'unknown';
|
|
540
|
+
memo?.set(member, kind);
|
|
541
|
+
return kind;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function dispatchMember(member) {
|
|
545
|
+
return !member.instanceAttribute && (member.isMethod || member.params ||
|
|
546
|
+
['function', 'method', 'constructor', 'property', 'getter', 'setter', 'classmethod', 'staticmethod', 'static'].includes(member.type));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function propertyEffect(context, fn, name, operation) {
|
|
550
|
+
return instanceMember(context, fn, name, (descriptor, tree, next) => {
|
|
551
|
+
if (descriptor.unknown) return true;
|
|
552
|
+
if (!descriptor.get && !descriptor.set && !descriptor.delete) return false;
|
|
553
|
+
const body = descriptor[operation];
|
|
554
|
+
if (!body || !next?.index) return true;
|
|
555
|
+
return bodyChangesIdentity(next.index, next.file, tree, body, next.helperSensitive || next.sensitive || new Set(),
|
|
556
|
+
(next.depth || 0) + 1, next.helperSensitive || next.sensitive || new Set(), next.definition, true);
|
|
557
|
+
}) !== false;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** Reading a field or a bound method does not pass the receiver itself.
|
|
561
|
+
* A method result may do so; read its returns before treating it as data.
|
|
562
|
+
* Containers and calls carrying an identity stay escapes when unmodeled. */
|
|
563
|
+
function expressionCarriesIdentity(value, aliases, fn, depth = 0, context = null) {
|
|
564
|
+
if (!value) return false;
|
|
565
|
+
if (depth > 4) return true;
|
|
566
|
+
if (value.type === 'identifier') return aliases.has(value.text);
|
|
567
|
+
// A syntax subtree without any alias spelling cannot carry this
|
|
568
|
+
// identity. This only avoids walks; the AST still decides every hit.
|
|
569
|
+
const text = value.text;
|
|
570
|
+
if (![...aliases].some(alias => text.includes(alias))) return false;
|
|
571
|
+
if (value.type === 'keyword_argument') return expressionCarriesIdentity(value.childForFieldName('value'), aliases, fn, depth, context);
|
|
572
|
+
if (['attribute', 'member_expression', 'subscript', 'subscript_expression'].includes(value.type)) {
|
|
573
|
+
const object = value.childForFieldName('object') || value.childForFieldName('value');
|
|
574
|
+
const member = value.childForFieldName('attribute') || value.childForFieldName('property');
|
|
575
|
+
if (!expressionCarriesIdentity(object, aliases, fn, depth, context)) return false;
|
|
576
|
+
if (!member) return true; // an identity's subscription result is unread
|
|
577
|
+
return instanceMember(context, fn, member.text, (descriptor, tree, next) => {
|
|
578
|
+
if (descriptor.unknown) return true;
|
|
579
|
+
if (!descriptor.get) return false;
|
|
580
|
+
if (propertyEffect(next, fn, member.text, 'get')) return true;
|
|
581
|
+
const parameter = firstParameterName(descriptor.get);
|
|
582
|
+
return ownReturns(descriptor.get.childForFieldName('body'), next?.index).some(ret => expressionCarriesIdentity(ret.namedChild(0),
|
|
583
|
+
identityAliases(descriptor.get.childForFieldName('body'), parameter, next?.index), descriptor.get, depth + 1, next));
|
|
584
|
+
}) !== false;
|
|
585
|
+
}
|
|
586
|
+
if (['call', 'call_expression'].includes(value.type)) {
|
|
587
|
+
if ((value.childForFieldName('arguments')?.namedChildren || []).some(arg =>
|
|
588
|
+
expressionCarriesIdentity(arg, aliases, fn, depth, context))) return true;
|
|
589
|
+
const callee = value.childForFieldName('function');
|
|
590
|
+
const object = callee?.childForFieldName('object');
|
|
591
|
+
if (object?.type === 'identifier' && aliases.has(object.text)) {
|
|
592
|
+
const member = callee.childForFieldName('attribute') || callee.childForFieldName('property');
|
|
593
|
+
return instanceMember(context, fn, member?.text, (descriptor, tree, next) => {
|
|
594
|
+
const helper = descriptor.method;
|
|
595
|
+
if (!helper || descriptor.instanceShadow) return true;
|
|
596
|
+
const parameter = firstParameterName(helper);
|
|
597
|
+
return ownReturns(helper.childForFieldName('body'), next?.index).some(ret => expressionCarriesIdentity(ret.namedChild(0),
|
|
598
|
+
identityAliases(helper.childForFieldName('body'), parameter, next?.index), helper, depth + 1, next));
|
|
599
|
+
}) !== false;
|
|
600
|
+
}
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
return value.namedChildren.some(child => expressionCarriesIdentity(child, aliases, fn, depth, context));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function identityAliases(body, parameter, index = null) {
|
|
607
|
+
return syntaxFact(index, body, `aliases:${parameter}`, () => identityAliasesUncached(body, parameter, index));
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function identityAliasesUncached(body, parameter, index) {
|
|
611
|
+
const nodes = ownNodes(body, index);
|
|
612
|
+
const aliases = new Set(parameter ? [parameter] : []);
|
|
613
|
+
let grew = true;
|
|
614
|
+
while (grew) {
|
|
615
|
+
grew = false;
|
|
616
|
+
for (const node of nodes) {
|
|
617
|
+
if (!['assignment', 'assignment_expression', 'variable_declarator'].includes(node.type)) continue;
|
|
618
|
+
const left = node.childForFieldName('left') || node.childForFieldName('name');
|
|
619
|
+
let right = node.childForFieldName('right') || node.childForFieldName('value');
|
|
620
|
+
while (right?.type === 'parenthesized_expression') right = right.namedChild(0);
|
|
621
|
+
if (left?.type === 'identifier' && right?.type === 'identifier' && aliases.has(right.text) && !aliases.has(left.text)) {
|
|
622
|
+
aliases.add(left.text); grew = true;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return aliases;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** Writes to a parameter, or to a lookup/constructor slot on its value,
|
|
630
|
+
* defeat an identity return. Writes to unrelated data attributes do not. */
|
|
631
|
+
function identityWrites(body, parameter, sensitive, trackIdentity = true, context = null) {
|
|
632
|
+
const nodes = ownNodes(body, context?.index);
|
|
633
|
+
const aliases = identityAliases(body, parameter, context?.index);
|
|
634
|
+
const fn = body?.parent;
|
|
635
|
+
const writes = new Set();
|
|
636
|
+
let changesValue = false;
|
|
637
|
+
const target = (node, operation = 'set') => {
|
|
638
|
+
if (!node) return;
|
|
639
|
+
if (node.type === 'identifier') { writes.add(node.text); return; }
|
|
640
|
+
const object = node.childForFieldName('object');
|
|
641
|
+
const member = node.childForFieldName('attribute') || node.childForFieldName('property');
|
|
642
|
+
if (object && expressionCarriesIdentity(object, aliases, fn, 0, context) &&
|
|
643
|
+
(!member || sensitive.has(member.text) || propertyEffect(context, fn, member.text, operation))) changesValue = true;
|
|
644
|
+
if (!object) for (const child of node.namedChildren) target(child, operation);
|
|
645
|
+
};
|
|
646
|
+
for (const node of nodes) {
|
|
647
|
+
if (['assignment', 'augmented_assignment', 'assignment_expression', 'augmented_assignment_expression',
|
|
648
|
+
'named_expression', 'for_in_clause'].includes(node.type)) target(node.childForFieldName('left') || node.childForFieldName('name'));
|
|
649
|
+
if (node.type === 'delete_statement') for (const child of node.namedChildren) target(child, 'delete');
|
|
650
|
+
if (['call', 'call_expression'].includes(node.type)) {
|
|
651
|
+
const fn = node.childForFieldName('function');
|
|
652
|
+
const args = node.childForFieldName('arguments')?.namedChildren || [];
|
|
653
|
+
if (fn?.type === 'identifier' && ['setattr', 'delattr'].includes(fn.text) &&
|
|
654
|
+
args[0]?.type === 'identifier' && aliases.has(args[0].text)) {
|
|
655
|
+
const key = literalMember(args[1]);
|
|
656
|
+
if (!key || sensitive.has(key) || propertyEffect(context, body?.parent, key, fn.text === 'delattr' ? 'delete' : 'set')) changesValue = true;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
// A captured identity or one put into a container escapes this proof.
|
|
661
|
+
// Do not make nested-function or container spelling an identity guarantee.
|
|
662
|
+
if (trackIdentity && fn?.type === 'function_definition') {
|
|
663
|
+
const { referenceScopeNode } = require('../languages/lexical-scope');
|
|
664
|
+
const { sameNode } = require('../languages/utils');
|
|
665
|
+
const scopeMemo = new Map();
|
|
666
|
+
for (const nested of body.descendantsOfType([...FUNCTION_NODES])) {
|
|
667
|
+
if (nested.childForFieldName('body')?.descendantsOfType('identifier').some(node => aliases.has(node.text) &&
|
|
668
|
+
sameNode(referenceScopeNode(node, node.text, scopeMemo), fn))) changesValue = true;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
for (const node of nodes) {
|
|
672
|
+
if (!['assignment', 'assignment_expression', 'variable_declarator'].includes(node.type)) continue;
|
|
673
|
+
let value = node.childForFieldName('right') || node.childForFieldName('value');
|
|
674
|
+
while (value?.type === 'parenthesized_expression') value = value.namedChild(0);
|
|
675
|
+
const left = node.childForFieldName('left') || node.childForFieldName('name');
|
|
676
|
+
if (trackIdentity && expressionCarriesIdentity(value, aliases, fn, 0, context) &&
|
|
677
|
+
!(value?.type === 'identifier' && left?.type === 'identifier')) changesValue = true;
|
|
678
|
+
}
|
|
679
|
+
const bindings = fn?.type === 'function_definition' && parameter
|
|
680
|
+
? require('../languages/lexical-scope').pythonScopeBindingKinds(fn, parameter) : null;
|
|
681
|
+
return { writes, aliases, changesValue: changesValue || writes.has(parameter) || (bindings && bindings.length !== 1) };
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/** An argument carrying the identity. Wrappers we cannot bind to a helper's
|
|
685
|
+
* parameter are escapes, including splats and containers. */
|
|
686
|
+
function identityArgument(call, aliases, fn, context) {
|
|
687
|
+
const args = call.childForFieldName('arguments')?.namedChildren || [];
|
|
688
|
+
const found = [];
|
|
689
|
+
for (let index = 0; index < args.length; index++) {
|
|
690
|
+
const arg = args[index];
|
|
691
|
+
let value = arg.type === 'keyword_argument' ? arg.childForFieldName('value') : arg;
|
|
692
|
+
while (value?.type === 'parenthesized_expression') value = value.namedChild(0);
|
|
693
|
+
const direct = value?.type === 'identifier' && aliases.has(value.text);
|
|
694
|
+
if (direct || expressionCarriesIdentity(value, aliases, fn, 0, context)) {
|
|
695
|
+
found.push({ index, keyword: arg.type === 'keyword_argument' ? arg.childForFieldName('name')?.text : null,
|
|
696
|
+
unread: !direct });
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return found.length === 0 ? null : found.length === 1 ? found[0] : { unread: true };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function argumentNamesFirstParameter(argument, helper) {
|
|
703
|
+
return !argument || (!!helper && !argument.unread && (argument.keyword
|
|
704
|
+
? argument.keyword === firstParameterName(helper) : argument.index === 0));
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function builtinReference(context, node) {
|
|
708
|
+
const entry = context?.index?.files.get(context.file);
|
|
709
|
+
return node?.type === 'identifier' && entry && !boundInModule(entry, node.text) &&
|
|
710
|
+
require('../languages/lexical-scope').referenceScope(node, entry.language,
|
|
711
|
+
new Map(), node.text, { includeClasses: true }) === 'module';
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** Follow project helpers handed the instance while it is being initialized
|
|
715
|
+
* or entered. Their return value does not matter here; writes to identity do. */
|
|
716
|
+
function bodyChangesIdentity(index, file, tree, fn, sensitive, depth = 0, helperSensitive = sensitive, definition = null, provenDescriptor = false) {
|
|
717
|
+
if (!fn || depth > 4 || (!provenDescriptor && fn.parent?.type === 'decorated_definition')) return true;
|
|
718
|
+
const memo = tree && fn.tree && index._opMemo?.('constructedIdentityEffects', () => new WeakMap());
|
|
719
|
+
if (!memo) return bodyChangesIdentityUncached(index, file, tree, fn, sensitive, depth, helperSensitive, definition, provenDescriptor);
|
|
720
|
+
let trees = memo.get(tree);
|
|
721
|
+
if (!trees) { trees = new WeakMap(); memo.set(tree, trees); }
|
|
722
|
+
let definitions = trees.get(fn.tree);
|
|
723
|
+
if (!definitions) { definitions = new Map(); trees.set(fn.tree, definitions); }
|
|
724
|
+
let effects = definitions.get(definition);
|
|
725
|
+
if (!effects) { effects = new Map(); definitions.set(definition, effects); }
|
|
726
|
+
// Both sensitivity sets and the remaining recursion depth affect the
|
|
727
|
+
// answer. Actual subclass identity selects self dispatch in base bodies.
|
|
728
|
+
const key = JSON.stringify([file, fn.id, depth, provenDescriptor,
|
|
729
|
+
[...sensitive].sort(), [...helperSensitive].sort()]);
|
|
730
|
+
if (effects.has(key)) return effects.get(key);
|
|
731
|
+
const value = bodyChangesIdentityUncached(index, file, tree, fn, sensitive, depth, helperSensitive, definition, provenDescriptor);
|
|
732
|
+
effects.set(key, value);
|
|
733
|
+
return value;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function bodyChangesIdentityUncached(index, file, tree, fn, sensitive, depth = 0, helperSensitive = sensitive, definition = null, provenDescriptor = false) {
|
|
737
|
+
if (!fn || depth > 4 || (!provenDescriptor && fn.parent?.type === 'decorated_definition')) return true;
|
|
738
|
+
const parameter = firstParameterName(fn);
|
|
739
|
+
const body = fn.childForFieldName('body');
|
|
740
|
+
const context = { index, file, definition, sensitive, helperSensitive, depth };
|
|
741
|
+
const facts = identityWrites(body, parameter, sensitive, true, context);
|
|
742
|
+
if (facts.changesValue) return true;
|
|
743
|
+
for (const call of ownNodes(body, index)) {
|
|
744
|
+
if (call.type === 'attribute' && call.childForFieldName('object')?.type === 'identifier' &&
|
|
745
|
+
facts.aliases.has(call.childForFieldName('object').text)) {
|
|
746
|
+
const { sameNode } = require('../languages/utils');
|
|
747
|
+
const written = ['assignment', 'augmented_assignment'].includes(call.parent?.type) &&
|
|
748
|
+
sameNode(call.parent.childForFieldName('left'), call);
|
|
749
|
+
if (!written && propertyEffect(context, fn, call.childForFieldName('attribute')?.text, 'get')) return true;
|
|
750
|
+
}
|
|
751
|
+
if (!['call', 'call_expression'].includes(call.type)) continue;
|
|
752
|
+
const callee = call.childForFieldName('function');
|
|
753
|
+
const argument = identityArgument(call, facts.aliases, fn, context);
|
|
754
|
+
const object = callee?.childForFieldName('object');
|
|
755
|
+
const member = callee?.childForFieldName('attribute') || callee?.childForFieldName('property');
|
|
756
|
+
const onInstance = object?.type === 'identifier' && facts.aliases.has(object.text);
|
|
757
|
+
if (!argument && !onInstance) continue;
|
|
758
|
+
if (argument?.unread) return true;
|
|
759
|
+
if (['setattr', 'delattr'].includes(callee?.text) && builtinReference(context, callee)) continue;
|
|
760
|
+
const name = member?.text || (callee?.type === 'identifier' ? callee.text : null);
|
|
761
|
+
if (!name) return true;
|
|
762
|
+
// A proven builtin attribute-presence check only invokes that
|
|
763
|
+
// attribute's getter. It does not hand the instance to an unread
|
|
764
|
+
// helper; any project descriptor still has to preserve identity.
|
|
765
|
+
if (!object && name === 'hasattr' && builtinReference(context, callee)) {
|
|
766
|
+
const args = call.childForFieldName('arguments')?.namedChildren || [];
|
|
767
|
+
const attribute = args.length === 2 && literalMember(args[1]);
|
|
768
|
+
if (argument.index !== 0 || !attribute || propertyEffect(context, fn, attribute, 'get')) return true;
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
if (onInstance) {
|
|
772
|
+
if (argument || instanceMember(context, fn, name, (descriptor, other, next) => !descriptor.method || descriptor.instanceShadow ||
|
|
773
|
+
bodyChangesIdentity(index, next.file, other || tree, descriptor.method, helperSensitive,
|
|
774
|
+
depth + 1, helperSensitive, definition)) !== false) return true;
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
const helper = !object ? functionNamed(tree, fn, name) : null;
|
|
778
|
+
if (helper) {
|
|
779
|
+
if (!argumentNamesFirstParameter(argument, helper) ||
|
|
780
|
+
bodyChangesIdentity(index, file, tree, helper, helperSensitive, depth + 1, helperSensitive, definition)) return true;
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
const entry = index.files.get(file);
|
|
784
|
+
const targets = [];
|
|
785
|
+
if (object) {
|
|
786
|
+
const { pythonModulePath, pythonModuleScope } = require('./python-modules');
|
|
787
|
+
const qualifier = object.text;
|
|
788
|
+
const modules = pythonModulePath(index, file, qualifier);
|
|
789
|
+
const site = { start: call.startIndex, end: call.endIndex };
|
|
790
|
+
if (!modules || modules.unknown || !pythonModuleScope(index, file, qualifier, site) ||
|
|
791
|
+
qualifiedBindingChanged(index, file, { name }, qualifier)) return true;
|
|
792
|
+
for (const targetFile of modules.files) targets.push({ file: targetFile, name });
|
|
793
|
+
} else {
|
|
794
|
+
const sites = require('../languages/lexical-scope').pythonScopeBindingKinds(fn, name);
|
|
795
|
+
if (sites.length) return true;
|
|
796
|
+
const bindings = localImportBindings(entry, name);
|
|
797
|
+
const binding = bindings.length === 1 ? bindings[0] : null;
|
|
798
|
+
const relative = binding && entry.moduleResolved?.[binding.module];
|
|
799
|
+
if (relative) targets.push({ file: require('path').resolve(index.root, relative), name: binding.name });
|
|
800
|
+
}
|
|
801
|
+
if (targets.length !== 1) return true;
|
|
802
|
+
const target = targets[0];
|
|
803
|
+
if (withTree(index, target.file, other => {
|
|
804
|
+
const imported = functionNamed(other, null, target.name);
|
|
805
|
+
return !imported || !argumentNamesFirstParameter(argument, imported) ||
|
|
806
|
+
bodyChangesIdentity(index, target.file, other, imported, helperSensitive, depth + 1, helperSensitive, definition);
|
|
807
|
+
}) !== false) return true;
|
|
808
|
+
}
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/** Select the returns in a decorator's class-or-options branch. Other
|
|
813
|
+
* conditions stay unknown, so every branch must keep the same value. */
|
|
814
|
+
function decoratorReturns(body, parameter, parameterNone) {
|
|
815
|
+
const output = [];
|
|
816
|
+
const visit = statements => {
|
|
817
|
+
for (const node of statements || []) {
|
|
818
|
+
if (FUNCTION_NODES.has(node.type)) continue;
|
|
819
|
+
if (node.type === 'return_statement') { output.push(node.namedChild(0) || null); return true; }
|
|
820
|
+
if (node.type === 'if_statement') {
|
|
821
|
+
const condition = node.childForFieldName('condition');
|
|
822
|
+
const parts = condition?.namedChildren || [];
|
|
823
|
+
const isNone = condition?.type === 'comparison_operator' && parts.length === 2 &&
|
|
824
|
+
parts[0].type === 'identifier' && parts[0].text === parameter && parts[1].type === 'none';
|
|
825
|
+
const operator = isNone ? condition.children.filter(child => !child.isNamed).map(child => child.text).join(' ') : '';
|
|
826
|
+
if (isNone && parameterNone != null && (operator === 'is' || operator === 'is not')) {
|
|
827
|
+
const selected = parameterNone === (operator === 'is') ? node.childForFieldName('consequence') : node.childForFieldName('alternative');
|
|
828
|
+
if (selected && visit(selected.type === 'else_clause' ? selected.childForFieldName('body')?.namedChildren : selected.namedChildren)) return true;
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
for (const ret of ownReturns(node)) output.push(ret.namedChild(0) || null);
|
|
833
|
+
}
|
|
834
|
+
return false;
|
|
835
|
+
};
|
|
836
|
+
visit(body?.namedChildren);
|
|
837
|
+
return output;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function decoratorFunctionKeeps(tree, fn, returnsNothingKeeps, depth = 0, options = {}) {
|
|
841
|
+
if (!fn || depth > 4) return false;
|
|
842
|
+
if (fn.parent?.type === 'decorated_definition') return false;
|
|
843
|
+
const parameter = firstParameterName(fn);
|
|
844
|
+
const body = fn.childForFieldName('body');
|
|
845
|
+
const sensitive = options.sensitive || new Set();
|
|
846
|
+
const changes = identityWrites(body, parameter, sensitive, !options.factory, options.context);
|
|
847
|
+
if (changes.changesValue) return false;
|
|
848
|
+
const values = body && !['block', 'statement_block'].includes(body.type) ? [body]
|
|
849
|
+
: decoratorReturns(body, parameter, options.factory ? options.parameterNone : false);
|
|
850
|
+
if (values.length === 0) return returnsNothingKeeps;
|
|
851
|
+
const stable = name => {
|
|
852
|
+
if (changes.writes.has(name)) return false;
|
|
853
|
+
if (fn.type !== 'function_definition') return true;
|
|
854
|
+
const sites = require('../languages/lexical-scope').pythonScopeBindingKinds(fn, name);
|
|
855
|
+
return sites.length === 0 || (sites.length === 1 && sites[0].kind === 'def');
|
|
856
|
+
};
|
|
857
|
+
const keepsNamed = (name, factory = false) => stable(name) &&
|
|
858
|
+
decoratorFunctionKeeps(tree, functionNamed(tree, fn, name), returnsNothingKeeps, depth + 1,
|
|
859
|
+
{ ...options, factory, parameterNone: factory ? true : false });
|
|
860
|
+
// Passing the class to an unread helper before returning it is not an
|
|
861
|
+
// identity proof: that helper may replace one of its dispatch slots.
|
|
862
|
+
for (const call of ownNodes(body, options.context?.index)) {
|
|
863
|
+
if (!['call', 'call_expression'].includes(call.type)) continue;
|
|
864
|
+
const argument = options.factory ? null : identityArgument(call, changes.aliases, fn, options.context);
|
|
865
|
+
if (!argument) continue;
|
|
866
|
+
if (argument.unread) return false;
|
|
867
|
+
const callee = call.childForFieldName('function');
|
|
868
|
+
if (callee?.type !== 'identifier') return false;
|
|
869
|
+
if (['setattr', 'delattr'].includes(callee.text) && builtinReference(options.context, callee)) continue; // slots checked above
|
|
870
|
+
if (!argumentNamesFirstParameter(argument, functionNamed(tree, fn, callee.text)) ||
|
|
871
|
+
!keepsNamed(callee.text)) return false;
|
|
872
|
+
}
|
|
873
|
+
return values.every(found => {
|
|
874
|
+
let value = found;
|
|
875
|
+
while (value?.type === 'parenthesized_expression') value = value.namedChild(0);
|
|
876
|
+
if (!value) return returnsNothingKeeps;
|
|
877
|
+
if (value.type === 'identifier') return value.text === parameter ? !options.factory : keepsNamed(value.text);
|
|
878
|
+
if (FUNCTION_VALUES.has(value.type)) return decoratorFunctionKeeps(tree, value, returnsNothingKeeps, depth + 1,
|
|
879
|
+
{ ...options, factory: false });
|
|
880
|
+
if (value.type !== 'call' && value.type !== 'call_expression') return false;
|
|
881
|
+
const callee = value.childForFieldName('function');
|
|
882
|
+
const args = (value.childForFieldName('arguments')?.namedChildren || []).filter(arg => !arg.type.endsWith('comment'));
|
|
883
|
+
if (args.some(arg => ['list_splat', 'dictionary_splat', 'spread_element'].includes(arg.type))) return false;
|
|
884
|
+
const positional = args.filter(arg => arg.type !== 'keyword_argument');
|
|
885
|
+
if (callee?.type === 'identifier' && options.partialNames?.has(callee.text)) {
|
|
886
|
+
return positional.length === 1 && positional[0].type === 'identifier' && keepsNamed(positional[0].text);
|
|
887
|
+
}
|
|
888
|
+
if (callee?.type !== 'identifier') return false;
|
|
889
|
+
if (positional.length === 0) return keepsNamed(callee.text, true);
|
|
890
|
+
return !options.factory && !!parameter && positional[0].type === 'identifier' &&
|
|
891
|
+
positional[0].text === parameter && keepsNamed(callee.text);
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Whether a class decorator can put something else under the class's name.
|
|
897
|
+
* A decorator of the project has to show that it does not (see
|
|
898
|
+
* `decoratorFunctionKeeps`): a module-level function of this file or one
|
|
899
|
+
* the file imports by name, used bare or with arguments. A project
|
|
900
|
+
* decorator that is not read (an attribute of a project module, a class, a
|
|
901
|
+
* computed value) counts as replacing. A decorator from outside the project
|
|
902
|
+
* is taken to return the class it is given.
|
|
903
|
+
*/
|
|
904
|
+
function decoratorReplacesClass(index, file, decorator, sensitive = new Set(), definition = null) {
|
|
905
|
+
const syntax = withTree(index, file, tree => {
|
|
906
|
+
const node = tree.rootNode.descendantsOfType('decorator').find(item =>
|
|
907
|
+
item.namedChild(0)?.text === decorator);
|
|
908
|
+
const expression = node?.namedChild(0);
|
|
909
|
+
if (!expression) return null;
|
|
910
|
+
const factory = ['call', 'call_expression'].includes(expression.type);
|
|
911
|
+
const head = factory ? expression.childForFieldName('function') : expression;
|
|
912
|
+
let root = head;
|
|
913
|
+
while (['attribute', 'member_expression'].includes(root?.type)) root = root.childForFieldName('object');
|
|
914
|
+
if (root?.type !== 'identifier') return null;
|
|
915
|
+
return { root: root.text, bare: head.type === 'identifier', factory,
|
|
916
|
+
args: factory ? (expression.childForFieldName('arguments')?.namedChildren || []).map(arg => ({
|
|
917
|
+
type: arg.type, name: arg.childForFieldName('name')?.text,
|
|
918
|
+
valueType: arg.childForFieldName('value')?.type,
|
|
919
|
+
})) : [] };
|
|
920
|
+
});
|
|
921
|
+
if (!syntax) return true;
|
|
922
|
+
const { root } = syntax;
|
|
923
|
+
const entry = index.files.get(file);
|
|
924
|
+
const declaredHere = (index.symbols.get(root) || []).some(def => def.file === file && !def.className &&
|
|
925
|
+
def.lexicalScopeStartLine == null) || (entry?.moduleAssignedNames || []).includes(root) ||
|
|
926
|
+
(entry?.moduleValueAliases || []).some(alias => alias.name === root);
|
|
927
|
+
const bindings = localImportBindings(entry, root);
|
|
928
|
+
const projectImports = bindings.filter(binding => entry.moduleResolved?.[binding.module]);
|
|
929
|
+
// Unbound here (a builtin) or imported from outside the project.
|
|
930
|
+
if (!declaredHere && projectImports.length === 0) return false;
|
|
931
|
+
if (!syntax.bare) return true;
|
|
932
|
+
// The one module-level function of a name: typing overload stubs
|
|
933
|
+
// declare no body of their own, and a module alias of a function
|
|
934
|
+
// (`rich_repr = auto`) names it.
|
|
935
|
+
const moduleFunction = (target, name, depth = 0) => {
|
|
936
|
+
const named = (index.symbols.get(name) || []).filter(def => def.file === target && !def.className &&
|
|
937
|
+
def.lexicalScopeStartLine == null);
|
|
938
|
+
const defs = named.filter(def => !(def.decorators || []).some(text => /(^|\.)overload$/.test(String(text))));
|
|
939
|
+
if (defs.length === 1 && defs[0].type === 'function') {
|
|
940
|
+
const targetEntry = index.files.get(target);
|
|
941
|
+
if ((targetEntry?.moduleAssignedNames || []).includes(name) ||
|
|
942
|
+
(targetEntry?.moduleValueAliases || []).some(alias => alias.name === name)) return null;
|
|
943
|
+
return defs[0];
|
|
944
|
+
}
|
|
945
|
+
// Anything but the variable an alias assignment declares is another kind of value.
|
|
946
|
+
if (named.some(def => def.type !== 'state') || depth > 2) return null;
|
|
947
|
+
const aliases = (index.files.get(target)?.moduleValueAliases || []).filter(alias => alias.name === name);
|
|
948
|
+
return aliases.length === 1 && aliases[0].target && !aliases[0].alternatives?.length &&
|
|
949
|
+
/^[A-Za-z_$][\w$]*$/.test(aliases[0].target) ? moduleFunction(target, aliases[0].target, depth + 1) : null;
|
|
950
|
+
};
|
|
951
|
+
let fn = declaredHere ? moduleFunction(file, root) : null;
|
|
952
|
+
if (!fn && !declaredHere && bindings.length === 1 && projectImports.length === 1) {
|
|
953
|
+
fn = moduleFunction(require('path').resolve(index.root, entry.moduleResolved[bindings[0].module]),
|
|
954
|
+
bindings[0].name);
|
|
955
|
+
}
|
|
956
|
+
if (!fn) return true;
|
|
957
|
+
const returnsNothingKeeps = langTraits(index.files.get(fn.file)?.language)?.constructorProtocol == null;
|
|
958
|
+
const { factory, args } = syntax;
|
|
959
|
+
if (args.some(arg => arg.type !== 'keyword_argument' &&
|
|
960
|
+
!['string', 'integer', 'float', 'true', 'false', 'none', 'number'].includes(arg.type))) return true;
|
|
961
|
+
const partialNames = new Set((index.files.get(fn.file)?.importBindings || []).filter(binding =>
|
|
962
|
+
binding.module === 'functools' && binding.name === 'partial').map(binding => binding.alias || binding.name));
|
|
963
|
+
return withTree(index, fn.file, tree => {
|
|
964
|
+
const node = functionNodeOf(tree, fn);
|
|
965
|
+
if (!node) return true;
|
|
966
|
+
const parameter = firstParameterName(node);
|
|
967
|
+
const positional = args.filter(arg => arg.type !== 'keyword_argument');
|
|
968
|
+
const keyword = args.find(arg => arg.type === 'keyword_argument' && arg.name === parameter);
|
|
969
|
+
const supplied = positional[0] || (keyword && { type: keyword.valueType });
|
|
970
|
+
if (keyword && supplied?.type !== 'none') return true;
|
|
971
|
+
const defaultValue = node.childForFieldName('parameters')?.namedChild(0)?.childForFieldName('value');
|
|
972
|
+
const parameterNone = supplied ? supplied.type === 'none' : defaultValue?.type === 'none' ? true : null;
|
|
973
|
+
return !decoratorFunctionKeeps(tree, node, returnsNothingKeeps, 0,
|
|
974
|
+
{ factory, parameterNone, sensitive, partialNames, context: { index, file: fn.file, definition } });
|
|
975
|
+
}) !== false;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/** A constructor that returns a value other than `this` (JS/TS). */
|
|
979
|
+
function constructorReturnsValue(index, constructor) {
|
|
980
|
+
const span = constructor.sourceSpan;
|
|
981
|
+
if (!span) return false;
|
|
982
|
+
return withTree(index, constructor.file, (tree, content) => {
|
|
983
|
+
// Prefilter only: the syntax tree decides.
|
|
984
|
+
if (!content.slice(span.start, span.end).includes('return')) return false;
|
|
985
|
+
const node = functionNodeOf(tree, constructor);
|
|
986
|
+
if (!node) return true;
|
|
987
|
+
return ownReturns(node.childForFieldName('body'), index).some(statement => {
|
|
988
|
+
let value = statement.namedChild(0);
|
|
989
|
+
while (value?.type === 'parenthesized_expression') value = value.namedChild(0);
|
|
990
|
+
return !!value && value.type !== 'this';
|
|
991
|
+
});
|
|
992
|
+
}) !== false;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** The class a `metaclass=` keyword of a class header names, as a class reference; null when none is written. */
|
|
996
|
+
function metaclassOf(index, definition, protocol) {
|
|
997
|
+
const read = withTree(index, definition.file, (tree, content) => {
|
|
998
|
+
// Prefilter only: the syntax tree decides.
|
|
999
|
+
if (!content.includes(protocol.metaclass)) return null;
|
|
1000
|
+
// Every accepted class ends on this line. Visit the final two rows
|
|
1001
|
+
// (including a zero-column end), keeping enclosing/nested matches.
|
|
1002
|
+
const nodes = tree.rootNode.descendantsOfType(protocol.classNode,
|
|
1003
|
+
{ row: Math.max(0, definition.endLine - 2), column: 0 }, { row: definition.endLine, column: 0 }).filter(node =>
|
|
1004
|
+
node.childForFieldName('name')?.text === definition.name &&
|
|
1005
|
+
node.startPosition.row + 1 >= definition.startLine && node.endPosition.row + 1 === definition.endLine);
|
|
1006
|
+
if (nodes.length !== 1) return { unread: true };
|
|
1007
|
+
const header = nodes[0].childForFieldName(protocol.headerField);
|
|
1008
|
+
const keyword = (header?.namedChildren || []).find(arg => arg.type === protocol.keywordNode &&
|
|
1009
|
+
arg.childForFieldName('name')?.text === protocol.metaclass);
|
|
1010
|
+
if (!keyword) return null;
|
|
1011
|
+
const value = keyword.childForFieldName('value');
|
|
1012
|
+
return { name: value?.type === 'identifier' ? value.text : null,
|
|
1013
|
+
line: keyword.startPosition.row + 1, written: value?.text || '',
|
|
1014
|
+
computed: !['identifier', 'attribute'].includes(value?.type) };
|
|
1015
|
+
});
|
|
1016
|
+
return read === undefined ? { unread: true } : read;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function hasComputedBase(index, definition, protocol) {
|
|
1020
|
+
return withTree(index, definition.file, tree => {
|
|
1021
|
+
// Every accepted class ends on this line. Visit the final two rows
|
|
1022
|
+
// (including a zero-column end), keeping enclosing/nested matches.
|
|
1023
|
+
const nodes = tree.rootNode.descendantsOfType(protocol.classNode,
|
|
1024
|
+
{ row: Math.max(0, definition.endLine - 2), column: 0 }, { row: definition.endLine, column: 0 }).filter(node =>
|
|
1025
|
+
node.childForFieldName('name')?.text === definition.name &&
|
|
1026
|
+
node.startPosition.row + 1 >= definition.startLine && node.endPosition.row + 1 === definition.endLine);
|
|
1027
|
+
if (nodes.length !== 1) return true;
|
|
1028
|
+
const header = nodes[0].childForFieldName(protocol.headerField);
|
|
1029
|
+
return (header?.namedChildren || []).some(base => {
|
|
1030
|
+
if (base.type === protocol.keywordNode) return false;
|
|
1031
|
+
if (base.type !== 'subscript') return !['identifier', 'attribute'].includes(base.type);
|
|
1032
|
+
// Subscription can run a project __class_getitem__ or metaclass.
|
|
1033
|
+
// Only an outside generic base keeps the ordinary alias meaning.
|
|
1034
|
+
const value = base.childForFieldName('value');
|
|
1035
|
+
if (!['identifier', 'attribute'].includes(value?.type)) return true;
|
|
1036
|
+
const name = value.type === 'identifier' ? value.text : value.childForFieldName('attribute')?.text;
|
|
1037
|
+
return (index.symbols.get(name) || []).some(def => CLASS_KINDS.has(def.type));
|
|
1038
|
+
});
|
|
1039
|
+
}) !== false;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Whether calling the class makes an instance of that very class. What can
|
|
1044
|
+
* change it is written in the class or in a project class it inherits from:
|
|
1045
|
+
* an allocator (`__new__`), a metaclass that takes the call over
|
|
1046
|
+
* (`__call__`), a constructor that returns a value, a decorator that puts
|
|
1047
|
+
* something else under the class's name. A base outside the project is
|
|
1048
|
+
* taken to construct the class it is called on.
|
|
1049
|
+
*/
|
|
1050
|
+
function constructionKeepsClass(index, definition) {
|
|
1051
|
+
const memo = index._opMemo?.('constructionKeepsClass', () => new WeakMap());
|
|
1052
|
+
if (memo?.has(definition)) return memo.get(definition);
|
|
1053
|
+
const result = constructionKeepsClassUncached(index, definition);
|
|
1054
|
+
memo?.set(definition, result);
|
|
1055
|
+
return result;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function constructionAncestry(index, definition) {
|
|
1059
|
+
const memo = index._opMemo?.('constructionAncestry', () => new WeakMap());
|
|
1060
|
+
if (memo?.has(definition)) return memo.get(definition);
|
|
1061
|
+
const result = constructionAncestryUncached(index, definition);
|
|
1062
|
+
memo?.set(definition, result);
|
|
1063
|
+
return result;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function constructionAncestryUncached(index, definition) {
|
|
1067
|
+
const { classDefsNamed, parentRefsOf } = require('./class-identity');
|
|
1068
|
+
const refOf = def => ({ name: def.name, def,
|
|
1069
|
+
key: classDefsNamed(index, def.name).entries.find(entry => entry.def === def)?.key || null });
|
|
1070
|
+
const ancestry = [];
|
|
1071
|
+
const seen = new Set();
|
|
1072
|
+
const queue = [refOf(definition)];
|
|
1073
|
+
while (queue.length > 0 && ancestry.length < 64) {
|
|
1074
|
+
const ref = queue.shift();
|
|
1075
|
+
if (!ref.def || !ref.key || seen.has(ref.key)) continue;
|
|
1076
|
+
seen.add(ref.key);
|
|
1077
|
+
ancestry.push(ref);
|
|
1078
|
+
for (const parent of parentRefsOf(index, ref)) {
|
|
1079
|
+
if (parent.external) continue;
|
|
1080
|
+
if (!parent.def && parent.key && !parent.importUnresolved && !(index.symbols.get(parent.name) || []).length) continue;
|
|
1081
|
+
if (!parent.def || !parent.key || parent.importUnresolved) return null;
|
|
1082
|
+
queue.push(parent);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return queue.length > 0 ? null : ancestry;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function constructionKeepsClassUncached(index, definition) {
|
|
1089
|
+
const { resolveClassRef, findDeclaringClass } = require('./class-identity');
|
|
1090
|
+
const ancestry = constructionAncestry(index, definition);
|
|
1091
|
+
if (!ancestry) return false;
|
|
1092
|
+
const memberNames = new Set(ancestry.flatMap(ref => (index.files.get(ref.def.file)?.symbols || [])
|
|
1093
|
+
.filter(member => dispatchMember(member) && member.className === ref.def.name && member.startLine >= ref.def.startLine &&
|
|
1094
|
+
member.endLine <= ref.def.endLine).map(member => member.name)));
|
|
1095
|
+
for (const ref of ancestry) {
|
|
1096
|
+
const cls = ref.def;
|
|
1097
|
+
const entry = index.files.get(cls.file);
|
|
1098
|
+
const traits = langTraits(entry?.language);
|
|
1099
|
+
const members = (entry?.symbols || []).filter(member => member.className === cls.name &&
|
|
1100
|
+
member.startLine >= cls.startLine && member.endLine <= cls.endLine);
|
|
1101
|
+
const protocol = traits?.constructorProtocol;
|
|
1102
|
+
if (protocol) {
|
|
1103
|
+
if (hasComputedBase(index, cls, protocol)) return false;
|
|
1104
|
+
if (members.some(member => member.name === protocol.allocator || protocol.identityOverrides.includes(member.name))) return false;
|
|
1105
|
+
for (const initializer of members.filter(member => member.name === protocol.initializer)) {
|
|
1106
|
+
if (initializer.decorators?.length) return false;
|
|
1107
|
+
if (withTree(index, initializer.file, tree => {
|
|
1108
|
+
const node = functionNodeOf(tree, initializer);
|
|
1109
|
+
if (!node) return true;
|
|
1110
|
+
return bodyChangesIdentity(index, initializer.file, tree, node, new Set(protocol.identityMembers), 0,
|
|
1111
|
+
new Set([...protocol.identityMembers, ...memberNames]), definition);
|
|
1112
|
+
}) !== false) return false;
|
|
1113
|
+
}
|
|
1114
|
+
const metaclass = metaclassOf(index, cls, protocol);
|
|
1115
|
+
if (metaclass?.unread || metaclass?.computed) return false;
|
|
1116
|
+
if (metaclass) {
|
|
1117
|
+
// A metaclass outside the project is taken to make the class
|
|
1118
|
+
// it is called on; a project one decides by its own `__call__`.
|
|
1119
|
+
const named = metaclass.name ? resolveClassRef(index, metaclass.name, cls.file, { line: metaclass.line }) : null;
|
|
1120
|
+
if (!named || (!named.def && !named.external)) {
|
|
1121
|
+
if ((index.symbols.get(metaclass.written.split('.').pop()) || []).length > 0) return false;
|
|
1122
|
+
} else if (named.external && boundInModule(entry, metaclass.name)) {
|
|
1123
|
+
const bindings = localImportBindings(entry, metaclass.name);
|
|
1124
|
+
if (bindings.length !== 1 || entry.moduleResolved?.[bindings[0].module]) return false;
|
|
1125
|
+
} else if (!named.external) {
|
|
1126
|
+
if (!named.key) return false;
|
|
1127
|
+
// No project class in the metaclass's ancestry declares
|
|
1128
|
+
// `__call__`: the call is `type`'s own.
|
|
1129
|
+
for (const name of [...protocol.metaclassMethods, ...protocol.identityOverrides]) {
|
|
1130
|
+
const call = findDeclaringClass(index, named, index.symbols.get(name) || []);
|
|
1131
|
+
if (call && call.members.length > 0) return false;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
} else if (members.some(member => member.type === 'constructor' && constructorReturnsValue(index, member))) {
|
|
1136
|
+
return false;
|
|
1137
|
+
}
|
|
1138
|
+
for (const decorator of cls.decorators || []) {
|
|
1139
|
+
const sensitive = new Set([...memberNames, ...(protocol?.identityMembers || []), ...(protocol?.identityOverrides || []),
|
|
1140
|
+
protocol?.allocator, protocol?.initializer]);
|
|
1141
|
+
if (decoratorReplacesClass(index, cls.file, String(decorator), sensitive, cls)) return false;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* `with Name(..) as v` binds what the manager's enter method returns. It is
|
|
1149
|
+
* the constructed instance when the method the class's ancestry declares
|
|
1150
|
+
* returns its own receiver on every path; an outside base's enter method is
|
|
1151
|
+
* taken to. False when a project enter method returns anything else.
|
|
1152
|
+
*/
|
|
1153
|
+
function contextBindingKeepsInstance(index, definition) {
|
|
1154
|
+
const memo = index._opMemo?.('constructedContextBinding', () => new WeakMap());
|
|
1155
|
+
if (memo?.has(definition)) return memo.get(definition);
|
|
1156
|
+
const value = contextBindingKeepsInstanceUncached(index, definition);
|
|
1157
|
+
memo?.set(definition, value);
|
|
1158
|
+
return value;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function contextBindingKeepsInstanceUncached(index, definition) {
|
|
1162
|
+
const names = langTraits(index.files.get(definition.file)?.language)?.contextEnterMethods || [];
|
|
1163
|
+
if (names.length === 0) return true;
|
|
1164
|
+
const { classDefsNamed, findDeclaringClass } = require('./class-identity');
|
|
1165
|
+
const ref = { name: definition.name, def: definition,
|
|
1166
|
+
key: classDefsNamed(index, definition.name).entries.find(entry => entry.def === definition)?.key || null };
|
|
1167
|
+
if (!ref.key) return false;
|
|
1168
|
+
for (const name of names) {
|
|
1169
|
+
// No project class of the ancestry declares it: an outside base's.
|
|
1170
|
+
const declaring = findDeclaringClass(index, ref, index.symbols.get(name) || []);
|
|
1171
|
+
if (!declaring || declaring.members.length === 0) continue;
|
|
1172
|
+
if (declaring.uncertain) return false;
|
|
1173
|
+
for (const member of declaring.members) {
|
|
1174
|
+
if (member.decorators?.length) return false;
|
|
1175
|
+
const own = withTree(index, member.file, tree => {
|
|
1176
|
+
const node = functionNodeOf(tree, member);
|
|
1177
|
+
if (!node) return false;
|
|
1178
|
+
const receiver = firstParameterName(node);
|
|
1179
|
+
const sensitive = new Set([...(langTraits(index.files.get(member.file)?.language)?.constructorProtocol?.identityMembers || []),
|
|
1180
|
+
...(constructionAncestry(index, definition) || []).flatMap(ref => (index.files.get(ref.def.file)?.symbols || [])
|
|
1181
|
+
.filter(symbol => dispatchMember(symbol) && symbol.className === ref.def.name && symbol.startLine >= ref.def.startLine &&
|
|
1182
|
+
symbol.endLine <= ref.def.endLine).map(symbol => symbol.name))]);
|
|
1183
|
+
if (bodyChangesIdentity(index, member.file, tree, node, sensitive, 0, sensitive, definition)) return false;
|
|
1184
|
+
const returns = ownReturns(node.childForFieldName('body'), index);
|
|
1185
|
+
return !!receiver && returns.length > 0 && returns.every(statement =>
|
|
1186
|
+
statement.namedChild(0)?.type === 'identifier' && statement.namedChild(0).text === receiver);
|
|
1187
|
+
});
|
|
1188
|
+
if (own !== true) return false;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return true;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
module.exports = { constructedClass, constructionRecordOf, constructionKeepsClass, contextBindingKeepsInstance,
|
|
1195
|
+
localImportBindings, qualifiedBindingChanged };
|