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/.claude/skills/ucn/SKILL.md +34 -4
- package/README.md +16 -13
- package/cli/index.js +112 -15
- package/core/cache.js +176 -51
- package/core/callers.js +350 -52
- package/core/deadcode.js +143 -17
- package/core/discovery.js +1 -1
- package/core/execute.js +245 -11
- package/core/output.js +423 -4
- package/core/project.js +1204 -94
- package/core/registry.js +18 -7
- package/core/shared.js +1 -1
- package/core/stacktrace.js +31 -2
- package/core/verify.js +11 -0
- package/languages/go.js +338 -24
- package/languages/index.js +20 -1
- package/languages/java.js +145 -6
- package/languages/javascript.js +199 -8
- package/languages/python.js +8 -2
- package/languages/rust.js +168 -8
- package/mcp/server.js +79 -17
- package/package.json +1 -1
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
|
|
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
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
148
|
-
|
|
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
|
-
//
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
375
|
+
const notes = [];
|
|
289
376
|
if (p.exact && p.name && (p.name.includes('*') || p.name.includes('?'))) {
|
|
290
|
-
|
|
377
|
+
notes.push(`Note: exact=true treats "${p.name}" as a literal name (glob expansion disabled).`);
|
|
291
378
|
}
|
|
292
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|