UNPKG

@sun-asterisk/sunlint

Version:

☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards

484 lines (403 loc) 14.7 kB
/** * Unified Rule Registry - Single Source of Truth * Following Rule C005: Single responsibility - centralized rule management * Following Rule C015: Use domain language - clear registry terms */ const fs = require('fs'); const path = require('path'); class UnifiedRuleRegistry { constructor() { this.rules = new Map(); this.engineCapabilities = new Map(); this.initialized = false; this.verbose = false; } /** * Initialize registry with auto-discovery * @param {Object} options - Configuration options */ async initialize(options = {}) { if (this.initialized) return; this.verbose = options.verbose || false; try { // 1. Load master rule definitions await this.loadMasterRegistry(); // 2. Auto-discover analyzer files await this.autoDiscoverAnalyzers(); // 3. Register engine capabilities this.registerEngineCapabilities(); // 4. Validate consistency await this.validateRegistry(); this.initialized = true; if (this.verbose) { console.log(`✅ Unified Registry initialized: ${this.rules.size} rules`); } } catch (error) { console.error('❌ Failed to initialize Unified Rule Registry:', error.message); throw error; } } /** * Load master rule definitions from primary source */ async loadMasterRegistry() { // Try enhanced registry first, fall back to original const registryPaths = [ path.resolve(__dirname, '../config/rules/enhanced-rules-registry.json'), path.resolve(__dirname, '../config/rules/rules-registry.json') ]; let registryPath = null; for (const tryPath of registryPaths) { if (fs.existsSync(tryPath)) { registryPath = tryPath; break; } } if (!registryPath) { throw new Error('No master registry found in config/rules/'); } if (this.verbose) { console.log(`📋 Loading enhanced registry from: ${path.basename(registryPath)}`); } try { const registryData = JSON.parse(fs.readFileSync(registryPath, 'utf8')); const rules = registryData.rules || registryData; for (const [ruleId, ruleConfig] of Object.entries(rules)) { const ruleDefinition = { id: ruleId, name: ruleConfig.name, description: ruleConfig.description, category: ruleConfig.category, severity: ruleConfig.severity || 'warning', languages: ruleConfig.languages || ['javascript', 'typescript'], // Use existing analyzer paths or initialize empty analyzers: ruleConfig.analyzers || {}, // Use existing engine mappings or initialize empty engineMappings: ruleConfig.engineMappings || {}, // Use existing strategy or initialize default strategy: ruleConfig.strategy || { preferred: 'regex', fallbacks: ['ast'], accuracy: {} }, // Metadata version: ruleConfig.version || '1.0.0', status: ruleConfig.status || 'stable', tags: ruleConfig.tags || [] }; this.rules.set(ruleId, ruleDefinition); } if (this.verbose) { console.log(`📋 Loaded ${this.rules.size} rules from master registry`); } } catch (error) { throw new Error(`Failed to parse master registry: ${error.message}`); } } /** * Auto-discover analyzer files for all rules */ async autoDiscoverAnalyzers() { const rulesBaseDir = path.resolve(__dirname, '../rules'); for (const [ruleId, ruleDefinition] of this.rules.entries()) { const analyzers = await this.discoverAnalyzersForRule(ruleId, rulesBaseDir); ruleDefinition.analyzers = analyzers; // Infer preferred analysis strategy based on available analyzers if (analyzers.semantic) { ruleDefinition.strategy.preferred = 'semantic'; ruleDefinition.strategy.fallbacks = ['ast', 'regex']; } else if (analyzers.ast) { ruleDefinition.strategy.preferred = 'ast'; ruleDefinition.strategy.fallbacks = ['regex']; } else if (analyzers.regex || analyzers.legacy) { ruleDefinition.strategy.preferred = 'regex'; ruleDefinition.strategy.fallbacks = []; } } if (this.verbose) { const rulesWithAnalyzers = Array.from(this.rules.values()).filter(rule => Object.keys(rule.analyzers).length > 0 ).length; console.log(`🔍 Auto-discovered analyzers for ${rulesWithAnalyzers}/${this.rules.size} rules`); } } /** * Discover analyzer files for a specific rule * @param {string} ruleId - Rule ID * @param {string} rulesBaseDir - Base rules directory * @returns {Object} Analyzer file paths */ async discoverAnalyzersForRule(ruleId, rulesBaseDir) { const analyzers = {}; // Direct search in common directory using exact folder names const commonRulesDir = path.join(rulesBaseDir, 'common'); if (fs.existsSync(commonRulesDir)) { const ruleFolders = fs.readdirSync(commonRulesDir); // Look for folder that starts with rule ID const matchingFolder = ruleFolders.find(folder => folder.startsWith(ruleId + '_') || folder === ruleId ); if (matchingFolder) { const rulePath = path.join(commonRulesDir, matchingFolder); // Check for different analyzer files const analyzerFiles = { semantic: path.join(rulePath, 'semantic-analyzer.js'), ast: path.join(rulePath, 'ast-analyzer.js'), regex: path.join(rulePath, 'regex-analyzer.js'), legacy: path.join(rulePath, 'analyzer.js') }; for (const [type, filePath] of Object.entries(analyzerFiles)) { if (fs.existsSync(filePath)) { analyzers[type] = filePath; } } } } // Also check other category directories (security, typescript, etc.) const otherDirs = ['security', 'typescript', 'react']; for (const categoryDir of otherDirs) { const categoryPath = path.join(rulesBaseDir, categoryDir); if (fs.existsSync(categoryPath)) { const ruleFolders = fs.readdirSync(categoryPath); const matchingFolder = ruleFolders.find(folder => folder.startsWith(ruleId + '_') || folder === ruleId ); if (matchingFolder) { const rulePath = path.join(categoryPath, matchingFolder); const analyzerFiles = { semantic: path.join(rulePath, 'semantic-analyzer.js'), ast: path.join(rulePath, 'ast-analyzer.js'), regex: path.join(rulePath, 'regex-analyzer.js'), legacy: path.join(rulePath, 'analyzer.js') }; for (const [type, filePath] of Object.entries(analyzerFiles)) { if (fs.existsSync(filePath)) { analyzers[type] = filePath; } } // If we found analyzers, stop searching if (Object.keys(analyzers).length > 0) { break; } } } } return analyzers; } /** * Expand glob-like patterns to actual paths * @param {string} baseDir - Base directory * @param {string} pattern - Pattern with * wildcards * @returns {string[]} Expanded paths */ expandPattern(baseDir, pattern) { if (!pattern.includes('*')) { return [path.join(baseDir, pattern)]; } const parts = pattern.split('/'); let currentPaths = [baseDir]; for (const part of parts) { if (part === '') continue; const newPaths = []; for (const currentPath of currentPaths) { if (part.includes('*')) { // Wildcard part - expand if (fs.existsSync(currentPath)) { const entries = fs.readdirSync(currentPath); const regex = new RegExp('^' + part.replace(/\*/g, '.*') + '$'); for (const entry of entries) { if (regex.test(entry)) { newPaths.push(path.join(currentPath, entry)); } } } } else { // Literal part newPaths.push(path.join(currentPath, part)); } } currentPaths = newPaths; } return currentPaths; } /** * Register engine capabilities */ registerEngineCapabilities() { // Define what each engine can handle this.engineCapabilities.set('heuristic', ['semantic', 'ast', 'regex']); this.engineCapabilities.set('eslint', ['ast', 'regex']); this.engineCapabilities.set('openai', ['semantic']); // Load ESLint mappings this.loadESLintMappings(); } /** * Load ESLint rule mappings */ loadESLintMappings() { const eslintMappingPath = path.resolve(__dirname, '../config/eslint-rule-mapping.json'); if (fs.existsSync(eslintMappingPath)) { try { const mappingData = JSON.parse(fs.readFileSync(eslintMappingPath, 'utf8')); const mappings = mappingData.mappings || mappingData; for (const [ruleId, eslintRules] of Object.entries(mappings)) { if (this.rules.has(ruleId)) { this.rules.get(ruleId).engineMappings.eslint = eslintRules; } } if (this.verbose) { console.log(`🔗 Loaded ESLint mappings for ${Object.keys(mappings).length} rules`); } } catch (error) { console.warn(`⚠️ Failed to load ESLint mappings: ${error.message}`); } } } /** * Validate registry consistency */ async validateRegistry() { const issues = []; for (const [ruleId, ruleDefinition] of this.rules.entries()) { // Check if rule has at least one analyzer if (Object.keys(ruleDefinition.analyzers).length === 0) { issues.push(`${ruleId}: No analyzers found`); } // Check if analyzer files actually exist for (const [type, filePath] of Object.entries(ruleDefinition.analyzers)) { if (!fs.existsSync(filePath)) { issues.push(`${ruleId}: ${type} analyzer not found at ${filePath}`); } } } if (issues.length > 0 && this.verbose) { console.warn(`⚠️ Registry validation found ${issues.length} issues:`); issues.slice(0, 5).forEach(issue => console.warn(` - ${issue}`)); if (issues.length > 5) { console.warn(` ... and ${issues.length - 5} more`); } } } // === PUBLIC API === /** * Get rule definition by ID * @param {string} ruleId - Rule ID * @returns {Object|null} Rule definition */ getRuleDefinition(ruleId) { return this.rules.get(ruleId) || null; } /** * Get all rules supported by an engine * @param {string} engine - Engine name * @returns {Object[]} Array of rule definitions */ getRulesForEngine(engine) { const capabilities = this.engineCapabilities.get(engine) || []; const supportedRules = []; for (const [ruleId, ruleDefinition] of this.rules.entries()) { // Check if engine can handle this rule's preferred strategy if (capabilities.includes(ruleDefinition.strategy.preferred)) { supportedRules.push(ruleDefinition); } // Or if engine can handle any fallback strategy else if (ruleDefinition.strategy.fallbacks.some(fallback => capabilities.includes(fallback))) { supportedRules.push(ruleDefinition); } } return supportedRules; } /** * Get all supported rule IDs * @returns {string[]} Array of rule IDs */ getSupportedRules() { return Array.from(this.rules.keys()); } /** * Resolve analyzer path for a rule and engine * @param {string} ruleId - Rule ID * @param {string} engine - Engine name * @returns {string|null} Analyzer file path */ resolveAnalyzerPath(ruleId, engine) { const ruleDefinition = this.rules.get(ruleId); if (!ruleDefinition) return null; const capabilities = this.engineCapabilities.get(engine) || []; const analyzers = ruleDefinition.analyzers; // Try preferred strategy first const preferred = ruleDefinition.strategy.preferred; if (capabilities.includes(preferred) && analyzers[preferred]) { return analyzers[preferred]; } // Try fallback strategies for (const fallback of ruleDefinition.strategy.fallbacks) { if (capabilities.includes(fallback) && analyzers[fallback]) { return analyzers[fallback]; } } // Fall back to legacy analyzer if available and engine supports regex/ast if (analyzers.legacy && (capabilities.includes('regex') || capabilities.includes('ast'))) { return analyzers.legacy; } return null; } /** * Get engine mapping for a rule (ESLint specific) * @param {string} ruleId - Rule ID * @param {string} engine - Engine name * @returns {string[]} Array of engine-specific rule names */ getEngineMapping(ruleId, engine) { const ruleDefinition = this.rules.get(ruleId); if (!ruleDefinition) return []; return ruleDefinition.engineMappings[engine] || []; } /** * Check if rule is supported by engine * @param {string} ruleId - Rule ID * @param {string} engine - Engine name * @returns {boolean} True if supported */ isRuleSupported(ruleId, engine) { const analyzerPath = this.resolveAnalyzerPath(ruleId, engine); return analyzerPath !== null; } /** * Get registry statistics * @returns {Object} Registry stats */ getStats() { const stats = { totalRules: this.rules.size, rulesByCategory: {}, rulesByEngine: {}, rulesWithAnalyzers: 0 }; for (const ruleDefinition of this.rules.values()) { // Count by category const category = ruleDefinition.category; stats.rulesByCategory[category] = (stats.rulesByCategory[category] || 0) + 1; // Count rules with analyzers if (Object.keys(ruleDefinition.analyzers).length > 0) { stats.rulesWithAnalyzers++; } } // Count by engine for (const engine of this.engineCapabilities.keys()) { stats.rulesByEngine[engine] = this.getRulesForEngine(engine).length; } return stats; } } // Singleton instance let instance = null; module.exports = { UnifiedRuleRegistry, getInstance: () => { if (!instance) { instance = new UnifiedRuleRegistry(); } return instance; } };