ucn 5.2.1 → 5.3.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 +49 -3
- package/.claude/skills/ucn/references/commands.md +3 -1
- package/README.md +158 -533
- package/cli/index.js +74 -10
- package/core/account.js +36 -8
- package/core/cache.js +167 -21
- package/core/callers.js +1229 -162
- package/core/execute.js +24 -6
- package/core/graph.js +167 -35
- package/core/index-ir.js +17 -12
- package/core/ir.js +56 -8
- package/core/output/graph.js +60 -11
- package/core/output/lines.js +259 -0
- package/core/output/public.js +15 -0
- package/core/output/reporting.js +9 -2
- package/core/output-budget.js +7 -4
- package/core/project.js +103 -5
- package/core/registry.js +7 -6
- package/core/reporting.js +159 -14
- package/languages/c-family.js +19 -17
- package/languages/go.js +170 -42
- package/languages/javascript.js +470 -15
- package/languages/python.js +678 -38
- package/languages/rust.js +1 -0
- package/mcp/server.js +3 -1
- package/package.json +2 -2
- package/assets/demo.svg +0 -31
package/core/registry.js
CHANGED
|
@@ -133,13 +133,13 @@ const FLAG_APPLICABILITY = {
|
|
|
133
133
|
// Understand one symbol. `sections` is a comma-separated projection:
|
|
134
134
|
// summary, callers, callees, source, dependencies, tests, types, example,
|
|
135
135
|
// related. Caller-bearing projections always preserve ACCOUNT/CONTRACT.
|
|
136
|
-
show: ['name', 'file', 'exclude', 'className', 'line', 'sections', 'includeMethods', 'includeTests', 'top', 'all', 'withTypes', 'minConfidence', 'showConfidence', 'unreachableOnly', 'compact', 'git', 'diverse'],
|
|
137
|
-
find: ['name', 'file', 'exclude', 'className', 'includeTests', 'limit', 'exact', 'in', 'compact', 'type', 'withSource'],
|
|
138
|
-
usages: ['name', 'file', 'exclude', 'className', 'includeTests', 'limit', 'codeOnly', 'context', 'in', 'compact', 'all'],
|
|
139
|
-
search: ['term', 'file', 'exclude', 'includeTests', 'top', 'limit', 'codeOnly', 'caseSensitive', 'context', 'regex', 'in', 'type', 'param', 'receiver', 'returns', 'decorator', 'exported', 'unused'],
|
|
140
|
-
source: ['name', 'file', 'className', 'line', 'range', 'all', 'maxLines'],
|
|
136
|
+
show: ['name', 'file', 'exclude', 'className', 'line', 'sections', 'includeMethods', 'includeTests', 'top', 'all', 'withTypes', 'minConfidence', 'showConfidence', 'unreachableOnly', 'compact', 'git', 'diverse', 'lines'],
|
|
137
|
+
find: ['name', 'file', 'exclude', 'className', 'includeTests', 'limit', 'exact', 'in', 'compact', 'type', 'withSource', 'lines'],
|
|
138
|
+
usages: ['name', 'file', 'exclude', 'className', 'includeTests', 'limit', 'codeOnly', 'context', 'in', 'compact', 'all', 'lines'],
|
|
139
|
+
search: ['term', 'file', 'exclude', 'includeTests', 'top', 'limit', 'codeOnly', 'caseSensitive', 'context', 'regex', 'in', 'type', 'param', 'receiver', 'returns', 'decorator', 'exported', 'unused', 'lines'],
|
|
140
|
+
source: ['name', 'file', 'className', 'line', 'range', 'all', 'maxLines', 'raw'],
|
|
141
141
|
trace: ['name', 'file', 'exclude', 'className', 'line', 'direction', 'to', 'includeMethods', 'depth', 'all', 'expandUnverified'],
|
|
142
|
-
impact: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'top', 'unreachableOnly', 'compact', 'base', 'staged', 'limit', 'all'],
|
|
142
|
+
impact: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'top', 'unreachableOnly', 'compact', 'base', 'staged', 'limit', 'all', 'lines'],
|
|
143
143
|
tests: ['name', 'file', 'exclude', 'className', 'line', 'callsOnly', 'depth', 'includeMethods', 'all'],
|
|
144
144
|
deps: ['file', 'exclude', 'depth', 'direction', 'all', 'detailed', 'cycles'],
|
|
145
145
|
api: ['file', 'in', 'limit'],
|
|
@@ -308,6 +308,7 @@ function formatSurfaceMessage(message, surface = 'cli') {
|
|
|
308
308
|
'expandUnverified', 'withSource', 'all', 'compact', 'exact',
|
|
309
309
|
'regex', 'exported', 'unused', 'staged', 'detailed', 'functions',
|
|
310
310
|
'hot', 'deep', 'cycles', 'bridge', 'unmatched', 'diverse', 'git',
|
|
311
|
+
'raw', 'lines',
|
|
311
312
|
]);
|
|
312
313
|
if (surface === 'mcp') {
|
|
313
314
|
rendered = rendered.replace(/--([a-z][a-z0-9-]*)(?:=([^\s,.)]+))?/g,
|
package/core/reporting.js
CHANGED
|
@@ -133,41 +133,155 @@ function getStats(index, options = {}) {
|
|
|
133
133
|
// decide which definitions are capable of entering the requested top
|
|
134
134
|
// N. Exact pinned caller resolution is then run only until no unseen
|
|
135
135
|
// candidate can beat the current Nth result.
|
|
136
|
-
|
|
136
|
+
// A record whose receiver is provably EXTERNAL (fix #340) cannot
|
|
137
|
+
// confirm a project definition — the engine routes such calls
|
|
138
|
+
// external-package / possible-dispatch, never confirmed — so it leaves
|
|
139
|
+
// the bound. `t.Fatalf(...)` (receiverType T, qualifier `testing`)
|
|
140
|
+
// used to hand every project `Fatalf`/`Fatal`/`Errorf`/`String` a
|
|
141
|
+
// four-digit upper bound, so the early stop never fired and grpc-go's
|
|
142
|
+
// `repo` refined 1375 candidates. Resolution is the engine's own
|
|
143
|
+
// module physics: an import naming the qualifier that resolves to a
|
|
144
|
+
// project file keeps the record; a resolver gap keeps it; only an
|
|
145
|
+
// import that is non-relative, non-project, and unresolved drops it.
|
|
146
|
+
const { _unresolvedModuleIsGap } = require('./callers');
|
|
147
|
+
const externalQualifierMemo = new Map(); // filePath + qualifier -> boolean
|
|
148
|
+
const receiverProvablyExternal = (filePath, fileEntry, c) => {
|
|
149
|
+
if (!c.isMethod || !fileEntry) return false;
|
|
150
|
+
const qualifier = c.receiverTypeQualifier ||
|
|
151
|
+
(c.receiverIsModule && c.receiver) || null;
|
|
152
|
+
if (!qualifier || typeof qualifier !== 'string') return false;
|
|
153
|
+
const key = filePath + '\u0000' + qualifier;
|
|
154
|
+
const memo = externalQualifierMemo.get(key);
|
|
155
|
+
if (memo !== undefined) return memo;
|
|
156
|
+
const head = qualifier.split('.')[0];
|
|
157
|
+
const modules = new Set();
|
|
158
|
+
for (const binding of fileEntry.importBindings || []) {
|
|
159
|
+
if (binding.name === head || binding.alias === head) modules.add(binding.module);
|
|
160
|
+
}
|
|
161
|
+
for (const mod of fileEntry.imports || []) {
|
|
162
|
+
const text = String(mod || '');
|
|
163
|
+
if (text === head || text.split('/').pop() === head ||
|
|
164
|
+
text.split('.').pop() === head) modules.add(text);
|
|
165
|
+
}
|
|
166
|
+
let verdict = false;
|
|
167
|
+
if (modules.size > 0) {
|
|
168
|
+
verdict = true;
|
|
169
|
+
for (const mod of modules) {
|
|
170
|
+
if (fileEntry.moduleResolved?.[mod] || _unresolvedModuleIsGap(index, mod)) {
|
|
171
|
+
verdict = false;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
externalQualifierMemo.set(key, verdict);
|
|
177
|
+
return verdict;
|
|
178
|
+
};
|
|
179
|
+
// Per-DEFINITION bound (fix #340). A record with a trusted parser
|
|
180
|
+
// receiver type can confirm only that type's own same-name method,
|
|
181
|
+
// or an inherited/promoted one when that type defines none itself —
|
|
182
|
+
// so it is charged to that class alone (or to every definition when
|
|
183
|
+
// the class is not a definer). Untyped and convention-guessed
|
|
184
|
+
// receivers (#266: never exclusion evidence) stay charged to every
|
|
185
|
+
// definition. grpc-go: `String` has 278 same-name methods and `Close`
|
|
186
|
+
// 146; under the per-NAME bound every one of them inherited the whole
|
|
187
|
+
// name's ceiling and had to be refined.
|
|
188
|
+
const normalizeTypeName = (text) => String(text || '')
|
|
189
|
+
.replace(/^[*&\s]+/, '').replace(/[<[(].*$/, '').split('.').pop() || null;
|
|
190
|
+
const untypedByName = new Map(); // name -> records with no trusted receiver type
|
|
191
|
+
const typedByName = new Map(); // name -> Map<className, records typed to it>
|
|
192
|
+
const chargeRecord = (name, c) => {
|
|
193
|
+
const typed = c.isMethod && c.receiverType && !c.receiverTypeGuessed
|
|
194
|
+
? normalizeTypeName(c.receiverType) : null;
|
|
195
|
+
if (typed) {
|
|
196
|
+
let byClass = typedByName.get(name);
|
|
197
|
+
if (!byClass) { byClass = new Map(); typedByName.set(name, byClass); }
|
|
198
|
+
byClass.set(typed, (byClass.get(typed) || 0) + 1);
|
|
199
|
+
} else {
|
|
200
|
+
untypedByName.set(name, (untypedByName.get(name) || 0) + 1);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
// Fix #343: symbols and call sites inside inline test modules
|
|
204
|
+
// (`#[cfg(test)] mod tests` / `#[test]` fns, fix #244's ranges) are
|
|
205
|
+
// test code even though their FILE is production — ripgrep's
|
|
206
|
+
// `TempDir.path` (197 calls, all from tests) ranked second among
|
|
207
|
+
// "production functions". Under productionCallsOnly they leave the
|
|
208
|
+
// candidate set, the exact count, AND the upper bound (a bound that
|
|
209
|
+
// still counted them stopped the early exit from firing, so the
|
|
210
|
+
// orientation fell back to its approximate refinement budget).
|
|
211
|
+
const { inlineTestRanges, lineInRanges } = require('./shared');
|
|
212
|
+
const inlineTestRangesByFile = new Map();
|
|
213
|
+
const inInlineTest = (file, line) => {
|
|
214
|
+
if (!file || !line) return false;
|
|
215
|
+
let ranges = inlineTestRangesByFile.get(file);
|
|
216
|
+
if (!ranges) {
|
|
217
|
+
ranges = inlineTestRanges(index.files.get(file) || {});
|
|
218
|
+
inlineTestRangesByFile.set(file, ranges);
|
|
219
|
+
}
|
|
220
|
+
return ranges.length > 0 && lineInRanges(line, ranges);
|
|
221
|
+
};
|
|
137
222
|
for (const [filePath, entry] of index.callsCache) {
|
|
138
223
|
if (!scopedPaths.has(filePath)) continue;
|
|
139
|
-
|
|
224
|
+
const fileEntry = index.files.get(filePath);
|
|
225
|
+
if (fileEntry?.isBundled) continue;
|
|
140
226
|
if (!entry || !Array.isArray(entry.calls)) continue;
|
|
141
227
|
const seenInFile = new Set();
|
|
142
228
|
for (const c of entry.calls) {
|
|
143
229
|
if (!c || !c.name) continue;
|
|
144
|
-
|
|
230
|
+
if (receiverProvablyExternal(filePath, fileEntry, c)) continue;
|
|
231
|
+
if (options.productionCallsOnly && inInlineTest(filePath, c.line)) continue;
|
|
232
|
+
// Distinct receiver types on one line can confirm DIFFERENT
|
|
233
|
+
// definitions. Dedup within a type bucket, never across them.
|
|
234
|
+
// Mixed typed/untyped buckets may overcount, which is safe for
|
|
235
|
+
// an upper bound; dropping a bucket can erase a true HOT item.
|
|
236
|
+
const bucket = c.isMethod && c.receiverType && !c.receiverTypeGuessed
|
|
237
|
+
? normalizeTypeName(c.receiverType) : '';
|
|
238
|
+
const key = `${c.name}::${c.line || 0}::${bucket}`;
|
|
145
239
|
if (!seenInFile.has(key)) {
|
|
146
240
|
seenInFile.add(key);
|
|
147
|
-
|
|
241
|
+
chargeRecord(c.name, c);
|
|
148
242
|
}
|
|
149
243
|
if (c.resolvedName && c.resolvedName !== c.name) {
|
|
150
|
-
const rkey = `${c.resolvedName}::${c.line || 0}`;
|
|
244
|
+
const rkey = `${c.resolvedName}::${c.line || 0}::${bucket}`;
|
|
151
245
|
if (!seenInFile.has(rkey)) {
|
|
152
246
|
seenInFile.add(rkey);
|
|
153
|
-
|
|
154
|
-
(rawUpperByName.get(c.resolvedName) || 0) + 1);
|
|
247
|
+
chargeRecord(c.resolvedName, c);
|
|
155
248
|
}
|
|
156
249
|
}
|
|
157
250
|
}
|
|
158
251
|
}
|
|
252
|
+
const ownerOf = (symbol) => normalizeTypeName(symbol.className || symbol.receiver || '');
|
|
159
253
|
|
|
160
254
|
const candidates = [];
|
|
161
255
|
const seenDefinitions = new Set();
|
|
162
256
|
for (const [name, symbols] of index.symbols) {
|
|
163
|
-
const
|
|
164
|
-
|
|
257
|
+
const untyped = untypedByName.get(name) || 0;
|
|
258
|
+
const typedByClass = typedByName.get(name);
|
|
259
|
+
if (untyped === 0 && !typedByClass) continue;
|
|
165
260
|
const callable = symbols.filter(symbol =>
|
|
166
261
|
FUNCTION_TYPES.has(symbol.type) &&
|
|
167
262
|
matchesReportingScope(index, symbol.relativePath, options));
|
|
263
|
+
const definers = new Set(callable.map(ownerOf).filter(Boolean));
|
|
168
264
|
for (const symbol of callable) {
|
|
265
|
+
let upper = untyped;
|
|
266
|
+
if (typedByClass) {
|
|
267
|
+
const owner = ownerOf(symbol);
|
|
268
|
+
for (const [cls, count] of typedByClass) {
|
|
269
|
+
if (cls === owner || !definers.has(cls)) upper += count;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (upper === 0) continue;
|
|
273
|
+
// Fair share of the name's ceiling: a name shared by 278
|
|
274
|
+
// methods cannot make all 278 hot, so a definition's likely
|
|
275
|
+
// count is nearer upper/definers than upper. Ordering by the
|
|
276
|
+
// share puts the genuinely hot definitions first; the early
|
|
277
|
+
// stop below still uses the exact remaining ceiling, so the
|
|
278
|
+
// exact answer is unchanged and only a bounded refinement
|
|
279
|
+
// (`maxRefine`) benefits from the order.
|
|
280
|
+
const share = upper / Math.max(1, callable.length);
|
|
169
281
|
if (index.files.get(symbol.file)?.isBundled) continue;
|
|
170
|
-
if (options.productionCallsOnly &&
|
|
282
|
+
if (options.productionCallsOnly &&
|
|
283
|
+
(require('./shared').isTestPath(symbol.relativePath) ||
|
|
284
|
+
inInlineTest(symbol.file, symbol.startLine))) continue;
|
|
171
285
|
const identity = `${symbol.file}:${symbol.startLine}:${name}:` +
|
|
172
286
|
`${symbol.className || symbol.receiver || ''}:${symbol.params || ''}`;
|
|
173
287
|
if (seenDefinitions.has(identity)) continue;
|
|
@@ -183,21 +297,39 @@ function getStats(index, options = {}) {
|
|
|
183
297
|
index.importGraph.get(symbol.file)?.has(candidate.file)))) {
|
|
184
298
|
continue;
|
|
185
299
|
}
|
|
186
|
-
candidates.push({ name, symbol, upper });
|
|
300
|
+
candidates.push({ name, symbol, upper, share });
|
|
187
301
|
}
|
|
188
302
|
}
|
|
303
|
+
const maxRefine = Number.isInteger(options.maxRefine) && options.maxRefine > 0
|
|
304
|
+
? options.maxRefine : Infinity;
|
|
305
|
+
// Exact mode walks the ceilings in descending order so the early stop
|
|
306
|
+
// fires as soon as possible; a bounded refinement walks fair shares so
|
|
307
|
+
// the budget lands on the definitions most likely to be hot.
|
|
189
308
|
candidates.sort((a, b) =>
|
|
309
|
+
(maxRefine !== Infinity ? (b.share - a.share) : 0) ||
|
|
190
310
|
(b.upper - a.upper) ||
|
|
191
311
|
codeUnitCompare(a.symbol.relativePath, b.symbol.relativePath) ||
|
|
192
312
|
(a.symbol.startLine || 0) - (b.symbol.startLine || 0));
|
|
313
|
+
// Suffix maximum of the exact ceilings: once no unseen candidate can
|
|
314
|
+
// beat the current Nth result, the answer is exact regardless of order.
|
|
315
|
+
const remainingUpper = new Array(candidates.length + 1).fill(-1);
|
|
316
|
+
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
317
|
+
remainingUpper[i] = Math.max(candidates[i].upper, remainingUpper[i + 1]);
|
|
318
|
+
}
|
|
193
319
|
|
|
194
320
|
const hotList = [];
|
|
195
321
|
const { findCallers } = require('./callers');
|
|
196
322
|
const scopedCallerQuery = !!(options.file || options.in ||
|
|
197
323
|
(options.exclude && options.exclude.length > 0));
|
|
198
324
|
let refined = 0;
|
|
325
|
+
let budgetExhausted = false;
|
|
326
|
+
// One operation scope for the whole refinement loop (fix #340): the
|
|
327
|
+
// per-file derivations findCallers builds are shared across candidates.
|
|
328
|
+
if (top > 0) index._beginOp();
|
|
329
|
+
try {
|
|
199
330
|
if (top > 0) {
|
|
200
331
|
for (let candidateIndex = 0; candidateIndex < candidates.length; candidateIndex++) {
|
|
332
|
+
if (refined >= maxRefine) { budgetExhausted = true; break; }
|
|
201
333
|
const { name, symbol } = candidates[candidateIndex];
|
|
202
334
|
const exact = findCallers(index, name, {
|
|
203
335
|
targetDefinitions: [symbol],
|
|
@@ -210,7 +342,8 @@ function getStats(index, options = {}) {
|
|
|
210
342
|
(!scopedCallerQuery || scopedPaths.has(caller.file)) &&
|
|
211
343
|
!index.files.get(caller.file)?.isBundled &&
|
|
212
344
|
(!options.productionCallsOnly ||
|
|
213
|
-
!require('./shared').isTestPath(caller.relativePath || caller.file)
|
|
345
|
+
(!require('./shared').isTestPath(caller.relativePath || caller.file) &&
|
|
346
|
+
!inInlineTest(caller.file, caller.line)))).length;
|
|
214
347
|
if (count > 0) {
|
|
215
348
|
const owner = symbol.className ||
|
|
216
349
|
(symbol.receiver || '').replace(/^\*/, '');
|
|
@@ -229,11 +362,11 @@ function getStats(index, options = {}) {
|
|
|
229
362
|
(a.startLine || 0) - (b.startLine || 0));
|
|
230
363
|
if (hotList.length >= top) {
|
|
231
364
|
const threshold = hotList[top - 1].callCount;
|
|
232
|
-
|
|
233
|
-
if (nextUpper < threshold) break;
|
|
365
|
+
if (remainingUpper[candidateIndex + 1] < threshold) break;
|
|
234
366
|
}
|
|
235
367
|
}
|
|
236
368
|
}
|
|
369
|
+
} finally { if (top > 0) index._endOp(); }
|
|
237
370
|
|
|
238
371
|
// Stable order: callCount desc, then (relativePath, startLine) asc.
|
|
239
372
|
hotList.sort((a, b) =>
|
|
@@ -248,6 +381,7 @@ function getStats(index, options = {}) {
|
|
|
248
381
|
totalKind: refined === candidates.length ? 'confirmed' : 'raw-call-candidates',
|
|
249
382
|
refined,
|
|
250
383
|
items: hotList.slice(0, top),
|
|
384
|
+
...(budgetExhausted && { budgetExhausted: true, maxRefine }),
|
|
251
385
|
note: refined === candidates.length
|
|
252
386
|
? 'Counts are confirmed caller-engine edges pinned to each displayed definition; unverified dispatch is excluded.'
|
|
253
387
|
: `Displayed counts are exact confirmed caller-engine edges; ${candidates.length} raw candidates were bounded and ${refined} required exact refinement.`,
|
|
@@ -805,6 +939,12 @@ function computeEvidenceProfile(index, { sampleSize, matchInFilter }) {
|
|
|
805
939
|
* trust verdict. Composes existing engine reads; counts and pointers only
|
|
806
940
|
* (no caller claims, so no account — the toc/stats category).
|
|
807
941
|
*/
|
|
942
|
+
// Orientation refines at most this many HOT candidates exactly (fix #340).
|
|
943
|
+
// grpc-go (1037 files): exact refinement walks 1089 candidates in ~20s; 400
|
|
944
|
+
// in fair-share order reproduces the exact top 8 in under 5s. When the budget
|
|
945
|
+
// binds, the header says so and points at the exact command.
|
|
946
|
+
const ORIENT_HOT_REFINE_BUDGET = 400;
|
|
947
|
+
|
|
808
948
|
function orient(index, options = {}) {
|
|
809
949
|
const top = options.top || 8;
|
|
810
950
|
const scope = {
|
|
@@ -827,6 +967,8 @@ function orient(index, options = {}) {
|
|
|
827
967
|
// actually contains production files. In an all-test repository it
|
|
828
968
|
// would erase the raw ranking that orient promises as its fallback.
|
|
829
969
|
productionCallsOnly: options.includeTests !== true && hasProductionFiles,
|
|
970
|
+
maxRefine: Number.isInteger(options.hotRefineBudget) && options.hotRefineBudget > 0
|
|
971
|
+
? options.hotRefineBudget : ORIENT_HOT_REFINE_BUDGET,
|
|
830
972
|
});
|
|
831
973
|
const health = doctor(index, scope);
|
|
832
974
|
|
|
@@ -908,6 +1050,9 @@ function orient(index, options = {}) {
|
|
|
908
1050
|
total: stats.hot?.total ?? 0,
|
|
909
1051
|
totalKind: stats.hot?.totalKind || 'confirmed',
|
|
910
1052
|
refined: stats.hot?.refined ?? 0,
|
|
1053
|
+
...(stats.hot?.budgetExhausted && {
|
|
1054
|
+
budgetExhausted: true, maxRefine: stats.hot.maxRefine,
|
|
1055
|
+
}),
|
|
911
1056
|
top,
|
|
912
1057
|
production,
|
|
913
1058
|
items: hotItems,
|
package/languages/c-family.js
CHANGED
|
@@ -2776,7 +2776,7 @@ function findImportsInCode(code, parser) {
|
|
|
2776
2776
|
} finally { /* cached with the selected tree */ }
|
|
2777
2777
|
}
|
|
2778
2778
|
|
|
2779
|
-
function findUsagesInCode(code, name, parser, existingTree) {
|
|
2779
|
+
function findUsagesInCode(code, name, parser, existingTree, options = {}) {
|
|
2780
2780
|
// Usage is the raw literal-name inventory. The literal C/C++ tree retains
|
|
2781
2781
|
// identifiers from every preprocessor branch and is sufficient for
|
|
2782
2782
|
// occurrence kind/line classification; symbol ownership still comes from
|
|
@@ -2860,22 +2860,24 @@ function findUsagesInCode(code, name, parser, existingTree) {
|
|
|
2860
2860
|
// call extractor reparses those AST-proven regions; surface the resulting
|
|
2861
2861
|
// call usages here as well so callers/callees, usages, and tests share one
|
|
2862
2862
|
// semantic fact set.
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
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
|
+
}
|
|
2879
2881
|
}
|
|
2880
2882
|
return usages;
|
|
2881
2883
|
}
|