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/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
 
@@ -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,
@@ -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 && object.type === 'identifier') {
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
  }
@@ -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.0",
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",