@stackguide/mcp-server 2.3.2 → 3.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/README.md +82 -16
- package/dist/config/healthWeights.d.ts +87 -0
- package/dist/config/healthWeights.d.ts.map +1 -0
- package/dist/config/healthWeights.js +238 -0
- package/dist/config/healthWeights.js.map +1 -0
- package/dist/config/types.d.ts +104 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js.map +1 -1
- package/dist/handlers/generate.d.ts +24 -0
- package/dist/handlers/generate.d.ts.map +1 -0
- package/dist/handlers/generate.js +845 -0
- package/dist/handlers/generate.js.map +1 -0
- package/dist/handlers/health.d.ts +19 -0
- package/dist/handlers/health.d.ts.map +1 -0
- package/dist/handlers/health.js +401 -0
- package/dist/handlers/health.js.map +1 -0
- package/dist/handlers/help.d.ts +1 -1
- package/dist/handlers/help.d.ts.map +1 -1
- package/dist/handlers/help.js +70 -10
- package/dist/handlers/help.js.map +1 -1
- package/dist/handlers/index.d.ts +2 -0
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/index.js +2 -0
- package/dist/handlers/index.js.map +1 -1
- package/dist/handlers/review.d.ts +12 -0
- package/dist/handlers/review.d.ts.map +1 -1
- package/dist/handlers/review.js +220 -21
- package/dist/handlers/review.js.map +1 -1
- package/dist/handlers/setup.d.ts +1 -0
- package/dist/handlers/setup.d.ts.map +1 -1
- package/dist/handlers/setup.js +102 -5
- package/dist/handlers/setup.js.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/services/analysisCache.d.ts +110 -0
- package/dist/services/analysisCache.d.ts.map +1 -0
- package/dist/services/analysisCache.js +233 -0
- package/dist/services/analysisCache.js.map +1 -0
- package/dist/services/codeAnalyzer.d.ts +73 -22
- package/dist/services/codeAnalyzer.d.ts.map +1 -1
- package/dist/services/codeAnalyzer.js +462 -44
- package/dist/services/codeAnalyzer.js.map +1 -1
- package/dist/services/conventionDetector.d.ts +40 -0
- package/dist/services/conventionDetector.d.ts.map +1 -0
- package/dist/services/conventionDetector.js +465 -0
- package/dist/services/conventionDetector.js.map +1 -0
- package/dist/services/cursorDirectory.d.ts +29 -2
- package/dist/services/cursorDirectory.d.ts.map +1 -1
- package/dist/services/cursorDirectory.js +260 -9
- package/dist/services/cursorDirectory.js.map +1 -1
- package/dist/tools/definitions.d.ts +125 -0
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +62 -1
- package/dist/tools/definitions.js.map +1 -1
- package/dist/utils/validation.d.ts +2 -2
- package/package.json +1 -1
|
@@ -1,309 +1,621 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Code Analyzer Service
|
|
3
|
-
*
|
|
2
|
+
* Code Analyzer Service - v3.0.0
|
|
3
|
+
* Unified rule pipeline that supports builtin, user, and project rules
|
|
4
4
|
*/
|
|
5
5
|
import { logger } from '../utils/logger.js';
|
|
6
|
-
//
|
|
7
|
-
|
|
6
|
+
// =============================================================================
|
|
7
|
+
// BUILTIN PATTERN RULES
|
|
8
|
+
// =============================================================================
|
|
9
|
+
const BUILTIN_PATTERN_RULES = [
|
|
8
10
|
// Security
|
|
9
11
|
{
|
|
10
12
|
id: 'SEC001',
|
|
13
|
+
type: 'pattern',
|
|
11
14
|
category: 'security',
|
|
12
15
|
pattern: /eval\s*\(/g,
|
|
13
16
|
severity: 'error',
|
|
14
17
|
message: 'Avoid using eval() - it can execute arbitrary code',
|
|
15
18
|
suggestion: 'Use JSON.parse() for JSON data or safer alternatives',
|
|
16
|
-
languages: ['javascript', 'typescript']
|
|
19
|
+
languages: ['javascript', 'typescript'],
|
|
20
|
+
enabled: true,
|
|
21
|
+
priority: 100,
|
|
22
|
+
source: 'builtin',
|
|
23
|
+
quickFix: (match) => ({
|
|
24
|
+
description: 'Replace eval() with JSON.parse()',
|
|
25
|
+
before: match,
|
|
26
|
+
after: 'JSON.parse('
|
|
27
|
+
})
|
|
17
28
|
},
|
|
18
29
|
{
|
|
19
30
|
id: 'SEC002',
|
|
31
|
+
type: 'pattern',
|
|
20
32
|
category: 'security',
|
|
21
33
|
pattern: /innerHTML\s*=/g,
|
|
22
34
|
severity: 'warning',
|
|
23
35
|
message: 'innerHTML can lead to XSS vulnerabilities',
|
|
24
36
|
suggestion: 'Use textContent or sanitize HTML input',
|
|
25
|
-
languages: ['javascript', 'typescript']
|
|
37
|
+
languages: ['javascript', 'typescript'],
|
|
38
|
+
enabled: true,
|
|
39
|
+
priority: 100,
|
|
40
|
+
source: 'builtin',
|
|
41
|
+
quickFix: (match) => ({
|
|
42
|
+
description: 'Replace innerHTML with textContent',
|
|
43
|
+
before: match,
|
|
44
|
+
after: 'textContent ='
|
|
45
|
+
})
|
|
26
46
|
},
|
|
27
47
|
{
|
|
28
48
|
id: 'SEC003',
|
|
49
|
+
type: 'pattern',
|
|
29
50
|
category: 'security',
|
|
30
51
|
pattern: /dangerouslySetInnerHTML/g,
|
|
31
52
|
severity: 'warning',
|
|
32
53
|
message: 'dangerouslySetInnerHTML can lead to XSS - ensure content is sanitized',
|
|
33
54
|
suggestion: 'Sanitize HTML with DOMPurify or similar library',
|
|
34
|
-
languages: ['javascript', 'typescript', 'jsx', 'tsx']
|
|
55
|
+
languages: ['javascript', 'typescript', 'jsx', 'tsx'],
|
|
56
|
+
enabled: true,
|
|
57
|
+
priority: 100,
|
|
58
|
+
source: 'builtin'
|
|
35
59
|
},
|
|
36
60
|
{
|
|
37
61
|
id: 'SEC004',
|
|
62
|
+
type: 'pattern',
|
|
38
63
|
category: 'security',
|
|
39
64
|
pattern: /password\s*[:=]\s*['"`][^'"`]+['"`]/gi,
|
|
40
65
|
severity: 'error',
|
|
41
66
|
message: 'Hardcoded password detected',
|
|
42
67
|
suggestion: 'Use environment variables for sensitive data',
|
|
68
|
+
enabled: true,
|
|
69
|
+
priority: 100,
|
|
70
|
+
source: 'builtin',
|
|
71
|
+
quickFix: (match) => ({
|
|
72
|
+
description: 'Replace hardcoded password with environment variable',
|
|
73
|
+
before: match,
|
|
74
|
+
after: 'password = process.env.PASSWORD'
|
|
75
|
+
})
|
|
43
76
|
},
|
|
44
77
|
{
|
|
45
78
|
id: 'SEC005',
|
|
79
|
+
type: 'pattern',
|
|
46
80
|
category: 'security',
|
|
47
81
|
pattern: /api[_-]?key\s*[:=]\s*['"`][a-zA-Z0-9]{20,}['"`]/gi,
|
|
48
82
|
severity: 'error',
|
|
49
83
|
message: 'Hardcoded API key detected',
|
|
50
84
|
suggestion: 'Use environment variables for API keys',
|
|
85
|
+
enabled: true,
|
|
86
|
+
priority: 100,
|
|
87
|
+
source: 'builtin',
|
|
88
|
+
quickFix: (match) => ({
|
|
89
|
+
description: 'Replace hardcoded API key with environment variable',
|
|
90
|
+
before: match,
|
|
91
|
+
after: 'apiKey = process.env.API_KEY'
|
|
92
|
+
})
|
|
51
93
|
},
|
|
52
94
|
{
|
|
53
95
|
id: 'SEC006',
|
|
96
|
+
type: 'pattern',
|
|
54
97
|
category: 'security',
|
|
55
98
|
pattern: /SELECT\s+\*\s+FROM\s+\w+\s+WHERE.*\+|SELECT\s+\*\s+FROM\s+\w+\s+WHERE.*\$\{/gi,
|
|
56
99
|
severity: 'error',
|
|
57
100
|
message: 'Potential SQL injection - string concatenation in query',
|
|
58
101
|
suggestion: 'Use parameterized queries or an ORM',
|
|
102
|
+
enabled: true,
|
|
103
|
+
priority: 100,
|
|
104
|
+
source: 'builtin'
|
|
59
105
|
},
|
|
60
106
|
{
|
|
61
107
|
id: 'SEC007',
|
|
108
|
+
type: 'pattern',
|
|
62
109
|
category: 'security',
|
|
63
110
|
pattern: /exec\s*\([^)]*\+|exec\s*\([^)]*\$\{/g,
|
|
64
111
|
severity: 'error',
|
|
65
112
|
message: 'Command injection risk - dynamic command execution',
|
|
66
113
|
suggestion: 'Validate and sanitize all user input',
|
|
67
|
-
languages: ['python']
|
|
114
|
+
languages: ['python'],
|
|
115
|
+
enabled: true,
|
|
116
|
+
priority: 100,
|
|
117
|
+
source: 'builtin'
|
|
68
118
|
},
|
|
69
119
|
{
|
|
70
120
|
id: 'SEC008',
|
|
121
|
+
type: 'pattern',
|
|
71
122
|
category: 'security',
|
|
72
123
|
pattern: /jwt\.decode\s*\([^)]*verify\s*=\s*False/gi,
|
|
73
124
|
severity: 'error',
|
|
74
125
|
message: 'JWT decoded without verification',
|
|
75
126
|
suggestion: 'Always verify JWT signatures',
|
|
76
|
-
languages: ['python']
|
|
127
|
+
languages: ['python'],
|
|
128
|
+
enabled: true,
|
|
129
|
+
priority: 100,
|
|
130
|
+
source: 'builtin'
|
|
77
131
|
},
|
|
78
132
|
// Performance
|
|
79
133
|
{
|
|
80
134
|
id: 'PERF001',
|
|
135
|
+
type: 'pattern',
|
|
81
136
|
category: 'performance',
|
|
82
137
|
pattern: /document\.querySelectorAll\([^)]+\)\.forEach/g,
|
|
83
138
|
severity: 'info',
|
|
84
139
|
message: 'Consider caching querySelectorAll result for multiple operations',
|
|
85
140
|
suggestion: 'Store the result in a variable before iterating',
|
|
86
|
-
languages: ['javascript', 'typescript']
|
|
141
|
+
languages: ['javascript', 'typescript'],
|
|
142
|
+
enabled: true,
|
|
143
|
+
priority: 50,
|
|
144
|
+
source: 'builtin'
|
|
87
145
|
},
|
|
88
146
|
{
|
|
89
147
|
id: 'PERF002',
|
|
148
|
+
type: 'pattern',
|
|
90
149
|
category: 'performance',
|
|
91
150
|
pattern: /JSON\.parse\(JSON\.stringify\(/g,
|
|
92
151
|
severity: 'warning',
|
|
93
152
|
message: 'Deep clone with JSON is slow for large objects',
|
|
94
153
|
suggestion: 'Use structuredClone() or a dedicated library like lodash.cloneDeep',
|
|
154
|
+
enabled: true,
|
|
155
|
+
priority: 50,
|
|
156
|
+
source: 'builtin'
|
|
95
157
|
},
|
|
96
158
|
{
|
|
97
159
|
id: 'PERF003',
|
|
160
|
+
type: 'pattern',
|
|
98
161
|
category: 'performance',
|
|
99
162
|
pattern: /useEffect\(\s*\(\)\s*=>\s*\{[^}]*\},\s*\[\s*\]\s*\)/gs,
|
|
100
163
|
severity: 'info',
|
|
101
164
|
message: 'Empty dependency array - effect runs only once',
|
|
102
165
|
suggestion: 'Verify this is intentional behavior',
|
|
103
|
-
languages: ['javascript', 'typescript', 'jsx', 'tsx']
|
|
166
|
+
languages: ['javascript', 'typescript', 'jsx', 'tsx'],
|
|
167
|
+
enabled: true,
|
|
168
|
+
priority: 50,
|
|
169
|
+
source: 'builtin'
|
|
104
170
|
},
|
|
105
171
|
{
|
|
106
172
|
id: 'PERF004',
|
|
173
|
+
type: 'pattern',
|
|
107
174
|
category: 'performance',
|
|
108
175
|
pattern: /await\s+\w+\([^)]*\)\s*;\s*await\s+\w+\([^)]*\)\s*;/g,
|
|
109
176
|
severity: 'suggestion',
|
|
110
177
|
message: 'Sequential awaits might be parallelizable',
|
|
111
178
|
suggestion: 'Consider Promise.all() for independent async operations',
|
|
179
|
+
enabled: true,
|
|
180
|
+
priority: 50,
|
|
181
|
+
source: 'builtin'
|
|
112
182
|
},
|
|
113
183
|
{
|
|
114
184
|
id: 'PERF005',
|
|
185
|
+
type: 'pattern',
|
|
115
186
|
category: 'performance',
|
|
116
187
|
pattern: /\.map\([^)]+\)\.filter\([^)]+\)/g,
|
|
117
188
|
severity: 'suggestion',
|
|
118
189
|
message: 'Chained map().filter() iterates twice',
|
|
119
190
|
suggestion: 'Consider using reduce() for single-pass transformation',
|
|
191
|
+
enabled: true,
|
|
192
|
+
priority: 50,
|
|
193
|
+
source: 'builtin'
|
|
120
194
|
},
|
|
121
195
|
// Coding Standards
|
|
122
196
|
{
|
|
123
197
|
id: 'STD001',
|
|
198
|
+
type: 'pattern',
|
|
124
199
|
category: 'coding-standards',
|
|
125
200
|
pattern: /console\.(log|debug|info)\s*\(/g,
|
|
126
201
|
severity: 'warning',
|
|
127
202
|
message: 'Console statement found - remove for production',
|
|
128
203
|
suggestion: 'Use a proper logging library or remove before deployment',
|
|
204
|
+
enabled: true,
|
|
205
|
+
priority: 50,
|
|
206
|
+
source: 'builtin',
|
|
207
|
+
quickFix: (match) => ({
|
|
208
|
+
description: 'Remove console statement',
|
|
209
|
+
before: match,
|
|
210
|
+
after: '// ' + match + ' // TODO: Remove or replace with logger'
|
|
211
|
+
})
|
|
129
212
|
},
|
|
130
213
|
{
|
|
131
214
|
id: 'STD002',
|
|
215
|
+
type: 'pattern',
|
|
132
216
|
category: 'coding-standards',
|
|
133
217
|
pattern: /\/\/\s*TODO:|\/\/\s*FIXME:|\/\/\s*HACK:/gi,
|
|
134
218
|
severity: 'info',
|
|
135
219
|
message: 'TODO/FIXME comment found - address before release',
|
|
220
|
+
enabled: true,
|
|
221
|
+
priority: 25,
|
|
222
|
+
source: 'builtin'
|
|
136
223
|
},
|
|
137
224
|
{
|
|
138
225
|
id: 'STD003',
|
|
226
|
+
type: 'pattern',
|
|
139
227
|
category: 'coding-standards',
|
|
140
228
|
pattern: /debugger\s*;/g,
|
|
141
229
|
severity: 'error',
|
|
142
230
|
message: 'Debugger statement found - remove before deployment',
|
|
143
|
-
languages: ['javascript', 'typescript']
|
|
231
|
+
languages: ['javascript', 'typescript'],
|
|
232
|
+
enabled: true,
|
|
233
|
+
priority: 100,
|
|
234
|
+
source: 'builtin',
|
|
235
|
+
quickFix: () => ({
|
|
236
|
+
description: 'Remove debugger statement',
|
|
237
|
+
before: 'debugger;',
|
|
238
|
+
after: ''
|
|
239
|
+
})
|
|
144
240
|
},
|
|
145
241
|
{
|
|
146
242
|
id: 'STD004',
|
|
243
|
+
type: 'pattern',
|
|
147
244
|
category: 'coding-standards',
|
|
148
|
-
pattern: /var\s
|
|
245
|
+
pattern: /var\s+(\w+)\s*=/g,
|
|
149
246
|
severity: 'warning',
|
|
150
247
|
message: 'Prefer const or let over var',
|
|
151
248
|
suggestion: 'Use const for immutable values, let for mutable',
|
|
152
|
-
languages: ['javascript', 'typescript']
|
|
249
|
+
languages: ['javascript', 'typescript'],
|
|
250
|
+
enabled: true,
|
|
251
|
+
priority: 50,
|
|
252
|
+
source: 'builtin',
|
|
253
|
+
quickFix: (match) => ({
|
|
254
|
+
description: 'Replace var with const',
|
|
255
|
+
before: match,
|
|
256
|
+
after: match.replace('var ', 'const ')
|
|
257
|
+
})
|
|
153
258
|
},
|
|
154
259
|
{
|
|
155
260
|
id: 'STD005',
|
|
261
|
+
type: 'pattern',
|
|
156
262
|
category: 'coding-standards',
|
|
157
263
|
pattern: /==(?!=)/g,
|
|
158
264
|
severity: 'warning',
|
|
159
265
|
message: 'Use strict equality (===) instead of loose equality (==)',
|
|
160
|
-
languages: ['javascript', 'typescript']
|
|
266
|
+
languages: ['javascript', 'typescript'],
|
|
267
|
+
enabled: true,
|
|
268
|
+
priority: 50,
|
|
269
|
+
source: 'builtin',
|
|
270
|
+
quickFix: () => ({
|
|
271
|
+
description: 'Replace == with ===',
|
|
272
|
+
before: '==',
|
|
273
|
+
after: '==='
|
|
274
|
+
})
|
|
161
275
|
},
|
|
162
276
|
{
|
|
163
277
|
id: 'STD006',
|
|
278
|
+
type: 'pattern',
|
|
164
279
|
category: 'coding-standards',
|
|
165
280
|
pattern: /!=(?!=)/g,
|
|
166
281
|
severity: 'warning',
|
|
167
282
|
message: 'Use strict inequality (!==) instead of loose inequality (!=)',
|
|
168
|
-
languages: ['javascript', 'typescript']
|
|
283
|
+
languages: ['javascript', 'typescript'],
|
|
284
|
+
enabled: true,
|
|
285
|
+
priority: 50,
|
|
286
|
+
source: 'builtin',
|
|
287
|
+
quickFix: () => ({
|
|
288
|
+
description: 'Replace != with !==',
|
|
289
|
+
before: '!=',
|
|
290
|
+
after: '!=='
|
|
291
|
+
})
|
|
169
292
|
},
|
|
170
293
|
{
|
|
171
294
|
id: 'STD007',
|
|
295
|
+
type: 'pattern',
|
|
172
296
|
category: 'coding-standards',
|
|
173
297
|
pattern: /function\s+\w+\s*\([^)]*\)\s*\{[\s\S]{500,}\}/g,
|
|
174
298
|
severity: 'warning',
|
|
175
299
|
message: 'Function appears too long (>500 chars)',
|
|
176
300
|
suggestion: 'Consider breaking into smaller functions',
|
|
301
|
+
enabled: true,
|
|
302
|
+
priority: 25,
|
|
303
|
+
source: 'builtin'
|
|
177
304
|
},
|
|
178
305
|
{
|
|
179
306
|
id: 'STD008',
|
|
307
|
+
type: 'pattern',
|
|
180
308
|
category: 'coding-standards',
|
|
181
309
|
pattern: /catch\s*\(\s*\w*\s*\)\s*\{\s*\}/g,
|
|
182
310
|
severity: 'warning',
|
|
183
311
|
message: 'Empty catch block - errors are silently ignored',
|
|
184
312
|
suggestion: 'Log the error or handle it appropriately',
|
|
313
|
+
enabled: true,
|
|
314
|
+
priority: 75,
|
|
315
|
+
source: 'builtin'
|
|
185
316
|
},
|
|
186
317
|
// Architecture
|
|
187
318
|
{
|
|
188
319
|
id: 'ARCH001',
|
|
320
|
+
type: 'pattern',
|
|
189
321
|
category: 'architecture',
|
|
190
322
|
pattern: /import.*from\s+['"]\.\.\/\.\.\/\.\.\/\.\./g,
|
|
191
323
|
severity: 'warning',
|
|
192
324
|
message: 'Deep relative import - consider path aliases',
|
|
193
325
|
suggestion: 'Configure path aliases in tsconfig.json',
|
|
194
|
-
languages: ['javascript', 'typescript']
|
|
326
|
+
languages: ['javascript', 'typescript'],
|
|
327
|
+
enabled: true,
|
|
328
|
+
priority: 25,
|
|
329
|
+
source: 'builtin'
|
|
195
330
|
},
|
|
196
331
|
{
|
|
197
332
|
id: 'ARCH002',
|
|
333
|
+
type: 'pattern',
|
|
198
334
|
category: 'architecture',
|
|
199
335
|
pattern: /class\s+\w+\s*\{[\s\S]*constructor\s*\([^)]{200,}\)/g,
|
|
200
336
|
severity: 'warning',
|
|
201
337
|
message: 'Constructor has many parameters - consider dependency injection',
|
|
202
338
|
suggestion: 'Use a DI container or builder pattern',
|
|
339
|
+
enabled: true,
|
|
340
|
+
priority: 25,
|
|
341
|
+
source: 'builtin'
|
|
203
342
|
},
|
|
204
343
|
{
|
|
205
344
|
id: 'ARCH003',
|
|
345
|
+
type: 'pattern',
|
|
206
346
|
category: 'architecture',
|
|
207
347
|
pattern: /new\s+(Date|Math\.random)\s*\(\)/g,
|
|
208
348
|
severity: 'suggestion',
|
|
209
349
|
message: 'Direct instantiation makes testing harder',
|
|
210
350
|
suggestion: 'Inject dependencies for better testability',
|
|
351
|
+
enabled: true,
|
|
352
|
+
priority: 25,
|
|
353
|
+
source: 'builtin'
|
|
211
354
|
},
|
|
212
355
|
// Python specific
|
|
213
356
|
{
|
|
214
357
|
id: 'PY001',
|
|
215
|
-
|
|
358
|
+
type: 'pattern',
|
|
359
|
+
category: 'python',
|
|
216
360
|
pattern: /except\s*:/g,
|
|
217
361
|
severity: 'warning',
|
|
218
362
|
message: 'Bare except catches all exceptions including SystemExit',
|
|
219
363
|
suggestion: 'Specify exception type: except Exception:',
|
|
220
|
-
languages: ['python']
|
|
364
|
+
languages: ['python'],
|
|
365
|
+
enabled: true,
|
|
366
|
+
priority: 75,
|
|
367
|
+
source: 'builtin'
|
|
221
368
|
},
|
|
222
369
|
{
|
|
223
370
|
id: 'PY002',
|
|
224
|
-
|
|
371
|
+
type: 'pattern',
|
|
372
|
+
category: 'python',
|
|
225
373
|
pattern: /print\s*\(/g,
|
|
226
374
|
severity: 'info',
|
|
227
375
|
message: 'Print statement found - consider using logging',
|
|
228
376
|
suggestion: 'Use the logging module for production code',
|
|
229
|
-
languages: ['python']
|
|
377
|
+
languages: ['python'],
|
|
378
|
+
enabled: true,
|
|
379
|
+
priority: 25,
|
|
380
|
+
source: 'builtin'
|
|
230
381
|
},
|
|
231
382
|
{
|
|
232
383
|
id: 'PY003',
|
|
384
|
+
type: 'pattern',
|
|
233
385
|
category: 'security',
|
|
234
386
|
pattern: /pickle\.load|pickle\.loads/g,
|
|
235
387
|
severity: 'error',
|
|
236
388
|
message: 'Pickle is unsafe for untrusted data',
|
|
237
389
|
suggestion: 'Use JSON or a safe serialization format',
|
|
238
|
-
languages: ['python']
|
|
390
|
+
languages: ['python'],
|
|
391
|
+
enabled: true,
|
|
392
|
+
priority: 100,
|
|
393
|
+
source: 'builtin'
|
|
239
394
|
},
|
|
240
395
|
{
|
|
241
396
|
id: 'PY004',
|
|
242
|
-
|
|
397
|
+
type: 'pattern',
|
|
398
|
+
category: 'python',
|
|
243
399
|
pattern: /from\s+\w+\s+import\s+\*/g,
|
|
244
400
|
severity: 'warning',
|
|
245
401
|
message: 'Wildcard imports make code harder to understand',
|
|
246
402
|
suggestion: 'Import specific names explicitly',
|
|
247
|
-
languages: ['python']
|
|
403
|
+
languages: ['python'],
|
|
404
|
+
enabled: true,
|
|
405
|
+
priority: 50,
|
|
406
|
+
source: 'builtin'
|
|
248
407
|
},
|
|
249
408
|
// Go specific
|
|
250
409
|
{
|
|
251
410
|
id: 'GO001',
|
|
252
|
-
|
|
411
|
+
type: 'pattern',
|
|
412
|
+
category: 'go',
|
|
253
413
|
pattern: /fmt\.Print|fmt\.Println/g,
|
|
254
414
|
severity: 'info',
|
|
255
415
|
message: 'fmt.Print found - consider using structured logging',
|
|
256
416
|
suggestion: 'Use log/slog or a logging library',
|
|
257
|
-
languages: ['go']
|
|
417
|
+
languages: ['go'],
|
|
418
|
+
enabled: true,
|
|
419
|
+
priority: 25,
|
|
420
|
+
source: 'builtin'
|
|
258
421
|
},
|
|
259
422
|
{
|
|
260
423
|
id: 'GO002',
|
|
261
|
-
|
|
424
|
+
type: 'pattern',
|
|
425
|
+
category: 'go',
|
|
262
426
|
pattern: /panic\s*\(/g,
|
|
263
427
|
severity: 'warning',
|
|
264
428
|
message: 'Panic should be used sparingly',
|
|
265
429
|
suggestion: 'Return errors instead of panicking',
|
|
266
|
-
languages: ['go']
|
|
430
|
+
languages: ['go'],
|
|
431
|
+
enabled: true,
|
|
432
|
+
priority: 75,
|
|
433
|
+
source: 'builtin'
|
|
267
434
|
},
|
|
268
435
|
{
|
|
269
436
|
id: 'GO003',
|
|
270
|
-
|
|
437
|
+
type: 'pattern',
|
|
438
|
+
category: 'go',
|
|
271
439
|
pattern: /if\s+err\s*!=\s*nil\s*\{\s*return\s+nil\s*,?\s*err\s*\}/g,
|
|
272
440
|
severity: 'suggestion',
|
|
273
441
|
message: 'Consider wrapping errors with context',
|
|
274
442
|
suggestion: 'Use fmt.Errorf("context: %w", err)',
|
|
275
|
-
languages: ['go']
|
|
443
|
+
languages: ['go'],
|
|
444
|
+
enabled: true,
|
|
445
|
+
priority: 25,
|
|
446
|
+
source: 'builtin'
|
|
276
447
|
},
|
|
277
448
|
// Rust specific
|
|
278
449
|
{
|
|
279
450
|
id: 'RS001',
|
|
280
|
-
|
|
451
|
+
type: 'pattern',
|
|
452
|
+
category: 'rust',
|
|
281
453
|
pattern: /\.unwrap\(\)/g,
|
|
282
454
|
severity: 'warning',
|
|
283
455
|
message: 'unwrap() can panic on None/Err',
|
|
284
456
|
suggestion: 'Use ? operator or match/if let for proper error handling',
|
|
285
|
-
languages: ['rust']
|
|
457
|
+
languages: ['rust'],
|
|
458
|
+
enabled: true,
|
|
459
|
+
priority: 75,
|
|
460
|
+
source: 'builtin'
|
|
286
461
|
},
|
|
287
462
|
{
|
|
288
463
|
id: 'RS002',
|
|
289
|
-
|
|
464
|
+
type: 'pattern',
|
|
465
|
+
category: 'rust',
|
|
290
466
|
pattern: /\.expect\("[^"]*"\)/g,
|
|
291
467
|
severity: 'info',
|
|
292
468
|
message: 'expect() can panic - ensure this is intentional',
|
|
293
469
|
suggestion: 'Use ? operator for propagating errors',
|
|
294
|
-
languages: ['rust']
|
|
470
|
+
languages: ['rust'],
|
|
471
|
+
enabled: true,
|
|
472
|
+
priority: 50,
|
|
473
|
+
source: 'builtin'
|
|
295
474
|
},
|
|
296
475
|
{
|
|
297
476
|
id: 'RS003',
|
|
298
|
-
|
|
477
|
+
type: 'pattern',
|
|
478
|
+
category: 'rust',
|
|
299
479
|
pattern: /unsafe\s*\{/g,
|
|
300
480
|
severity: 'warning',
|
|
301
481
|
message: 'Unsafe block found - ensure safety invariants are documented',
|
|
302
482
|
suggestion: 'Add SAFETY comment explaining why this is safe',
|
|
303
|
-
languages: ['rust']
|
|
483
|
+
languages: ['rust'],
|
|
484
|
+
enabled: true,
|
|
485
|
+
priority: 75,
|
|
486
|
+
source: 'builtin'
|
|
304
487
|
}
|
|
305
488
|
];
|
|
306
|
-
|
|
489
|
+
// =============================================================================
|
|
490
|
+
// RULE REGISTRY - Unified Management
|
|
491
|
+
// =============================================================================
|
|
492
|
+
/**
|
|
493
|
+
* In-memory registry of all active rules
|
|
494
|
+
*/
|
|
495
|
+
class RuleRegistry {
|
|
496
|
+
builtinRules = [...BUILTIN_PATTERN_RULES];
|
|
497
|
+
userRules = [];
|
|
498
|
+
projectRules = [];
|
|
499
|
+
/**
|
|
500
|
+
* Get all builtin rules
|
|
501
|
+
*/
|
|
502
|
+
getBuiltinRules() {
|
|
503
|
+
return this.builtinRules.filter(r => r.enabled);
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Get all user-defined rules
|
|
507
|
+
*/
|
|
508
|
+
getUserRules() {
|
|
509
|
+
return this.userRules.filter(r => r.enabled);
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Get all project-specific rules
|
|
513
|
+
*/
|
|
514
|
+
getProjectRules() {
|
|
515
|
+
return this.projectRules.filter(r => r.enabled);
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Get all active rules, sorted by priority (highest first)
|
|
519
|
+
*/
|
|
520
|
+
getAllRules() {
|
|
521
|
+
const all = [
|
|
522
|
+
...this.builtinRules,
|
|
523
|
+
...this.userRules,
|
|
524
|
+
...this.projectRules
|
|
525
|
+
].filter(r => r.enabled);
|
|
526
|
+
// Sort by priority (higher priority = runs first)
|
|
527
|
+
return all.sort((a, b) => b.priority - a.priority);
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Register a user-defined pattern rule
|
|
531
|
+
*/
|
|
532
|
+
registerUserRule(rule) {
|
|
533
|
+
const fullRule = {
|
|
534
|
+
...rule,
|
|
535
|
+
type: 'pattern',
|
|
536
|
+
source: 'user'
|
|
537
|
+
};
|
|
538
|
+
// Remove existing rule with same ID
|
|
539
|
+
this.userRules = this.userRules.filter(r => r.id !== rule.id);
|
|
540
|
+
this.userRules.push(fullRule);
|
|
541
|
+
logger.info('Registered user rule', { ruleId: rule.id });
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Register a project-specific pattern rule
|
|
545
|
+
*/
|
|
546
|
+
registerProjectRule(rule) {
|
|
547
|
+
const fullRule = {
|
|
548
|
+
...rule,
|
|
549
|
+
type: 'pattern',
|
|
550
|
+
source: 'project'
|
|
551
|
+
};
|
|
552
|
+
// Remove existing rule with same ID
|
|
553
|
+
this.projectRules = this.projectRules.filter(r => r.id !== rule.id);
|
|
554
|
+
this.projectRules.push(fullRule);
|
|
555
|
+
logger.info('Registered project rule', { ruleId: rule.id });
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Register multiple project rules at once
|
|
559
|
+
*/
|
|
560
|
+
registerProjectRules(rules) {
|
|
561
|
+
for (const rule of rules) {
|
|
562
|
+
this.registerProjectRule(rule);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Clear all user rules
|
|
567
|
+
*/
|
|
568
|
+
clearUserRules() {
|
|
569
|
+
this.userRules = [];
|
|
570
|
+
logger.info('Cleared user rules');
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Clear all project rules
|
|
574
|
+
*/
|
|
575
|
+
clearProjectRules() {
|
|
576
|
+
this.projectRules = [];
|
|
577
|
+
logger.info('Cleared project rules');
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Disable a builtin rule by ID
|
|
581
|
+
*/
|
|
582
|
+
disableBuiltinRule(ruleId) {
|
|
583
|
+
const rule = this.builtinRules.find(r => r.id === ruleId);
|
|
584
|
+
if (rule) {
|
|
585
|
+
rule.enabled = false;
|
|
586
|
+
logger.info('Disabled builtin rule', { ruleId });
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Enable a builtin rule by ID
|
|
593
|
+
*/
|
|
594
|
+
enableBuiltinRule(ruleId) {
|
|
595
|
+
const rule = this.builtinRules.find(r => r.id === ruleId);
|
|
596
|
+
if (rule) {
|
|
597
|
+
rule.enabled = true;
|
|
598
|
+
logger.info('Enabled builtin rule', { ruleId });
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Get statistics about registered rules
|
|
605
|
+
*/
|
|
606
|
+
getStats() {
|
|
607
|
+
const builtin = this.builtinRules.filter(r => r.enabled).length;
|
|
608
|
+
const user = this.userRules.filter(r => r.enabled).length;
|
|
609
|
+
const project = this.projectRules.filter(r => r.enabled).length;
|
|
610
|
+
return { builtin, user, project, total: builtin + user + project };
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// Global rule registry instance
|
|
614
|
+
export const ruleRegistry = new RuleRegistry();
|
|
615
|
+
// =============================================================================
|
|
616
|
+
// ANALYSIS FUNCTIONS
|
|
617
|
+
// =============================================================================
|
|
618
|
+
function detectLanguage(file, _content) {
|
|
307
619
|
const ext = file.split('.').pop()?.toLowerCase() || '';
|
|
308
620
|
const langMap = {
|
|
309
621
|
'ts': 'typescript',
|
|
@@ -325,34 +637,56 @@ function detectLanguage(file, content) {
|
|
|
325
637
|
function getLineNumber(content, index) {
|
|
326
638
|
return content.substring(0, index).split('\n').length;
|
|
327
639
|
}
|
|
640
|
+
/**
|
|
641
|
+
* Analyze code using the unified rule pipeline
|
|
642
|
+
*/
|
|
328
643
|
export function analyzeCode(file, content, focus = 'all') {
|
|
329
644
|
const language = detectLanguage(file, content);
|
|
330
645
|
const issues = [];
|
|
331
646
|
logger.debug('Analyzing code', { file, language, focus, contentLength: content.length });
|
|
647
|
+
// Get all active rules from registry
|
|
648
|
+
const allRules = ruleRegistry.getAllRules();
|
|
649
|
+
// Track rules applied by source
|
|
650
|
+
const rulesApplied = { builtin: 0, user: 0, project: 0 };
|
|
332
651
|
// Filter rules by focus and language
|
|
333
|
-
const applicableRules =
|
|
652
|
+
const applicableRules = allRules.filter(rule => {
|
|
334
653
|
if (focus !== 'all' && rule.category !== focus)
|
|
335
654
|
return false;
|
|
336
655
|
if (rule.languages && !rule.languages.includes(language))
|
|
337
656
|
return false;
|
|
338
657
|
return true;
|
|
339
658
|
});
|
|
659
|
+
logger.debug('Applying rules', {
|
|
660
|
+
totalRules: allRules.length,
|
|
661
|
+
applicableRules: applicableRules.length,
|
|
662
|
+
focus,
|
|
663
|
+
language
|
|
664
|
+
});
|
|
340
665
|
// Apply pattern rules
|
|
341
666
|
for (const rule of applicableRules) {
|
|
667
|
+
if (rule.type !== 'pattern')
|
|
668
|
+
continue; // Skip non-pattern rules for now
|
|
342
669
|
let match;
|
|
343
670
|
// Reset regex state
|
|
344
671
|
rule.pattern.lastIndex = 0;
|
|
345
672
|
while ((match = rule.pattern.exec(content)) !== null) {
|
|
346
673
|
const line = getLineNumber(content, match.index);
|
|
674
|
+
const matchedText = match[0];
|
|
675
|
+
// Generate quick fix if available
|
|
676
|
+
const quickFix = rule.quickFix ? rule.quickFix(matchedText) : undefined;
|
|
347
677
|
issues.push({
|
|
348
678
|
severity: rule.severity,
|
|
349
679
|
rule: rule.id,
|
|
350
680
|
category: rule.category,
|
|
351
681
|
message: rule.message,
|
|
352
682
|
line,
|
|
353
|
-
code:
|
|
354
|
-
suggestion: rule.suggestion
|
|
683
|
+
code: matchedText.substring(0, 100),
|
|
684
|
+
suggestion: rule.suggestion,
|
|
685
|
+
quickFix,
|
|
686
|
+
source: rule.source
|
|
355
687
|
});
|
|
688
|
+
// Track which source contributed
|
|
689
|
+
rulesApplied[rule.source]++;
|
|
356
690
|
// Prevent infinite loop on zero-length matches
|
|
357
691
|
if (match.index === rule.pattern.lastIndex) {
|
|
358
692
|
rule.pattern.lastIndex++;
|
|
@@ -381,13 +715,26 @@ export function analyzeCode(file, content, focus = 'all') {
|
|
|
381
715
|
score -= summary.info * 1;
|
|
382
716
|
score -= summary.suggestions * 0.5;
|
|
383
717
|
score = Math.max(0, Math.min(100, score));
|
|
384
|
-
|
|
718
|
+
// Collect unique quick fixes
|
|
719
|
+
const quickFixes = issues
|
|
720
|
+
.filter(i => i.quickFix)
|
|
721
|
+
.map(i => i.quickFix)
|
|
722
|
+
.filter((fix, idx, arr) => arr.findIndex(f => f.before === fix.before) === idx);
|
|
723
|
+
logger.info('Analysis complete', {
|
|
724
|
+
file,
|
|
725
|
+
issuesFound: issues.length,
|
|
726
|
+
score,
|
|
727
|
+
quickFixesAvailable: quickFixes.length,
|
|
728
|
+
rulesApplied
|
|
729
|
+
});
|
|
385
730
|
return {
|
|
386
731
|
file,
|
|
387
732
|
language,
|
|
388
733
|
issues,
|
|
389
734
|
score: Math.round(score),
|
|
390
|
-
summary
|
|
735
|
+
summary,
|
|
736
|
+
quickFixes: quickFixes.length > 0 ? quickFixes : undefined,
|
|
737
|
+
rulesApplied
|
|
391
738
|
};
|
|
392
739
|
}
|
|
393
740
|
export function analyzeMultipleFiles(files, focus = 'all') {
|
|
@@ -402,13 +749,19 @@ export function analyzeMultipleFiles(files, focus = 'all') {
|
|
|
402
749
|
info: results.reduce((sum, r) => sum + r.summary.info, 0),
|
|
403
750
|
suggestions: results.reduce((sum, r) => sum + r.summary.suggestions, 0)
|
|
404
751
|
};
|
|
752
|
+
const rulesApplied = {
|
|
753
|
+
builtin: results.reduce((sum, r) => sum + r.rulesApplied.builtin, 0),
|
|
754
|
+
user: results.reduce((sum, r) => sum + r.rulesApplied.user, 0),
|
|
755
|
+
project: results.reduce((sum, r) => sum + r.rulesApplied.project, 0)
|
|
756
|
+
};
|
|
405
757
|
return {
|
|
406
758
|
files: results,
|
|
407
759
|
overall: {
|
|
408
760
|
totalFiles: results.length,
|
|
409
761
|
totalIssues,
|
|
410
762
|
averageScore,
|
|
411
|
-
summary
|
|
763
|
+
summary,
|
|
764
|
+
rulesApplied
|
|
412
765
|
}
|
|
413
766
|
};
|
|
414
767
|
}
|
|
@@ -416,6 +769,7 @@ export function formatAnalysisReport(result) {
|
|
|
416
769
|
const lines = [];
|
|
417
770
|
lines.push(`## Code Review: ${result.file}`);
|
|
418
771
|
lines.push(`**Language:** ${result.language} | **Score:** ${result.score}/100`);
|
|
772
|
+
lines.push(`**Rules Applied:** ${result.rulesApplied.builtin} builtin, ${result.rulesApplied.user} user, ${result.rulesApplied.project} project`);
|
|
419
773
|
lines.push('');
|
|
420
774
|
if (result.issues.length === 0) {
|
|
421
775
|
lines.push('✅ No issues found!');
|
|
@@ -432,16 +786,80 @@ export function formatAnalysisReport(result) {
|
|
|
432
786
|
const icon = issue.severity === 'error' ? '🔴' :
|
|
433
787
|
issue.severity === 'warning' ? '🟡' :
|
|
434
788
|
issue.severity === 'info' ? '🔵' : '💡';
|
|
435
|
-
|
|
789
|
+
const sourceTag = issue.source !== 'builtin' ? ` [${issue.source}]` : '';
|
|
790
|
+
lines.push(`#### ${icon} [${issue.rule}]${sourceTag} ${issue.message}`);
|
|
436
791
|
if (issue.line)
|
|
437
792
|
lines.push(`- Line: ${issue.line}`);
|
|
438
793
|
if (issue.code)
|
|
439
794
|
lines.push(`- Code: \`${issue.code}\``);
|
|
440
795
|
if (issue.suggestion)
|
|
441
796
|
lines.push(`- 💡 ${issue.suggestion}`);
|
|
797
|
+
if (issue.quickFix) {
|
|
798
|
+
lines.push(`- 🔧 **Quick Fix:** ${issue.quickFix.description}`);
|
|
799
|
+
lines.push(` - Replace: \`${issue.quickFix.before}\``);
|
|
800
|
+
lines.push(` - With: \`${issue.quickFix.after}\``);
|
|
801
|
+
}
|
|
802
|
+
lines.push('');
|
|
803
|
+
}
|
|
804
|
+
// Add quick fixes summary at the end
|
|
805
|
+
if (result.quickFixes && result.quickFixes.length > 0) {
|
|
806
|
+
lines.push('### 🔧 Available Quick Fixes');
|
|
807
|
+
lines.push('');
|
|
808
|
+
for (const fix of result.quickFixes) {
|
|
809
|
+
lines.push(`- **${fix.description}**`);
|
|
810
|
+
lines.push(` \`${fix.before}\` → \`${fix.after}\``);
|
|
811
|
+
}
|
|
442
812
|
lines.push('');
|
|
443
813
|
}
|
|
444
814
|
}
|
|
445
815
|
return lines.join('\n');
|
|
446
816
|
}
|
|
817
|
+
// =============================================================================
|
|
818
|
+
// HELPER: Convert user rules from RuleManager format to PatternRule
|
|
819
|
+
// =============================================================================
|
|
820
|
+
/**
|
|
821
|
+
* Parse a user-defined rule content to extract pattern rules
|
|
822
|
+
* This bridges the gap between RuleManager's documentation rules and analysis rules
|
|
823
|
+
*/
|
|
824
|
+
export function parseUserRuleToPatternRule(ruleId, content, category) {
|
|
825
|
+
// Look for patterns defined in markdown code blocks with special syntax
|
|
826
|
+
// Example: ```pattern:error
|
|
827
|
+
// /console\.log/g
|
|
828
|
+
// ```
|
|
829
|
+
const patternMatch = content.match(/```pattern:(error|warning|info|suggestion)\s*\n(\/[^/]+\/[gimsu]*)\s*\n```/);
|
|
830
|
+
if (!patternMatch) {
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
const severity = patternMatch[1];
|
|
834
|
+
const patternStr = patternMatch[2];
|
|
835
|
+
try {
|
|
836
|
+
// Parse the regex string
|
|
837
|
+
const regexMatch = patternStr.match(/^\/(.+)\/([gimsu]*)$/);
|
|
838
|
+
if (!regexMatch)
|
|
839
|
+
return null;
|
|
840
|
+
const pattern = new RegExp(regexMatch[1], regexMatch[2] || 'g');
|
|
841
|
+
// Extract message from the rule content
|
|
842
|
+
const messageMatch = content.match(/##?\s*(?:Rule|Message):\s*(.+)/i);
|
|
843
|
+
const message = messageMatch?.[1] || `Custom rule: ${ruleId}`;
|
|
844
|
+
// Extract suggestion if present
|
|
845
|
+
const suggestionMatch = content.match(/##?\s*Suggestion:\s*(.+)/i);
|
|
846
|
+
const suggestion = suggestionMatch?.[1];
|
|
847
|
+
return {
|
|
848
|
+
id: ruleId,
|
|
849
|
+
type: 'pattern',
|
|
850
|
+
category: category,
|
|
851
|
+
pattern,
|
|
852
|
+
severity,
|
|
853
|
+
message,
|
|
854
|
+
suggestion,
|
|
855
|
+
enabled: true,
|
|
856
|
+
priority: 150, // User rules have higher priority than builtin
|
|
857
|
+
source: 'user'
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
catch {
|
|
861
|
+
logger.warn('Failed to parse pattern rule', { ruleId });
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
447
865
|
//# sourceMappingURL=codeAnalyzer.js.map
|