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.
Files changed (93) hide show
  1. package/.claude/skills/ucn/SKILL.md +35 -9
  2. package/.claude/skills/ucn/references/commands.md +38 -16
  3. package/.claude/skills/ucn/references/resolution-rules.md +126 -20
  4. package/.claude/skills/ucn/references/trust-contract.md +5 -3
  5. package/README.md +30 -21
  6. package/cli/index.js +1 -1
  7. package/core/accessors.js +341 -54
  8. package/core/account.js +74 -20
  9. package/core/analysis.js +257 -125
  10. package/core/ast-analysis.js +12 -3
  11. package/core/brief.js +8 -4
  12. package/core/build-worker.js +2 -1
  13. package/core/cache.js +137 -6
  14. package/core/call-name-cache.js +134 -0
  15. package/core/callable-scope.js +21 -0
  16. package/core/callers.js +4726 -940
  17. package/core/check.js +22 -5
  18. package/core/class-identity.js +399 -12
  19. package/core/constructor-calls.js +831 -0
  20. package/core/constructor-hierarchy.js +147 -0
  21. package/core/constructor-protocol.js +196 -0
  22. package/core/contract-membership.js +346 -140
  23. package/core/cpp-conversion-constraints.js +123 -0
  24. package/core/cpp-conversion-syntax.js +216 -0
  25. package/core/cpp-conversions.js +414 -0
  26. package/core/cpp-scope.js +245 -44
  27. package/core/deadcode.js +35 -15
  28. package/core/decorator-binding.js +137 -3
  29. package/core/enum-reference-syntax.js +134 -0
  30. package/core/execute.js +17 -1
  31. package/core/external-macros.js +42 -10
  32. package/core/go-packages.js +171 -0
  33. package/core/graph-build.js +2 -0
  34. package/core/graph.js +75 -59
  35. package/core/ignore-rules.js +2 -1
  36. package/core/imports.js +145 -19
  37. package/core/index-ir.js +41 -4
  38. package/core/ir.js +27 -6
  39. package/core/macro-expansion.js +38 -2
  40. package/core/out-of-class-members.js +182 -0
  41. package/core/output/analysis-ext.js +4 -2
  42. package/core/output/analysis.js +89 -20
  43. package/core/output/graph.js +12 -3
  44. package/core/output/lines.js +4 -3
  45. package/core/output/public.js +1 -0
  46. package/core/output/reporting.js +6 -1
  47. package/core/output/shared.js +2 -0
  48. package/core/output/tracing.js +28 -22
  49. package/core/output-budget.js +2 -2
  50. package/core/parallel-build.js +52 -1
  51. package/core/project.js +217 -112
  52. package/core/provenance-constructor-base.js +161 -0
  53. package/core/provenance-facts.js +257 -27
  54. package/core/provenance-overload.js +458 -54
  55. package/core/provenance-type.js +487 -0
  56. package/core/provenance.js +234 -15
  57. package/core/public-command.js +17 -2
  58. package/core/query-bodies.js +159 -0
  59. package/core/reflection.js +255 -2
  60. package/core/rename-token-syntax.js +111 -0
  61. package/core/reporting.js +13 -7
  62. package/core/rust-macro-expansion.js +151 -19
  63. package/core/rust-modules.js +1 -1
  64. package/core/rust-names.js +607 -0
  65. package/core/search.js +70 -17
  66. package/core/shared.js +98 -1
  67. package/core/source-memo.js +201 -0
  68. package/core/text-usage-syntax.js +38 -0
  69. package/core/tracing.js +7 -1
  70. package/core/type-denotation.js +124 -37
  71. package/core/type-references.js +615 -246
  72. package/core/value-references.js +2777 -0
  73. package/core/verify.js +1097 -281
  74. package/languages/adapter.js +28 -4
  75. package/languages/c-family.js +1118 -164
  76. package/languages/constructor-bases.js +105 -0
  77. package/languages/cpp-platform.js +26 -0
  78. package/languages/cpp-site-syntax.js +83 -0
  79. package/languages/csharp-scope.js +439 -0
  80. package/languages/csharp.js +428 -68
  81. package/languages/go.js +385 -207
  82. package/languages/html.js +4 -2
  83. package/languages/index.js +397 -2
  84. package/languages/java-platform.js +226 -0
  85. package/languages/java-unicode.js +269 -0
  86. package/languages/java.js +461 -117
  87. package/languages/javascript.js +418 -66
  88. package/languages/lexical-scope.js +190 -23
  89. package/languages/python.js +404 -54
  90. package/languages/rust.js +510 -82
  91. package/languages/utils.js +67 -8
  92. package/mcp/server.js +8 -2
  93. 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
- if ((index.symbols.get(name) || []).some(d => d.className === owner && d.file === file)) return false;
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 (['attribute', 'member_expression', 'member_access_expression'].includes(node.parent?.type)) node = node.parent;
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 (parent?.type === 'update_expression' || parent?.type === 'delete_statement') return parent.type === 'update_expression' ? 'read/write' : 'delete';
39
- if (['assignment', 'assignment_expression', 'augmented_assignment', 'augmented_assignment_expression'].includes(parent?.type)) {
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
- return parent.type.startsWith('augmented') ? 'read/write' : 'write';
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, ownerFile) {
202
+ function typeMatchesOwner(index, typeName, contextFile, owner, definition) {
163
203
  const bare = bareTypeName(typeName);
164
204
  if (!bare || !owner || bare !== owner) return false;
165
- const resolved = index._resolveClassFile?.(bare, contextFile);
166
- if (resolved) return path.resolve(resolved) === path.resolve(ownerFile);
167
- const ownerDefs = (index.symbols.get(owner) || []).filter(symbol =>
168
- ['class', 'struct', 'interface', 'trait', 'record'].includes(symbol.type));
169
- return ownerDefs.length === 1 &&
170
- path.resolve(ownerDefs[0].file) === path.resolve(ownerFile);
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
- const refs = [];
212
- for (const [file, entry] of index.files) {
213
- // `files` (fix #398F): a caller that needs one file family (tests)
214
- // never parses the rest of the project.
215
- if (options.files && !options.files.has(file)) continue;
216
- if (!index.matchesFilters(entry.relativePath, options)) continue;
217
- let content;
218
- let occurrences;
219
- try {
220
- content = index._readFile(file);
221
- if (!content.includes(name)) continue;
222
- occurrences = index._getCachedUsages(file, name);
223
- } catch { continue; }
224
- if (!occurrences) continue;
225
- const parser = getParser(entry.language);
226
- const tree = parser ? safeParse(parser, content) : null;
227
- const lines = content.split('\n');
228
- for (const usage of occurrences) {
229
- if (usage.usageType !== 'reference') continue;
230
- let shape = null;
231
- if (!usage.receiver) {
232
- shape = receiverlessShape(tree, usage);
233
- if (shape === 'none') continue;
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.file)) {
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.file)) {
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
  };