codedev-mcp 3.2.2 → 3.2.5
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/dist/analyzers/api-contract.d.ts +1 -1
- package/dist/analyzers/api-contract.d.ts.map +1 -1
- package/dist/analyzers/api-contract.js +917 -19
- package/dist/analyzers/api-contract.js.map +1 -1
- package/dist/analyzers/cicd.js +4 -4
- package/dist/analyzers/cicd.js.map +1 -1
- package/dist/analyzers/db-schema.d.ts.map +1 -1
- package/dist/analyzers/db-schema.js +124 -34
- package/dist/analyzers/db-schema.js.map +1 -1
- package/dist/analyzers/dep-vuln.d.ts +1 -0
- package/dist/analyzers/dep-vuln.d.ts.map +1 -1
- package/dist/analyzers/dep-vuln.js +163 -25
- package/dist/analyzers/dep-vuln.js.map +1 -1
- package/dist/db/connection.js +1 -1
- package/dist/db/connection.js.map +1 -1
- package/dist/db/sqlite-store.d.ts +16 -1
- package/dist/db/sqlite-store.d.ts.map +1 -1
- package/dist/db/sqlite-store.js +80 -14
- package/dist/db/sqlite-store.js.map +1 -1
- package/dist/tools/quality.d.ts.map +1 -1
- package/dist/tools/quality.js +444 -80
- package/dist/tools/quality.js.map +1 -1
- package/dist/tools/security.d.ts.map +1 -1
- package/dist/tools/security.js +9 -2
- package/dist/tools/security.js.map +1 -1
- package/package.json +2 -2
package/dist/tools/quality.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { outputSchemas } from '../schemas/output-schemas.js';
|
|
3
3
|
import { CWD, safePath } from '../config.js';
|
|
4
|
-
import { searchCode } from '../search/fast-search.js';
|
|
4
|
+
import { searchCode, listFiles } from '../search/fast-search.js';
|
|
5
|
+
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { parseAST } from '../analyzers/tree-sitter.js';
|
|
8
|
+
import { extractSymbols } from '../analyzers/symbols.js';
|
|
9
|
+
import { detectLanguage } from '../utils/languages.js';
|
|
5
10
|
/**
|
|
6
11
|
* Registers code quality tools for finding TODOs, debug logs, secrets, empty catches, duplicates, and dead code.
|
|
7
12
|
* @param server - The MCP server instance to register tools on.
|
|
@@ -21,9 +26,11 @@ export function registerQualityTools(server) {
|
|
|
21
26
|
}, async (params) => {
|
|
22
27
|
try {
|
|
23
28
|
const searchCwd = params.directory ? safePath(params.directory) : CWD;
|
|
29
|
+
// Only match TODO comments, not function calls like logger.warn()
|
|
30
|
+
// Match TODO/FIXME/HACK/XXX/BUG/OPTIMIZE that appear in comments (// or /* */)
|
|
24
31
|
const results = await searchCode({
|
|
25
32
|
cwd: searchCwd,
|
|
26
|
-
pattern: '(TODO|FIXME|HACK|XXX|
|
|
33
|
+
pattern: '(//|/\\*|#|<!--).*?(TODO|FIXME|HACK|XXX|BUG|OPTIMIZE)\\b',
|
|
27
34
|
isRegex: true,
|
|
28
35
|
fileGlob: params.file_glob,
|
|
29
36
|
maxResults: 100,
|
|
@@ -137,40 +144,94 @@ export function registerQualityTools(server) {
|
|
|
137
144
|
}, async (params) => {
|
|
138
145
|
try {
|
|
139
146
|
const searchCwd = params.directory ? safePath(params.directory) : CWD;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
//
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
147
|
+
const fileGlob = params.file_glob || '**/*.{ts,tsx,js,jsx,py,rb}';
|
|
148
|
+
const files = await listFiles(searchCwd, { glob: fileGlob });
|
|
149
|
+
const matches = [];
|
|
150
|
+
// Process files to find empty catch blocks
|
|
151
|
+
for (const file of files.slice(0, 500)) {
|
|
152
|
+
try {
|
|
153
|
+
const filePath = path.join(searchCwd, file);
|
|
154
|
+
const content = await readFile(filePath, 'utf-8');
|
|
155
|
+
const language = detectLanguage(file);
|
|
156
|
+
if (language === 'python') {
|
|
157
|
+
// Python: except: pass or except Exception: pass
|
|
158
|
+
const exceptRegex = /except\s*(?:\([^)]+\))?\s*:\s*pass/g;
|
|
159
|
+
let match;
|
|
160
|
+
while ((match = exceptRegex.exec(content)) !== null) {
|
|
161
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
162
|
+
matches.push({
|
|
163
|
+
file,
|
|
164
|
+
line: lineNum,
|
|
165
|
+
match: 'Empty except: pass',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else if (language === 'ruby') {
|
|
170
|
+
// Ruby: rescue => nil or rescue; end
|
|
171
|
+
const rescueRegex = /rescue\s*(?:=>\s*nil|;?\s*end)/g;
|
|
172
|
+
let match;
|
|
173
|
+
while ((match = rescueRegex.exec(content)) !== null) {
|
|
174
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
175
|
+
matches.push({
|
|
176
|
+
file,
|
|
177
|
+
line: lineNum,
|
|
178
|
+
match: 'Empty rescue block',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
// JavaScript/TypeScript: catch() { } or catch(e) { }
|
|
184
|
+
// Find all catch blocks
|
|
185
|
+
const catchRegex = /catch\s*\([^)]*\)\s*\{/g;
|
|
186
|
+
let catchMatch;
|
|
187
|
+
while ((catchMatch = catchRegex.exec(content)) !== null) {
|
|
188
|
+
const catchStart = catchMatch.index;
|
|
189
|
+
const catchLine = content.substring(0, catchStart).split('\n').length;
|
|
190
|
+
const afterCatch = content.substring(catchStart + catchMatch[0].length);
|
|
191
|
+
// Find the matching closing brace
|
|
192
|
+
let braceCount = 1;
|
|
193
|
+
let pos = 0;
|
|
194
|
+
let foundEnd = false;
|
|
195
|
+
while (pos < afterCatch.length && braceCount > 0) {
|
|
196
|
+
if (afterCatch[pos] === '{')
|
|
197
|
+
braceCount++;
|
|
198
|
+
else if (afterCatch[pos] === '}') {
|
|
199
|
+
braceCount--;
|
|
200
|
+
if (braceCount === 0) {
|
|
201
|
+
foundEnd = true;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
pos++;
|
|
206
|
+
}
|
|
207
|
+
if (foundEnd) {
|
|
208
|
+
const catchBody = afterCatch.substring(0, pos);
|
|
209
|
+
// Check if body is empty (only whitespace/comments)
|
|
210
|
+
const trimmedBody = catchBody
|
|
211
|
+
.replace(/\/\/.*$/gm, '')
|
|
212
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
213
|
+
.trim();
|
|
214
|
+
if (trimmedBody === '') {
|
|
215
|
+
matches.push({
|
|
216
|
+
file,
|
|
217
|
+
line: catchLine,
|
|
218
|
+
match: 'Empty catch block',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const results = matches.map((m) => ({
|
|
230
|
+
file: m.file,
|
|
231
|
+
line: m.line,
|
|
232
|
+
text: m.match,
|
|
233
|
+
column: 0,
|
|
234
|
+
}));
|
|
174
235
|
const output = results.map((r) => `${r.file}:${r.line} │ ${r.text.trim()}`).join('\n');
|
|
175
236
|
return {
|
|
176
237
|
content: [{ type: 'text', text: `Found ${results.length} empty/swallowed error handlers:\n\n${output}` }],
|
|
@@ -201,16 +262,117 @@ export function registerQualityTools(server) {
|
|
|
201
262
|
outputSchema: outputSchemas.find_pattern,
|
|
202
263
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
203
264
|
}, async (params) => {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
265
|
+
try {
|
|
266
|
+
const threshold = params.threshold || 50;
|
|
267
|
+
const searchCwd = params.directory ? safePath(params.directory) : CWD;
|
|
268
|
+
const fileGlob = params.file_glob || '**/*.{ts,tsx,js,jsx,py,go,java,rs,c,cpp}';
|
|
269
|
+
const files = await listFiles(searchCwd, { glob: fileGlob });
|
|
270
|
+
const matches = [];
|
|
271
|
+
// Increased limit from 500 to 2000 to handle large projects (949+ files)
|
|
272
|
+
for (const file of files.slice(0, 2000)) {
|
|
273
|
+
try {
|
|
274
|
+
const filePath = path.join(searchCwd, file);
|
|
275
|
+
const language = detectLanguage(file);
|
|
276
|
+
const content = await readFile(filePath, 'utf-8');
|
|
277
|
+
const lines = content.split('\n');
|
|
278
|
+
// Try AST parsing first
|
|
279
|
+
const astSymbols = await parseAST(filePath, language);
|
|
280
|
+
if (astSymbols && astSymbols.length > 0) {
|
|
281
|
+
for (const symbol of astSymbols) {
|
|
282
|
+
if (symbol.type === 'function' && symbol.endLine && symbol.startLine) {
|
|
283
|
+
const lineCount = symbol.endLine - symbol.startLine + 1;
|
|
284
|
+
if (lineCount > threshold) {
|
|
285
|
+
matches.push({
|
|
286
|
+
file,
|
|
287
|
+
line: symbol.startLine,
|
|
288
|
+
match: `${symbol.name || 'anonymous'}() - ${lineCount} lines`,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Always also try regex-based extraction as fallback/complement
|
|
295
|
+
const symbols = await extractSymbols(filePath);
|
|
296
|
+
const processedFunctions = new Set();
|
|
297
|
+
for (const symbol of symbols.filter((s) => s.kind === 'function')) {
|
|
298
|
+
const key = `${file}:${symbol.line}`;
|
|
299
|
+
if (processedFunctions.has(key))
|
|
300
|
+
continue;
|
|
301
|
+
processedFunctions.add(key);
|
|
302
|
+
// Find function start and end by parsing braces
|
|
303
|
+
let braceCount = 0;
|
|
304
|
+
let startLine = symbol.line;
|
|
305
|
+
let endLine = startLine;
|
|
306
|
+
let foundStart = false;
|
|
307
|
+
// Look backwards to find function start
|
|
308
|
+
for (let i = symbol.line - 1; i >= 0 && i >= symbol.line - 10; i--) {
|
|
309
|
+
const line = lines[i];
|
|
310
|
+
if (/^\s*(?:export\s+)?(?:async\s+)?function\s+\w+|^\s*(?:export\s+)?(?:async\s+)?\w+\s*[:=]\s*(?:async\s*)?\(|^\s*(?:export\s+)?(?:async\s+)?\w+\s*[:=]\s*(?:async\s*)?\w+\s*=>/.test(line)) {
|
|
311
|
+
startLine = i + 1;
|
|
312
|
+
foundStart = true;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (!foundStart)
|
|
317
|
+
startLine = symbol.line;
|
|
318
|
+
// Find function end by counting braces
|
|
319
|
+
for (let i = startLine - 1; i < lines.length; i++) {
|
|
320
|
+
const line = lines[i];
|
|
321
|
+
const openBraces = (line.match(/\{/g) || []).length;
|
|
322
|
+
const closeBraces = (line.match(/\}/g) || []).length;
|
|
323
|
+
if (i === startLine - 1 || braceCount > 0) {
|
|
324
|
+
braceCount += openBraces;
|
|
325
|
+
braceCount -= closeBraces;
|
|
326
|
+
if (braceCount === 0 && i >= startLine) {
|
|
327
|
+
endLine = i + 1;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// If we didn't find the end, estimate from remaining content
|
|
333
|
+
if (endLine === startLine && startLine < lines.length) {
|
|
334
|
+
endLine = lines.length;
|
|
335
|
+
}
|
|
336
|
+
const lineCount = endLine - startLine + 1;
|
|
337
|
+
if (lineCount > threshold) {
|
|
338
|
+
// Check if we already added this from AST
|
|
339
|
+
const alreadyAdded = matches.some((m) => m.file === file && Math.abs(m.line - startLine) <= 2);
|
|
340
|
+
if (!alreadyAdded) {
|
|
341
|
+
matches.push({
|
|
342
|
+
file,
|
|
343
|
+
line: startLine,
|
|
344
|
+
match: `${symbol.name}() - ${lineCount} lines`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
// Skip files that can't be parsed, but log for debugging
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const output = matches.map((m) => `${m.file}:${m.line} │ ${m.match}`).join('\n');
|
|
356
|
+
return {
|
|
357
|
+
content: [
|
|
358
|
+
{
|
|
359
|
+
type: 'text',
|
|
360
|
+
text: `Found ${matches.length} functions exceeding ${threshold} lines:\n\n${output || 'None found'}`,
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
structuredContent: {
|
|
364
|
+
check: 'long_functions',
|
|
365
|
+
matches,
|
|
366
|
+
total: matches.length,
|
|
210
367
|
},
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
return {
|
|
372
|
+
content: [{ type: 'text', text: `find_long_functions failed: ${error.message}` }],
|
|
373
|
+
structuredContent: { check: 'long_functions', matches: [], total: 0 },
|
|
374
|
+
};
|
|
375
|
+
}
|
|
214
376
|
});
|
|
215
377
|
// ═══════════════════════════════════════════════════════════════════════
|
|
216
378
|
// TOOL: find_large_files — Find overly large files
|
|
@@ -225,16 +387,63 @@ export function registerQualityTools(server) {
|
|
|
225
387
|
outputSchema: outputSchemas.find_pattern,
|
|
226
388
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
227
389
|
}, async (params) => {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
390
|
+
try {
|
|
391
|
+
const threshold = params.threshold || 500;
|
|
392
|
+
const searchCwd = params.directory ? safePath(params.directory) : CWD;
|
|
393
|
+
const fileGlob = params.file_glob || '**/*';
|
|
394
|
+
const files = await listFiles(searchCwd, { glob: fileGlob });
|
|
395
|
+
const matches = [];
|
|
396
|
+
// Process more files and ensure we're scanning the right directory
|
|
397
|
+
// Increased limit from 1000 to 2000 to handle large projects (949+ files)
|
|
398
|
+
for (const file of files.slice(0, 2000)) {
|
|
399
|
+
try {
|
|
400
|
+
const filePath = path.join(searchCwd, file);
|
|
401
|
+
// Verify file exists and is readable
|
|
402
|
+
const stats = await import('node:fs/promises').then((fs) => fs.stat(filePath));
|
|
403
|
+
if (!stats.isFile())
|
|
404
|
+
continue;
|
|
405
|
+
const content = await readFile(filePath, 'utf-8');
|
|
406
|
+
const lineCount = content.split('\n').length;
|
|
407
|
+
if (lineCount > threshold) {
|
|
408
|
+
matches.push({
|
|
409
|
+
file,
|
|
410
|
+
line: 1,
|
|
411
|
+
match: `${lineCount} lines`,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
// Skip files that can't be read
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// Sort by line count descending
|
|
421
|
+
matches.sort((a, b) => {
|
|
422
|
+
const aLines = parseInt(a.match.match(/\d+/)?.[0] || '0', 10);
|
|
423
|
+
const bLines = parseInt(b.match.match(/\d+/)?.[0] || '0', 10);
|
|
424
|
+
return bLines - aLines;
|
|
425
|
+
});
|
|
426
|
+
const output = matches.map((m) => `${m.file}:${m.match}`).join('\n');
|
|
427
|
+
return {
|
|
428
|
+
content: [
|
|
429
|
+
{
|
|
430
|
+
type: 'text',
|
|
431
|
+
text: `Found ${matches.length} files exceeding ${threshold} lines:\n\n${output || 'None found'}`,
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
structuredContent: {
|
|
435
|
+
check: 'large_files',
|
|
436
|
+
matches,
|
|
437
|
+
total: matches.length,
|
|
234
438
|
},
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
return {
|
|
443
|
+
content: [{ type: 'text', text: `find_large_files failed: ${error.message}` }],
|
|
444
|
+
structuredContent: { check: 'large_files', matches: [], total: 0 },
|
|
445
|
+
};
|
|
446
|
+
}
|
|
238
447
|
});
|
|
239
448
|
// ═══════════════════════════════════════════════════════════════════════
|
|
240
449
|
// TOOL: find_duplicates — Find duplicate code patterns
|
|
@@ -243,15 +452,17 @@ export function registerQualityTools(server) {
|
|
|
243
452
|
description: 'Find duplicate code patterns and copy-pasted blocks.',
|
|
244
453
|
inputSchema: {
|
|
245
454
|
pattern: z.string().optional().describe('Specific pattern to search for duplicates'),
|
|
455
|
+
min_length: z.number().optional().describe('Minimum length of duplicate block in lines (default: 5)'),
|
|
246
456
|
file_glob: z.string().optional().describe('Filter by file pattern'),
|
|
247
457
|
directory: z.string().optional().describe('Subdirectory to search'),
|
|
248
458
|
},
|
|
249
459
|
outputSchema: outputSchemas.find_pattern,
|
|
250
460
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
251
461
|
}, async (params) => {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
462
|
+
try {
|
|
463
|
+
const searchCwd = params.directory ? safePath(params.directory) : CWD;
|
|
464
|
+
if (params.pattern) {
|
|
465
|
+
// Pattern-based search
|
|
255
466
|
const results = await searchCode({
|
|
256
467
|
cwd: searchCwd,
|
|
257
468
|
pattern: params.pattern,
|
|
@@ -269,22 +480,110 @@ export function registerQualityTools(server) {
|
|
|
269
480
|
},
|
|
270
481
|
};
|
|
271
482
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
483
|
+
// AST-based duplicate detection
|
|
484
|
+
const minLength = params.min_length || 5;
|
|
485
|
+
const fileGlob = params.file_glob || '**/*.{ts,tsx,js,jsx,py,go,java,rs}';
|
|
486
|
+
const files = await listFiles(searchCwd, { glob: fileGlob });
|
|
487
|
+
const codeBlocks = new Map();
|
|
488
|
+
const matches = [];
|
|
489
|
+
// Extract function bodies and look for duplicates
|
|
490
|
+
// Increased limit from 100 to 2000 to handle large projects
|
|
491
|
+
for (const file of files.slice(0, 2000)) {
|
|
492
|
+
try {
|
|
493
|
+
const filePath = path.join(searchCwd, file);
|
|
494
|
+
const language = detectLanguage(file);
|
|
495
|
+
const content = await readFile(filePath, 'utf-8');
|
|
496
|
+
const lines = content.split('\n');
|
|
497
|
+
// Try AST parsing first
|
|
498
|
+
const astSymbols = await parseAST(filePath, language);
|
|
499
|
+
if (astSymbols) {
|
|
500
|
+
for (const symbol of astSymbols) {
|
|
501
|
+
if ((symbol.type === 'function' || symbol.type === 'method') && symbol.endLine && symbol.startLine) {
|
|
502
|
+
const lineCount = symbol.endLine - symbol.startLine + 1;
|
|
503
|
+
if (lineCount >= minLength) {
|
|
504
|
+
const body = lines.slice(symbol.startLine - 1, symbol.endLine).join('\n');
|
|
505
|
+
// Normalize whitespace for comparison
|
|
506
|
+
const normalized = body.replace(/\s+/g, ' ').trim();
|
|
507
|
+
if (normalized.length > 50) {
|
|
508
|
+
// Only consider substantial blocks
|
|
509
|
+
const key = normalized.slice(0, 200); // Use first 200 chars as key
|
|
510
|
+
if (!codeBlocks.has(key)) {
|
|
511
|
+
codeBlocks.set(key, []);
|
|
512
|
+
}
|
|
513
|
+
codeBlocks.get(key).push({ file, line: symbol.startLine });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// Also detect structural patterns (e.g., CRUD patterns)
|
|
520
|
+
// Look for common patterns like: getXById, createX, updateX, deleteX
|
|
521
|
+
// These patterns appear across multiple files with similar structure
|
|
522
|
+
const structuralPatterns = [
|
|
523
|
+
{
|
|
524
|
+
pattern: /(?:export\s+)?(?:const|async\s+function|function)\s+get\w+ById\s*=\s*async\s*\([^)]*id[^)]*\)\s*=>[\s\S]{0,500}?return\s+await\s+db\.query\.\w+\.findFirst/,
|
|
525
|
+
name: 'getXById pattern',
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
pattern: /return\s+await\s+db\.query\.\w+\.findFirst\s*\(\s*\{[\s\S]{0,200}?where:\s*\([^)]*\)\s*=>\s*eq\([^)]*\)[\s\S]{0,200}?\}\)/,
|
|
529
|
+
name: 'db.query.findFirst with eq pattern',
|
|
530
|
+
},
|
|
531
|
+
];
|
|
532
|
+
for (const { pattern, name } of structuralPatterns) {
|
|
533
|
+
const patternMatches = content.matchAll(pattern);
|
|
534
|
+
for (const match of patternMatches) {
|
|
535
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
536
|
+
const key = `structural:${name}`;
|
|
537
|
+
if (!codeBlocks.has(key)) {
|
|
538
|
+
codeBlocks.set(key, []);
|
|
539
|
+
}
|
|
540
|
+
codeBlocks.get(key).push({ file, line });
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
277
547
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
548
|
+
// Find duplicates (appearing in 2+ files)
|
|
549
|
+
for (const [, locations] of codeBlocks.entries()) {
|
|
550
|
+
if (locations.length >= 2) {
|
|
551
|
+
const uniqueFiles = new Set(locations.map((l) => l.file));
|
|
552
|
+
if (uniqueFiles.size >= 2) {
|
|
553
|
+
for (const loc of locations) {
|
|
554
|
+
matches.push({
|
|
555
|
+
file: loc.file,
|
|
556
|
+
line: loc.line,
|
|
557
|
+
match: `Duplicate code block (found in ${locations.length} locations)`,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const output = matches
|
|
564
|
+
.slice(0, 50)
|
|
565
|
+
.map((m) => `${m.file}:${m.line} │ ${m.match}`)
|
|
566
|
+
.join('\n');
|
|
567
|
+
return {
|
|
568
|
+
content: [
|
|
569
|
+
{
|
|
570
|
+
type: 'text',
|
|
571
|
+
text: `Found ${matches.length} duplicate code blocks (min ${minLength} lines):\n\n${output || 'None found'}`,
|
|
572
|
+
},
|
|
573
|
+
],
|
|
574
|
+
structuredContent: {
|
|
575
|
+
check: 'duplicates',
|
|
576
|
+
matches: matches.slice(0, 100),
|
|
577
|
+
total: matches.length,
|
|
284
578
|
},
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
return {
|
|
583
|
+
content: [{ type: 'text', text: `find_duplicates failed: ${error.message}` }],
|
|
584
|
+
structuredContent: { check: 'duplicates', matches: [], total: 0 },
|
|
585
|
+
};
|
|
586
|
+
}
|
|
288
587
|
});
|
|
289
588
|
// ═══════════════════════════════════════════════════════════════════════
|
|
290
589
|
// TOOL: find_dead_code — Find unused exports
|
|
@@ -297,16 +596,81 @@ export function registerQualityTools(server) {
|
|
|
297
596
|
},
|
|
298
597
|
outputSchema: outputSchemas.find_pattern,
|
|
299
598
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
300
|
-
}, async () => {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
599
|
+
}, async (params) => {
|
|
600
|
+
try {
|
|
601
|
+
const searchCwd = params.directory ? safePath(params.directory) : CWD;
|
|
602
|
+
const fileGlob = params.file_glob || '**/*.{ts,tsx,js,jsx,py,go,java,rs}';
|
|
603
|
+
const files = await listFiles(searchCwd, { glob: fileGlob });
|
|
604
|
+
const matches = [];
|
|
605
|
+
// Collect all exported symbols
|
|
606
|
+
const exportedSymbols = [];
|
|
607
|
+
// Increased limit from 200 to 2000 to handle large projects
|
|
608
|
+
for (const file of files.slice(0, 2000)) {
|
|
609
|
+
try {
|
|
610
|
+
const filePath = path.join(searchCwd, file);
|
|
611
|
+
const symbols = await extractSymbols(filePath);
|
|
612
|
+
for (const symbol of symbols) {
|
|
613
|
+
// Check if it's exported
|
|
614
|
+
if (symbol.kind === 'export' || symbol.signature?.includes('export')) {
|
|
615
|
+
exportedSymbols.push({
|
|
616
|
+
file,
|
|
617
|
+
name: symbol.name,
|
|
618
|
+
line: symbol.line,
|
|
619
|
+
kind: symbol.kind,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// Check references for each exported symbol
|
|
629
|
+
// Increased limit from 100 to 1000 to handle large projects
|
|
630
|
+
for (const symbol of exportedSymbols.slice(0, 1000)) {
|
|
631
|
+
try {
|
|
632
|
+
// Search for references to this symbol
|
|
633
|
+
const references = await searchCode({
|
|
634
|
+
cwd: searchCwd,
|
|
635
|
+
pattern: symbol.name,
|
|
636
|
+
isRegex: false,
|
|
637
|
+
fileGlob,
|
|
638
|
+
wholeWord: true,
|
|
639
|
+
maxResults: 50,
|
|
640
|
+
});
|
|
641
|
+
// Filter out the definition itself
|
|
642
|
+
const externalRefs = references.filter((r) => !(r.file === symbol.file && r.line === symbol.line));
|
|
643
|
+
if (externalRefs.length === 0) {
|
|
644
|
+
matches.push({
|
|
645
|
+
file: symbol.file,
|
|
646
|
+
line: symbol.line,
|
|
647
|
+
match: `Unused export: ${symbol.name} (${symbol.kind})`,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
// Skip if reference finding fails
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const output = matches.map((m) => `${m.file}:${m.line} │ ${m.match}`).join('\n');
|
|
657
|
+
return {
|
|
658
|
+
content: [
|
|
659
|
+
{ type: 'text', text: `Found ${matches.length} potentially unused exports:\n\n${output || 'None found'}` },
|
|
660
|
+
],
|
|
661
|
+
structuredContent: {
|
|
662
|
+
check: 'dead_code',
|
|
663
|
+
matches,
|
|
664
|
+
total: matches.length,
|
|
306
665
|
},
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
return {
|
|
670
|
+
content: [{ type: 'text', text: `find_dead_code failed: ${error.message}` }],
|
|
671
|
+
structuredContent: { check: 'dead_code', matches: [], total: 0 },
|
|
672
|
+
};
|
|
673
|
+
}
|
|
310
674
|
});
|
|
311
675
|
}
|
|
312
676
|
//# sourceMappingURL=quality.js.map
|