ucn 3.7.46 → 3.8.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/core/deadcode.js CHANGED
@@ -7,14 +7,29 @@
7
7
 
8
8
  const { detectLanguage, getParser, getLanguageModule, safeParse } = require('../languages');
9
9
  const { isTestFile } = require('./discovery');
10
+ const { escapeRegExp } = require('./shared');
11
+
12
+ /** Check if a position in a line is inside a string literal (quotes/backticks) */
13
+ function isInsideString(line, pos) {
14
+ let inSingle = false, inDouble = false, inBacktick = false;
15
+ for (let j = 0; j < pos; j++) {
16
+ const ch = line[j];
17
+ if (ch === '\\') { j++; continue; }
18
+ if (ch === '"' && !inSingle && !inBacktick) inDouble = !inDouble;
19
+ if (ch === "'" && !inDouble && !inBacktick) inSingle = !inSingle;
20
+ if (ch === '`' && !inDouble && !inSingle) inBacktick = !inBacktick;
21
+ }
22
+ return inSingle || inDouble || inBacktick;
23
+ }
10
24
 
11
25
  /**
12
- * Build a usage index for all identifiers in the codebase (optimized for deadcode)
26
+ * Build a usage index for identifiers in the codebase (optimized for deadcode)
13
27
  * Scans all files ONCE and builds a reverse index: name -> [usages]
14
28
  * @param {object} index - ProjectIndex instance
29
+ * @param {Set<string>} [filterNames] - If provided, only track these names (reduces memory)
15
30
  * @returns {Map<string, Array>} Usage index
16
31
  */
17
- function buildUsageIndex(index) {
32
+ function buildUsageIndex(index, filterNames) {
18
33
  const usageIndex = new Map(); // name -> [{file, line}]
19
34
 
20
35
  for (const [filePath, fileEntry] of index.files) {
@@ -24,6 +39,19 @@ function buildUsageIndex(index) {
24
39
 
25
40
  const content = index._readFile(filePath);
26
41
 
42
+ // Text pre-filter: skip files that don't contain any target names
43
+ // (avoids expensive tree-sitter parse + AST traversal for irrelevant files)
44
+ if (filterNames && filterNames.size > 0) {
45
+ let hasAny = false;
46
+ for (const name of filterNames) {
47
+ if (content.includes(name)) {
48
+ hasAny = true;
49
+ break;
50
+ }
51
+ }
52
+ if (!hasAny) continue;
53
+ }
54
+
27
55
  // For HTML files, parse the virtual JS content instead of raw HTML
28
56
  // (HTML tree-sitter sees script content as raw_text, not JS identifiers)
29
57
  let tree;
@@ -72,6 +100,8 @@ function buildUsageIndex(index) {
72
100
  }
73
101
  }
74
102
  // Member expression property: obj.Separator — not a standalone reference
103
+ // EXCEPTION: If the selector/member expression is part of a call_expression,
104
+ // this IS a method call (e.g., dc.syncDeployment()) and should count as usage.
75
105
  if (parentType === 'member_expression' ||
76
106
  parentType === 'field_expression' ||
77
107
  parentType === 'member_access_expression' ||
@@ -82,15 +112,31 @@ function buildUsageIndex(index) {
82
112
  // by checking if it's NOT the object (left side)
83
113
  const firstChild = node.parent.child(0);
84
114
  if (firstChild !== node) {
85
- // This is the property part skip it for deadcode counting
86
- for (let i = 0; i < node.childCount; i++) {
87
- traverse(node.child(i));
115
+ // Check if this member expression is part of a call or
116
+ // used as a function reference (callback argument)
117
+ const grandparent = node.parent.parent;
118
+ const isCall = grandparent &&
119
+ (grandparent.type === 'call_expression' ||
120
+ grandparent.type === 'argument_list' ||
121
+ grandparent.type === 'arguments');
122
+ if (!isCall) {
123
+ // Pure field access — skip for deadcode counting
124
+ for (let i = 0; i < node.childCount; i++) {
125
+ traverse(node.child(i));
126
+ }
127
+ return;
88
128
  }
89
- return;
129
+ // Method call or callback — fall through to count as usage
90
130
  }
91
131
  }
92
132
  }
93
133
  const name = node.text;
134
+ if (filterNames && !filterNames.has(name)) {
135
+ for (let i = 0; i < node.childCount; i++) {
136
+ traverse(node.child(i));
137
+ }
138
+ return;
139
+ }
94
140
  if (!usageIndex.has(name)) {
95
141
  usageIndex.set(name, []);
96
142
  }
@@ -144,19 +190,89 @@ function deadcode(index, options = {}) {
144
190
  let excludedDecorated = 0;
145
191
  let excludedExported = 0;
146
192
 
147
- // Build usage index once (instead of per-symbol)
148
- const usageIndex = buildUsageIndex(index);
193
+ // Ensure callee index is built (lazy, reused across operations)
194
+ if (!index.calleeIndex) {
195
+ index.buildCalleeIndex();
196
+ }
197
+
198
+ // Collect callable symbol names to reduce usage index scope
199
+ const callableTypes = ['function', 'method', 'static', 'public', 'abstract', 'constructor'];
200
+ const callableNames = new Set();
201
+ for (const [symbolName, symbols] of index.symbols) {
202
+ if (symbols.some(s => callableTypes.includes(s.type))) {
203
+ callableNames.add(symbolName);
204
+ }
205
+ }
206
+
207
+ // Pre-filter: names in the callee index have call sites → definitely used → not dead.
208
+ const potentiallyDeadNames = new Set();
209
+ for (const name of callableNames) {
210
+ if (!index.calleeIndex.has(name)) {
211
+ potentiallyDeadNames.add(name);
212
+ }
213
+ }
214
+
215
+ // Build usage index for potentially dead names using text scan (no tree-sitter reparsing).
216
+ // The callee index already covers all call-based usages. For remaining names, a word-boundary
217
+ // text scan catches imports, exports, shorthand properties, type refs, and variable refs.
218
+ // Trade-off: may match names in comments/strings (false "used" → fewer dead code reports),
219
+ // but avoids ~1.9s of tree-sitter re-parsing. buildUsageIndex() is kept for direct callers.
220
+ const usageIndex = new Map();
221
+ if (potentiallyDeadNames.size > 0) {
222
+ for (const [filePath, fileEntry] of index.files) {
223
+ try {
224
+ const content = index._readFile(filePath);
225
+ const lines = content.split('\n');
226
+ for (const name of potentiallyDeadNames) {
227
+ if (!content.includes(name)) continue;
228
+ const nameLen = name.length;
229
+ for (let i = 0; i < lines.length; i++) {
230
+ const line = lines[i];
231
+ if (!line.includes(name)) continue;
232
+ // Skip line if entirely inside a line comment (// or #)
233
+ const commentIdx = line.indexOf('//');
234
+ const hashIdx = line.indexOf('#');
235
+ let searchFrom = 0;
236
+ while (searchFrom < line.length) {
237
+ const pos = line.indexOf(name, searchFrom);
238
+ if (pos === -1) break;
239
+ searchFrom = pos + 1;
240
+ // Word boundary check
241
+ if (pos > 0 && /\w/.test(line[pos - 1])) continue;
242
+ if (pos + nameLen < line.length && /\w/.test(line[pos + nameLen])) continue;
243
+ // Skip if inside a // comment (not :// URL)
244
+ if (commentIdx !== -1 && commentIdx < pos &&
245
+ (commentIdx === 0 || line[commentIdx - 1] !== ':')) continue;
246
+ // Skip if inside a # comment (Python — # preceded by whitespace or at start)
247
+ if (hashIdx !== -1 && hashIdx < pos &&
248
+ (hashIdx === 0 || /\s/.test(line[hashIdx - 1]))) continue;
249
+ // Skip if inside a string literal
250
+ if (isInsideString(line, pos)) continue;
251
+ // Skip property/field access: preceded by '.' unless followed by '(' (method call)
252
+ if (pos > 0 && line[pos - 1] === '.' &&
253
+ (pos + nameLen >= line.length || line[pos + nameLen] !== '(')) continue;
254
+ // Skip object literal key: name followed by ':' (not '::' for Rust paths)
255
+ const afterChar = pos + nameLen < line.length ? line[pos + nameLen] : '';
256
+ const afterChar2 = pos + nameLen + 1 < line.length ? line[pos + nameLen + 1] : '';
257
+ if (afterChar === ':' && afterChar2 !== ':') continue;
258
+ // Valid reference found
259
+ if (!usageIndex.has(name)) usageIndex.set(name, []);
260
+ usageIndex.get(name).push({
261
+ file: filePath,
262
+ line: i + 1,
263
+ relativePath: fileEntry.relativePath
264
+ });
265
+ break; // one match per line is enough for deadcode
266
+ }
267
+ }
268
+ }
269
+ } catch {}
270
+ }
271
+ }
149
272
 
150
273
  for (const [name, symbols] of index.symbols) {
151
274
  for (const symbol of symbols) {
152
- // Skip non-function/class types
153
- // Include various method types from different languages:
154
- // - function: standalone functions
155
- // - class, struct, interface: type definitions (skip them in deadcode)
156
- // - method: class methods
157
- // - static, public, abstract: Java method modifiers used as types
158
- // - constructor: constructors
159
- const callableTypes = ['function', 'method', 'static', 'public', 'abstract', 'constructor'];
275
+ // Skip non-function/class types (callableTypes defined above)
160
276
  if (!callableTypes.includes(symbol.type)) {
161
277
  continue;
162
278
  }
@@ -174,6 +290,11 @@ function deadcode(index, options = {}) {
174
290
  continue;
175
291
  }
176
292
 
293
+ // Apply file filter (scopes deadcode to matching files)
294
+ if (options.file && !symbol.relativePath.includes(options.file)) {
295
+ continue;
296
+ }
297
+
177
298
  // Apply exclude and in filters
178
299
  if ((options.exclude && options.exclude.length > 0) || options.in) {
179
300
  if (!index.matchesFilters(symbol.relativePath, { exclude: options.exclude, in: options.in })) {
@@ -282,7 +403,12 @@ function deadcode(index, options = {}) {
282
403
  continue;
283
404
  }
284
405
 
285
- // Use pre-built index for O(1) lookup instead of O(files) scan
406
+ // Fast path: name has call sites in callee index definitely used not dead
407
+ if (index.calleeIndex.has(name)) {
408
+ continue;
409
+ }
410
+
411
+ // Slow path: check AST-based usage index for remaining names
286
412
  const allUsages = usageIndex.get(name) || [];
287
413
 
288
414
  // Filter out usages that are at the definition location
package/core/discovery.js CHANGED
@@ -191,7 +191,7 @@ function expandGlob(pattern, options = {}) {
191
191
  const root = path.resolve(options.root || process.cwd());
192
192
  const ignores = options.ignores || DEFAULT_IGNORES;
193
193
  const maxDepth = options.maxDepth || 20;
194
- const maxFiles = options.maxFiles || 10000;
194
+ const maxFiles = options.maxFiles || 50000;
195
195
  const followSymlinks = options.followSymlinks !== false; // default true
196
196
 
197
197
  // Handle home directory expansion
package/core/execute.js CHANGED
@@ -124,6 +124,34 @@ function num(val, fallback) {
124
124
  return isNaN(n) ? fallback : n;
125
125
  }
126
126
 
127
+ /**
128
+ * Apply limit to an array result.
129
+ * Returns { items, total, limited } where limited is true if truncated.
130
+ */
131
+ function applyLimit(arr, limit) {
132
+ if (!arr || !limit || limit <= 0 || arr.length <= limit) {
133
+ return { items: arr, total: arr ? arr.length : 0, limited: false };
134
+ }
135
+ return { items: arr.slice(0, limit), total: arr.length, limited: true };
136
+ }
137
+
138
+ /** Build a limit note string */
139
+ function limitNote(limit, total) {
140
+ return `Showing ${limit} of ${total} results. Use --limit N to see more.`;
141
+ }
142
+
143
+ /**
144
+ * Check if a --file pattern matches any files in the index.
145
+ * Returns error string if no files match, null otherwise.
146
+ */
147
+ function checkFilePatternMatch(index, filePattern) {
148
+ if (!filePattern) return null;
149
+ for (const [, fileEntry] of index.files) {
150
+ if (fileEntry.relativePath.includes(filePattern)) return null;
151
+ }
152
+ return `No files matched pattern '${filePattern}'.`;
153
+ }
154
+
127
155
  /** Read a file and extract lines for a symbol match, applying HTML cleanup. */
128
156
  function readAndExtract(match) {
129
157
  const content = fs.readFileSync(match.file, 'utf-8');
@@ -180,6 +208,8 @@ const HANDLERS = {
180
208
  const err = requireName(p.name);
181
209
  if (err) return { ok: false, error: err };
182
210
  applyClassMethodSyntax(p);
211
+ const fileErr = checkFilePatternMatch(index, p.file);
212
+ if (fileErr) return { ok: false, error: fileErr };
183
213
  const classErr = validateClassName(index, p.name, p.className);
184
214
  if (classErr) return { ok: false, error: classErr };
185
215
  const result = index.context(p.name, {
@@ -197,6 +227,8 @@ const HANDLERS = {
197
227
  const err = requireName(p.name);
198
228
  if (err) return { ok: false, error: err };
199
229
  applyClassMethodSyntax(p);
230
+ const fileErr = checkFilePatternMatch(index, p.file);
231
+ if (fileErr) return { ok: false, error: fileErr };
200
232
  const classErr = validateClassName(index, p.name, p.className);
201
233
  if (classErr) return { ok: false, error: classErr };
202
234
  const result = index.impact(p.name, {
@@ -209,10 +241,56 @@ const HANDLERS = {
209
241
  return { ok: true, result };
210
242
  },
211
243
 
244
+ blast: (index, p) => {
245
+ const err = requireName(p.name);
246
+ if (err) return { ok: false, error: err };
247
+ applyClassMethodSyntax(p);
248
+ const fileErr = checkFilePatternMatch(index, p.file);
249
+ if (fileErr) return { ok: false, error: fileErr };
250
+ const classErr = validateClassName(index, p.name, p.className);
251
+ if (classErr) return { ok: false, error: classErr };
252
+ const depthVal = num(p.depth, undefined);
253
+ const result = index.blast(p.name, {
254
+ depth: depthVal ?? 3,
255
+ file: p.file,
256
+ className: p.className,
257
+ all: p.all || depthVal !== undefined,
258
+ exclude: toExcludeArray(p.exclude),
259
+ includeMethods: p.includeMethods,
260
+ includeUncertain: p.includeUncertain || false,
261
+ });
262
+ if (!result) return { ok: false, error: `Function "${p.name}" not found.` };
263
+ return { ok: true, result };
264
+ },
265
+
266
+ reverseTrace: (index, p) => {
267
+ const err = requireName(p.name);
268
+ if (err) return { ok: false, error: err };
269
+ applyClassMethodSyntax(p);
270
+ const fileErr = checkFilePatternMatch(index, p.file);
271
+ if (fileErr) return { ok: false, error: fileErr };
272
+ const classErr = validateClassName(index, p.name, p.className);
273
+ if (classErr) return { ok: false, error: classErr };
274
+ const depthVal = num(p.depth, undefined);
275
+ const result = index.reverseTrace(p.name, {
276
+ depth: depthVal ?? 5,
277
+ file: p.file,
278
+ className: p.className,
279
+ all: p.all || depthVal !== undefined,
280
+ exclude: toExcludeArray(p.exclude),
281
+ includeMethods: p.includeMethods,
282
+ includeUncertain: p.includeUncertain || false,
283
+ });
284
+ if (!result) return { ok: false, error: `Function "${p.name}" not found.` };
285
+ return { ok: true, result };
286
+ },
287
+
212
288
  smart: (index, p) => {
213
289
  const err = requireName(p.name);
214
290
  if (err) return { ok: false, error: err };
215
291
  applyClassMethodSyntax(p);
292
+ const fileErr = checkFilePatternMatch(index, p.file);
293
+ if (fileErr) return { ok: false, error: fileErr };
216
294
  const result = index.smart(p.name, {
217
295
  file: p.file,
218
296
  className: p.className,
@@ -228,6 +306,8 @@ const HANDLERS = {
228
306
  const err = requireName(p.name);
229
307
  if (err) return { ok: false, error: err };
230
308
  applyClassMethodSyntax(p);
309
+ const fileErr = checkFilePatternMatch(index, p.file);
310
+ if (fileErr) return { ok: false, error: fileErr };
231
311
  const depthVal = num(p.depth, undefined);
232
312
  const result = index.trace(p.name, {
233
313
  depth: depthVal ?? 3,
@@ -245,6 +325,8 @@ const HANDLERS = {
245
325
  const err = requireName(p.name);
246
326
  if (err) return { ok: false, error: err };
247
327
  applyClassMethodSyntax(p);
328
+ const fileErr = checkFilePatternMatch(index, p.file);
329
+ if (fileErr) return { ok: false, error: fileErr };
248
330
  const result = index.example(p.name, { file: p.file, className: p.className });
249
331
  if (!result) return { ok: false, error: `No examples found for "${p.name}".` };
250
332
  return { ok: true, result };
@@ -254,6 +336,8 @@ const HANDLERS = {
254
336
  const err = requireName(p.name);
255
337
  if (err) return { ok: false, error: err };
256
338
  applyClassMethodSyntax(p);
339
+ const fileErr = checkFilePatternMatch(index, p.file);
340
+ if (fileErr) return { ok: false, error: fileErr };
257
341
  const result = index.related(p.name, {
258
342
  file: p.file,
259
343
  className: p.className,
@@ -270,14 +354,17 @@ const HANDLERS = {
270
354
  const err = requireName(p.name);
271
355
  if (err) return { ok: false, error: err };
272
356
  applyClassMethodSyntax(p);
357
+ // Check if --file pattern matches any files
358
+ const fileErr = checkFilePatternMatch(index, p.file);
359
+ if (fileErr) return { ok: false, error: fileErr };
273
360
  // Auto-include tests when pattern clearly targets test functions
274
361
  // But only if the user didn't explicitly set include_tests=false
275
362
  let includeTests = p.includeTests;
276
- if (includeTests === undefined && p.name && /^test[_*?]/i.test(p.name)) {
363
+ if (includeTests === undefined && p.name && /^test[_*?A-Z]/i.test(p.name)) {
277
364
  includeTests = true;
278
365
  }
279
366
  const exclude = applyTestExclusions(p.exclude, includeTests);
280
- const result = index.find(p.name, {
367
+ let result = index.find(p.name, {
281
368
  file: p.file,
282
369
  className: p.className,
283
370
  exact: p.exact || false,
@@ -285,11 +372,18 @@ const HANDLERS = {
285
372
  in: p.in,
286
373
  });
287
374
  // Warn if exact mode silently disables glob expansion
288
- let note;
375
+ const notes = [];
289
376
  if (p.exact && p.name && (p.name.includes('*') || p.name.includes('?'))) {
290
- note = `Note: exact=true treats "${p.name}" as a literal name (glob expansion disabled).`;
377
+ notes.push(`Note: exact=true treats "${p.name}" as a literal name (glob expansion disabled).`);
291
378
  }
292
- return { ok: true, result, note };
379
+ // Apply limit
380
+ const limit = num(p.limit, undefined);
381
+ if (limit && limit > 0) {
382
+ const { items, total, limited } = applyLimit(result, limit);
383
+ if (limited) notes.push(limitNote(limit, total));
384
+ result = items;
385
+ }
386
+ return { ok: true, result, note: notes.length ? notes.join('\n') : undefined };
293
387
  },
294
388
 
295
389
  usages: (index, p) => {
@@ -304,7 +398,29 @@ const HANDLERS = {
304
398
  exclude,
305
399
  in: p.in,
306
400
  });
307
- return { ok: true, result };
401
+ // Apply limit to total usages across files
402
+ const limit = num(p.limit, undefined);
403
+ let note;
404
+ if (limit && limit > 0 && result.files) {
405
+ let total = result.files.reduce((s, f) => s + f.usages.length, 0);
406
+ if (total > limit) {
407
+ let remaining = limit;
408
+ const truncated = [];
409
+ for (const f of result.files) {
410
+ if (remaining <= 0) break;
411
+ if (f.usages.length <= remaining) {
412
+ truncated.push(f);
413
+ remaining -= f.usages.length;
414
+ } else {
415
+ truncated.push({ ...f, usages: f.usages.slice(0, remaining) });
416
+ remaining = 0;
417
+ }
418
+ }
419
+ result.files = truncated;
420
+ note = limitNote(limit, total);
421
+ }
422
+ }
423
+ return { ok: true, result, note };
308
424
  },
309
425
 
310
426
  toc: (index, p) => {
@@ -315,14 +431,73 @@ const HANDLERS = {
315
431
  top: num(p.top, undefined),
316
432
  file: p.file,
317
433
  });
318
- return { ok: true, result };
434
+ // Apply limit to detailed toc entries
435
+ const limit = num(p.limit, undefined);
436
+ let note;
437
+ if (limit && limit > 0 && p.detailed && result.files) {
438
+ let totalEntries = result.files.reduce((s, f) => s + (f.functions?.length || 0) + (f.classes?.length || 0), 0);
439
+ if (totalEntries > limit) {
440
+ let remaining = limit;
441
+ for (const f of result.files) {
442
+ if (remaining <= 0) {
443
+ f.functions = [];
444
+ f.classes = [];
445
+ continue;
446
+ }
447
+ const fns = f.functions?.length || 0;
448
+ const cls = f.classes?.length || 0;
449
+ if (fns + cls <= remaining) {
450
+ remaining -= fns + cls;
451
+ } else {
452
+ if (f.functions && remaining > 0) {
453
+ f.functions = f.functions.slice(0, remaining);
454
+ remaining -= f.functions.length;
455
+ }
456
+ if (f.classes && remaining > 0) {
457
+ f.classes = f.classes.slice(0, remaining);
458
+ remaining -= f.classes.length;
459
+ } else if (f.classes) {
460
+ f.classes = [];
461
+ }
462
+ }
463
+ }
464
+ note = limitNote(limit, totalEntries);
465
+ }
466
+ }
467
+ return { ok: true, result, note };
319
468
  },
320
469
 
321
470
  search: (index, p) => {
471
+ // Detect structural search mode: any of these flags triggers index-based search
472
+ const isStructural = p.type || p.param || p.receiver || p.returns || p.decorator || p.exported || p.unused;
473
+ if (isStructural) {
474
+ const exclude = applyTestExclusions(p.exclude, p.includeTests);
475
+ const topVal = num(p.top, undefined) || num(p.limit, undefined);
476
+ const result = index.structuralSearch({
477
+ term: p.term || p.name,
478
+ type: p.type,
479
+ param: p.param,
480
+ receiver: p.receiver,
481
+ returns: p.returns,
482
+ decorator: p.decorator,
483
+ exported: p.exported || false,
484
+ unused: p.unused || false,
485
+ caseSensitive: p.caseSensitive || false,
486
+ exclude,
487
+ in: p.in,
488
+ file: p.file,
489
+ top: topVal || 50,
490
+ });
491
+ if (result.meta.error) return { ok: false, error: result.meta.error };
492
+ return { ok: true, result, structural: true };
493
+ }
494
+
322
495
  const err = requireTerm(p.term);
323
496
  if (err) return { ok: false, error: err };
324
497
  const testsExcluded = !p.includeTests;
325
498
  const exclude = applyTestExclusions(p.exclude, p.includeTests);
499
+ // Use limit as top if top not set
500
+ const topVal = num(p.top, undefined) || num(p.limit, undefined);
326
501
  const result = index.search(p.term, {
327
502
  codeOnly: p.codeOnly || false,
328
503
  context: num(p.context, 0),
@@ -330,7 +505,8 @@ const HANDLERS = {
330
505
  exclude,
331
506
  in: p.in,
332
507
  regex: p.regex,
333
- top: num(p.top, undefined),
508
+ top: topVal,
509
+ file: p.file,
334
510
  });
335
511
  if (result.meta) result.meta.testsExcluded = testsExcluded;
336
512
  return { ok: true, result };
@@ -347,15 +523,49 @@ const HANDLERS = {
347
523
  return { ok: true, result };
348
524
  },
349
525
 
526
+ affectedTests: (index, p) => {
527
+ const err = requireName(p.name);
528
+ if (err) return { ok: false, error: err };
529
+ applyClassMethodSyntax(p);
530
+ const fileErr = checkFilePatternMatch(index, p.file);
531
+ if (fileErr) return { ok: false, error: fileErr };
532
+ const classErr = validateClassName(index, p.name, p.className);
533
+ if (classErr) return { ok: false, error: classErr };
534
+ const depthVal = num(p.depth, undefined);
535
+ const result = index.affectedTests(p.name, {
536
+ depth: depthVal ?? 3,
537
+ file: p.file,
538
+ className: p.className,
539
+ exclude: toExcludeArray(p.exclude),
540
+ includeMethods: p.includeMethods,
541
+ includeUncertain: p.includeUncertain || false,
542
+ });
543
+ if (!result) return { ok: false, error: `Function "${p.name}" not found.` };
544
+ return { ok: true, result };
545
+ },
546
+
350
547
  deadcode: (index, p) => {
548
+ const fileErr = checkFilePatternMatch(index, p.file);
549
+ if (fileErr) return { ok: false, error: fileErr };
351
550
  const result = index.deadcode({
352
551
  includeExported: p.includeExported || false,
353
552
  includeDecorated: p.includeDecorated || false,
354
553
  includeTests: p.includeTests || false,
355
554
  exclude: toExcludeArray(p.exclude),
356
555
  in: p.in,
556
+ file: p.file,
357
557
  });
358
- return { ok: true, result };
558
+ // Apply limit to dead code results
559
+ const limit = num(p.limit, undefined);
560
+ let note;
561
+ if (limit && limit > 0 && result.dead) {
562
+ const { items, total, limited } = applyLimit(result.dead, limit);
563
+ if (limited) {
564
+ note = limitNote(limit, total);
565
+ result.dead = items;
566
+ }
567
+ }
568
+ return { ok: true, result, note };
359
569
  },
360
570
 
361
571
  // ── Extracting Code ─────────────────────────────────────────────────
@@ -364,6 +574,8 @@ const HANDLERS = {
364
574
  const err = requireName(p.name);
365
575
  if (err) return { ok: false, error: err };
366
576
  applyClassMethodSyntax(p);
577
+ const fileErr = checkFilePatternMatch(index, p.file);
578
+ if (fileErr) return { ok: false, error: fileErr };
367
579
 
368
580
  const fnNames = p.name.includes(',')
369
581
  ? p.name.split(',').map(n => n.trim()).filter(Boolean)
@@ -424,6 +636,8 @@ const HANDLERS = {
424
636
  class: (index, p) => {
425
637
  const err = requireName(p.name);
426
638
  if (err) return { ok: false, error: err };
639
+ const fileErr = checkFilePatternMatch(index, p.file);
640
+ if (fileErr) return { ok: false, error: fileErr };
427
641
 
428
642
  const CLASS_TYPES = ['class', 'interface', 'type', 'enum', 'struct', 'trait'];
429
643
  const matches = index.find(p.name, { file: p.file, skipCounts: true })
@@ -579,12 +793,22 @@ const HANDLERS = {
579
793
  return { ok: true, result };
580
794
  },
581
795
 
796
+ circularDeps: (index, p) => {
797
+ const result = index.circularDeps({
798
+ file: p.file,
799
+ exclude: toExcludeArray(p.exclude),
800
+ });
801
+ return { ok: true, result };
802
+ },
803
+
582
804
  // ── Refactoring ─────────────────────────────────────────────────────
583
805
 
584
806
  verify: (index, p) => {
585
807
  const err = requireName(p.name);
586
808
  if (err) return { ok: false, error: err };
587
809
  applyClassMethodSyntax(p);
810
+ const fileErr = checkFilePatternMatch(index, p.file);
811
+ if (fileErr) return { ok: false, error: fileErr };
588
812
  const classErr = validateClassName(index, p.name, p.className);
589
813
  if (classErr) return { ok: false, error: classErr };
590
814
  const result = index.verify(p.name, { file: p.file, className: p.className });
@@ -595,6 +819,8 @@ const HANDLERS = {
595
819
  const err = requireName(p.name);
596
820
  if (err) return { ok: false, error: err };
597
821
  applyClassMethodSyntax(p);
822
+ const fileErr = checkFilePatternMatch(index, p.file);
823
+ if (fileErr) return { ok: false, error: fileErr };
598
824
  const classErr = validateClassName(index, p.name, p.className);
599
825
  if (classErr) return { ok: false, error: classErr };
600
826
  if (!p.addParam && !p.removeParam && !p.renameTo) {
@@ -639,12 +865,20 @@ const HANDLERS = {
639
865
  },
640
866
 
641
867
  api: (index, p) => {
642
- const result = index.api(p.file);
868
+ let result = index.api(p.file);
643
869
  if (p.file) {
644
870
  const fileErr = checkFileError(result, p.file);
645
871
  if (fileErr) return { ok: false, error: fileErr };
646
872
  }
647
- return { ok: true, result };
873
+ // Apply limit to api results (api returns an array)
874
+ const limit = num(p.limit, undefined);
875
+ let note;
876
+ if (limit && limit > 0 && Array.isArray(result)) {
877
+ const { items, total, limited } = applyLimit(result, limit);
878
+ if (limited) note = limitNote(limit, total);
879
+ result = items;
880
+ }
881
+ return { ok: true, result, note };
648
882
  },
649
883
 
650
884
  stats: (index, p) => {