ucn 5.2.0 → 5.2.1
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/core/accessors.js +183 -0
- package/core/analysis.js +46 -0
- package/core/ast-analysis.js +104 -0
- package/core/cache.js +5 -1
- package/core/callers.js +13 -7
- 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 +10 -0
- package/core/ir.js +2 -1
- 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/verify.js +85 -3
- package/languages/c-family.js +8 -17
- package/languages/csharp.js +26 -1
- package/languages/javascript.js +1 -1
- package/languages/python.js +28 -6
- package/package.json +1 -1
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
|
|
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,
|
package/languages/javascript.js
CHANGED
|
@@ -3844,7 +3844,7 @@ function findUsagesInCode(code, name, parser, tree) {
|
|
|
3844
3844
|
usageType = 'reference';
|
|
3845
3845
|
}
|
|
3846
3846
|
// Track receiver for member expressions (obj.name → receiver = 'obj')
|
|
3847
|
-
if (object &&
|
|
3847
|
+
if (object && ['identifier', 'this', 'super'].includes(object.type)) {
|
|
3848
3848
|
usages.push({ line, column, usageType, receiver: object.text });
|
|
3849
3849
|
return true;
|
|
3850
3850
|
}
|
package/languages/python.js
CHANGED
|
@@ -2468,10 +2468,24 @@ function findImportsInCode(code, parser) {
|
|
|
2468
2468
|
const imports = [];
|
|
2469
2469
|
let importAliases = null; // {original, local}[] — tracks renamed imports
|
|
2470
2470
|
|
|
2471
|
+
// Imports nested in a function/lambda do not execute during ordinary
|
|
2472
|
+
// module initialization. Preserve that AST fact so dependency-cycle
|
|
2473
|
+
// reporting can distinguish an eager import loop from a deliberate lazy
|
|
2474
|
+
// edge without deleting either edge from the graph.
|
|
2475
|
+
const isDeferredImport = (node) => {
|
|
2476
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
2477
|
+
if (parent.type === 'function_definition' || parent.type === 'lambda') {
|
|
2478
|
+
return true;
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
return false;
|
|
2482
|
+
};
|
|
2483
|
+
|
|
2471
2484
|
traverseTreeCached(tree.rootNode, (node) => {
|
|
2472
2485
|
// import statement: import os, import sys as system
|
|
2473
2486
|
if (node.type === 'import_statement') {
|
|
2474
2487
|
const line = node.startPosition.row + 1;
|
|
2488
|
+
const deferred = isDeferredImport(node);
|
|
2475
2489
|
|
|
2476
2490
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2477
2491
|
const child = node.namedChild(i);
|
|
@@ -2487,20 +2501,23 @@ function findImportsInCode(code, parser) {
|
|
|
2487
2501
|
module: parts[0],
|
|
2488
2502
|
names: [parts[0]],
|
|
2489
2503
|
type: 'import',
|
|
2490
|
-
line
|
|
2504
|
+
line,
|
|
2505
|
+
...(deferred && { deferred: true })
|
|
2491
2506
|
});
|
|
2492
2507
|
imports.push({
|
|
2493
2508
|
module: child.text,
|
|
2494
2509
|
names: [],
|
|
2495
2510
|
type: 'import-submodule',
|
|
2496
|
-
line
|
|
2511
|
+
line,
|
|
2512
|
+
...(deferred && { deferred: true })
|
|
2497
2513
|
});
|
|
2498
2514
|
} else {
|
|
2499
2515
|
imports.push({
|
|
2500
2516
|
module: child.text,
|
|
2501
2517
|
names: [child.text],
|
|
2502
2518
|
type: 'import',
|
|
2503
|
-
line
|
|
2519
|
+
line,
|
|
2520
|
+
...(deferred && { deferred: true })
|
|
2504
2521
|
});
|
|
2505
2522
|
}
|
|
2506
2523
|
} else if (child.type === 'aliased_import') {
|
|
@@ -2512,7 +2529,8 @@ function findImportsInCode(code, parser) {
|
|
|
2512
2529
|
module: nameNode.text,
|
|
2513
2530
|
names: [aliasNode ? aliasNode.text : nameNode.text.split('.').pop()],
|
|
2514
2531
|
type: 'import',
|
|
2515
|
-
line
|
|
2532
|
+
line,
|
|
2533
|
+
...(deferred && { deferred: true })
|
|
2516
2534
|
});
|
|
2517
2535
|
if (aliasNode && aliasNode.text !== nameNode.text) {
|
|
2518
2536
|
if (!importAliases) importAliases = [];
|
|
@@ -2527,6 +2545,7 @@ function findImportsInCode(code, parser) {
|
|
|
2527
2545
|
// from ... import statement
|
|
2528
2546
|
if (node.type === 'import_from_statement') {
|
|
2529
2547
|
const line = node.startPosition.row + 1;
|
|
2548
|
+
const deferred = isDeferredImport(node);
|
|
2530
2549
|
let modulePath = '';
|
|
2531
2550
|
const names = [];
|
|
2532
2551
|
|
|
@@ -2559,7 +2578,8 @@ function findImportsInCode(code, parser) {
|
|
|
2559
2578
|
module: modulePath,
|
|
2560
2579
|
names,
|
|
2561
2580
|
type: isRelative ? 'relative' : 'from',
|
|
2562
|
-
line
|
|
2581
|
+
line,
|
|
2582
|
+
...(deferred && { deferred: true })
|
|
2563
2583
|
});
|
|
2564
2584
|
}
|
|
2565
2585
|
return true;
|
|
@@ -2574,13 +2594,15 @@ function findImportsInCode(code, parser) {
|
|
|
2574
2594
|
const firstArg = argsNode.namedChild(0);
|
|
2575
2595
|
if ((funcName === 'importlib.import_module' || funcName === '__import__') && firstArg) {
|
|
2576
2596
|
const line = node.startPosition.row + 1;
|
|
2597
|
+
const deferred = isDeferredImport(node);
|
|
2577
2598
|
const isLiteral = firstArg.type === 'string';
|
|
2578
2599
|
imports.push({
|
|
2579
2600
|
module: isLiteral ? firstArg.text.replace(/^['"]|['"]$/g, '') : firstArg.text,
|
|
2580
2601
|
names: [],
|
|
2581
2602
|
type: 'dynamic',
|
|
2582
2603
|
line,
|
|
2583
|
-
dynamic: !isLiteral
|
|
2604
|
+
dynamic: !isLiteral,
|
|
2605
|
+
...(deferred && { deferred: true })
|
|
2584
2606
|
});
|
|
2585
2607
|
}
|
|
2586
2608
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "5.2.
|
|
3
|
+
"version": "5.2.1",
|
|
4
4
|
"mcpName": "io.github.mleoca/ucn",
|
|
5
5
|
"description": "Auditable AST code intelligence for AI agents: 18 task-oriented commands through one MCP tool, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java, C, C++, C#, and HTML.",
|
|
6
6
|
"main": "index.js",
|