@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.
- package/CHANGELOG.md +40 -1
- package/CONTRIBUTING.md +533 -70
- package/README.md +16 -2
- package/config/engines/engines-enhanced.json +86 -0
- package/config/engines/semantic-config.json +114 -0
- package/config/eslint-rule-mapping.json +50 -38
- package/config/rules/enhanced-rules-registry.json +2503 -0
- package/config/rules/rules-registry-generated.json +785 -837
- package/core/adapters/sunlint-rule-adapter.js +25 -30
- package/core/analysis-orchestrator.js +42 -2
- package/core/categories.js +52 -0
- package/core/category-constants.js +39 -0
- package/core/cli-action-handler.js +32 -5
- package/core/config-manager.js +111 -0
- package/core/config-merger.js +61 -0
- package/core/constants/categories.js +168 -0
- package/core/constants/defaults.js +165 -0
- package/core/constants/engines.js +185 -0
- package/core/constants/index.js +30 -0
- package/core/constants/rules.js +215 -0
- package/core/file-targeting-service.js +128 -7
- package/core/interfaces/rule-plugin.interface.js +207 -0
- package/core/plugin-manager.js +448 -0
- package/core/rule-selection-service.js +42 -15
- package/core/semantic-engine.js +560 -0
- package/core/semantic-rule-base.js +433 -0
- package/core/unified-rule-registry.js +484 -0
- package/docs/CONSTANTS-ARCHITECTURE.md +288 -0
- package/engines/core/base-engine.js +249 -0
- package/engines/engine-factory.js +275 -0
- package/engines/eslint-engine.js +171 -19
- package/engines/heuristic-engine.js +511 -78
- package/integrations/eslint/plugin/index.js +27 -27
- package/package.json +10 -6
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +1 -1
- package/rules/common/C029_catch_block_logging/analyzer.js +17 -5
- package/rules/common/C047_no_duplicate_retry_logic/c047-semantic-rule.js +278 -0
- package/rules/common/C047_no_duplicate_retry_logic/symbol-analyzer-enhanced.js +968 -0
- package/rules/common/C047_no_duplicate_retry_logic/symbol-config.json +71 -0
- package/rules/index.js +7 -0
- package/scripts/category-manager.js +150 -0
- package/scripts/generate-rules-registry.js +88 -0
- package/scripts/migrate-rule-registry.js +157 -0
- package/scripts/validate-system.js +48 -0
- package/.sunlint.json +0 -35
- package/config/README.md +0 -88
- package/config/engines/eslint-rule-mapping.json +0 -74
- package/config/schemas/sunlint-schema.json +0 -0
- package/config/testing/test-s005-working.ts +0 -22
- package/core/multi-rule-runner.js +0 -0
- package/engines/tree-sitter-parser.js +0 -0
- package/engines/universal-ast-engine.js +0 -0
- package/rules/common/C029_catch_block_logging/analyzer-backup.js +0 -426
- package/rules/common/C029_catch_block_logging/analyzer-fixed.js +0 -130
- package/rules/common/C029_catch_block_logging/analyzer-multi-tech.js +0 -487
- package/rules/common/C029_catch_block_logging/analyzer-simple.js +0 -110
- package/rules/common/C029_catch_block_logging/ast-analyzer-backup.js +0 -441
- package/rules/common/C029_catch_block_logging/ast-analyzer-new.js +0 -127
- package/rules/common/C029_catch_block_logging/ast-analyzer.js +0 -133
- package/rules/common/C029_catch_block_logging/cfg-analyzer.js +0 -408
- package/rules/common/C029_catch_block_logging/dataflow-analyzer.js +0 -454
- package/rules/common/C029_catch_block_logging/multi-language-ast-engine.js +0 -700
- package/rules/common/C029_catch_block_logging/pattern-learning-analyzer.js +0 -568
- 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
|
+
};
|