ucn 5.2.0 → 5.2.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.
@@ -70,7 +70,7 @@ function formatPlan(plan, options = {}) {
70
70
  lines.push(` Files affected: ${plan.filesAffected}`);
71
71
  if (plan.changeSummary) {
72
72
  const summary = plan.changeSummary;
73
- lines.push(` Definition ${summary.definitions}, calls ${summary.calls}, references ${summary.references || 0}, imports ${summary.imports}, exports ${summary.exports}; manual review required for ${summary.reviewRequired} of these changes`);
73
+ lines.push(` Definition ${summary.definitions}, calls ${summary.calls}, references ${summary.references || 0}, text dependencies ${summary.textReferences || 0}, imports ${summary.imports}, exports ${summary.exports}; manual review items ${summary.reviewRequired}`);
74
74
  }
75
75
  if (plan.unchangedSites > 0) {
76
76
  lines.push(` ${plan.unchangedSites} existing call site${plan.unchangedSites === 1 ? '' : 's'} require no edit because the new parameter has a default.`);
@@ -78,6 +78,9 @@ function formatPlan(plan, options = {}) {
78
78
  if (plan.scopeWarning) {
79
79
  lines.push(` Note: ${plan.scopeWarning.hint}`);
80
80
  }
81
+ if (plan.outsideIndexedSource) {
82
+ lines.push(` Source boundary: ${plan.outsideIndexedSource.action}`);
83
+ }
81
84
  lines.push('');
82
85
 
83
86
  // Group by file
@@ -101,6 +104,17 @@ function formatPlan(plan, options = {}) {
101
104
  }
102
105
  }
103
106
 
107
+ if (plan.reviewItems?.length > 0) {
108
+ lines.push(`\nREVIEW ITEMS (${plan.reviewItems.length}) — source text dependencies are never rewritten automatically:`);
109
+ for (const item of plan.reviewItems.slice(0, 20)) {
110
+ lines.push(` ${item.file}:${item.line}: ${item.expression.replace(/\s+/g, ' ').slice(0, 120)}`);
111
+ lines.push(` → ${item.suggestion}`);
112
+ }
113
+ if (plan.reviewItems.length > 20) {
114
+ lines.push(` (+${plan.reviewItems.length - 20} more review items)`);
115
+ }
116
+ }
117
+
104
118
  // v4 tiered contract: candidates without evidence are not planned but
105
119
  // stay visible — a rename that misses one of these breaks at runtime.
106
120
  lines.push(..._unverifiedBandLines(plan.unverifiedSites));
@@ -139,7 +153,8 @@ function formatPlanJson(plan) {
139
153
  // commands still emitting a bare result object).
140
154
  return JSON.stringify({
141
155
  meta: {
142
- complete: (plan.unverifiedCount || 0) === 0,
156
+ complete: (plan.unverifiedCount || 0) === 0 &&
157
+ (plan.changeSummary?.reviewRequired || 0) === 0,
143
158
  unverified: plan.unverifiedCount || 0,
144
159
  ...(plan.account && { account: plan.account }),
145
160
  ...(plan.warnings?.length > 0 && { warnings: plan.warnings }),
@@ -153,8 +168,12 @@ function formatPlanJson(plan) {
153
168
  before: { signature: plan.before.signature },
154
169
  after: { signature: plan.after.signature },
155
170
  totalChanges: plan.totalChanges,
171
+ totalReviewItems: plan.totalReviewItems || 0,
156
172
  filesAffected: plan.filesAffected,
157
173
  ...(plan.changeSummary && { changeSummary: plan.changeSummary }),
174
+ ...(plan.outsideIndexedSource && {
175
+ outsideIndexedSource: plan.outsideIndexedSource,
176
+ }),
158
177
  ...(plan.unchangedSites > 0 && { unchangedSites: plan.unchangedSites }),
159
178
  changes: plan.changes.map(c => ({
160
179
  file: c.file,
@@ -167,6 +186,16 @@ function formatPlanJson(plan) {
167
186
  ...(c.isImport && { isImport: true }),
168
187
  ...(c.isExport && { isExport: true }),
169
188
  ...(c.needsReview && { needsReview: true }),
189
+ ...(c.textDependency && { textDependency: true }),
190
+ })),
191
+ reviewItems: (plan.reviewItems || []).map(item => ({
192
+ file: item.file,
193
+ line: item.line,
194
+ expression: item.expression,
195
+ suggestion: item.suggestion,
196
+ editKind: item.editKind,
197
+ needsReview: true,
198
+ textDependency: true,
170
199
  })),
171
200
  // v4 tiered contract passthrough
172
201
  unverifiedCount: plan.unverifiedCount,
@@ -294,6 +294,12 @@ function formatDeadcode(results, options = {}) {
294
294
  if (results.computedDispatch?.count > 0) {
295
295
  lines.push(`\nWARNING: ${results.computedDispatch.count} computed dispatch call(s) in ${results.computedDispatch.fileCount} file(s). Dead-code results are review candidates; runtime-selected members may not have a named static edge.`);
296
296
  }
297
+ if (results.reflection?.literalCount > 0) {
298
+ lines.push(`\n${results.reflection.literalCount} literal reflection use(s) name ${results.reflection.names.length} member spelling(s); matching symbols were withheld from deletion candidates.`);
299
+ }
300
+ if (results.reflection?.dynamicCount > 0) {
301
+ lines.push(`\nWARNING: ${results.reflection.dynamicCount} dynamic reflection use(s) have no static member spelling. Dead-code results remain review candidates because runtime-selected members cannot be attributed.`);
302
+ }
297
303
  if (results.coverage?.complete === false) {
298
304
  const c = results.coverage;
299
305
  const reasonText = Object.entries(c.reasons || {})
@@ -337,6 +343,7 @@ function formatDeadcodeJson(results) {
337
343
  ...(results.pythonImplicitExportFiles > 0 && { pythonImplicitExportFiles: results.pythonImplicitExportFiles }),
338
344
  ...(results.excludedDynamicDispatch > 0 && { excludedDynamicDispatch: results.excludedDynamicDispatch }),
339
345
  ...(results.computedDispatch?.count > 0 && { computedDispatch: results.computedDispatch }),
346
+ ...(results.reflection?.count > 0 && { reflection: results.reflection }),
340
347
  ...(results.coverage?.complete === false && { coverage: results.coverage }),
341
348
  symbols: results.map(item => {
342
349
  const handleSym = { ...item, relativePath: item.relativePath || item.file };
package/core/project.js CHANGED
@@ -79,8 +79,33 @@ class ProjectIndex {
79
79
  this._opLinesCache = null; // per-operation split-lines cache (Map<filePath, string[]>, bounded FIFO)
80
80
  this._opInnerSymbolRangesCache = null; // per-operation sorted class-method ranges by file
81
81
  this._opFlowTypeOriginCache = null; // per-operation annotation type identity results
82
+ this._opCppTypeCategoryCache = null; // per-operation normalized C++ parameter categories
83
+ this._opCppPathReceiverTypeCache = null; // per-operation C++ qualified receiver identity
84
+ this._opDerefPairs = null; // per-operation Rust Deref identity pairs
85
+ this._opAliasPairs = null; // per-operation language type-alias identity pairs
82
86
  this._parsedTreeCache = new Map(); // cross-operation LRU: filePath -> immutable tree entry
83
87
  this._parsedTreeCacheSourceBytes = 0;
88
+ // Cross-operation, content-hash-keyed usage classifications. Account
89
+ // queries often ask several projections for the same hot symbol; the
90
+ // AST answer is immutable until the file hash changes. Bounded by
91
+ // both entries and approximate payload size to avoid turning a warm
92
+ // MCP process into an unbounded repository mirror.
93
+ this._usageResultCache = new Map();
94
+ this._usageResultCacheWeight = 0;
95
+ this.usageCacheDirty = false;
96
+ // Exact text-ground sets are likewise immutable for one built index.
97
+ // The cache is cleared at every build and bounded in account.js.
98
+ this._groundSetCache = new Map();
99
+ this._groundSetCacheLines = 0;
100
+ // Bounded cross-operation memo for immutable name-level export
101
+ // ownership. Agent workflows ask show/impact/tests about related
102
+ // symbols in sequence; retaining these tri-state barrel verdicts
103
+ // avoids repeating the same bounded graph walks after every command.
104
+ this._nameBindingReachCache = new Map();
105
+ // Query-derived return flow depends on cross-file annotations and is
106
+ // deliberately never persisted. It is safe across commands only
107
+ // until the next build, which resets it below.
108
+ this._returnTypeFlowCache = new Map();
84
109
  this.calleeIndex = null; // name -> Set<filePath> — inverted call index (built lazily)
85
110
  }
86
111
 
@@ -120,6 +145,10 @@ class ProjectIndex {
120
145
  this._opInnerSymbolRangesCache = new Map();
121
146
  this._opFlowTypeOriginCache = new Map();
122
147
  this._opImportReachCache = new Map();
148
+ this._opCppTypeCategoryCache = new Map();
149
+ this._opCppPathReceiverTypeCache = new Map();
150
+ this._opDerefPairs = undefined;
151
+ this._opAliasPairs = undefined;
123
152
  this._opDepth = 0;
124
153
  }
125
154
  this._opDepth++;
@@ -145,6 +174,10 @@ class ProjectIndex {
145
174
  this._opInnerSymbolRangesCache = null;
146
175
  this._opFlowTypeOriginCache = null;
147
176
  this._opImportReachCache = null;
177
+ this._opCppTypeCategoryCache = null;
178
+ this._opCppPathReceiverTypeCache = null;
179
+ this._opDerefPairs = null;
180
+ this._opAliasPairs = null;
148
181
  // Free cached file content from callsCache entries (retained during
149
182
  // operation for _readFile caching, not needed between operations)
150
183
  for (const entry of this.callsCache.values()) {
@@ -255,15 +288,38 @@ class ProjectIndex {
255
288
  * multiple times within one operation (e.g., about() calls both countSymbolUsages and usages).
256
289
  * @param {string} filePath - File to scan
257
290
  * @param {string} name - Symbol name to find
291
+ * @param {object} [options]
292
+ * @param {boolean} [options.skipCallRecovery] - omit usage-only call
293
+ * recovery when the caller has already classified lines from the call index
258
294
  * @returns {Array|null} Array of usage objects or null if parsing failed
259
295
  */
260
- _getCachedUsages(filePath, name) {
261
- const cacheKey = `${filePath}\0${name}`;
296
+ _getCachedUsages(filePath, name, options = {}) {
297
+ // Account construction checks the complete calls cache before it asks
298
+ // the language adapter to classify the remaining name occurrences.
299
+ // C/C++ can therefore skip its expensive macro replacement-list call
300
+ // recovery in that mode. Partition both cache layers so a partial
301
+ // account classification can never poison the full `usages` result.
302
+ const mode = [
303
+ options.skipCallRecovery ? 'skip-call-recovery' : '',
304
+ ].filter(Boolean).join('+');
305
+ const modeSuffix = mode ? `\0${mode}` : '';
306
+ const cacheKey = `${filePath}\0${name}${modeSuffix}`;
262
307
  if (this._opUsagesCache) {
263
308
  const cached = this._opUsagesCache.get(cacheKey);
264
309
  if (cached !== undefined) return cached;
265
310
  }
266
311
 
312
+ const fileHash = this.files.get(filePath)?.hash || '';
313
+ const persistentKey = `${filePath}\0${fileHash}\0${name}${modeSuffix}`;
314
+ if (this._usageResultCache?.has(persistentKey)) {
315
+ const cached = this._usageResultCache.get(persistentKey);
316
+ // Map insertion order is the LRU order.
317
+ this._usageResultCache.delete(persistentKey);
318
+ this._usageResultCache.set(persistentKey, cached);
319
+ if (this._opUsagesCache) this._opUsagesCache.set(cacheKey, cached.value);
320
+ return cached.value;
321
+ }
322
+
267
323
  // Header language is resolved during indexing from compilation
268
324
  // databases/include context. Re-detecting `.h` here can choose C for
269
325
  // a C++ header and silently drop member/template usages from the raw
@@ -292,10 +348,26 @@ class ProjectIndex {
292
348
  !langModule.managesOwnParseTree
293
349
  ? this._getParsedTree(filePath, content, lang)
294
350
  : null;
295
- const usages = langModule.findUsagesInCode(content, name, parser, tree);
351
+ const usages = langModule.findUsagesInCode(
352
+ content, name, parser, tree, options);
296
353
  if (this._opUsagesCache) {
297
354
  this._opUsagesCache.set(cacheKey, usages);
298
355
  }
356
+ if (Array.isArray(usages) && this._usageResultCache) {
357
+ const weight = 64 + usages.length * 40;
358
+ this._usageResultCache.set(persistentKey, { value: usages, weight });
359
+ this._usageResultCacheWeight += weight;
360
+ this.usageCacheDirty = true;
361
+ const maxEntries = 4096;
362
+ const maxWeight = 16 * 1024 * 1024;
363
+ while (this._usageResultCache.size > maxEntries ||
364
+ this._usageResultCacheWeight > maxWeight) {
365
+ const oldest = this._usageResultCache.entries().next().value;
366
+ if (!oldest) break;
367
+ this._usageResultCache.delete(oldest[0]);
368
+ this._usageResultCacheWeight -= oldest[1].weight;
369
+ }
370
+ }
299
371
  return usages;
300
372
  } catch (e) {
301
373
  return null;
@@ -327,6 +399,14 @@ class ProjectIndex {
327
399
  const startTime = Date.now();
328
400
  const quiet = options.quiet !== false;
329
401
 
402
+ // Build/discovery can add, remove, or reclassify files. Do not retain
403
+ // a text-universe answer across that boundary. Hash-keyed usage
404
+ // results remain safe and useful for unchanged files.
405
+ this._groundSetCache = new Map();
406
+ this._groundSetCacheLines = 0;
407
+ this._nameBindingReachCache = new Map();
408
+ this._returnTypeFlowCache = new Map();
409
+
330
410
  // A (re)build invalidates any cache-loaded reachability set — the
331
411
  // fingerprint guard in computeReachability is content-shaped and
332
412
  // cannot see every rebuild (fix #249: a stale loaded set survived
@@ -2414,6 +2494,11 @@ class ProjectIndex {
2414
2494
  /** Load index from cache file */
2415
2495
  loadCache(cachePath) { return indexCache.loadCache(this, cachePath); }
2416
2496
 
2497
+ /** Persist the bounded, content-hash-keyed usage-query cache. */
2498
+ saveUsageCache(cachePath = undefined) {
2499
+ return indexCache.saveUsageCache(this, cachePath);
2500
+ }
2501
+
2417
2502
  /** Return this project's default per-user cache file path. */
2418
2503
  getCachePath() { return indexCache.getProjectCachePath(this.root); }
2419
2504
 
package/core/verify.js CHANGED
@@ -8,6 +8,7 @@
8
8
  const { detectLanguage, getParser, getLanguageAdapter, safeParse, langTraits } = require('../languages');
9
9
  const { sameNode } = require('../languages/utils');
10
10
  const { escapeRegExp, codeUnitCompare, NON_CALLABLE_TYPES } = require('./shared');
11
+ const { findAccessorReferences } = require('./accessors');
11
12
 
12
13
  function codeUnitColumnForByteColumn(line, byteColumn) {
13
14
  if (!Number.isInteger(byteColumn) || byteColumn < 0) return null;
@@ -2084,6 +2085,7 @@ function plan(index, name, options = {}) {
2084
2085
  let newSignature = currentSignature;
2085
2086
  let operation = null;
2086
2087
  let changes = [];
2088
+ const reviewItems = [];
2087
2089
  let unchangedSites = 0;
2088
2090
 
2089
2091
  if (options.addParam) {
@@ -2361,7 +2363,7 @@ function plan(index, name, options = {}) {
2361
2363
  // tests; the import/reference sweep must not silently hide them via
2362
2364
  // usages()' navigation-oriented default test exclusion.
2363
2365
  const usages = index.usages(name, {
2364
- codeOnly: true,
2366
+ codeOnly: false,
2365
2367
  includeTests: true,
2366
2368
  internalEvidence: true,
2367
2369
  });
@@ -3166,6 +3168,75 @@ function plan(index, name, options = {}) {
3166
3168
  }
3167
3169
  }
3168
3170
  }
3171
+
3172
+ // Property/getter/setter renames must cover their normal consumption
3173
+ // form: attribute reads and writes. Reuse impact's receiver-evidence
3174
+ // query so a typed field is edited mechanically and an unresolved
3175
+ // receiver is surfaced for review instead of silently omitted.
3176
+ const accessorReferences = findAccessorReferences(index, name, def, {
3177
+ includeTests: true,
3178
+ });
3179
+ if (accessorReferences) {
3180
+ const confirmedByLine = new Map();
3181
+ for (const ref of accessorReferences.confirmed) {
3182
+ const key = `${ref.absoluteFile}\0${ref.line}`;
3183
+ if (!confirmedByLine.has(key)) confirmedByLine.set(key, []);
3184
+ confirmedByLine.get(key).push(ref);
3185
+ }
3186
+ for (const refs of confirmedByLine.values()) {
3187
+ const ref = refs[0];
3188
+ const columns = refs.map(item => item.column)
3189
+ .filter(Number.isInteger);
3190
+ const edit = renameIdentifierTokens(index, ref.absoluteFile,
3191
+ ref.line, name, options.renameTo,
3192
+ columns.length === refs.length ? columns : null);
3193
+ if (edit.renamed === edit.source) continue;
3194
+ const concrete = {
3195
+ file: ref.file,
3196
+ line: ref.line,
3197
+ expression: edit.source,
3198
+ suggestion: `Update property access: ${edit.renamed}`,
3199
+ newExpression: edit.renamed,
3200
+ editKind: 'reference',
3201
+ };
3202
+ const existing = changes.find(change =>
3203
+ change.file === ref.file && change.line === ref.line &&
3204
+ !change.needsReview);
3205
+ if (existing) Object.assign(existing, concrete);
3206
+ else changes.push(concrete);
3207
+ }
3208
+ for (const ref of accessorReferences.unverified) {
3209
+ changes.push({
3210
+ file: ref.file,
3211
+ line: ref.line,
3212
+ expression: ref.expression,
3213
+ suggestion: `Verify this property access resolves to ${name} on ` +
3214
+ `${accessorReferences.owner} before renaming`,
3215
+ needsReview: true,
3216
+ editKind: 'reference',
3217
+ });
3218
+ }
3219
+ }
3220
+
3221
+ // String/comment occurrences in indexed source can encode guards,
3222
+ // reflection keys, protocol names, snapshots, or documentation. AST
3223
+ // identifier replacement must never rewrite them automatically, but a
3224
+ // complete plan must list them as explicit review work.
3225
+ for (const textRef of usages.filter(usage => usage.usageType === 'text')) {
3226
+ const rel = textRef.relativePath || textRef.file;
3227
+ if (reviewItems.some(item =>
3228
+ item.file === rel && item.line === textRef.line)) continue;
3229
+ reviewItems.push({
3230
+ file: rel,
3231
+ line: textRef.line,
3232
+ expression: (textRef.content || '').trim(),
3233
+ suggestion: `Review comment/string dependency on "${name}"; ` +
3234
+ 'rename manually only if its contract changes',
3235
+ needsReview: true,
3236
+ textDependency: true,
3237
+ editKind: 'text-reference',
3238
+ });
3239
+ }
3169
3240
  }
3170
3241
 
3171
3242
  // Every operation changes the selected declaration. Historically `plan`
@@ -3222,7 +3293,9 @@ function plan(index, name, options = {}) {
3222
3293
  imports: changes.filter(change => change.editKind === 'import').length,
3223
3294
  exports: changes.filter(change => change.editKind === 'export').length,
3224
3295
  references: changes.filter(change => change.editKind === 'reference').length,
3225
- reviewRequired: changes.filter(change => change.needsReview).length,
3296
+ textReferences: reviewItems.length,
3297
+ reviewRequired: changes.filter(change => change.needsReview).length +
3298
+ reviewItems.length,
3226
3299
  };
3227
3300
 
3228
3301
  return {
@@ -3243,9 +3316,11 @@ function plan(index, name, options = {}) {
3243
3316
  params: newParams.map(p => formatPlanParamName(p)).filter(Boolean)
3244
3317
  },
3245
3318
  totalChanges: changes.length,
3246
- filesAffected: new Set(changes.map(c => c.file)).size,
3319
+ filesAffected: new Set([...changes, ...reviewItems].map(c => c.file)).size,
3247
3320
  changeSummary,
3248
3321
  changes,
3322
+ reviewItems,
3323
+ totalReviewItems: reviewItems.length,
3249
3324
  ...(unchangedSites > 0 && { unchangedSites }),
3250
3325
  // v4 tiered contract: sites that MAY also need this change but lack
3251
3326
  // binding/receiver evidence — review manually before refactoring.
@@ -3253,6 +3328,13 @@ function plan(index, name, options = {}) {
3253
3328
  unverifiedSites: planUnverified,
3254
3329
  account: planAccount,
3255
3330
  scopeWarning: impactScopeWarning,
3331
+ ...(options.renameTo && {
3332
+ outsideIndexedSource: {
3333
+ scope: 'indexed-source-files',
3334
+ excluded: ['documentation', 'configuration', 'generated files', 'unsupported languages'],
3335
+ action: `Search non-source project files for the exact spelling "${name}" before applying the rename.`,
3336
+ },
3337
+ }),
3256
3338
  ...(resolved.warnings.length > 0 && { warnings: resolved.warnings }),
3257
3339
  };
3258
3340
  } finally { index._endOp(); }
@@ -513,27 +513,18 @@ function conditionalRecoverySources(code) {
513
513
  }
514
514
 
515
515
  function treeStructureScore(tree) {
516
- let declarations = 0;
517
- let calls = 0;
518
516
  const declarationTypes = new Set([
519
517
  'function_definition', 'class_specifier', 'struct_specifier',
520
518
  'union_specifier', 'enum_specifier', 'type_definition',
521
519
  ]);
522
- const cursor = tree.walk();
523
- let entered = true;
524
- while (entered) {
525
- const type = cursor.nodeType;
526
- if (declarationTypes.has(type)) declarations++;
527
- else if (type === 'call_expression') calls++;
528
- if (cursor.gotoFirstChild()) continue;
529
- while (!cursor.gotoNextSibling()) {
530
- if (!cursor.gotoParent()) {
531
- entered = false;
532
- break;
533
- }
534
- }
535
- }
536
- cursor.delete?.();
520
+ // `descendantsOfType` performs the filtering in tree-sitter's native
521
+ // cursor. Recovery can score the same large source under as many as 14
522
+ // bounded preprocessor views; walking every node through the JS bridge
523
+ // made scoring alone a material part of cold C/C++ build CPU. The native
524
+ // query returns the exact same node sets and therefore preserves the
525
+ // recovery ordering contract while avoiding thousands of wrapper calls.
526
+ const declarations = tree.rootNode.descendantsOfType([...declarationTypes]).length;
527
+ const calls = tree.rootNode.descendantsOfType('call_expression').length;
537
528
  return declarations * 1000 + calls;
538
529
  }
539
530
 
@@ -2785,7 +2776,7 @@ function findImportsInCode(code, parser) {
2785
2776
  } finally { /* cached with the selected tree */ }
2786
2777
  }
2787
2778
 
2788
- function findUsagesInCode(code, name, parser, existingTree) {
2779
+ function findUsagesInCode(code, name, parser, existingTree, options = {}) {
2789
2780
  // Usage is the raw literal-name inventory. The literal C/C++ tree retains
2790
2781
  // identifiers from every preprocessor branch and is sufficient for
2791
2782
  // occurrence kind/line classification; symbol ownership still comes from
@@ -2869,22 +2860,24 @@ function findUsagesInCode(code, name, parser, existingTree) {
2869
2860
  // call extractor reparses those AST-proven regions; surface the resulting
2870
2861
  // call usages here as well so callers/callees, usages, and tests share one
2871
2862
  // semantic fact set.
2872
- const seenCalls = new Set(usages
2873
- .filter(usage => usage.usageType === 'call')
2874
- .map(usage => `${usage.line}:${usage.column ?? ''}`));
2875
- const macroCalls = findMacroBodyCalls(tree, code, parser, name);
2876
- for (const call of macroCalls) {
2877
- if (call.name !== name) continue;
2878
- const key = `${call.line}:${call.column ?? ''}`;
2879
- if (seenCalls.has(key)) continue;
2880
- seenCalls.add(key);
2881
- addUsage({
2882
- line: call.line,
2883
- column: call.column,
2884
- usageType: 'call',
2885
- ...(call.receiver && { receiver: call.receiver }),
2886
- ...(call.macroParameter && { macroParameter: true }),
2887
- });
2863
+ if (!options.skipCallRecovery) {
2864
+ const seenCalls = new Set(usages
2865
+ .filter(usage => usage.usageType === 'call')
2866
+ .map(usage => `${usage.line}:${usage.column ?? ''}`));
2867
+ const macroCalls = findMacroBodyCalls(tree, code, parser, name);
2868
+ for (const call of macroCalls) {
2869
+ if (call.name !== name) continue;
2870
+ const key = `${call.line}:${call.column ?? ''}`;
2871
+ if (seenCalls.has(key)) continue;
2872
+ seenCalls.add(key);
2873
+ addUsage({
2874
+ line: call.line,
2875
+ column: call.column,
2876
+ usageType: 'call',
2877
+ ...(call.receiver && { receiver: call.receiver }),
2878
+ ...(call.macroParameter && { macroParameter: true }),
2879
+ });
2880
+ }
2888
2881
  }
2889
2882
  return usages;
2890
2883
  }
@@ -293,7 +293,7 @@ function propertyMember(node, lines) {
293
293
  endLine,
294
294
  indent,
295
295
  modifiers: modifiersOf(node),
296
- memberType: 'field',
296
+ memberType: 'property',
297
297
  fieldType: typeNode?.text || null,
298
298
  };
299
299
  }
@@ -1312,6 +1312,7 @@ function findImportsInCode(code, parser) {
1312
1312
  function findUsagesInCode(code, name, parser, existingTree) {
1313
1313
  const tree = existingTree || parseTree(parser, code);
1314
1314
  const usages = [];
1315
+ const variableTypesByScope = buildVariableTypes(tree, parser);
1315
1316
  visitNameNodes(tree, code, name, node => {
1316
1317
  if (!IDENTIFIER_NODES.has(node.type) || node.text !== name) return;
1317
1318
  let usageType = 'reference';
@@ -1320,6 +1321,8 @@ function findUsagesInCode(code, name, parser, existingTree) {
1320
1321
  if ((parent.type === 'method_declaration' ||
1321
1322
  parent.type === 'constructor_declaration' ||
1322
1323
  TYPE_DECLARATIONS.has(parent.type) ||
1324
+ parent.type === 'property_declaration' ||
1325
+ parent.type === 'event_declaration' ||
1323
1326
  parent.type === 'parameter' ||
1324
1327
  parent.type === 'variable_declarator') &&
1325
1328
  (sameNode(parent.childForFieldName('name'), node))) {
@@ -1330,6 +1333,28 @@ function findUsagesInCode(code, name, parser, existingTree) {
1330
1333
  } else if (parent.type === 'using_directive') {
1331
1334
  usageType = 'import';
1332
1335
  }
1336
+ if (parent.type === 'member_access_expression' &&
1337
+ sameNode(parent.childForFieldName('name'), node)) {
1338
+ const receiverNode = parent.childForFieldName('expression') ||
1339
+ parent.namedChild(0);
1340
+ const receiver = receiverNode?.text;
1341
+ const scopeTypes = variableTypesByScope.get(variableScopeKey(node)) ||
1342
+ variableTypesByScope.get('global');
1343
+ const declared = receiverNode?.type === 'identifier'
1344
+ ? normalizeReceiverType(scopeTypes?.get(receiver)) : null;
1345
+ const sameClass = ['this', 'base'].includes(receiver)
1346
+ ? enclosingClassName(node) : null;
1347
+ usages.push({
1348
+ line: node.startPosition.row + 1,
1349
+ column: node.startPosition.column,
1350
+ usageType,
1351
+ ...(receiver && { receiver }),
1352
+ ...((declared?.name || sameClass) && {
1353
+ receiverType: declared?.name || sameClass,
1354
+ }),
1355
+ });
1356
+ return true;
1357
+ }
1333
1358
  }
1334
1359
  usages.push({
1335
1360
  line: node.startPosition.row + 1,