project-graph-mcp 1.0.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/AGENT_ROLE.md +126 -0
- package/AGENT_ROLE_MINIMAL.md +54 -0
- package/CONFIGURATION.md +188 -0
- package/LICENSE +21 -0
- package/README.md +279 -0
- package/package.json +46 -0
- package/references/symbiote-3x.md +834 -0
- package/rules/express-5.json +76 -0
- package/rules/fastify-5.json +75 -0
- package/rules/nestjs-10.json +88 -0
- package/rules/nextjs-15.json +87 -0
- package/rules/node-22.json +156 -0
- package/rules/react-18.json +87 -0
- package/rules/react-19.json +76 -0
- package/rules/symbiote-2x.json +158 -0
- package/rules/symbiote-3x.json +221 -0
- package/rules/typescript-5.json +69 -0
- package/rules/vue-3.json +79 -0
- package/src/cli-handlers.js +140 -0
- package/src/cli.js +83 -0
- package/src/complexity.js +223 -0
- package/src/custom-rules.js +583 -0
- package/src/dead-code.js +468 -0
- package/src/filters.js +226 -0
- package/src/framework-references.js +177 -0
- package/src/full-analysis.js +159 -0
- package/src/graph-builder.js +269 -0
- package/src/instructions.js +175 -0
- package/src/jsdoc-generator.js +214 -0
- package/src/large-files.js +162 -0
- package/src/mcp-server.js +375 -0
- package/src/outdated-patterns.js +295 -0
- package/src/parser.js +293 -0
- package/src/server.js +28 -0
- package/src/similar-functions.js +278 -0
- package/src/test-annotations.js +301 -0
- package/src/tool-defs.js +444 -0
- package/src/tools.js +240 -0
- package/src/undocumented.js +260 -0
- package/src/workspace.js +70 -0
- package/vendor/acorn.mjs +6145 -0
- package/vendor/walk.mjs +437 -0
package/src/dead-code.js
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dead Code Detector
|
|
3
|
+
* Finds unused functions, classes, exports, variables, and imports
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
7
|
+
import { join, relative, resolve, dirname } from 'path';
|
|
8
|
+
import { parse } from '../vendor/acorn.mjs';
|
|
9
|
+
import * as walk from '../vendor/walk.mjs';
|
|
10
|
+
import { shouldExcludeDir, shouldExcludeFile, parseGitignore } from './filters.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} DeadCodeItem
|
|
14
|
+
* @property {string} name
|
|
15
|
+
* @property {string} type - 'function' | 'class' | 'export' | 'variable' | 'import'
|
|
16
|
+
* @property {string} file
|
|
17
|
+
* @property {number} line
|
|
18
|
+
* @property {string} reason
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Find all JS files
|
|
23
|
+
* @param {string} dir
|
|
24
|
+
* @param {string} rootDir
|
|
25
|
+
* @returns {string[]}
|
|
26
|
+
*/
|
|
27
|
+
function findJSFiles(dir, rootDir = dir) {
|
|
28
|
+
if (dir === rootDir) parseGitignore(rootDir);
|
|
29
|
+
const files = [];
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
for (const entry of readdirSync(dir)) {
|
|
33
|
+
const fullPath = join(dir, entry);
|
|
34
|
+
const relativePath = relative(rootDir, fullPath);
|
|
35
|
+
const stat = statSync(fullPath);
|
|
36
|
+
|
|
37
|
+
if (stat.isDirectory()) {
|
|
38
|
+
if (!shouldExcludeDir(entry, relativePath)) {
|
|
39
|
+
files.push(...findJSFiles(fullPath, rootDir));
|
|
40
|
+
}
|
|
41
|
+
} else if (entry.endsWith('.js') && !entry.endsWith('.css.js') && !entry.endsWith('.tpl.js')) {
|
|
42
|
+
if (!shouldExcludeFile(entry, relativePath)) {
|
|
43
|
+
files.push(fullPath);
|
|
44
|
+
}
|
|
45
|
+
} else if (entry.endsWith('.css.js') || entry.endsWith('.tpl.js')) {
|
|
46
|
+
if (!shouldExcludeFile(entry, relativePath)) {
|
|
47
|
+
files.push(fullPath);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch (e) { }
|
|
52
|
+
|
|
53
|
+
return files;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Find project root by walking up from dir to find package.json
|
|
58
|
+
* @param {string} dir
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
function findProjectRoot(dir) {
|
|
62
|
+
let current = resolve(dir);
|
|
63
|
+
while (current !== dirname(current)) {
|
|
64
|
+
if (existsSync(join(current, 'package.json'))) return current;
|
|
65
|
+
current = dirname(current);
|
|
66
|
+
}
|
|
67
|
+
return resolve(dir);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @typedef {Object} ImportInfo
|
|
72
|
+
* @property {string} name - imported name
|
|
73
|
+
* @property {string} source - import source path
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @typedef {Object} ExportInfo
|
|
78
|
+
* @property {string} name - exported name
|
|
79
|
+
* @property {number} line - line number
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parse file and extract definitions, calls, exports, and imports
|
|
84
|
+
* @param {string} code
|
|
85
|
+
* @returns {{definitions: Set<string>, calls: Set<string>, exports: Set<string>, imports: ImportInfo[], namedExports: ExportInfo[]}}
|
|
86
|
+
*/
|
|
87
|
+
function analyzeFile(code) {
|
|
88
|
+
const definitions = new Set();
|
|
89
|
+
const calls = new Set();
|
|
90
|
+
const exports = new Set();
|
|
91
|
+
/** @type {ImportInfo[]} */
|
|
92
|
+
const imports = [];
|
|
93
|
+
/** @type {ExportInfo[]} */
|
|
94
|
+
const namedExports = [];
|
|
95
|
+
|
|
96
|
+
let ast;
|
|
97
|
+
try {
|
|
98
|
+
ast = parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
|
|
99
|
+
} catch (e) {
|
|
100
|
+
return { definitions, calls, exports, imports, namedExports };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
walk.simple(ast, {
|
|
104
|
+
FunctionDeclaration(node) {
|
|
105
|
+
if (node.id) {
|
|
106
|
+
definitions.add(node.id.name);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
ClassDeclaration(node) {
|
|
111
|
+
if (node.id) {
|
|
112
|
+
definitions.add(node.id.name);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
CallExpression(node) {
|
|
117
|
+
if (node.callee.type === 'Identifier') {
|
|
118
|
+
calls.add(node.callee.name);
|
|
119
|
+
} else if (node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier') {
|
|
120
|
+
calls.add(node.callee.object.name);
|
|
121
|
+
}
|
|
122
|
+
// Track function references passed as arguments: .map(funcName)
|
|
123
|
+
for (const arg of node.arguments) {
|
|
124
|
+
if (arg.type === 'Identifier') {
|
|
125
|
+
calls.add(arg.name);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
NewExpression(node) {
|
|
131
|
+
if (node.callee.type === 'Identifier') {
|
|
132
|
+
calls.add(node.callee.name);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
ImportDeclaration(node) {
|
|
137
|
+
const source = node.source.value;
|
|
138
|
+
for (const spec of node.specifiers) {
|
|
139
|
+
if (spec.type === 'ImportSpecifier') {
|
|
140
|
+
imports.push({ name: spec.imported.name, source });
|
|
141
|
+
} else if (spec.type === 'ImportDefaultSpecifier') {
|
|
142
|
+
imports.push({ name: 'default', source });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
ExportNamedDeclaration(node) {
|
|
148
|
+
if (node.declaration) {
|
|
149
|
+
if (node.declaration.id) {
|
|
150
|
+
const name = node.declaration.id.name;
|
|
151
|
+
exports.add(name);
|
|
152
|
+
namedExports.push({ name, line: node.loc.start.line });
|
|
153
|
+
} else if (node.declaration.declarations) {
|
|
154
|
+
for (const decl of node.declaration.declarations) {
|
|
155
|
+
if (decl.id.type === 'Identifier') {
|
|
156
|
+
const name = decl.id.name;
|
|
157
|
+
exports.add(name);
|
|
158
|
+
namedExports.push({ name, line: node.loc.start.line });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (node.specifiers) {
|
|
164
|
+
for (const spec of node.specifiers) {
|
|
165
|
+
const name = spec.exported.name;
|
|
166
|
+
exports.add(name);
|
|
167
|
+
namedExports.push({ name, line: node.loc.start.line });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
ExportDefaultDeclaration(node) {
|
|
173
|
+
if (node.declaration?.id) {
|
|
174
|
+
exports.add(node.declaration.id.name);
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return { definitions, calls, exports, imports, namedExports };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Analyze file for unused local variables and imports
|
|
184
|
+
* @param {string} code
|
|
185
|
+
* @returns {{unusedVars: Array<{name: string, line: number}>, unusedImports: Array<{name: string, local: string, source: string, line: number}>}}
|
|
186
|
+
*/
|
|
187
|
+
function analyzeFileLocals(code) {
|
|
188
|
+
const unusedVars = [];
|
|
189
|
+
const unusedImports = [];
|
|
190
|
+
|
|
191
|
+
let ast;
|
|
192
|
+
try {
|
|
193
|
+
ast = parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
|
|
194
|
+
} catch (e) {
|
|
195
|
+
return { unusedVars, unusedImports };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Collect all identifier references (non-declaration sites)
|
|
199
|
+
const refs = new Set();
|
|
200
|
+
// Collect variable declarations: { name, line, isExported }
|
|
201
|
+
const varDecls = [];
|
|
202
|
+
// Collect import specifiers: { name, local, source, line }
|
|
203
|
+
const importDecls = [];
|
|
204
|
+
// Track names at declaration sites to exclude
|
|
205
|
+
const declSites = new Set();
|
|
206
|
+
|
|
207
|
+
// First pass: collect declarations
|
|
208
|
+
walk.simple(ast, {
|
|
209
|
+
VariableDeclaration(node) {
|
|
210
|
+
const isExported = node.parent?.type === 'ExportNamedDeclaration';
|
|
211
|
+
for (const decl of node.declarations) {
|
|
212
|
+
if (decl.id.type === 'Identifier') {
|
|
213
|
+
varDecls.push({ name: decl.id.name, line: decl.loc.start.line, isExported });
|
|
214
|
+
declSites.add(decl.id);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
ImportDeclaration(node) {
|
|
219
|
+
for (const spec of node.specifiers) {
|
|
220
|
+
const local = spec.local.name;
|
|
221
|
+
const imported = spec.type === 'ImportSpecifier'
|
|
222
|
+
? spec.imported.name
|
|
223
|
+
: spec.type === 'ImportDefaultSpecifier'
|
|
224
|
+
? 'default' : '*';
|
|
225
|
+
importDecls.push({
|
|
226
|
+
name: imported,
|
|
227
|
+
local,
|
|
228
|
+
source: node.source.value,
|
|
229
|
+
line: node.loc.start.line,
|
|
230
|
+
});
|
|
231
|
+
declSites.add(spec.local);
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Walk AST to find variable declaration sites more precisely
|
|
237
|
+
// (the walk.simple above doesn't set parents, so we need ancestor walk)
|
|
238
|
+
// Instead, use top-level statement analysis
|
|
239
|
+
const topLevelExportedNames = new Set();
|
|
240
|
+
for (const node of ast.body) {
|
|
241
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
242
|
+
if (node.declaration.declarations) {
|
|
243
|
+
for (const decl of node.declaration.declarations) {
|
|
244
|
+
if (decl.id.type === 'Identifier') topLevelExportedNames.add(decl.id.name);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (node.declaration.id) topLevelExportedNames.add(node.declaration.id.name);
|
|
248
|
+
}
|
|
249
|
+
if (node.type === 'ExportNamedDeclaration' && node.specifiers) {
|
|
250
|
+
for (const spec of node.specifiers) {
|
|
251
|
+
topLevelExportedNames.add(spec.local.name);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Collect all identifier references across the AST
|
|
257
|
+
walk.simple(ast, {
|
|
258
|
+
Identifier(node) {
|
|
259
|
+
refs.add(node.name);
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Find unused variables (declared but never referenced beyond declaration)
|
|
264
|
+
// We count total references: if name appears only in declaration, it's unused
|
|
265
|
+
for (const v of varDecls) {
|
|
266
|
+
// Skip exported variables (handled by orphan exports)
|
|
267
|
+
if (topLevelExportedNames.has(v.name)) continue;
|
|
268
|
+
// Skip destructuring names and common patterns
|
|
269
|
+
if (v.name.startsWith('_')) continue;
|
|
270
|
+
// Check if name is referenced anywhere in the code beyond its declaration
|
|
271
|
+
// Simple heuristic: count occurrences in the source
|
|
272
|
+
const regex = new RegExp(`\\b${v.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
|
|
273
|
+
const matches = code.match(regex);
|
|
274
|
+
// If only 1 occurrence, it's the declaration itself
|
|
275
|
+
if (matches && matches.length <= 1) {
|
|
276
|
+
unusedVars.push({ name: v.name, line: v.line });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Find unused imports
|
|
281
|
+
for (const imp of importDecls) {
|
|
282
|
+
// Skip namespace imports
|
|
283
|
+
if (imp.name === '*') continue;
|
|
284
|
+
// Check if local name is referenced beyond the import declaration
|
|
285
|
+
const regex = new RegExp(`\\b${imp.local.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
|
|
286
|
+
const matches = code.match(regex);
|
|
287
|
+
// If only 1 occurrence, it's the import declaration itself
|
|
288
|
+
if (matches && matches.length <= 1) {
|
|
289
|
+
unusedImports.push(imp);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return { unusedVars, unusedImports };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get dead code items
|
|
298
|
+
* @param {string} dir
|
|
299
|
+
* @returns {Promise<{total: number, byType: Object, items: DeadCodeItem[]}>}
|
|
300
|
+
*/
|
|
301
|
+
export async function getDeadCode(dir) {
|
|
302
|
+
const resolvedDir = resolve(dir);
|
|
303
|
+
const files = findJSFiles(dir);
|
|
304
|
+
const items = [];
|
|
305
|
+
|
|
306
|
+
// Collect all calls and exports across target directory
|
|
307
|
+
const allCalls = new Set();
|
|
308
|
+
const allExports = new Set();
|
|
309
|
+
const fileData = [];
|
|
310
|
+
|
|
311
|
+
// Track imports project-wide: key = "importedName@resolvedSourcePath", value = consumer files
|
|
312
|
+
/** @type {Map<string, Set<string>>} */
|
|
313
|
+
const importConsumers = new Map();
|
|
314
|
+
|
|
315
|
+
// Scan entire project for import consumers (not just target dir)
|
|
316
|
+
const projectRoot = findProjectRoot(dir);
|
|
317
|
+
const projectFiles = findJSFiles(projectRoot);
|
|
318
|
+
|
|
319
|
+
for (const file of projectFiles) {
|
|
320
|
+
let code;
|
|
321
|
+
try { code = readFileSync(file, 'utf-8'); } catch { continue; }
|
|
322
|
+
const relPath = relative(resolvedDir, file);
|
|
323
|
+
const { imports } = analyzeFile(code);
|
|
324
|
+
|
|
325
|
+
for (const imp of imports) {
|
|
326
|
+
if (!imp.source.startsWith('.')) continue;
|
|
327
|
+
const fileDir = dirname(file);
|
|
328
|
+
let resolvedSource = resolve(fileDir, imp.source);
|
|
329
|
+
if (!resolvedSource.endsWith('.js')) resolvedSource += '.js';
|
|
330
|
+
const relSource = relative(resolvedDir, resolvedSource);
|
|
331
|
+
const key = `${imp.name}@${relSource}`;
|
|
332
|
+
if (!importConsumers.has(key)) importConsumers.set(key, new Set());
|
|
333
|
+
importConsumers.get(key).add(relPath);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Analyze target directory files for definitions, calls, exports
|
|
338
|
+
for (const file of files) {
|
|
339
|
+
const code = readFileSync(file, 'utf-8');
|
|
340
|
+
const relPath = relative(resolvedDir, file);
|
|
341
|
+
const { definitions, calls, exports, namedExports } = analyzeFile(code);
|
|
342
|
+
|
|
343
|
+
for (const call of calls) allCalls.add(call);
|
|
344
|
+
for (const exp of exports) allExports.add(exp);
|
|
345
|
+
|
|
346
|
+
fileData.push({ file: relPath, code, definitions, calls, exports, namedExports });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Find dead code (functions/classes)
|
|
350
|
+
for (const { file, code, definitions, exports } of fileData) {
|
|
351
|
+
// Skip test files and presentation files
|
|
352
|
+
if (file.includes('.test.') || file.includes('/tests/')) continue;
|
|
353
|
+
if (file.endsWith('.css.js') || file.endsWith('.tpl.js')) continue;
|
|
354
|
+
|
|
355
|
+
let ast;
|
|
356
|
+
try {
|
|
357
|
+
ast = parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
|
|
358
|
+
} catch (e) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
walk.simple(ast, {
|
|
363
|
+
FunctionDeclaration(node) {
|
|
364
|
+
if (!node.id) return;
|
|
365
|
+
const name = node.id.name;
|
|
366
|
+
|
|
367
|
+
// Skip if exported
|
|
368
|
+
if (exports.has(name) || allExports.has(name)) return;
|
|
369
|
+
|
|
370
|
+
// Skip if called anywhere
|
|
371
|
+
if (allCalls.has(name)) return;
|
|
372
|
+
|
|
373
|
+
// Skip private functions
|
|
374
|
+
if (name.startsWith('_')) return;
|
|
375
|
+
|
|
376
|
+
items.push({
|
|
377
|
+
name,
|
|
378
|
+
type: 'function',
|
|
379
|
+
file,
|
|
380
|
+
line: node.loc.start.line,
|
|
381
|
+
reason: 'Never called',
|
|
382
|
+
});
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
ClassDeclaration(node) {
|
|
386
|
+
if (!node.id) return;
|
|
387
|
+
const name = node.id.name;
|
|
388
|
+
|
|
389
|
+
// Skip if exported
|
|
390
|
+
if (exports.has(name) || allExports.has(name)) return;
|
|
391
|
+
|
|
392
|
+
// Skip if used (new Class() or extended)
|
|
393
|
+
if (allCalls.has(name)) return;
|
|
394
|
+
|
|
395
|
+
items.push({
|
|
396
|
+
name,
|
|
397
|
+
type: 'class',
|
|
398
|
+
file,
|
|
399
|
+
line: node.loc.start.line,
|
|
400
|
+
reason: 'Never instantiated',
|
|
401
|
+
});
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Find orphan exports (named exports not imported by any other file)
|
|
407
|
+
for (const { file, calls, namedExports } of fileData) {
|
|
408
|
+
if (file.includes('.test.') || file.includes('/tests/')) continue;
|
|
409
|
+
|
|
410
|
+
for (const exp of namedExports) {
|
|
411
|
+
// Skip exports used as call targets within the same file (e.g. ClassName.reg())
|
|
412
|
+
if (calls.has(exp.name)) continue;
|
|
413
|
+
const key = `${exp.name}@${file}`;
|
|
414
|
+
const consumers = importConsumers.get(key);
|
|
415
|
+
if (!consumers || consumers.size === 0) {
|
|
416
|
+
items.push({
|
|
417
|
+
name: exp.name,
|
|
418
|
+
type: 'export',
|
|
419
|
+
file,
|
|
420
|
+
line: exp.line,
|
|
421
|
+
reason: 'Exported but never imported',
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Find unused variables and imports per file
|
|
428
|
+
for (const { file, code } of fileData) {
|
|
429
|
+
if (file.includes('.test.') || file.includes('/tests/')) continue;
|
|
430
|
+
if (file.endsWith('.css.js') || file.endsWith('.tpl.js')) continue;
|
|
431
|
+
|
|
432
|
+
const { unusedVars, unusedImports } = analyzeFileLocals(code);
|
|
433
|
+
|
|
434
|
+
for (const v of unusedVars) {
|
|
435
|
+
items.push({
|
|
436
|
+
name: v.name,
|
|
437
|
+
type: 'variable',
|
|
438
|
+
file,
|
|
439
|
+
line: v.line,
|
|
440
|
+
reason: 'Declared but never used',
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
for (const imp of unusedImports) {
|
|
445
|
+
items.push({
|
|
446
|
+
name: imp.local,
|
|
447
|
+
type: 'import',
|
|
448
|
+
file,
|
|
449
|
+
line: imp.line,
|
|
450
|
+
reason: `Imported from '${imp.source}' but never used`,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const byType = {
|
|
456
|
+
function: items.filter(i => i.type === 'function').length,
|
|
457
|
+
class: items.filter(i => i.type === 'class').length,
|
|
458
|
+
export: items.filter(i => i.type === 'export').length,
|
|
459
|
+
variable: items.filter(i => i.type === 'variable').length,
|
|
460
|
+
import: items.filter(i => i.type === 'import').length,
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
total: items.length,
|
|
465
|
+
byType,
|
|
466
|
+
items: items.slice(0, 50),
|
|
467
|
+
};
|
|
468
|
+
}
|
package/src/filters.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter Configuration for Project Graph
|
|
3
|
+
* Manages excludes, includes, and gitignore parsing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, existsSync } from 'fs';
|
|
7
|
+
import { join, relative } from 'path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Default directories to exclude
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_EXCLUDES = [
|
|
13
|
+
'node_modules',
|
|
14
|
+
'dist',
|
|
15
|
+
'build',
|
|
16
|
+
'coverage',
|
|
17
|
+
'.next',
|
|
18
|
+
'.nuxt',
|
|
19
|
+
'.output',
|
|
20
|
+
'__pycache__',
|
|
21
|
+
'.cache',
|
|
22
|
+
'.turbo',
|
|
23
|
+
'out',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Default file patterns to exclude
|
|
28
|
+
*/
|
|
29
|
+
const DEFAULT_EXCLUDE_PATTERNS = [
|
|
30
|
+
'*.test.js',
|
|
31
|
+
'*.spec.js',
|
|
32
|
+
'*.min.js',
|
|
33
|
+
'*.bundle.js',
|
|
34
|
+
'*.d.ts',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// Current filter configuration (mutable via MCP)
|
|
38
|
+
let config = {
|
|
39
|
+
excludeDirs: [...DEFAULT_EXCLUDES],
|
|
40
|
+
excludePatterns: [...DEFAULT_EXCLUDE_PATTERNS],
|
|
41
|
+
includeHidden: false,
|
|
42
|
+
useGitignore: true,
|
|
43
|
+
gitignorePatterns: [],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get current filter configuration
|
|
48
|
+
* @returns {Object}
|
|
49
|
+
*/
|
|
50
|
+
export function getFilters() {
|
|
51
|
+
return { ...config };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Update filter configuration
|
|
56
|
+
* @param {Object} updates
|
|
57
|
+
* @returns {Object}
|
|
58
|
+
*/
|
|
59
|
+
export function setFilters(updates) {
|
|
60
|
+
if (updates.excludeDirs !== undefined) {
|
|
61
|
+
config.excludeDirs = updates.excludeDirs;
|
|
62
|
+
}
|
|
63
|
+
if (updates.excludePatterns !== undefined) {
|
|
64
|
+
config.excludePatterns = updates.excludePatterns;
|
|
65
|
+
}
|
|
66
|
+
if (updates.includeHidden !== undefined) {
|
|
67
|
+
config.includeHidden = updates.includeHidden;
|
|
68
|
+
}
|
|
69
|
+
if (updates.useGitignore !== undefined) {
|
|
70
|
+
config.useGitignore = updates.useGitignore;
|
|
71
|
+
}
|
|
72
|
+
return getFilters();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Add directories to exclude list
|
|
77
|
+
* @param {string[]} dirs
|
|
78
|
+
* @returns {Object}
|
|
79
|
+
*/
|
|
80
|
+
export function addExcludes(dirs) {
|
|
81
|
+
config.excludeDirs = [...new Set([...config.excludeDirs, ...dirs])];
|
|
82
|
+
return getFilters();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Remove directories from exclude list
|
|
87
|
+
* @param {string[]} dirs
|
|
88
|
+
* @returns {Object}
|
|
89
|
+
*/
|
|
90
|
+
export function removeExcludes(dirs) {
|
|
91
|
+
config.excludeDirs = config.excludeDirs.filter(d => !dirs.includes(d));
|
|
92
|
+
return getFilters();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Reset filters to defaults
|
|
97
|
+
* @returns {Object}
|
|
98
|
+
*/
|
|
99
|
+
export function resetFilters() {
|
|
100
|
+
config = {
|
|
101
|
+
excludeDirs: [...DEFAULT_EXCLUDES],
|
|
102
|
+
excludePatterns: [...DEFAULT_EXCLUDE_PATTERNS],
|
|
103
|
+
includeHidden: false,
|
|
104
|
+
useGitignore: true,
|
|
105
|
+
gitignorePatterns: [],
|
|
106
|
+
};
|
|
107
|
+
return getFilters();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse .gitignore file
|
|
112
|
+
* @param {string} rootDir
|
|
113
|
+
* @returns {string[]}
|
|
114
|
+
*/
|
|
115
|
+
export function parseGitignore(rootDir) {
|
|
116
|
+
const gitignorePath = join(rootDir, '.gitignore');
|
|
117
|
+
|
|
118
|
+
if (!existsSync(gitignorePath)) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const content = readFileSync(gitignorePath, 'utf-8');
|
|
124
|
+
const patterns = content
|
|
125
|
+
.split('\n')
|
|
126
|
+
.map(line => line.trim())
|
|
127
|
+
.filter(line => line && !line.startsWith('#'))
|
|
128
|
+
.map(line => line.replace(/\/$/, '')); // Remove trailing slashes
|
|
129
|
+
|
|
130
|
+
config.gitignorePatterns = patterns;
|
|
131
|
+
return patterns;
|
|
132
|
+
} catch (e) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if a directory should be excluded
|
|
139
|
+
* @param {string} dirName - Directory name (not path)
|
|
140
|
+
* @param {string} relativePath - Relative path from root
|
|
141
|
+
* @returns {boolean}
|
|
142
|
+
*/
|
|
143
|
+
export function shouldExcludeDir(dirName, relativePath = '') {
|
|
144
|
+
// Check hidden directories
|
|
145
|
+
if (!config.includeHidden && dirName.startsWith('.')) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check default excludes
|
|
150
|
+
if (config.excludeDirs.includes(dirName)) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check gitignore patterns
|
|
155
|
+
if (config.useGitignore) {
|
|
156
|
+
for (const pattern of config.gitignorePatterns) {
|
|
157
|
+
if (matchGitignorePattern(pattern, dirName, relativePath)) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check if a file should be excluded
|
|
168
|
+
* @param {string} fileName
|
|
169
|
+
* @param {string} relativePath
|
|
170
|
+
* @returns {boolean}
|
|
171
|
+
*/
|
|
172
|
+
export function shouldExcludeFile(fileName, relativePath = '') {
|
|
173
|
+
// Check exclude patterns
|
|
174
|
+
for (const pattern of config.excludePatterns) {
|
|
175
|
+
if (matchWildcard(pattern, fileName)) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check gitignore patterns
|
|
181
|
+
if (config.useGitignore) {
|
|
182
|
+
for (const pattern of config.gitignorePatterns) {
|
|
183
|
+
if (matchGitignorePattern(pattern, fileName, relativePath)) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Match simple wildcard pattern (*.js, *.test.js)
|
|
194
|
+
* @param {string} pattern
|
|
195
|
+
* @param {string} str
|
|
196
|
+
* @returns {boolean}
|
|
197
|
+
*/
|
|
198
|
+
function matchWildcard(pattern, str) {
|
|
199
|
+
const regex = pattern
|
|
200
|
+
.replace(/\./g, '\\.')
|
|
201
|
+
.replace(/\*/g, '.*');
|
|
202
|
+
return new RegExp(`^${regex}$`).test(str);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Match gitignore pattern
|
|
207
|
+
* @param {string} pattern
|
|
208
|
+
* @param {string} name
|
|
209
|
+
* @param {string} relativePath
|
|
210
|
+
* @returns {boolean}
|
|
211
|
+
*/
|
|
212
|
+
function matchGitignorePattern(pattern, name, relativePath) {
|
|
213
|
+
// Simple matching: exact name or wildcard
|
|
214
|
+
if (pattern === name) return true;
|
|
215
|
+
|
|
216
|
+
// Pattern with wildcard
|
|
217
|
+
if (pattern.includes('*')) {
|
|
218
|
+
return matchWildcard(pattern, name);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Pattern in path
|
|
222
|
+
const fullPath = relativePath ? `${relativePath}/${name}` : name;
|
|
223
|
+
if (fullPath.includes(pattern)) return true;
|
|
224
|
+
|
|
225
|
+
return false;
|
|
226
|
+
}
|