ucn 5.5.1 → 5.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +35 -9
- package/.claude/skills/ucn/references/commands.md +38 -16
- package/.claude/skills/ucn/references/resolution-rules.md +126 -20
- package/.claude/skills/ucn/references/trust-contract.md +5 -3
- package/README.md +30 -21
- package/cli/index.js +1 -1
- package/core/accessors.js +341 -54
- package/core/account.js +74 -20
- package/core/analysis.js +257 -125
- package/core/ast-analysis.js +12 -3
- package/core/brief.js +8 -4
- package/core/build-worker.js +2 -1
- package/core/cache.js +137 -6
- package/core/call-name-cache.js +134 -0
- package/core/callable-scope.js +21 -0
- package/core/callers.js +4726 -940
- package/core/check.js +22 -5
- package/core/class-identity.js +399 -12
- package/core/constructor-calls.js +831 -0
- package/core/constructor-hierarchy.js +147 -0
- package/core/constructor-protocol.js +196 -0
- package/core/contract-membership.js +346 -140
- package/core/cpp-conversion-constraints.js +123 -0
- package/core/cpp-conversion-syntax.js +216 -0
- package/core/cpp-conversions.js +414 -0
- package/core/cpp-scope.js +245 -44
- package/core/deadcode.js +35 -15
- package/core/decorator-binding.js +137 -3
- package/core/enum-reference-syntax.js +134 -0
- package/core/execute.js +17 -1
- package/core/external-macros.js +42 -10
- package/core/go-packages.js +171 -0
- package/core/graph-build.js +2 -0
- package/core/graph.js +75 -59
- package/core/ignore-rules.js +2 -1
- package/core/imports.js +145 -19
- package/core/index-ir.js +41 -4
- package/core/ir.js +27 -6
- package/core/macro-expansion.js +38 -2
- package/core/out-of-class-members.js +182 -0
- package/core/output/analysis-ext.js +4 -2
- package/core/output/analysis.js +89 -20
- package/core/output/graph.js +12 -3
- package/core/output/lines.js +4 -3
- package/core/output/public.js +1 -0
- package/core/output/reporting.js +6 -1
- package/core/output/shared.js +2 -0
- package/core/output/tracing.js +28 -22
- package/core/output-budget.js +2 -2
- package/core/parallel-build.js +52 -1
- package/core/project.js +217 -112
- package/core/provenance-constructor-base.js +161 -0
- package/core/provenance-facts.js +257 -27
- package/core/provenance-overload.js +458 -54
- package/core/provenance-type.js +487 -0
- package/core/provenance.js +234 -15
- package/core/public-command.js +17 -2
- package/core/query-bodies.js +159 -0
- package/core/reflection.js +255 -2
- package/core/rename-token-syntax.js +111 -0
- package/core/reporting.js +13 -7
- package/core/rust-macro-expansion.js +151 -19
- package/core/rust-modules.js +1 -1
- package/core/rust-names.js +607 -0
- package/core/search.js +70 -17
- package/core/shared.js +98 -1
- package/core/source-memo.js +201 -0
- package/core/text-usage-syntax.js +38 -0
- package/core/tracing.js +7 -1
- package/core/type-denotation.js +124 -37
- package/core/type-references.js +615 -246
- package/core/value-references.js +2777 -0
- package/core/verify.js +1097 -281
- package/languages/adapter.js +28 -4
- package/languages/c-family.js +1118 -164
- package/languages/constructor-bases.js +105 -0
- package/languages/cpp-platform.js +26 -0
- package/languages/cpp-site-syntax.js +83 -0
- package/languages/csharp-scope.js +439 -0
- package/languages/csharp.js +428 -68
- package/languages/go.js +385 -207
- package/languages/html.js +4 -2
- package/languages/index.js +397 -2
- package/languages/java-platform.js +226 -0
- package/languages/java-unicode.js +269 -0
- package/languages/java.js +461 -117
- package/languages/javascript.js +418 -66
- package/languages/lexical-scope.js +190 -23
- package/languages/python.js +404 -54
- package/languages/rust.js +510 -82
- package/languages/utils.js +67 -8
- package/mcp/server.js +8 -2
- package/package.json +2 -2
package/core/accessors.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { codeUnitCompare, CALLABLE_SYMBOL_KINDS } = require('./shared');
|
|
5
|
-
const { getParser, safeParse } = require('../languages');
|
|
5
|
+
const { getParser, safeParse, langTraits, getLanguageAdapter } = require('../languages');
|
|
6
6
|
|
|
7
7
|
function inheritedAccessor(index, enclosing, name, definition) {
|
|
8
8
|
let owner = enclosing?.className;
|
|
@@ -13,7 +13,10 @@ function inheritedAccessor(index, enclosing, name, definition) {
|
|
|
13
13
|
if (owner === ownerName(definition) && file === definition.file) return true;
|
|
14
14
|
// An override owns this spelling. Multiple inheritance and duplicate
|
|
15
15
|
// declarations need richer lookup evidence, so keep them unverified.
|
|
16
|
-
|
|
16
|
+
// An instance attribute assigned through an inherited property
|
|
17
|
+
// runs the property (fix #430): it overrides nothing.
|
|
18
|
+
if ((index.symbols.get(name) || []).some(d => d.className === owner && d.file === file &&
|
|
19
|
+
!d.instanceAttribute)) return false;
|
|
17
20
|
const definitions = (index.symbols.get(owner) || []).filter(d => d.file === file && ['class', 'struct'].includes(d.type));
|
|
18
21
|
if (definitions.length !== 1) return false;
|
|
19
22
|
const parents = index._getInheritanceParentsAt(owner, file, definitions[0].startLine) || [];
|
|
@@ -30,16 +33,38 @@ function inheritedAccessor(index, enclosing, name, definition) {
|
|
|
30
33
|
return false;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
function accessKind(tree, usage) {
|
|
36
|
+
function accessKind(tree, usage, token = null) {
|
|
34
37
|
if (!tree || !Number.isInteger(usage.column)) return 'access';
|
|
35
|
-
let node = tree.rootNode.descendantForPosition({ row: usage.line - 1, column: usage.column });
|
|
36
|
-
if (
|
|
38
|
+
let node = token || tree.rootNode.descendantForPosition({ row: usage.line - 1, column: usage.column });
|
|
39
|
+
if (!node) return 'access';
|
|
40
|
+
if (node.tree.rootNode.hasError) {
|
|
41
|
+
for (let current = node; current; current = current.parent) {
|
|
42
|
+
if (current.type === 'ERROR') return 'access';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
while ((MEMBER_ACCESS_TYPES.has(node.parent?.type) ||
|
|
46
|
+
['member_binding_expression', 'conditional_access_expression'].includes(node.parent?.type)) &&
|
|
47
|
+
node.endIndex === node.parent.endIndex && node.startIndex > node.parent.startIndex) node = node.parent;
|
|
48
|
+
if (node.parent?.type === 'expression_list' && node.parent.parent?.type === 'assignment_statement') {
|
|
49
|
+
node = node.parent;
|
|
50
|
+
}
|
|
51
|
+
// A destructuring assignment target (`[a, b] = f()`, fix #431).
|
|
52
|
+
if (['array_pattern', 'object_pattern', 'pair_pattern', 'rest_pattern', 'assignment_pattern',
|
|
53
|
+
'object_assignment_pattern'].includes(node.parent?.type) &&
|
|
54
|
+
require('../languages/lexical-scope').jsPatternTarget(node) === 'assignment') return 'write';
|
|
37
55
|
const parent = node.parent;
|
|
38
|
-
if (
|
|
39
|
-
if (
|
|
56
|
+
if (['update_expression', 'inc_statement', 'dec_statement'].includes(parent?.type)) return 'read/write';
|
|
57
|
+
if (parent?.type === 'delete_statement') return 'delete';
|
|
58
|
+
if (['prefix_unary_expression', 'postfix_unary_expression'].includes(parent?.type) &&
|
|
59
|
+
parent.children.some(child => child.type === '++' || child.type === '--')) return 'read/write';
|
|
60
|
+
if (['assignment', 'assignment_expression', 'augmented_assignment', 'augmented_assignment_expression',
|
|
61
|
+
'compound_assignment_expr', 'assignment_statement'].includes(parent?.type)) {
|
|
40
62
|
const left = parent.childForFieldName('left');
|
|
41
63
|
if (left && left.startIndex === node.startIndex && left.endIndex === node.endIndex) {
|
|
42
|
-
|
|
64
|
+
const operator = parent.childForFieldName('operator');
|
|
65
|
+
return parent.type.startsWith('augmented') || parent.type === 'compound_assignment_expr' ||
|
|
66
|
+
(operator && operator.text !== '=')
|
|
67
|
+
? 'read/write' : 'write';
|
|
43
68
|
}
|
|
44
69
|
}
|
|
45
70
|
return 'read';
|
|
@@ -73,11 +98,26 @@ function receiverlessShape(tree, usage) {
|
|
|
73
98
|
return 'none';
|
|
74
99
|
}
|
|
75
100
|
|
|
101
|
+
// The class whose body reads a bare name as its own attribute (fix #431):
|
|
102
|
+
// directly in the body (not in a method), after the body binds the name.
|
|
103
|
+
function classBodyRead(tree, usage, name) {
|
|
104
|
+
if (!tree || !Number.isInteger(usage.column)) return null;
|
|
105
|
+
const node = tree.rootNode.descendantForPosition({ row: usage.line - 1, column: usage.column });
|
|
106
|
+
if (!node || node.text !== name) return null;
|
|
107
|
+
const { pythonClassBodyOf } = require('./value-references');
|
|
108
|
+
const { pythonClassBodyOrder } = require('../languages/lexical-scope');
|
|
109
|
+
const classNode = pythonClassBodyOf(node);
|
|
110
|
+
if (!classNode || pythonClassBodyOrder(classNode, node, name) !== 'class') return null;
|
|
111
|
+
const className = classNode.childForFieldName('name')?.text;
|
|
112
|
+
return className ? { name: className, startLine: classNode.startPosition.row + 1,
|
|
113
|
+
endLine: classNode.endPosition.row + 1 } : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
76
116
|
// Source offset of an occurrence (fix #398F): orders it against same-line
|
|
77
117
|
// assignments when its receiver is typed by return-type flow.
|
|
78
|
-
function flowSiteOf(tree, usage) {
|
|
118
|
+
function flowSiteOf(tree, usage, token = null) {
|
|
79
119
|
if (!tree || !Number.isInteger(usage.column)) return null;
|
|
80
|
-
const node = tree.rootNode.descendantForPosition({ row: usage.line - 1, column: usage.column });
|
|
120
|
+
const node = token || tree.rootNode.descendantForPosition({ row: usage.line - 1, column: usage.column });
|
|
81
121
|
return node ? node.startIndex : null;
|
|
82
122
|
}
|
|
83
123
|
|
|
@@ -159,15 +199,19 @@ function bareTypeName(value) {
|
|
|
159
199
|
.replace(/[?*&\s]/g, '') || null;
|
|
160
200
|
}
|
|
161
201
|
|
|
162
|
-
function typeMatchesOwner(index, typeName, contextFile, owner,
|
|
202
|
+
function typeMatchesOwner(index, typeName, contextFile, owner, definition) {
|
|
163
203
|
const bare = bareTypeName(typeName);
|
|
164
204
|
if (!bare || !owner || bare !== owner) return false;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
205
|
+
// The class the name denotes where it is written, by the language's
|
|
206
|
+
// lookup (fix #430: a same-namespace `Ping` before a sample project's
|
|
207
|
+
// `Ping`), is the accessor's owner class (the parts of a partial type
|
|
208
|
+
// are one class); a name that does not resolve to one class is no
|
|
209
|
+
// evidence.
|
|
210
|
+
const { resolveClassRef, ownerRefOf } = require('./class-identity');
|
|
211
|
+
const ref = resolveClassRef(index, bare, contextFile);
|
|
212
|
+
const own = ownerRefOf(index, definition);
|
|
213
|
+
if (ref?.key && own?.key) return ref.key === own.key;
|
|
214
|
+
return !!ref?.def?.file && path.resolve(ref.def.file) === path.resolve(definition.file);
|
|
171
215
|
}
|
|
172
216
|
|
|
173
217
|
// Members of the same object literal (fix #397): `this` in one names the
|
|
@@ -188,6 +232,177 @@ function insideSelectedAccessorBody(index, name, definition, file, line) {
|
|
|
188
232
|
line <= (candidate.endLine || candidate.startLine));
|
|
189
233
|
}
|
|
190
234
|
|
|
235
|
+
// Query-time grammar facts identify an implicit member's lexical type;
|
|
236
|
+
// class identities decide which declaration that type actually provides.
|
|
237
|
+
function scopeClassRef(index, file, scope, memo) {
|
|
238
|
+
const key = `scope\0${file}\0${scope.name}\0${scope.line}\0${scope.namespace}\0${scope.enclosingType}\0${scope.typeArity}`;
|
|
239
|
+
if (memo.has(key)) return memo.get(key);
|
|
240
|
+
const { classDefsNamed } = require('./class-identity');
|
|
241
|
+
const entries = classDefsNamed(index, scope.name).entries.filter(({ def }) =>
|
|
242
|
+
def.file === file && def.startLine <= scope.line && def.endLine >= scope.line &&
|
|
243
|
+
(def.namespace || '') === scope.namespace &&
|
|
244
|
+
(def.enclosingType || '') === scope.enclosingType && (def.typeArity || 0) === scope.typeArity);
|
|
245
|
+
const ref = new Set(entries.map(entry => entry.key)).size === 1
|
|
246
|
+
? { name: scope.name, key: entries[0].key, def: entries[0].def } : null;
|
|
247
|
+
memo.set(key, ref);
|
|
248
|
+
return ref;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function writtenClassRef(index, file, line, written, memo) {
|
|
252
|
+
if (!written) return null;
|
|
253
|
+
const { classDefsNamed } = require('./class-identity');
|
|
254
|
+
const { TypeReferenceResolver } = require('./type-references');
|
|
255
|
+
const key = `${file}\0${line}\0${written.node.startIndex}\0${written.text || written.node.text}\0${written.namespace}`;
|
|
256
|
+
if (memo.has(key)) return memo.get(key);
|
|
257
|
+
const traits = langTraits(index.files.get(file)?.language);
|
|
258
|
+
const resolve = (name, qualifier, siteNode = written.node) => {
|
|
259
|
+
const entries = classDefsNamed(index, name).entries.filter(entry =>
|
|
260
|
+
index.files.get(entry.def.file)?.language === index.files.get(file)?.language);
|
|
261
|
+
const matches = [];
|
|
262
|
+
let unknown = false;
|
|
263
|
+
for (const entry of entries) {
|
|
264
|
+
const resolver = new TypeReferenceResolver(index, entry.def, name);
|
|
265
|
+
resolver.siteNode = siteNode;
|
|
266
|
+
resolver.siteFile = file;
|
|
267
|
+
const verdict = qualifier
|
|
268
|
+
? resolver.resolveQualified(file, line, written.qualifier, qualifier, { role: 'type' })
|
|
269
|
+
: resolver.resolveName(file, line, name);
|
|
270
|
+
if (verdict.verdict === 'yes') matches.push(entry);
|
|
271
|
+
else if (verdict.verdict === 'unknown') unknown = true;
|
|
272
|
+
}
|
|
273
|
+
const keys = new Set(matches.map(entry => entry.key));
|
|
274
|
+
return { ref: !unknown && keys.size === 1
|
|
275
|
+
? { name, key: matches[0].key, def: matches[0].def } : null, unknown };
|
|
276
|
+
};
|
|
277
|
+
let result = resolve(written.name, written.qualifierText);
|
|
278
|
+
if (!result.ref && !result.unknown && !written.qualifierText && traits.typeNameScoping === 'namespace-usings') {
|
|
279
|
+
const { csharpUsings } = require('./type-denotation');
|
|
280
|
+
const scope = csharpUsings(index, file, written.namespace, written.node);
|
|
281
|
+
const target = scope.aliases.get(written.name);
|
|
282
|
+
if (target && !scope.ambiguousAliases.has(written.name)) {
|
|
283
|
+
const text = scope.aliasSyntax.get(written.name) || target;
|
|
284
|
+
const syntaxKey = 'syntax\0' + text;
|
|
285
|
+
if (!memo.has(syntaxKey)) memo.set(syntaxKey, traits.accessorTypeSyntax?.(text) || null);
|
|
286
|
+
const syntax = memo.get(syntaxKey);
|
|
287
|
+
if (syntax) result = resolve(syntax.name,
|
|
288
|
+
'global::' + (syntax.qualifierText || '').replace(/^global::/, ''), syntax.node);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const ref = result.ref;
|
|
292
|
+
memo.set(key, ref);
|
|
293
|
+
return ref;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const NESTED_TYPE_KINDS = new Set(['class', 'struct', 'interface', 'enum', 'record', 'type']);
|
|
297
|
+
|
|
298
|
+
function certainMemberOwner(index, member, memo) {
|
|
299
|
+
if (memo.has(member)) return memo.get(member);
|
|
300
|
+
const { classDefsNamed } = require('./class-identity');
|
|
301
|
+
if (!member.className) return null;
|
|
302
|
+
const entries = classDefsNamed(index, member.className).entries.filter(({ def }) =>
|
|
303
|
+
def.file === member.file && def.startLine <= member.startLine && def.endLine >= member.startLine &&
|
|
304
|
+
(def.namespace || '') === (member.namespace || '') &&
|
|
305
|
+
(def.typeArity || 0) === (member.ownerTypeArity || 0));
|
|
306
|
+
const keys = new Set(entries.map(entry => entry.key));
|
|
307
|
+
const key = keys.size === 1 ? entries[0].key : null;
|
|
308
|
+
memo.set(member, key);
|
|
309
|
+
return key;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function shapedAccessorVerdict(index, name, definition, ref, members, typeMemo) {
|
|
313
|
+
const shape = ref.dependencyShape;
|
|
314
|
+
if (!['implicit', 'initializer'].includes(shape.kind) || shape.uncertainRole) return 'review';
|
|
315
|
+
const { findDeclaringClass, supertypeRefsOf, ownerRefOf, shareDescendant } = require('./class-identity');
|
|
316
|
+
const targetOwner = ownerRefOf(index, definition);
|
|
317
|
+
if (!targetOwner?.key || certainMemberOwner(index, definition, typeMemo) !== targetOwner.key) return 'review';
|
|
318
|
+
const contractOwner = owner => ['interface', 'trait'].includes(owner.def?.type);
|
|
319
|
+
const hierarchyOf = owner => {
|
|
320
|
+
const key = `hierarchy\0${owner.key}`;
|
|
321
|
+
if (typeMemo.has(key)) return typeMemo.get(key);
|
|
322
|
+
const keys = new Set();
|
|
323
|
+
const queue = [owner];
|
|
324
|
+
let open = false;
|
|
325
|
+
while (queue.length) {
|
|
326
|
+
const current = queue.pop();
|
|
327
|
+
if (!current.key || current.external) { open = true; continue; }
|
|
328
|
+
if (keys.has(current.key)) continue;
|
|
329
|
+
keys.add(current.key);
|
|
330
|
+
queue.push(...supertypeRefsOf(index, current, { implements: true }));
|
|
331
|
+
}
|
|
332
|
+
const result = { keys, open };
|
|
333
|
+
typeMemo.set(key, result);
|
|
334
|
+
return result;
|
|
335
|
+
};
|
|
336
|
+
const verdictAt = owner => {
|
|
337
|
+
const declaring = findDeclaringClass(index, owner, members);
|
|
338
|
+
if (!declaring) {
|
|
339
|
+
const hierarchy = hierarchyOf(owner);
|
|
340
|
+
return hierarchy.open || contractOwner(owner) || hierarchy.keys.has(targetOwner.key) ? 'review' : null;
|
|
341
|
+
}
|
|
342
|
+
if (declaring.uncertain || declaring.members.some(member =>
|
|
343
|
+
certainMemberOwner(index, member, typeMemo) !== ownerRefOf(index, member)?.key)) return 'review';
|
|
344
|
+
if (!declaring.members.includes(definition)) {
|
|
345
|
+
// An implicit receiver can dispatch through an ancestor or a
|
|
346
|
+
// shared implementation. A construction names its exact type.
|
|
347
|
+
if (shape.kind === 'implicit') {
|
|
348
|
+
const key = `dispatch\0${owner.key}\0${targetOwner.key}`;
|
|
349
|
+
if (!typeMemo.has(key)) typeMemo.set(key,
|
|
350
|
+
hierarchyOf(targetOwner).keys.has(owner.key) || hierarchyOf(targetOwner).open ||
|
|
351
|
+
// An interface receiver and a class member can meet in
|
|
352
|
+
// an implementation that inherits that member. The
|
|
353
|
+
// class-only descendant graph cannot disprove this.
|
|
354
|
+
contractOwner(owner) || contractOwner(targetOwner) ||
|
|
355
|
+
shareDescendant(index, owner, [definition]));
|
|
356
|
+
if (typeMemo.get(key)) return 'review';
|
|
357
|
+
}
|
|
358
|
+
return 'skip';
|
|
359
|
+
}
|
|
360
|
+
const target = ownerRefOf(index, definition);
|
|
361
|
+
return declaring.members.every(member => isAccessorDefinition(member) &&
|
|
362
|
+
ownerRefOf(index, member)?.key === target?.key) ? 'edit' : 'review';
|
|
363
|
+
};
|
|
364
|
+
if (shape.kind === 'initializer') {
|
|
365
|
+
const owner = writtenClassRef(index, ref.file, ref.line, shape.type, typeMemo);
|
|
366
|
+
return owner ? verdictAt(owner) || 'skip' : 'review';
|
|
367
|
+
}
|
|
368
|
+
for (const scope of shape.scopes) {
|
|
369
|
+
const owner = scopeClassRef(index, ref.file, scope, typeMemo);
|
|
370
|
+
if (!owner) return 'review';
|
|
371
|
+
const verdict = verdictAt(owner);
|
|
372
|
+
if (!verdict) continue;
|
|
373
|
+
if (verdict === 'edit' && shape.inferredName) return 'review';
|
|
374
|
+
return verdict;
|
|
375
|
+
}
|
|
376
|
+
if (langTraits(index.files.get(ref.file)?.language)?.staticImportsDeclaredOnly) {
|
|
377
|
+
const { csharpUsings } = require('./type-denotation');
|
|
378
|
+
const scope = csharpUsings(index, ref.file, shape.namespace, shape.node);
|
|
379
|
+
const traits = langTraits(index.files.get(ref.file)?.language);
|
|
380
|
+
const namespaces = [];
|
|
381
|
+
for (let parts = shape.namespace ? shape.namespace.split('.') : []; parts.length; parts = parts.slice(0, -1)) {
|
|
382
|
+
namespaces.push(parts.join('.'));
|
|
383
|
+
}
|
|
384
|
+
namespaces.push('');
|
|
385
|
+
for (const namespace of namespaces) {
|
|
386
|
+
const hits = [];
|
|
387
|
+
let unknown = false;
|
|
388
|
+
for (const module of scope.statics) {
|
|
389
|
+
if (!scope.staticScopes.get(module)?.has(namespace)) continue;
|
|
390
|
+
const type = traits.accessorTypeSyntax?.(scope.staticSyntax.get(module) || module);
|
|
391
|
+
const owner = type && writtenClassRef(index, ref.file, ref.line, { ...type,
|
|
392
|
+
qualifierText: 'global::' + (type.qualifierText || '').replace(/^global::/, '') }, typeMemo);
|
|
393
|
+
if (!owner) { unknown = true; continue; }
|
|
394
|
+
hits.push(...members.filter(member => certainMemberOwner(index, member, typeMemo) === owner.key &&
|
|
395
|
+
member.modifiers?.includes('static')));
|
|
396
|
+
}
|
|
397
|
+
if (unknown) return 'review';
|
|
398
|
+
if (!hits.length) continue;
|
|
399
|
+
if (!hits.includes(definition)) return 'skip';
|
|
400
|
+
return hits.length === 1 && !shape.uncertainRole && !shape.inferredName ? 'edit' : 'review';
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return shape.scopes.length ? 'skip' : 'review';
|
|
404
|
+
}
|
|
405
|
+
|
|
191
406
|
/**
|
|
192
407
|
* Find reads/writes that may consume a selected accessor. Confirmed entries
|
|
193
408
|
* require receiver identity (`self`/`this` in the owner or a Python instance
|
|
@@ -204,44 +419,95 @@ function findAccessorReferences(index, name, definition, options = {}) {
|
|
|
204
419
|
const unverified = [];
|
|
205
420
|
const excluded = [];
|
|
206
421
|
const flowMaps = new Map();
|
|
422
|
+
const typeMemo = new Map();
|
|
423
|
+
const members = (index.symbols.get(name) || []).filter(member => member.className &&
|
|
424
|
+
!member.explicitInterface && !['class', 'struct', 'interface', 'enum', 'record'].includes(member.type));
|
|
425
|
+
// A nested type is a member of the type declaring it: where the grammar
|
|
426
|
+
// reports implicit member shapes (C# simple-name lookup), a nested type
|
|
427
|
+
// named like the property hides it inside that type (fix #419).
|
|
428
|
+
if (langTraits(index.files.get(definition.file)?.language)?.accessorReferenceShape) {
|
|
429
|
+
for (const type of index.symbols.get(name) || []) {
|
|
430
|
+
if (type.className || !type.enclosingType || !NESTED_TYPE_KINDS.has(type.type)) continue;
|
|
431
|
+
members.push({ name, type: 'nested-type', nestedType: true, file: type.file,
|
|
432
|
+
relativePath: type.relativePath, startLine: type.startLine, endLine: type.endLine,
|
|
433
|
+
className: String(type.enclosingType).split('.').pop(),
|
|
434
|
+
...(type.namespace && { namespace: type.namespace }) });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
207
437
|
// Use the parser's raw occurrence records rather than usages()' public
|
|
208
438
|
// line-oriented inventory. A line can contain two same-spelled member
|
|
209
439
|
// accesses with different receivers; collapsing them by file+line would
|
|
210
440
|
// make a rename edit one token while silently losing the other.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
441
|
+
// The raw occurrence records depend on the name and the scan options
|
|
442
|
+
// only: one scan per operation serves every member of a property slot
|
|
443
|
+
// (fix #426: plan asks each overriding accessor too).
|
|
444
|
+
let refsMemoKey;
|
|
445
|
+
try { refsMemoKey = JSON.stringify([name, options.files ? [...options.files].sort() : null, options]); }
|
|
446
|
+
catch { refsMemoKey = null; }
|
|
447
|
+
const refsMemo = refsMemoKey && !index.isProvenanceFactIndex
|
|
448
|
+
? index._opMemo?.('accessorReferenceRecords', () => new Map()) : null;
|
|
449
|
+
const memoRefs = refsMemo?.get(refsMemoKey);
|
|
450
|
+
const refs = memoRefs || [];
|
|
451
|
+
if (!memoRefs) {
|
|
452
|
+
for (const [file, entry] of index.files) {
|
|
453
|
+
// `files` (fix #398F): a caller that needs one file family (tests)
|
|
454
|
+
// never parses the rest of the project.
|
|
455
|
+
if (options.files && !options.files.has(file)) continue;
|
|
456
|
+
if (!index.matchesFilters(entry.relativePath, options)) continue;
|
|
457
|
+
let content;
|
|
458
|
+
let occurrences;
|
|
459
|
+
try {
|
|
460
|
+
content = index._readFile(file);
|
|
461
|
+
if (!content.includes(name)) continue;
|
|
462
|
+
occurrences = index._getCachedUsages(file, name);
|
|
463
|
+
} catch { continue; }
|
|
464
|
+
if (!occurrences) continue;
|
|
465
|
+
const grammarShape = langTraits(entry.language)?.accessorReferenceShape;
|
|
466
|
+
const candidates = occurrences.filter(usage => usage.usageType === 'reference' ||
|
|
467
|
+
(grammarShape && usage.usageType === 'call'));
|
|
468
|
+
if (!candidates.length) continue;
|
|
469
|
+
const parser = getParser(entry.language);
|
|
470
|
+
const tree = parser ? index._getParsedTree?.(file, content, entry.language) || safeParse(parser, content) : null;
|
|
471
|
+
const lines = index._getFileLines?.(file) || content.split('\n');
|
|
472
|
+
let adapter;
|
|
473
|
+
try { adapter = getLanguageAdapter(entry.language); } catch { adapter = null; }
|
|
474
|
+
const annotatedType = typeof adapter?.annotatedReceiverType === 'function' ? adapter.annotatedReceiverType : null;
|
|
475
|
+
for (const usage of candidates) {
|
|
476
|
+
const dependencyShape = grammarShape ? grammarShape(tree, usage) : null;
|
|
477
|
+
if (dependencyShape === 'none') continue;
|
|
478
|
+
let shape = null;
|
|
479
|
+
if (!usage.receiver && !dependencyShape) {
|
|
480
|
+
shape = receiverlessShape(tree, usage);
|
|
481
|
+
if (shape === 'none') {
|
|
482
|
+
// A bare name in a class body after the class binds it
|
|
483
|
+
// reads the class attribute (fix #431: `@plain.setter`
|
|
484
|
+
// reads the `plain` property).
|
|
485
|
+
const classBody = langTraits(entry.language)?.bareValueMemberScope === 'class-body'
|
|
486
|
+
? classBodyRead(tree, usage, name) : null;
|
|
487
|
+
if (!classBody) continue;
|
|
488
|
+
shape = { classBody };
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// A parameter annotation types the receiver (fix #424), the
|
|
492
|
+
// evidence method calls on it already take.
|
|
493
|
+
const annotated = annotatedType && usage.receiver && !usage.receiverType && tree &&
|
|
494
|
+
Number.isInteger(usage.column) ? annotatedType(tree, usage.line, usage.column, usage.receiver) : undefined;
|
|
495
|
+
refs.push({
|
|
496
|
+
...usage,
|
|
497
|
+
...(annotated && { receiverType: annotated, receiverAnnotation: true }),
|
|
498
|
+
...(shape?.pattern && { patternKey: shape, ...(shape.thisSource && { receiver: 'this' }) }),
|
|
499
|
+
...(shape?.classBody && { classBody: shape.classBody }),
|
|
500
|
+
...(dependencyShape && { dependencyShape }),
|
|
501
|
+
accessKind: dependencyShape?.accessKind || (shape?.pattern ? 'read' : accessKind(tree, usage, dependencyShape?.node)),
|
|
502
|
+
...(usage.receiver && { flowSite: flowSiteOf(tree, usage, dependencyShape?.node) }),
|
|
503
|
+
file,
|
|
504
|
+
relativePath: entry.relativePath,
|
|
505
|
+
content: lines[usage.line - 1] || '',
|
|
506
|
+
});
|
|
234
507
|
}
|
|
235
|
-
refs.push({
|
|
236
|
-
...usage,
|
|
237
|
-
...(shape?.pattern && { patternKey: shape, ...(shape.thisSource && { receiver: 'this' }) }),
|
|
238
|
-
accessKind: shape?.pattern ? 'read' : accessKind(tree, usage),
|
|
239
|
-
...(usage.receiver && { flowSite: flowSiteOf(tree, usage) }),
|
|
240
|
-
file,
|
|
241
|
-
relativePath: entry.relativePath,
|
|
242
|
-
content: lines[usage.line - 1] || '',
|
|
243
|
-
});
|
|
244
508
|
}
|
|
509
|
+
|
|
510
|
+
refsMemo?.set(refsMemoKey, refs);
|
|
245
511
|
}
|
|
246
512
|
|
|
247
513
|
for (const ref of refs) {
|
|
@@ -257,10 +523,30 @@ function findAccessorReferences(index, name, definition, options = {}) {
|
|
|
257
523
|
...(ref.patternKey && { patternKey: { shorthand: !!ref.patternKey.shorthand } }),
|
|
258
524
|
};
|
|
259
525
|
|
|
526
|
+
if (ref.dependencyShape && ref.dependencyShape.kind !== 'member') {
|
|
527
|
+
const verdict = shapedAccessorVerdict(index, name, definition, ref, members, typeMemo);
|
|
528
|
+
if (verdict === 'edit') confirmed.push({ ...shaped,
|
|
529
|
+
callerName: index.findEnclosingFunction(ref.file, ref.line, true)?.name || null,
|
|
530
|
+
resolution: 'member-lookup-accessor', tier: 'confirmed' });
|
|
531
|
+
else if (verdict === 'skip') excluded.push({ ...shaped, reason: 'other-member-owner' });
|
|
532
|
+
else unverified.push({ ...shaped,
|
|
533
|
+
reason: ref.dependencyShape.inferredName ? 'inferred-member-name' : 'member-owner-unresolved',
|
|
534
|
+
tier: 'unverified' });
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (ref.classBody) {
|
|
539
|
+
const own = definition.className === ref.classBody.name && path.resolve(ref.file) === path.resolve(definition.file) &&
|
|
540
|
+
definition.startLine >= ref.classBody.startLine && definition.endLine <= ref.classBody.endLine;
|
|
541
|
+
if (own) confirmed.push({ ...shaped, callerName: null, resolution: 'class-body-binding', tier: 'confirmed' });
|
|
542
|
+
else excluded.push({ ...shaped, reason: 'other-class-body' });
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
|
|
260
546
|
// A backing-store attribute inside the selected getter/setter body
|
|
261
547
|
// often has the same spelling (`self._local.value`). It is not a
|
|
262
548
|
// consumption of the descriptor being queried.
|
|
263
|
-
if (insideSelectedAccessorBody(index, name, definition, ref.file, ref.line) &&
|
|
549
|
+
if (!ref.dependencyShape && insideSelectedAccessorBody(index, name, definition, ref.file, ref.line) &&
|
|
264
550
|
!['self', 'cls', 'this'].includes(ref.receiver)) {
|
|
265
551
|
excluded.push({ ...shaped, reason: 'accessor-definition-body' });
|
|
266
552
|
continue;
|
|
@@ -288,12 +574,12 @@ function findAccessorReferences(index, name, definition, options = {}) {
|
|
|
288
574
|
ref.file, enclosing.className)?.get(receiver) || null;
|
|
289
575
|
}
|
|
290
576
|
if (receiverType && typeMatchesOwner(
|
|
291
|
-
index, receiverType, ref.file, owner, definition
|
|
577
|
+
index, receiverType, ref.file, owner, definition)) {
|
|
292
578
|
confirmed.push({
|
|
293
579
|
...shaped,
|
|
294
580
|
callerName: enclosing?.name || null,
|
|
295
581
|
receiverType,
|
|
296
|
-
resolution: 'receiver-field-type',
|
|
582
|
+
resolution: ref.receiverAnnotation ? 'receiver-annotation' : 'receiver-field-type',
|
|
297
583
|
tier: 'confirmed',
|
|
298
584
|
});
|
|
299
585
|
continue;
|
|
@@ -310,7 +596,7 @@ function findAccessorReferences(index, name, definition, options = {}) {
|
|
|
310
596
|
});
|
|
311
597
|
continue;
|
|
312
598
|
}
|
|
313
|
-
if (receiver && typeMatchesOwner(index, receiver, ref.file, owner, definition
|
|
599
|
+
if (receiver && typeMatchesOwner(index, receiver, ref.file, owner, definition)) {
|
|
314
600
|
confirmed.push({
|
|
315
601
|
...shaped,
|
|
316
602
|
callerName: enclosing?.name || null,
|
|
@@ -331,7 +617,7 @@ function findAccessorReferences(index, name, definition, options = {}) {
|
|
|
331
617
|
});
|
|
332
618
|
}
|
|
333
619
|
|
|
334
|
-
const sort = (a, b) => codeUnitCompare(a.file, b.file) || a.line - b.line;
|
|
620
|
+
const sort = (a, b) => codeUnitCompare(a.file, b.file) || a.line - b.line || (a.column ?? -1) - (b.column ?? -1);
|
|
335
621
|
confirmed.sort(sort);
|
|
336
622
|
unverified.sort(sort);
|
|
337
623
|
excluded.sort(sort);
|
|
@@ -342,4 +628,5 @@ module.exports = {
|
|
|
342
628
|
ACCESSOR_KINDS,
|
|
343
629
|
isAccessorDefinition,
|
|
344
630
|
findAccessorReferences,
|
|
631
|
+
referenceAccessKind: accessKind,
|
|
345
632
|
};
|