@sun-asterisk/sunlint 1.2.2 → 1.3.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 (64) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/CONTRIBUTING.md +533 -70
  3. package/README.md +16 -2
  4. package/config/engines/engines-enhanced.json +86 -0
  5. package/config/engines/semantic-config.json +114 -0
  6. package/config/eslint-rule-mapping.json +50 -38
  7. package/config/rules/enhanced-rules-registry.json +2503 -0
  8. package/config/rules/rules-registry-generated.json +785 -837
  9. package/core/adapters/sunlint-rule-adapter.js +25 -30
  10. package/core/analysis-orchestrator.js +42 -2
  11. package/core/categories.js +52 -0
  12. package/core/category-constants.js +39 -0
  13. package/core/cli-action-handler.js +32 -5
  14. package/core/config-manager.js +111 -0
  15. package/core/config-merger.js +61 -0
  16. package/core/constants/categories.js +168 -0
  17. package/core/constants/defaults.js +165 -0
  18. package/core/constants/engines.js +185 -0
  19. package/core/constants/index.js +30 -0
  20. package/core/constants/rules.js +215 -0
  21. package/core/file-targeting-service.js +128 -7
  22. package/core/interfaces/rule-plugin.interface.js +207 -0
  23. package/core/plugin-manager.js +448 -0
  24. package/core/rule-selection-service.js +42 -15
  25. package/core/semantic-engine.js +560 -0
  26. package/core/semantic-rule-base.js +433 -0
  27. package/core/unified-rule-registry.js +484 -0
  28. package/docs/CONSTANTS-ARCHITECTURE.md +288 -0
  29. package/engines/core/base-engine.js +249 -0
  30. package/engines/engine-factory.js +275 -0
  31. package/engines/eslint-engine.js +171 -19
  32. package/engines/heuristic-engine.js +511 -78
  33. package/integrations/eslint/plugin/index.js +27 -27
  34. package/package.json +10 -6
  35. package/rules/common/C003_no_vague_abbreviations/analyzer.js +1 -1
  36. package/rules/common/C029_catch_block_logging/analyzer.js +17 -5
  37. package/rules/common/C047_no_duplicate_retry_logic/c047-semantic-rule.js +278 -0
  38. package/rules/common/C047_no_duplicate_retry_logic/symbol-analyzer-enhanced.js +968 -0
  39. package/rules/common/C047_no_duplicate_retry_logic/symbol-config.json +71 -0
  40. package/rules/index.js +7 -0
  41. package/scripts/category-manager.js +150 -0
  42. package/scripts/generate-rules-registry.js +88 -0
  43. package/scripts/migrate-rule-registry.js +157 -0
  44. package/scripts/validate-system.js +48 -0
  45. package/.sunlint.json +0 -35
  46. package/config/README.md +0 -88
  47. package/config/engines/eslint-rule-mapping.json +0 -74
  48. package/config/schemas/sunlint-schema.json +0 -0
  49. package/config/testing/test-s005-working.ts +0 -22
  50. package/core/multi-rule-runner.js +0 -0
  51. package/engines/tree-sitter-parser.js +0 -0
  52. package/engines/universal-ast-engine.js +0 -0
  53. package/rules/common/C029_catch_block_logging/analyzer-backup.js +0 -426
  54. package/rules/common/C029_catch_block_logging/analyzer-fixed.js +0 -130
  55. package/rules/common/C029_catch_block_logging/analyzer-multi-tech.js +0 -487
  56. package/rules/common/C029_catch_block_logging/analyzer-simple.js +0 -110
  57. package/rules/common/C029_catch_block_logging/ast-analyzer-backup.js +0 -441
  58. package/rules/common/C029_catch_block_logging/ast-analyzer-new.js +0 -127
  59. package/rules/common/C029_catch_block_logging/ast-analyzer.js +0 -133
  60. package/rules/common/C029_catch_block_logging/cfg-analyzer.js +0 -408
  61. package/rules/common/C029_catch_block_logging/dataflow-analyzer.js +0 -454
  62. package/rules/common/C029_catch_block_logging/multi-language-ast-engine.js +0 -700
  63. package/rules/common/C029_catch_block_logging/pattern-learning-analyzer.js +0 -568
  64. package/rules/common/C029_catch_block_logging/semantic-analyzer.js +0 -459
@@ -0,0 +1,484 @@
1
+ /**
2
+ * Unified Rule Registry - Single Source of Truth
3
+ * Following Rule C005: Single responsibility - centralized rule management
4
+ * Following Rule C015: Use domain language - clear registry terms
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ class UnifiedRuleRegistry {
11
+ constructor() {
12
+ this.rules = new Map();
13
+ this.engineCapabilities = new Map();
14
+ this.initialized = false;
15
+ this.verbose = false;
16
+ }
17
+
18
+ /**
19
+ * Initialize registry with auto-discovery
20
+ * @param {Object} options - Configuration options
21
+ */
22
+ async initialize(options = {}) {
23
+ if (this.initialized) return;
24
+
25
+ this.verbose = options.verbose || false;
26
+
27
+ try {
28
+ // 1. Load master rule definitions
29
+ await this.loadMasterRegistry();
30
+
31
+ // 2. Auto-discover analyzer files
32
+ await this.autoDiscoverAnalyzers();
33
+
34
+ // 3. Register engine capabilities
35
+ this.registerEngineCapabilities();
36
+
37
+ // 4. Validate consistency
38
+ await this.validateRegistry();
39
+
40
+ this.initialized = true;
41
+
42
+ if (this.verbose) {
43
+ console.log(`✅ Unified Registry initialized: ${this.rules.size} rules`);
44
+ }
45
+
46
+ } catch (error) {
47
+ console.error('❌ Failed to initialize Unified Rule Registry:', error.message);
48
+ throw error;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Load master rule definitions from primary source
54
+ */
55
+ async loadMasterRegistry() {
56
+ // Try enhanced registry first, fall back to original
57
+ const registryPaths = [
58
+ path.resolve(__dirname, '../config/rules/enhanced-rules-registry.json'),
59
+ path.resolve(__dirname, '../config/rules/rules-registry.json')
60
+ ];
61
+
62
+ let registryPath = null;
63
+ for (const tryPath of registryPaths) {
64
+ if (fs.existsSync(tryPath)) {
65
+ registryPath = tryPath;
66
+ break;
67
+ }
68
+ }
69
+
70
+ if (!registryPath) {
71
+ throw new Error('No master registry found in config/rules/');
72
+ }
73
+
74
+ if (this.verbose) {
75
+ console.log(`📋 Loading enhanced registry from: ${path.basename(registryPath)}`);
76
+ }
77
+
78
+ try {
79
+ const registryData = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
80
+ const rules = registryData.rules || registryData;
81
+
82
+ for (const [ruleId, ruleConfig] of Object.entries(rules)) {
83
+ const ruleDefinition = {
84
+ id: ruleId,
85
+ name: ruleConfig.name,
86
+ description: ruleConfig.description,
87
+ category: ruleConfig.category,
88
+ severity: ruleConfig.severity || 'warning',
89
+ languages: ruleConfig.languages || ['javascript', 'typescript'],
90
+
91
+ // Use existing analyzer paths or initialize empty
92
+ analyzers: ruleConfig.analyzers || {},
93
+
94
+ // Use existing engine mappings or initialize empty
95
+ engineMappings: ruleConfig.engineMappings || {},
96
+
97
+ // Use existing strategy or initialize default
98
+ strategy: ruleConfig.strategy || {
99
+ preferred: 'regex',
100
+ fallbacks: ['ast'],
101
+ accuracy: {}
102
+ },
103
+
104
+ // Metadata
105
+ version: ruleConfig.version || '1.0.0',
106
+ status: ruleConfig.status || 'stable',
107
+ tags: ruleConfig.tags || []
108
+ };
109
+
110
+ this.rules.set(ruleId, ruleDefinition);
111
+ }
112
+
113
+ if (this.verbose) {
114
+ console.log(`📋 Loaded ${this.rules.size} rules from master registry`);
115
+ }
116
+
117
+ } catch (error) {
118
+ throw new Error(`Failed to parse master registry: ${error.message}`);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Auto-discover analyzer files for all rules
124
+ */
125
+ async autoDiscoverAnalyzers() {
126
+ const rulesBaseDir = path.resolve(__dirname, '../rules');
127
+
128
+ for (const [ruleId, ruleDefinition] of this.rules.entries()) {
129
+ const analyzers = await this.discoverAnalyzersForRule(ruleId, rulesBaseDir);
130
+ ruleDefinition.analyzers = analyzers;
131
+
132
+ // Infer preferred analysis strategy based on available analyzers
133
+ if (analyzers.semantic) {
134
+ ruleDefinition.strategy.preferred = 'semantic';
135
+ ruleDefinition.strategy.fallbacks = ['ast', 'regex'];
136
+ } else if (analyzers.ast) {
137
+ ruleDefinition.strategy.preferred = 'ast';
138
+ ruleDefinition.strategy.fallbacks = ['regex'];
139
+ } else if (analyzers.regex || analyzers.legacy) {
140
+ ruleDefinition.strategy.preferred = 'regex';
141
+ ruleDefinition.strategy.fallbacks = [];
142
+ }
143
+ }
144
+
145
+ if (this.verbose) {
146
+ const rulesWithAnalyzers = Array.from(this.rules.values()).filter(rule =>
147
+ Object.keys(rule.analyzers).length > 0
148
+ ).length;
149
+ console.log(`🔍 Auto-discovered analyzers for ${rulesWithAnalyzers}/${this.rules.size} rules`);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Discover analyzer files for a specific rule
155
+ * @param {string} ruleId - Rule ID
156
+ * @param {string} rulesBaseDir - Base rules directory
157
+ * @returns {Object} Analyzer file paths
158
+ */
159
+ async discoverAnalyzersForRule(ruleId, rulesBaseDir) {
160
+ const analyzers = {};
161
+
162
+ // Direct search in common directory using exact folder names
163
+ const commonRulesDir = path.join(rulesBaseDir, 'common');
164
+
165
+ if (fs.existsSync(commonRulesDir)) {
166
+ const ruleFolders = fs.readdirSync(commonRulesDir);
167
+
168
+ // Look for folder that starts with rule ID
169
+ const matchingFolder = ruleFolders.find(folder =>
170
+ folder.startsWith(ruleId + '_') || folder === ruleId
171
+ );
172
+
173
+ if (matchingFolder) {
174
+ const rulePath = path.join(commonRulesDir, matchingFolder);
175
+
176
+ // Check for different analyzer files
177
+ const analyzerFiles = {
178
+ semantic: path.join(rulePath, 'semantic-analyzer.js'),
179
+ ast: path.join(rulePath, 'ast-analyzer.js'),
180
+ regex: path.join(rulePath, 'regex-analyzer.js'),
181
+ legacy: path.join(rulePath, 'analyzer.js')
182
+ };
183
+
184
+ for (const [type, filePath] of Object.entries(analyzerFiles)) {
185
+ if (fs.existsSync(filePath)) {
186
+ analyzers[type] = filePath;
187
+ }
188
+ }
189
+ }
190
+ }
191
+
192
+ // Also check other category directories (security, typescript, etc.)
193
+ const otherDirs = ['security', 'typescript', 'react'];
194
+ for (const categoryDir of otherDirs) {
195
+ const categoryPath = path.join(rulesBaseDir, categoryDir);
196
+
197
+ if (fs.existsSync(categoryPath)) {
198
+ const ruleFolders = fs.readdirSync(categoryPath);
199
+ const matchingFolder = ruleFolders.find(folder =>
200
+ folder.startsWith(ruleId + '_') || folder === ruleId
201
+ );
202
+
203
+ if (matchingFolder) {
204
+ const rulePath = path.join(categoryPath, matchingFolder);
205
+
206
+ const analyzerFiles = {
207
+ semantic: path.join(rulePath, 'semantic-analyzer.js'),
208
+ ast: path.join(rulePath, 'ast-analyzer.js'),
209
+ regex: path.join(rulePath, 'regex-analyzer.js'),
210
+ legacy: path.join(rulePath, 'analyzer.js')
211
+ };
212
+
213
+ for (const [type, filePath] of Object.entries(analyzerFiles)) {
214
+ if (fs.existsSync(filePath)) {
215
+ analyzers[type] = filePath;
216
+ }
217
+ }
218
+
219
+ // If we found analyzers, stop searching
220
+ if (Object.keys(analyzers).length > 0) {
221
+ break;
222
+ }
223
+ }
224
+ }
225
+ }
226
+
227
+ return analyzers;
228
+ }
229
+
230
+ /**
231
+ * Expand glob-like patterns to actual paths
232
+ * @param {string} baseDir - Base directory
233
+ * @param {string} pattern - Pattern with * wildcards
234
+ * @returns {string[]} Expanded paths
235
+ */
236
+ expandPattern(baseDir, pattern) {
237
+ if (!pattern.includes('*')) {
238
+ return [path.join(baseDir, pattern)];
239
+ }
240
+
241
+ const parts = pattern.split('/');
242
+ let currentPaths = [baseDir];
243
+
244
+ for (const part of parts) {
245
+ if (part === '') continue;
246
+
247
+ const newPaths = [];
248
+ for (const currentPath of currentPaths) {
249
+ if (part.includes('*')) {
250
+ // Wildcard part - expand
251
+ if (fs.existsSync(currentPath)) {
252
+ const entries = fs.readdirSync(currentPath);
253
+ const regex = new RegExp('^' + part.replace(/\*/g, '.*') + '$');
254
+
255
+ for (const entry of entries) {
256
+ if (regex.test(entry)) {
257
+ newPaths.push(path.join(currentPath, entry));
258
+ }
259
+ }
260
+ }
261
+ } else {
262
+ // Literal part
263
+ newPaths.push(path.join(currentPath, part));
264
+ }
265
+ }
266
+ currentPaths = newPaths;
267
+ }
268
+
269
+ return currentPaths;
270
+ }
271
+
272
+ /**
273
+ * Register engine capabilities
274
+ */
275
+ registerEngineCapabilities() {
276
+ // Define what each engine can handle
277
+ this.engineCapabilities.set('heuristic', ['semantic', 'ast', 'regex']);
278
+ this.engineCapabilities.set('eslint', ['ast', 'regex']);
279
+ this.engineCapabilities.set('openai', ['semantic']);
280
+
281
+ // Load ESLint mappings
282
+ this.loadESLintMappings();
283
+ }
284
+
285
+ /**
286
+ * Load ESLint rule mappings
287
+ */
288
+ loadESLintMappings() {
289
+ const eslintMappingPath = path.resolve(__dirname, '../config/eslint-rule-mapping.json');
290
+
291
+ if (fs.existsSync(eslintMappingPath)) {
292
+ try {
293
+ const mappingData = JSON.parse(fs.readFileSync(eslintMappingPath, 'utf8'));
294
+ const mappings = mappingData.mappings || mappingData;
295
+
296
+ for (const [ruleId, eslintRules] of Object.entries(mappings)) {
297
+ if (this.rules.has(ruleId)) {
298
+ this.rules.get(ruleId).engineMappings.eslint = eslintRules;
299
+ }
300
+ }
301
+
302
+ if (this.verbose) {
303
+ console.log(`🔗 Loaded ESLint mappings for ${Object.keys(mappings).length} rules`);
304
+ }
305
+
306
+ } catch (error) {
307
+ console.warn(`⚠️ Failed to load ESLint mappings: ${error.message}`);
308
+ }
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Validate registry consistency
314
+ */
315
+ async validateRegistry() {
316
+ const issues = [];
317
+
318
+ for (const [ruleId, ruleDefinition] of this.rules.entries()) {
319
+ // Check if rule has at least one analyzer
320
+ if (Object.keys(ruleDefinition.analyzers).length === 0) {
321
+ issues.push(`${ruleId}: No analyzers found`);
322
+ }
323
+
324
+ // Check if analyzer files actually exist
325
+ for (const [type, filePath] of Object.entries(ruleDefinition.analyzers)) {
326
+ if (!fs.existsSync(filePath)) {
327
+ issues.push(`${ruleId}: ${type} analyzer not found at ${filePath}`);
328
+ }
329
+ }
330
+ }
331
+
332
+ if (issues.length > 0 && this.verbose) {
333
+ console.warn(`⚠️ Registry validation found ${issues.length} issues:`);
334
+ issues.slice(0, 5).forEach(issue => console.warn(` - ${issue}`));
335
+ if (issues.length > 5) {
336
+ console.warn(` ... and ${issues.length - 5} more`);
337
+ }
338
+ }
339
+ }
340
+
341
+ // === PUBLIC API ===
342
+
343
+ /**
344
+ * Get rule definition by ID
345
+ * @param {string} ruleId - Rule ID
346
+ * @returns {Object|null} Rule definition
347
+ */
348
+ getRuleDefinition(ruleId) {
349
+ return this.rules.get(ruleId) || null;
350
+ }
351
+
352
+ /**
353
+ * Get all rules supported by an engine
354
+ * @param {string} engine - Engine name
355
+ * @returns {Object[]} Array of rule definitions
356
+ */
357
+ getRulesForEngine(engine) {
358
+ const capabilities = this.engineCapabilities.get(engine) || [];
359
+ const supportedRules = [];
360
+
361
+ for (const [ruleId, ruleDefinition] of this.rules.entries()) {
362
+ // Check if engine can handle this rule's preferred strategy
363
+ if (capabilities.includes(ruleDefinition.strategy.preferred)) {
364
+ supportedRules.push(ruleDefinition);
365
+ }
366
+ // Or if engine can handle any fallback strategy
367
+ else if (ruleDefinition.strategy.fallbacks.some(fallback => capabilities.includes(fallback))) {
368
+ supportedRules.push(ruleDefinition);
369
+ }
370
+ }
371
+
372
+ return supportedRules;
373
+ }
374
+
375
+ /**
376
+ * Get all supported rule IDs
377
+ * @returns {string[]} Array of rule IDs
378
+ */
379
+ getSupportedRules() {
380
+ return Array.from(this.rules.keys());
381
+ }
382
+
383
+ /**
384
+ * Resolve analyzer path for a rule and engine
385
+ * @param {string} ruleId - Rule ID
386
+ * @param {string} engine - Engine name
387
+ * @returns {string|null} Analyzer file path
388
+ */
389
+ resolveAnalyzerPath(ruleId, engine) {
390
+ const ruleDefinition = this.rules.get(ruleId);
391
+ if (!ruleDefinition) return null;
392
+
393
+ const capabilities = this.engineCapabilities.get(engine) || [];
394
+ const analyzers = ruleDefinition.analyzers;
395
+
396
+ // Try preferred strategy first
397
+ const preferred = ruleDefinition.strategy.preferred;
398
+ if (capabilities.includes(preferred) && analyzers[preferred]) {
399
+ return analyzers[preferred];
400
+ }
401
+
402
+ // Try fallback strategies
403
+ for (const fallback of ruleDefinition.strategy.fallbacks) {
404
+ if (capabilities.includes(fallback) && analyzers[fallback]) {
405
+ return analyzers[fallback];
406
+ }
407
+ }
408
+
409
+ // Fall back to legacy analyzer if available and engine supports regex/ast
410
+ if (analyzers.legacy && (capabilities.includes('regex') || capabilities.includes('ast'))) {
411
+ return analyzers.legacy;
412
+ }
413
+
414
+ return null;
415
+ }
416
+
417
+ /**
418
+ * Get engine mapping for a rule (ESLint specific)
419
+ * @param {string} ruleId - Rule ID
420
+ * @param {string} engine - Engine name
421
+ * @returns {string[]} Array of engine-specific rule names
422
+ */
423
+ getEngineMapping(ruleId, engine) {
424
+ const ruleDefinition = this.rules.get(ruleId);
425
+ if (!ruleDefinition) return [];
426
+
427
+ return ruleDefinition.engineMappings[engine] || [];
428
+ }
429
+
430
+ /**
431
+ * Check if rule is supported by engine
432
+ * @param {string} ruleId - Rule ID
433
+ * @param {string} engine - Engine name
434
+ * @returns {boolean} True if supported
435
+ */
436
+ isRuleSupported(ruleId, engine) {
437
+ const analyzerPath = this.resolveAnalyzerPath(ruleId, engine);
438
+ return analyzerPath !== null;
439
+ }
440
+
441
+ /**
442
+ * Get registry statistics
443
+ * @returns {Object} Registry stats
444
+ */
445
+ getStats() {
446
+ const stats = {
447
+ totalRules: this.rules.size,
448
+ rulesByCategory: {},
449
+ rulesByEngine: {},
450
+ rulesWithAnalyzers: 0
451
+ };
452
+
453
+ for (const ruleDefinition of this.rules.values()) {
454
+ // Count by category
455
+ const category = ruleDefinition.category;
456
+ stats.rulesByCategory[category] = (stats.rulesByCategory[category] || 0) + 1;
457
+
458
+ // Count rules with analyzers
459
+ if (Object.keys(ruleDefinition.analyzers).length > 0) {
460
+ stats.rulesWithAnalyzers++;
461
+ }
462
+ }
463
+
464
+ // Count by engine
465
+ for (const engine of this.engineCapabilities.keys()) {
466
+ stats.rulesByEngine[engine] = this.getRulesForEngine(engine).length;
467
+ }
468
+
469
+ return stats;
470
+ }
471
+ }
472
+
473
+ // Singleton instance
474
+ let instance = null;
475
+
476
+ module.exports = {
477
+ UnifiedRuleRegistry,
478
+ getInstance: () => {
479
+ if (!instance) {
480
+ instance = new UnifiedRuleRegistry();
481
+ }
482
+ return instance;
483
+ }
484
+ };