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