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.
- package/.claude/skills/ucn/SKILL.md +17 -3
- package/.claude/skills/ucn/references/commands.md +4 -4
- package/README.md +17 -5
- package/cli/index.js +3 -0
- package/core/accessors.js +183 -0
- package/core/account.js +36 -8
- package/core/analysis.js +46 -0
- package/core/ast-analysis.js +104 -0
- package/core/cache.js +150 -9
- package/core/callers.js +1196 -122
- package/core/command-contracts.js +13 -13
- package/core/deadcode.js +41 -2
- package/core/execute.js +4 -1
- package/core/graph.js +72 -5
- package/core/index-ir.js +15 -3
- package/core/ir.js +58 -9
- package/core/output/analysis.js +30 -1
- package/core/output/graph.js +28 -8
- package/core/output/public.js +4 -0
- package/core/output/refactoring.js +31 -2
- package/core/output/reporting.js +7 -0
- package/core/project.js +88 -3
- package/core/verify.js +85 -3
- package/languages/c-family.js +27 -34
- package/languages/csharp.js +26 -1
- package/languages/go.js +170 -42
- package/languages/javascript.js +258 -10
- package/languages/python.js +591 -32
- package/languages/rust.js +1 -0
- package/package.json +1 -1
|
@@ -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
|
|
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,
|
package/core/output/reporting.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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(); }
|
package/languages/c-family.js
CHANGED
|
@@ -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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
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
|
}
|
package/languages/csharp.js
CHANGED
|
@@ -293,7 +293,7 @@ function propertyMember(node, lines) {
|
|
|
293
293
|
endLine,
|
|
294
294
|
indent,
|
|
295
295
|
modifiers: modifiersOf(node),
|
|
296
|
-
memberType: '
|
|
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,
|