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
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core MCP Server Logic
|
|
3
|
+
*
|
|
4
|
+
* Implements bidirectional JSON-RPC 2.0 over stdio:
|
|
5
|
+
* - Handles client→server requests (tools/list, tools/call)
|
|
6
|
+
* - Sends server→client requests (roots/list) to get workspace info
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { TOOLS } from './tool-defs.js';
|
|
10
|
+
import { getSkeleton, getFocusZone, expand, deps, usages, invalidateCache } from './tools.js';
|
|
11
|
+
import { getPendingTests, markTestPassed, markTestFailed, getTestSummary, resetTestState } from './test-annotations.js';
|
|
12
|
+
import { getFilters, setFilters, addExcludes, removeExcludes, resetFilters } from './filters.js';
|
|
13
|
+
import { getInstructions } from './instructions.js';
|
|
14
|
+
import { getUndocumentedSummary } from './undocumented.js';
|
|
15
|
+
import { getDeadCode } from './dead-code.js';
|
|
16
|
+
import { generateJSDoc, generateJSDocFor } from './jsdoc-generator.js';
|
|
17
|
+
import { getSimilarFunctions } from './similar-functions.js';
|
|
18
|
+
import { getComplexity } from './complexity.js';
|
|
19
|
+
import { getLargeFiles } from './large-files.js';
|
|
20
|
+
import { getOutdatedPatterns } from './outdated-patterns.js';
|
|
21
|
+
import { getFullAnalysis } from './full-analysis.js';
|
|
22
|
+
import { getCustomRules, setCustomRule, checkCustomRules } from './custom-rules.js';
|
|
23
|
+
import { getFrameworkReference } from './framework-references.js';
|
|
24
|
+
import { setRoots, resolvePath } from './workspace.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Tool handlers registry
|
|
28
|
+
* Maps tool names to their handler functions
|
|
29
|
+
*/
|
|
30
|
+
const TOOL_HANDLERS = {
|
|
31
|
+
// Graph Tools
|
|
32
|
+
get_skeleton: (args) => getSkeleton(resolvePath(args.path)),
|
|
33
|
+
get_focus_zone: (args) => getFocusZone({ ...args, path: resolvePath(args.path) }),
|
|
34
|
+
expand: (args) => expand(args.symbol),
|
|
35
|
+
deps: (args) => deps(args.symbol),
|
|
36
|
+
usages: (args) => usages(args.symbol),
|
|
37
|
+
invalidate_cache: () => { invalidateCache(); return { success: true }; },
|
|
38
|
+
|
|
39
|
+
// Test Checklist Tools
|
|
40
|
+
get_pending_tests: (args) => getPendingTests(resolvePath(args.path)),
|
|
41
|
+
mark_test_passed: (args) => markTestPassed(args.testId),
|
|
42
|
+
mark_test_failed: (args) => markTestFailed(args.testId, args.reason),
|
|
43
|
+
get_test_summary: (args) => getTestSummary(resolvePath(args.path)),
|
|
44
|
+
reset_test_state: () => resetTestState(),
|
|
45
|
+
|
|
46
|
+
// Filter Tools
|
|
47
|
+
get_filters: () => getFilters(),
|
|
48
|
+
set_filters: (args) => setFilters(args),
|
|
49
|
+
add_excludes: (args) => addExcludes(args.dirs),
|
|
50
|
+
remove_excludes: (args) => removeExcludes(args.dirs),
|
|
51
|
+
reset_filters: () => resetFilters(),
|
|
52
|
+
|
|
53
|
+
// Guidelines
|
|
54
|
+
get_agent_instructions: () => getInstructions(),
|
|
55
|
+
|
|
56
|
+
// Documentation
|
|
57
|
+
get_undocumented: (args) => getUndocumentedSummary(resolvePath(args.path), args.level || 'tests'),
|
|
58
|
+
|
|
59
|
+
// Code Quality
|
|
60
|
+
get_dead_code: (args) => getDeadCode(resolvePath(args.path)),
|
|
61
|
+
generate_jsdoc: (args) => args.name
|
|
62
|
+
? generateJSDocFor(resolvePath(args.path), args.name)
|
|
63
|
+
: generateJSDoc(resolvePath(args.path)),
|
|
64
|
+
get_similar_functions: (args) => getSimilarFunctions(resolvePath(args.path), { threshold: args.threshold }),
|
|
65
|
+
get_complexity: (args) => getComplexity(resolvePath(args.path), {
|
|
66
|
+
minComplexity: args.minComplexity,
|
|
67
|
+
onlyProblematic: args.onlyProblematic,
|
|
68
|
+
}),
|
|
69
|
+
get_large_files: (args) => getLargeFiles(resolvePath(args.path), { onlyProblematic: args.onlyProblematic }),
|
|
70
|
+
get_outdated_patterns: (args) => getOutdatedPatterns(resolvePath(args.path), {
|
|
71
|
+
codeOnly: args.codeOnly,
|
|
72
|
+
depsOnly: args.depsOnly,
|
|
73
|
+
}),
|
|
74
|
+
get_full_analysis: (args) => getFullAnalysis(resolvePath(args.path), { includeItems: args.includeItems }),
|
|
75
|
+
|
|
76
|
+
// Custom Rules
|
|
77
|
+
get_custom_rules: () => getCustomRules(),
|
|
78
|
+
set_custom_rule: (args) => setCustomRule(args.ruleSet, args.rule),
|
|
79
|
+
check_custom_rules: (args) => checkCustomRules(resolvePath(args.path), {
|
|
80
|
+
ruleSet: args.ruleSet,
|
|
81
|
+
severity: args.severity,
|
|
82
|
+
}),
|
|
83
|
+
|
|
84
|
+
// Framework References
|
|
85
|
+
get_framework_reference: (args) => getFrameworkReference({
|
|
86
|
+
framework: args.framework,
|
|
87
|
+
path: args.path ? resolvePath(args.path) : undefined,
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Response hints — contextual coaching tips appended to tool responses.
|
|
93
|
+
* Maps tool names to hint generators. Each receives the result and returns
|
|
94
|
+
* an array of hint strings (or empty array for no hints).
|
|
95
|
+
*
|
|
96
|
+
* @type {Record<string, (result: any) => string[]>}
|
|
97
|
+
*/
|
|
98
|
+
const RESPONSE_HINTS = {
|
|
99
|
+
get_skeleton: () => [
|
|
100
|
+
'💡 Use expand("SYMBOL") to see code for a specific class.',
|
|
101
|
+
'💡 Use deps("SYMBOL") to see architecture dependencies.',
|
|
102
|
+
'💡 After code changes, run invalidate_cache() to refresh the graph.',
|
|
103
|
+
],
|
|
104
|
+
|
|
105
|
+
expand: (result) => {
|
|
106
|
+
const hints = [];
|
|
107
|
+
if (result.methods?.length > 10) {
|
|
108
|
+
hints.push('💡 Large class detected. Run get_complexity() to find refactoring targets.');
|
|
109
|
+
}
|
|
110
|
+
hints.push('💡 Use deps() to see what depends on this symbol.');
|
|
111
|
+
return hints;
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
deps: () => [
|
|
115
|
+
'💡 Use usages() for cross-project reference search.',
|
|
116
|
+
],
|
|
117
|
+
|
|
118
|
+
invalidate_cache: () => [
|
|
119
|
+
'✅ Cache cleared. Run get_skeleton() to rebuild the project graph.',
|
|
120
|
+
],
|
|
121
|
+
|
|
122
|
+
get_dead_code: (result) => {
|
|
123
|
+
const hints = ['💡 Review each item before removing — some may be used dynamically.'];
|
|
124
|
+
if (result.unusedExports?.length > 20) {
|
|
125
|
+
hints.push('💡 Consider delegating cleanup to agent-pool: delegate_task({ prompt: "Remove dead code..." })');
|
|
126
|
+
}
|
|
127
|
+
return hints;
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
get_full_analysis: () => [
|
|
131
|
+
'💡 Focus on items with "critical" severity first.',
|
|
132
|
+
'💡 Run individual tools (get_complexity, get_dead_code) for detailed breakdowns.',
|
|
133
|
+
],
|
|
134
|
+
|
|
135
|
+
get_complexity: () => [
|
|
136
|
+
'💡 Functions with complexity >10 are candidates for refactoring.',
|
|
137
|
+
'💡 Use expand() to read the function code before refactoring.',
|
|
138
|
+
],
|
|
139
|
+
|
|
140
|
+
get_undocumented: () => [
|
|
141
|
+
'💡 Use generate_jsdoc() to auto-generate documentation templates.',
|
|
142
|
+
],
|
|
143
|
+
|
|
144
|
+
get_similar_functions: () => [
|
|
145
|
+
'💡 Consider extracting duplicated logic into a shared utility.',
|
|
146
|
+
],
|
|
147
|
+
|
|
148
|
+
get_pending_tests: () => [
|
|
149
|
+
'💡 Use mark_test_passed(testId) or mark_test_failed(testId, reason) to track progress.',
|
|
150
|
+
],
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Create MCP server instance
|
|
155
|
+
* @param {Function} sendToClient - Function to send JSON-RPC messages to client
|
|
156
|
+
* @returns {Object}
|
|
157
|
+
*/
|
|
158
|
+
export function createServer(sendToClient) {
|
|
159
|
+
let nextRequestId = 1;
|
|
160
|
+
|
|
161
|
+
/** @type {Map<number, {resolve: Function, reject: Function}>} */
|
|
162
|
+
const pendingRequests = new Map();
|
|
163
|
+
|
|
164
|
+
/** @type {boolean} */
|
|
165
|
+
let clientSupportsRoots = false;
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
pendingRequests,
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Handle incoming JSON-RPC message (request, response, or notification)
|
|
172
|
+
* @param {Object} message
|
|
173
|
+
* @returns {Promise<Object|null>}
|
|
174
|
+
*/
|
|
175
|
+
async handleMessage(message) {
|
|
176
|
+
// Check if this is a response to our server→client request
|
|
177
|
+
if (message.result !== undefined || message.error !== undefined) {
|
|
178
|
+
const pending = pendingRequests.get(message.id);
|
|
179
|
+
if (pending) {
|
|
180
|
+
pendingRequests.delete(message.id);
|
|
181
|
+
if (message.error) {
|
|
182
|
+
pending.reject(new Error(message.error.message));
|
|
183
|
+
} else {
|
|
184
|
+
pending.resolve(message.result);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const { method, params, id } = message;
|
|
191
|
+
|
|
192
|
+
// Notification (no id) — handle but don't respond
|
|
193
|
+
if (id === undefined) {
|
|
194
|
+
await this.handleNotification(method, params);
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Request — handle and respond
|
|
199
|
+
try {
|
|
200
|
+
switch (method) {
|
|
201
|
+
case 'initialize':
|
|
202
|
+
// Track client capabilities
|
|
203
|
+
if (params?.capabilities?.roots) {
|
|
204
|
+
clientSupportsRoots = true;
|
|
205
|
+
}
|
|
206
|
+
// Also check for inline roots
|
|
207
|
+
if (params?.roots) {
|
|
208
|
+
setRoots(params.roots);
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
jsonrpc: '2.0',
|
|
212
|
+
id,
|
|
213
|
+
result: {
|
|
214
|
+
protocolVersion: '2024-11-05',
|
|
215
|
+
capabilities: { tools: {} },
|
|
216
|
+
serverInfo: { name: 'project-graph', version: '1.1.0' },
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
case 'tools/list':
|
|
221
|
+
return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
|
|
222
|
+
|
|
223
|
+
case 'tools/call': {
|
|
224
|
+
const result = await this.executeTool(params.name, params.arguments);
|
|
225
|
+
const content = [{ type: 'text', text: JSON.stringify(result, null, 2) }];
|
|
226
|
+
|
|
227
|
+
// Inject contextual hints
|
|
228
|
+
const hintFn = RESPONSE_HINTS[params.name];
|
|
229
|
+
if (hintFn) {
|
|
230
|
+
const hints = hintFn(result);
|
|
231
|
+
if (hints.length > 0) {
|
|
232
|
+
content.push({ type: 'text', text: '\n' + hints.join('\n') });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
jsonrpc: '2.0',
|
|
238
|
+
id,
|
|
239
|
+
result: { content },
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
default:
|
|
244
|
+
return {
|
|
245
|
+
jsonrpc: '2.0',
|
|
246
|
+
id,
|
|
247
|
+
error: { code: -32601, message: `Method not found: ${method}` },
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
} catch (error) {
|
|
251
|
+
return { jsonrpc: '2.0', id, error: { code: -32000, message: error.message } };
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Handle MCP notifications
|
|
257
|
+
* @param {string} method
|
|
258
|
+
* @param {Object} params
|
|
259
|
+
*/
|
|
260
|
+
async handleNotification(method, params) {
|
|
261
|
+
switch (method) {
|
|
262
|
+
case 'notifications/initialized':
|
|
263
|
+
// Client is ready — request workspace roots if supported
|
|
264
|
+
if (clientSupportsRoots) {
|
|
265
|
+
try {
|
|
266
|
+
const roots = await this.requestRoots();
|
|
267
|
+
if (roots && roots.length > 0) {
|
|
268
|
+
setRoots(roots);
|
|
269
|
+
}
|
|
270
|
+
} catch (e) {
|
|
271
|
+
console.error(`[project-graph] Failed to get roots: ${e.message}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
break;
|
|
275
|
+
|
|
276
|
+
case 'notifications/roots/list_changed':
|
|
277
|
+
// Workspace roots changed — re-request
|
|
278
|
+
if (clientSupportsRoots) {
|
|
279
|
+
try {
|
|
280
|
+
const roots = await this.requestRoots();
|
|
281
|
+
if (roots && roots.length > 0) {
|
|
282
|
+
setRoots(roots);
|
|
283
|
+
invalidateCache();
|
|
284
|
+
}
|
|
285
|
+
} catch (e) {
|
|
286
|
+
console.error(`[project-graph] Failed to refresh roots: ${e.message}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Send roots/list request to client
|
|
295
|
+
* @returns {Promise<Array<{uri: string, name?: string}>>}
|
|
296
|
+
*/
|
|
297
|
+
requestRoots() {
|
|
298
|
+
return new Promise((resolve, reject) => {
|
|
299
|
+
const id = nextRequestId++;
|
|
300
|
+
const timeout = setTimeout(() => {
|
|
301
|
+
pendingRequests.delete(id);
|
|
302
|
+
reject(new Error('roots/list request timed out'));
|
|
303
|
+
}, 5000);
|
|
304
|
+
|
|
305
|
+
pendingRequests.set(id, {
|
|
306
|
+
resolve: (result) => {
|
|
307
|
+
clearTimeout(timeout);
|
|
308
|
+
resolve(result.roots || []);
|
|
309
|
+
},
|
|
310
|
+
reject: (err) => {
|
|
311
|
+
clearTimeout(timeout);
|
|
312
|
+
reject(err);
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
sendToClient({
|
|
317
|
+
jsonrpc: '2.0',
|
|
318
|
+
id,
|
|
319
|
+
method: 'roots/list',
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Execute a tool by name
|
|
326
|
+
* @param {string} name
|
|
327
|
+
* @param {Object} args
|
|
328
|
+
* @returns {Promise<any>}
|
|
329
|
+
*/
|
|
330
|
+
async executeTool(name, args) {
|
|
331
|
+
const handler = TOOL_HANDLERS[name];
|
|
332
|
+
if (!handler) {
|
|
333
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
334
|
+
}
|
|
335
|
+
return await handler(args);
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Start server with stdio transport
|
|
342
|
+
*/
|
|
343
|
+
export async function startStdioServer() {
|
|
344
|
+
/**
|
|
345
|
+
* Send JSON-RPC message to client via stdout
|
|
346
|
+
* @param {Object} message
|
|
347
|
+
*/
|
|
348
|
+
const sendToClient = (message) => {
|
|
349
|
+
console.log(JSON.stringify(message));
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const server = createServer(sendToClient);
|
|
353
|
+
const readline = await import('readline');
|
|
354
|
+
|
|
355
|
+
const rl = readline.createInterface({
|
|
356
|
+
input: process.stdin,
|
|
357
|
+
output: process.stdout,
|
|
358
|
+
terminal: false,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
rl.on('line', async (line) => {
|
|
362
|
+
try {
|
|
363
|
+
const message = JSON.parse(line);
|
|
364
|
+
const response = await server.handleMessage(message);
|
|
365
|
+
if (response !== null) {
|
|
366
|
+
sendToClient(response);
|
|
367
|
+
}
|
|
368
|
+
} catch (e) {
|
|
369
|
+
sendToClient({
|
|
370
|
+
jsonrpc: '2.0',
|
|
371
|
+
error: { code: -32700, message: 'Parse error' },
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outdated Patterns Detector
|
|
3
|
+
* Finds legacy code patterns and redundant npm dependencies
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
7
|
+
import { join, relative, resolve } 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
|
+
* Redundant npm packages that are now built into Node.js 18+
|
|
14
|
+
*/
|
|
15
|
+
const REDUNDANT_DEPS = {
|
|
16
|
+
'node-fetch': { replacement: 'fetch()', since: 'Node 18' },
|
|
17
|
+
'cross-fetch': { replacement: 'fetch()', since: 'Node 18' },
|
|
18
|
+
'isomorphic-fetch': { replacement: 'fetch()', since: 'Node 18' },
|
|
19
|
+
'uuid': { replacement: 'crypto.randomUUID()', since: 'Node 19' },
|
|
20
|
+
'deep-clone': { replacement: 'structuredClone()', since: 'Node 17' },
|
|
21
|
+
'lodash.clonedeep': { replacement: 'structuredClone()', since: 'Node 17' },
|
|
22
|
+
'abort-controller': { replacement: 'AbortController (global)', since: 'Node 15' },
|
|
23
|
+
'form-data': { replacement: 'FormData (global)', since: 'Node 18' },
|
|
24
|
+
'web-streams-polyfill': { replacement: 'ReadableStream (global)', since: 'Node 18' },
|
|
25
|
+
'url-parse': { replacement: 'URL (global)', since: 'Node 10' },
|
|
26
|
+
'querystring': { replacement: 'URLSearchParams', since: 'Node 10' },
|
|
27
|
+
'rimraf': { replacement: 'fs.rm({ recursive: true })', since: 'Node 14' },
|
|
28
|
+
'mkdirp': { replacement: 'fs.mkdir({ recursive: true })', since: 'Node 10' },
|
|
29
|
+
'recursive-readdir': { replacement: 'fs.readdir({ recursive: true })', since: 'Node 20' },
|
|
30
|
+
'glob': { replacement: 'fs.glob()', since: 'Node 22' },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Legacy code patterns to detect
|
|
35
|
+
*/
|
|
36
|
+
const CODE_PATTERNS = [
|
|
37
|
+
{
|
|
38
|
+
name: 'var-usage',
|
|
39
|
+
description: 'Use const/let instead of var',
|
|
40
|
+
check: (node) => node.type === 'VariableDeclaration' && node.kind === 'var',
|
|
41
|
+
severity: 'warning',
|
|
42
|
+
replacement: 'const/let',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'require-usage',
|
|
46
|
+
description: 'Use ESM import instead of require()',
|
|
47
|
+
check: (node) => node.type === 'CallExpression' &&
|
|
48
|
+
node.callee.type === 'Identifier' && node.callee.name === 'require',
|
|
49
|
+
severity: 'info',
|
|
50
|
+
replacement: 'import ... from',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'module-exports',
|
|
54
|
+
description: 'Use ESM export instead of module.exports',
|
|
55
|
+
check: (node) => node.type === 'AssignmentExpression' &&
|
|
56
|
+
node.left.type === 'MemberExpression' &&
|
|
57
|
+
node.left.object.type === 'Identifier' && node.left.object.name === 'module' &&
|
|
58
|
+
node.left.property.type === 'Identifier' && node.left.property.name === 'exports',
|
|
59
|
+
severity: 'info',
|
|
60
|
+
replacement: 'export default/export',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'buffer-constructor',
|
|
64
|
+
description: 'new Buffer() is deprecated',
|
|
65
|
+
check: (node) => node.type === 'NewExpression' &&
|
|
66
|
+
node.callee.type === 'Identifier' && node.callee.name === 'Buffer',
|
|
67
|
+
severity: 'error',
|
|
68
|
+
replacement: 'Buffer.from() / Buffer.alloc()',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'arguments-usage',
|
|
72
|
+
description: 'Use rest parameters instead of arguments',
|
|
73
|
+
check: (node) => node.type === 'Identifier' && node.name === 'arguments',
|
|
74
|
+
severity: 'warning',
|
|
75
|
+
replacement: '...args',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'promisify-usage',
|
|
79
|
+
description: 'Use fs/promises instead of util.promisify',
|
|
80
|
+
check: (node) => node.type === 'CallExpression' &&
|
|
81
|
+
node.callee.type === 'MemberExpression' &&
|
|
82
|
+
node.callee.object.type === 'Identifier' && node.callee.object.name === 'util' &&
|
|
83
|
+
node.callee.property.type === 'Identifier' && node.callee.property.name === 'promisify',
|
|
84
|
+
severity: 'info',
|
|
85
|
+
replacement: 'fs/promises module',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'sync-in-async',
|
|
89
|
+
description: 'Avoid sync methods in async context (readFileSync, etc.)',
|
|
90
|
+
check: (node, context) => {
|
|
91
|
+
if (node.type !== 'CallExpression') return false;
|
|
92
|
+
const callee = node.callee;
|
|
93
|
+
if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {
|
|
94
|
+
const name = callee.property.name;
|
|
95
|
+
return name.endsWith('Sync') && context.inAsync;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
},
|
|
99
|
+
severity: 'warning',
|
|
100
|
+
replacement: 'async fs/promises methods',
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @typedef {Object} PatternMatch
|
|
106
|
+
* @property {string} pattern
|
|
107
|
+
* @property {string} description
|
|
108
|
+
* @property {string} file
|
|
109
|
+
* @property {number} line
|
|
110
|
+
* @property {string} severity
|
|
111
|
+
* @property {string} replacement
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @typedef {Object} RedundantDep
|
|
116
|
+
* @property {string} name
|
|
117
|
+
* @property {string} replacement
|
|
118
|
+
* @property {string} since
|
|
119
|
+
*/
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Find all JS files
|
|
123
|
+
* @param {string} dir
|
|
124
|
+
* @param {string} rootDir
|
|
125
|
+
* @returns {string[]}
|
|
126
|
+
*/
|
|
127
|
+
function findJSFiles(dir, rootDir = dir) {
|
|
128
|
+
if (dir === rootDir) parseGitignore(rootDir);
|
|
129
|
+
const files = [];
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
for (const entry of readdirSync(dir)) {
|
|
133
|
+
const fullPath = join(dir, entry);
|
|
134
|
+
const relativePath = relative(rootDir, fullPath);
|
|
135
|
+
const stat = statSync(fullPath);
|
|
136
|
+
|
|
137
|
+
if (stat.isDirectory()) {
|
|
138
|
+
if (!shouldExcludeDir(entry, relativePath)) {
|
|
139
|
+
files.push(...findJSFiles(fullPath, rootDir));
|
|
140
|
+
}
|
|
141
|
+
} else if (entry.endsWith('.js') && !entry.endsWith('.css.js') && !entry.endsWith('.tpl.js')) {
|
|
142
|
+
if (!shouldExcludeFile(entry, relativePath)) {
|
|
143
|
+
files.push(fullPath);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} catch (e) { }
|
|
148
|
+
|
|
149
|
+
return files;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Analyze file for outdated patterns
|
|
154
|
+
* @param {string} filePath
|
|
155
|
+
* @returns {PatternMatch[]}
|
|
156
|
+
*/
|
|
157
|
+
function analyzeFilePatterns(filePath, rootDir) {
|
|
158
|
+
const code = readFileSync(filePath, 'utf-8');
|
|
159
|
+
const relPath = relative(rootDir, filePath);
|
|
160
|
+
const matches = [];
|
|
161
|
+
|
|
162
|
+
let ast;
|
|
163
|
+
try {
|
|
164
|
+
ast = parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
|
|
165
|
+
} catch (e) {
|
|
166
|
+
return matches;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Track async context
|
|
170
|
+
const context = { inAsync: false };
|
|
171
|
+
|
|
172
|
+
walk.simple(ast, {
|
|
173
|
+
FunctionDeclaration(node) {
|
|
174
|
+
context.inAsync = node.async;
|
|
175
|
+
},
|
|
176
|
+
ArrowFunctionExpression(node) {
|
|
177
|
+
context.inAsync = node.async;
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Reset and check patterns
|
|
182
|
+
context.inAsync = false;
|
|
183
|
+
|
|
184
|
+
walk.ancestor(ast, {
|
|
185
|
+
'*'(node, ancestors) {
|
|
186
|
+
// Update async context
|
|
187
|
+
for (const anc of ancestors) {
|
|
188
|
+
if ((anc.type === 'FunctionDeclaration' || anc.type === 'ArrowFunctionExpression' ||
|
|
189
|
+
anc.type === 'FunctionExpression') && anc.async) {
|
|
190
|
+
context.inAsync = true;
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
for (const pattern of CODE_PATTERNS) {
|
|
196
|
+
if (pattern.check(node, context)) {
|
|
197
|
+
matches.push({
|
|
198
|
+
pattern: pattern.name,
|
|
199
|
+
description: pattern.description,
|
|
200
|
+
file: relPath,
|
|
201
|
+
line: node.loc?.start?.line || 0,
|
|
202
|
+
severity: pattern.severity,
|
|
203
|
+
replacement: pattern.replacement,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return matches;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Analyze package.json for redundant dependencies
|
|
215
|
+
* @param {string} dir
|
|
216
|
+
* @returns {RedundantDep[]}
|
|
217
|
+
*/
|
|
218
|
+
function analyzePackageJson(dir) {
|
|
219
|
+
const pkgPath = join(dir, 'package.json');
|
|
220
|
+
const redundant = [];
|
|
221
|
+
|
|
222
|
+
if (!existsSync(pkgPath)) return redundant;
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
226
|
+
const allDeps = {
|
|
227
|
+
...pkg.dependencies,
|
|
228
|
+
...pkg.devDependencies,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
for (const depName of Object.keys(allDeps)) {
|
|
232
|
+
if (REDUNDANT_DEPS[depName]) {
|
|
233
|
+
redundant.push({
|
|
234
|
+
name: depName,
|
|
235
|
+
...REDUNDANT_DEPS[depName],
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch (e) { }
|
|
240
|
+
|
|
241
|
+
return redundant;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get outdated patterns analysis
|
|
246
|
+
* @param {string} dir
|
|
247
|
+
* @param {Object} [options]
|
|
248
|
+
* @param {boolean} [options.codeOnly=false] - Only check code patterns
|
|
249
|
+
* @param {boolean} [options.depsOnly=false] - Only check dependencies
|
|
250
|
+
* @returns {Promise<{codePatterns: PatternMatch[], redundantDeps: RedundantDep[], stats: Object}>}
|
|
251
|
+
*/
|
|
252
|
+
export async function getOutdatedPatterns(dir, options = {}) {
|
|
253
|
+
const codeOnly = options.codeOnly || false;
|
|
254
|
+
const depsOnly = options.depsOnly || false;
|
|
255
|
+
const resolvedDir = resolve(dir);
|
|
256
|
+
|
|
257
|
+
let codePatterns = [];
|
|
258
|
+
let redundantDeps = [];
|
|
259
|
+
|
|
260
|
+
if (!depsOnly) {
|
|
261
|
+
const files = findJSFiles(dir);
|
|
262
|
+
for (const file of files) {
|
|
263
|
+
codePatterns.push(...analyzeFilePatterns(file, resolvedDir));
|
|
264
|
+
}
|
|
265
|
+
// Sort by severity
|
|
266
|
+
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
267
|
+
codePatterns.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!codeOnly) {
|
|
271
|
+
redundantDeps = analyzePackageJson(dir);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const stats = {
|
|
275
|
+
totalPatterns: codePatterns.length,
|
|
276
|
+
byPattern: {},
|
|
277
|
+
bySeverity: {
|
|
278
|
+
error: codePatterns.filter(p => p.severity === 'error').length,
|
|
279
|
+
warning: codePatterns.filter(p => p.severity === 'warning').length,
|
|
280
|
+
info: codePatterns.filter(p => p.severity === 'info').length,
|
|
281
|
+
},
|
|
282
|
+
redundantDeps: redundantDeps.length,
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// Group by pattern name
|
|
286
|
+
for (const p of codePatterns) {
|
|
287
|
+
stats.byPattern[p.pattern] = (stats.byPattern[p.pattern] || 0) + 1;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
codePatterns: codePatterns.slice(0, 50),
|
|
292
|
+
redundantDeps,
|
|
293
|
+
stats,
|
|
294
|
+
};
|
|
295
|
+
}
|