@sun-asterisk/sunlint
Version:
☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards
538 lines (462 loc) • 17.9 kB
JavaScript
/**
* 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');
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');
}
/**
* 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');
}
// Group rules by their preferred engines
const engineGroups = this.groupRulesByEngine(rulesToRun, config);
if (!options.quiet) {
console.log(chalk.cyan(`🔍 Analyzing ${rulesToRun.length} rules across ${engineGroups.size} engines...`));
}
// Run analysis on each engine
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;
}
if (!options.quiet) {
console.log(chalk.blue(`⚙️ Running ${rules.length} rules on ${engineName} engine...`));
}
try {
const engineResult = await this.runEngineWithTimeout(
engine,
options.files || [options.input],
rules,
options
);
results.push({
engine: engineName,
rules: rules.map(r => r.id),
...engineResult
});
if (!options.quiet) {
const violationCount = this.countViolations(engineResult);
console.log(chalk.blue(`✅ ${engineName}: ${violationCount} violations found`));
}
} catch (error) {
console.error(chalk.red(`❌ Engine ${engineName} failed:`), error.message);
// Continue with other engines instead of failing completely
}
}
return this.mergeEngineResults(results, options);
} 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) {
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
* 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) {
const timeout = options.timeout || this.defaultTimeout;
return Promise.race([
engine.analyze(files, rules, options),
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Engine ${engine.name} timed out after ${timeout}ms`)), timeout)
)
]);
}
/**
* 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) {
const mergedResults = {
results: [],
summary: {
totalEngines: engineResults.length,
totalViolations: 0,
totalFiles: 0,
engines: {}
},
metadata: {
timestamp: new Date().toISOString(),
orchestrator: 'sunlint-v2',
version: '2.0.0'
}
};
// Combine results from all engines
for (const engineResult of engineResults) {
// Add engine-specific results
if (engineResult.results) {
mergedResults.results.push(...engineResult.results);
}
// Track engine statistics
const violationCount = this.countViolations(engineResult);
mergedResults.summary.engines[engineResult.engine] = {
rules: engineResult.rules || [],
violations: violationCount,
files: engineResult.filesAnalyzed || 0
};
mergedResults.summary.totalViolations += violationCount;
mergedResults.summary.totalFiles += engineResult.filesAnalyzed || 0;
}
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
* 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);
}
}
this.initialized = false;
console.log(chalk.blue('🧹 Engine cleanup completed'));
}
}
module.exports = AnalysisOrchestrator;