UNPKG

@sun-asterisk/sunlint

Version:

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

673 lines (577 loc) 23.4 kB
/** * Analysis Orchestrator - Plugin-Based Architecture * Following Rule C005: Single responsibility - orchestrate analysis plugins * Following Rule C014: Dependency injection - inject analysis engines * Following Rule C015: Use domain language - clear orchestration terms */ const chalk = require('chalk'); const path = require('path'); const fs = require('fs'); const AnalysisEngineInterface = require('./interfaces/analysis-engine.interface'); const SunlintRuleAdapter = require('./adapters/sunlint-rule-adapter'); const PerformanceOptimizer = require('./performance-optimizer'); class AnalysisOrchestrator { constructor() { this.engines = new Map(); // Plugin registry this.initialized = false; this.defaultTimeout = 30000; // 30 seconds default timeout this.ruleAdapter = SunlintRuleAdapter.getInstance(); this.enginesConfigPath = path.join(__dirname, '..', 'config', 'engines', 'engines.json'); this.performanceOptimizer = new PerformanceOptimizer(); } /** * Auto-load engines from configuration * Following Rule C006: Verb-noun naming * @param {Object} config - Configuration for engines */ async loadEnginesFromConfig(config = {}) { try { // Load engines config let enginesConfig = {}; if (fs.existsSync(this.enginesConfigPath)) { const configData = fs.readFileSync(this.enginesConfigPath, 'utf8'); enginesConfig = JSON.parse(configData); } // Load each configured engine const enabledEngines = config.enabledEngines || enginesConfig.defaultEngines || Object.keys(enginesConfig.engines || {}); for (const engineName of enabledEngines) { const engineConfig = enginesConfig.engines?.[engineName]; if (!engineConfig || !engineConfig.enabled) { continue; } try { // Load engine module const enginePath = path.resolve(__dirname, '..', engineConfig.path); const EngineClass = require(enginePath); // Create and register engine instance const engine = new EngineClass(); this.registerEngine(engine); } catch (error) { console.warn(chalk.yellow(`⚠️ Failed to load engine ${engineName}:`), error.message); } } } catch (error) { console.warn(chalk.yellow(`⚠️ Failed to load engines config:`), error.message); // Fall back to manual registration if config fails } } /** * Register an analysis engine plugin * Following Rule C014: Dependency injection - register engines * Following Rule C006: Verb-noun naming * @param {AnalysisEngineInterface} engine - Engine to register * @param {Object} options - Options including verbose flag */ registerEngine(engine, options = {}) { if (!(engine instanceof AnalysisEngineInterface)) { throw new Error('Engine must implement AnalysisEngineInterface'); } if (this.engines.has(engine.id)) { if (options.verbose) { console.warn(chalk.yellow(`⚠️ Engine ${engine.id} already registered, replacing...`)); } } this.engines.set(engine.id, engine); if (options.verbose) { console.log(chalk.green(`✅ Registered engine: ${engine.id} v${engine.version}`)); if (!this.initialized) { console.log(chalk.gray(` Supported languages: ${engine.supportedLanguages.join(', ')}`)); // Note: Supported rules count will be shown after initialization } } } /** * Initialize all registered engines * Following Rule C006: Verb-noun naming * @param {Object} config - Configuration for engines */ async initialize(config) { if (this.initialized) { return; } // Auto-load engines from config if none are registered if (this.engines.size === 0) { await this.loadEnginesFromConfig(config); } if (config.verbose) { console.log(chalk.blue(`🔧 Initializing ${this.engines.size} analysis engines...`)); } // Initialize rule adapter await this.ruleAdapter.initialize(); // Initialize enabled engines const enabledEngines = config.enabledEngines || Array.from(this.engines.keys()); for (const engineName of enabledEngines) { const engine = this.engines.get(engineName); if (!engine) { if (config.verbose) { console.warn(chalk.yellow(`⚠️ Engine ${engineName} not registered, skipping...`)); } continue; } try { await engine.initialize(config[`${engineName}Config`] || {}); if (config.verbose) { console.log(chalk.green(`✅ ${engineName} engine initialized`)); console.log(chalk.gray(` Supported rules: ${engine.getSupportedRules().length} rules`)); } } catch (error) { console.error(chalk.red(`❌ Failed to initialize ${engineName} engine:`), error.message); // Remove failed engine from registry this.engines.delete(engineName); } } if (this.engines.size === 0) { throw new Error('No analysis engines successfully initialized'); } this.initialized = true; if (config.verbose) { console.log(chalk.blue(`🚀 All engines initialized successfully`)); } } /** * Run analysis using appropriate engines * Following Rule C005: Single responsibility - orchestrate analysis * Following Rule C006: Verb-noun naming * @param {Object[]} rulesToRun - Rules to analyze * @param {Object} options - Analysis options * @param {Object} config - Configuration * @returns {Promise<Object>} Combined analysis results */ async runAnalysis(rulesToRun, options, config = {}) { try { // Initialize engines if not already done if (!this.initialized) { await this.initialize(config); } if (this.engines.size === 0) { throw new Error('No analysis engines registered'); } // Initialize performance optimizer await this.performanceOptimizer.initialize(config); // Apply performance optimizations to files and rules const { optimizedFiles, optimizedRules, performanceMetrics } = await this.performanceOptimizer.optimizeAnalysis( options.files || [options.input], rulesToRun, config ); if (!options.quiet) { console.log(chalk.cyan(`🔍 Analyzing ${optimizedRules.length} rules on ${optimizedFiles.length} files...`)); if (performanceMetrics.filteredFiles > 0) { console.log(chalk.gray(` 📦 Filtered ${performanceMetrics.filteredFiles} files for performance`)); } if (performanceMetrics.ruleBatches > 1) { console.log(chalk.gray(` 🔄 Using ${performanceMetrics.ruleBatches} rule batches`)); } } // Group rules by their preferred engines const engineGroups = this.groupRulesByEngine(optimizedRules, config); if (!options.quiet) { console.log(chalk.cyan(`🚀 Running analysis across ${engineGroups.size} engines...`)); } // Run analysis on each engine with batching const results = []; for (const [engineName, rules] of engineGroups) { const engine = this.engines.get(engineName); if (!engine) { console.warn(chalk.yellow(`⚠️ Engine ${engineName} not found, skipping ${rules.length} rules`)); continue; } // Process rules in batches for performance const ruleBatches = this.performanceOptimizer.createRuleBatches(rules, config); for (let i = 0; i < ruleBatches.length; i++) { const batch = ruleBatches[i]; const batchNumber = i + 1; if (!options.quiet && ruleBatches.length > 1) { console.log(chalk.blue(`⚙️ ${engineName} - Batch ${batchNumber}/${ruleBatches.length}: ${batch.length} rules`)); } else if (!options.quiet) { console.log(chalk.blue(`⚙️ Running ${batch.length} rules on ${engineName} engine...`)); } try { const engineResult = await this.runEngineWithOptimizations( engine, optimizedFiles, batch, options, { batchNumber, totalBatches: ruleBatches.length } ); results.push({ engine: engineName, batch: batchNumber, rules: batch.map(r => r.id), ...engineResult }); if (!options.quiet) { const violationCount = this.countViolations(engineResult); console.log(chalk.blue(`✅ ${engineName} batch ${batchNumber}: ${violationCount} violations found`)); } } catch (error) { // Enhanced error recovery with batch context const errorInfo = this.performanceOptimizer.handleAnalysisError(error, { engine: engineName, batch: batchNumber, rules: batch.map(r => r.id), files: optimizedFiles.length }); console.error(chalk.red(`❌ Engine ${engineName} batch ${batchNumber} failed:`), errorInfo.message); if (errorInfo.shouldRetry && !options.noRetry) { console.log(chalk.yellow(`🔄 Retrying with reduced batch size...`)); // Split batch and retry - implement recursive retry logic here } // Continue with other batches instead of failing completely } } } // Merge results and add performance metrics const mergedResults = this.mergeEngineResults(results, options, optimizedFiles.length); mergedResults.performance = performanceMetrics; return mergedResults; } catch (error) { console.error(chalk.red('❌ Analysis orchestration failed:'), error.message); throw error; } } /** * Group rules by their preferred analysis engines * Following Rule C005: Single responsibility * Following Rule C006: Verb-noun naming * @param {Object[]} rulesToRun - Rules to group * @param {Object} config - Configuration with engine preferences * @returns {Map} Map of engine names to rule arrays */ groupRulesByEngine(rulesToRun, config) { const groups = new Map(); const skippedRules = []; if (config.verbose) { console.log(`📊 [Orchestrator] Grouping ${rulesToRun.length} rules by engine...`); } for (const rule of rulesToRun) { if (config.verbose) { console.log(`🔄 [Orchestrator] Processing rule ${rule.id}`); } // Determine engine preference order const enginePreference = this.getEnginePreference(rule, config); if (config.verbose) { console.log(`🎯 [Orchestrator] Engine preference for ${rule.id}:`, enginePreference); } const selectedEngine = this.selectBestEngine(enginePreference, rule, config); // If rule is skipped (no engine supports it when specific engine requested) if (selectedEngine === null) { skippedRules.push(rule); if (config.verbose) { console.log(`⚠️ [Orchestrator] Skipped rule ${rule.id} - not supported by requested engine`); } continue; } if (config.verbose) { console.log(`✅ [Orchestrator] Selected engine for ${rule.id}: ${selectedEngine}`); } if (!groups.has(selectedEngine)) { groups.set(selectedEngine, []); } groups.get(selectedEngine).push(rule); } // Report skipped rules if any if (skippedRules.length > 0) { const skippedRuleIds = skippedRules.map(r => r.id).join(', '); console.warn(`⚠️ [Orchestrator] Skipped ${skippedRules.length} rules not supported by requested engine: ${skippedRuleIds}`); } if (config.verbose) { console.log(`📋 [Orchestrator] Final groups:`, Array.from(groups.entries()).map(([k, v]) => [k, v.length])); } return groups; } /** * Get engine preference for a rule * Following Rule C006: Verb-noun naming * @param {Object} rule - Rule object * @param {Object} config - Configuration * @returns {string[]} Array of engine names in preference order */ getEnginePreference(rule, config) { // If user specified a specific engine via --engine option, use only that engine if (config.requestedEngine) { // Handle "auto" engine selection if (config.requestedEngine === 'auto') { // Auto-select best engines: default to heuristic, add eslint for JS/TS return ['heuristic', 'eslint']; } return [config.requestedEngine]; } // Special preference for C047: Always use semantic analysis (heuristic engine) if (rule.id === 'C047') { return ['heuristic', 'openai']; } // Check config-level rule preferences const ruleConfig = config.rules?.[rule.id]; if (ruleConfig?.engines) { return ruleConfig.engines; } // Check CLI --eslint-integration flag (high priority) if (config.eslintIntegration) { // ESLint integration: prioritize ESLint engine for all rules // ESLint engine can handle JS/TS rules, heuristic handles others return ['eslint', 'heuristic', 'openai']; } // Check rule analyzer field for compatibility if (rule.analyzer) { if (rule.analyzer === 'eslint' || rule.analyzer === 'typescript') { return ['eslint', 'heuristic']; } if (rule.analyzer.includes('heuristic')) { return ['heuristic', 'openai']; } } // Default preference order return ['heuristic', 'openai', 'eslint']; } /** * Select best available engine for a rule * Following Rule C006: Verb-noun naming * @param {string[]} preferences - Engine preferences in order * @param {Object} rule - Rule object * @param {Object} config - Configuration with verbose flag * @returns {string|null} Selected engine name or null if no engine supports the rule */ selectBestEngine(preferences, rule, config) { if (config.verbose) { console.log(`🎯 [Orchestrator] Selecting engine for rule ${rule.id}, preferences:`, preferences); } // If user specified a specific engine (--engine=eslint), only use that engine // Don't fallback to other engines to maintain consistency const isSpecificEngineRequested = config.requestedEngine && preferences.length === 1; for (const engineName of preferences) { const engine = this.engines.get(engineName); if (config.verbose) { console.log(`🔍 [Orchestrator] Checking engine ${engineName}: exists=${!!engine}`); } if (engine && engine.isRuleSupported(rule.id)) { if (config.verbose) { console.log(`✅ [Orchestrator] Selected engine ${engineName} for rule ${rule.id}`); } return engineName; } } // If specific engine is requested and it doesn't support the rule, skip fallback if (isSpecificEngineRequested) { if (config.verbose) { console.log(`⚠️ [Orchestrator] Rule ${rule.id} not supported by requested engine ${preferences[0]}, skipping`); } return null; // Skip this rule } if (config.verbose) { console.log(`🔄 [Orchestrator] No preferred engine supports ${rule.id}, checking all engines...`); } // Fallback to first available engine that supports the rule (only when no specific engine requested) for (const [engineName, engine] of this.engines) { if (config.verbose) { console.log(`🔍 [Orchestrator] Fallback checking engine ${engineName}`); } if (engine.isRuleSupported(rule.id)) { if (config.verbose) { console.log(`✅ [Orchestrator] Fallback selected engine ${engineName} for rule ${rule.id}`); } return engineName; } } if (config.verbose) { console.log(`🚨 [Orchestrator] No engine supports rule ${rule.id}, falling back to heuristic`); } // Final fallback to 'heuristic' (most flexible) return 'heuristic'; } /** * Run engine analysis with timeout protection and performance optimizations * Following Rule C006: Verb-noun naming * @param {AnalysisEngineInterface} engine - Engine to run * @param {string[]} files - Files to analyze * @param {Object[]} rules - Rules to apply * @param {Object} options - Analysis options * @param {Object} batchInfo - Batch context information * @returns {Promise<Object>} Engine results */ async runEngineWithOptimizations(engine, files, rules, options, batchInfo = {}) { // Dynamic timeout based on file count and rules const adaptiveTimeout = this.performanceOptimizer.calculateAdaptiveTimeout( files.length, rules.length, options.timeout || this.defaultTimeout ); const enhancedOptions = { ...options, timeout: adaptiveTimeout, batchInfo }; try { return await Promise.race([ engine.analyze(files, rules, enhancedOptions), new Promise((_, reject) => setTimeout(() => reject(new Error( `Engine ${engine.name} batch ${batchInfo.batchNumber || 1} timed out after ${adaptiveTimeout}ms` )), adaptiveTimeout) ) ]); } catch (error) { // Enhanced error context for debugging const errorContext = { engine: engine.name, filesCount: files.length, rulesCount: rules.length, timeout: adaptiveTimeout, batch: batchInfo }; // Wrap error with context const enhancedError = new Error(`${error.message} (Context: ${JSON.stringify(errorContext)})`); enhancedError.originalError = error; enhancedError.context = errorContext; throw enhancedError; } } /** * Run engine analysis with timeout protection (legacy method for backward compatibility) * Following Rule C006: Verb-noun naming * @param {AnalysisEngineInterface} engine - Engine to run * @param {string[]} files - Files to analyze * @param {Object[]} rules - Rules to apply * @param {Object} options - Analysis options * @returns {Promise<Object>} Engine results */ async runEngineWithTimeout(engine, files, rules, options) { return this.runEngineWithOptimizations(engine, files, rules, options); } /** * Merge results from multiple engines * Following Rule C005: Single responsibility * Following Rule C006: Verb-noun naming * @param {Object[]} engineResults - Results from all engines * @param {Object} options - Analysis options * @returns {Object} Merged results */ mergeEngineResults(engineResults, options, actualFilesCount = 0) { const mergedResults = { results: [], summary: { totalEngines: engineResults.length, totalBatches: engineResults.length, totalViolations: 0, totalFiles: 0, engines: {} }, metadata: { timestamp: new Date().toISOString(), orchestrator: 'sunlint-v2', version: '2.0.0' } }; // Track unique engines for summary const uniqueEngines = new Set(); // Combine results from all engines for (const engineResult of engineResults) { uniqueEngines.add(engineResult.engine); // Add engine-specific results if (engineResult.results) { mergedResults.results.push(...engineResult.results); } // Track engine statistics const violationCount = this.countViolations(engineResult); const engineName = engineResult.engine; if (!mergedResults.summary.engines[engineName]) { mergedResults.summary.engines[engineName] = { rules: [], violations: 0, files: 0, batches: 0 }; } // Accumulate engine statistics across batches mergedResults.summary.engines[engineName].rules.push(...(engineResult.rules || [])); mergedResults.summary.engines[engineName].violations += violationCount; // Don't accumulate filesAnalyzed for each batch - use actual unique file count if (!mergedResults.summary.engines[engineName].filesSet) { mergedResults.summary.engines[engineName].files = actualFilesCount; mergedResults.summary.engines[engineName].filesSet = true; } mergedResults.summary.engines[engineName].batches += 1; mergedResults.summary.totalViolations += violationCount; } // Update unique engine count and correct total files count mergedResults.summary.totalEngines = uniqueEngines.size; mergedResults.summary.totalFiles = actualFilesCount; return mergedResults; } /** * Count violations in engine results * Following Rule C006: Verb-noun naming * @param {Object} engineResult - Result from an engine * @returns {number} Number of violations */ countViolations(engineResult) { if (!engineResult.results) return 0; return engineResult.results.reduce((total, fileResult) => { return total + (fileResult.violations?.length || 0); }, 0); } /** * Get information about registered engines * Following Rule C006: Verb-noun naming * @returns {Object} Engine information */ getEngineInfo() { const engines = {}; for (const [name, engine] of this.engines) { engines[name] = engine.getEngineInfo(); } return engines; } /** * Get available engines * Following Rule C006: Verb-noun naming * @returns {string[]} Array of available engine IDs */ getAvailableEngines() { return Array.from(this.engines.keys()); } /** * Analyze files with rules (main analysis interface) * Following Rule C006: Verb-noun naming * @param {string[]} files - Files to analyze * @param {Object[]} rules - Rules to apply * @param {Object} options - Analysis options * @returns {Promise<Object>} Analysis results */ async analyze(files, rules, options = {}) { // Ensure files are in options for compatibility const analysisOptions = { ...options, files }; // Ensure verbose/quiet flags are available in config const config = { ...options.config || {}, verbose: options.verbose || options.config?.verbose, quiet: options.quiet || options.config?.quiet }; return await this.runAnalysis(rules, analysisOptions, config); } /** * Cleanup all engines and performance optimizer * Following Rule C006: Verb-noun naming * @returns {Promise<void>} */ async cleanup() { for (const engine of this.engines.values()) { try { await engine.cleanup(); } catch (error) { console.warn(chalk.yellow(`⚠️ Failed to cleanup engine ${engine.id}:`), error.message); } } // Cleanup performance optimizer try { await this.performanceOptimizer.cleanup(); } catch (error) { console.warn(chalk.yellow(`⚠️ Failed to cleanup performance optimizer:`), error.message); } this.initialized = false; console.log(chalk.blue('🧹 Engine cleanup completed')); } } module.exports = AnalysisOrchestrator;