@sun-asterisk/sunlint
Version:
☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards
455 lines (386 loc) • 15.1 kB
JavaScript
/**
* Modern CLI Action Handler with Modular Engine Architecture
* Following Rule C005: Single responsibility - handle CLI execution flow with new architecture
* Following Rule C014: Dependency injection - inject orchestrator v2
* Following Rule C015: Use domain language - clear analysis and orchestration terms
*/
const chalk = require('chalk');
const fs = require('fs');
const ConfigManager = require('./config-manager');
const RuleSelectionService = require('./rule-selection-service');
const AnalysisOrchestrator = require('./analysis-orchestrator');
const OutputService = require('./output-service');
const GitUtils = require('./git-utils');
const FileTargetingService = require('./file-targeting-service');
// Legacy orchestrator for fallback
// const LegacyOrchestrator = require('./legacy-analysis-orchestrator'); // Removed
class CliActionHandler {
constructor(options = {}) {
this.options = options;
this.configManager = null;
this.ruleSelectionService = new RuleSelectionService();
// Use new orchestrator by default, fallback to legacy if needed
this.orchestrator = new AnalysisOrchestrator();
this.outputService = new OutputService(options);
this.fileTargetingService = new FileTargetingService();
this.isModernMode = !options.useLegacy;
}
/**
* Execute CLI analysis with new architecture
* Following Rule C006: Verb-noun naming
*/
async execute() {
try {
this.displayModernBanner();
this.handleShortcuts();
// Load configuration
const config = await this.loadConfiguration();
// Validate input with priority system
this.validateInput(config);
// Show dry run preview if requested
if (this.options.dryRun) {
// We need to get rules first for accurate dry run info
const rulesToRun = await this.ruleSelectionService.selectRules(config, this.options);
await this.showDryRunPreview(config, rulesToRun);
return;
}
// Select rules to run
const rulesToRun = await this.ruleSelectionService.selectRules(config, this.options);
if (rulesToRun.length === 0) {
console.log(chalk.yellow('⚠️ No rules to run'));
return;
}
// Apply enhanced file targeting
const targetingResult = await this.applyFileTargeting(config);
if (targetingResult.files.length === 0) {
console.log(chalk.yellow('⚠️ No files to analyze after applying filters'));
this.displayTargetingStats(targetingResult.stats);
return;
}
// Update options with filtered files
this.options.targetFiles = targetingResult.files;
// Display analysis info
this.displayAnalysisInfo(rulesToRun, targetingResult);
// Run analysis with appropriate orchestrator
const startTime = Date.now();
const results = await this.runModernAnalysis(rulesToRun, targetingResult.files, config);
const duration = Date.now() - startTime;
// Output results
await this.outputService.outputResults(results, this.options, {
duration,
rulesRun: rulesToRun.length
});
// Exit with appropriate code
this.handleExit(results);
} catch (error) {
console.error(chalk.red('❌ Sun Lint Error:'), error.message);
// Following Rule C035: Log complete error context
if (this.options.debug) {
console.error('Full error context:', {
message: error.message,
stack: error.stack,
options: this.options,
mode: this.isModernMode ? 'modern' : 'legacy'
});
}
process.exit(1);
}
}
/**
* Run analysis using modern orchestrator
*/
async runModernAnalysis(rulesToRun, files, config) {
if (this.isModernMode) {
console.log(chalk.blue('🚀 Using modern engine architecture'));
// Initialize orchestrator with configuration including targetFiles for optimization
await this.orchestrator.initialize({
enabledEngines: this.determineEnabledEngines(config),
aiConfig: config.ai || {},
eslintConfig: config.eslint || {},
heuristicConfig: {
...config.heuristic || {},
targetFiles: this.options.targetFiles, // Pass filtered files for semantic optimization
maxSemanticFiles: this.options.maxSemanticFiles !== undefined ? parseInt(this.options.maxSemanticFiles) : 1000, // Configurable semantic file limit
verbose: this.options.verbose // Pass verbose for debugging
}
});
if (this.options.verbose) {
console.log(`🔧 Debug: maxSemanticFiles option = ${this.options.maxSemanticFiles}`);
console.log(`🔧 Debug: parsed maxSemanticFiles = ${this.options.maxSemanticFiles !== undefined ? parseInt(this.options.maxSemanticFiles) : 1000}`);
}
// Run analysis with new orchestrator
const results = await this.orchestrator.analyze(files, rulesToRun, {
...this.options,
timeout: parseInt(this.options.timeout) || 30000,
config: {
...config,
verbose: this.options.verbose,
quiet: this.options.quiet,
// Pass requested engine to enable strict engine mode (no fallback)
requestedEngine: this.options.engine,
// Performance optimization settings
performanceMode: this.options.performanceMode,
ruleBatchSize: parseInt(this.options.ruleBatchSize) || 10,
fileBatchSize: parseInt(this.options.fileBatchSize) || 50,
maxFiles: parseInt(this.options.maxFiles) || 1000,
enableFileFiltering: !this.options.noFileFiltering,
enableBatching: !this.options.noBatching
}
});
return results;
}
}
/**
* Determine which engines to enable based on configuration
* Following Rule C006: Verb-noun naming
* Following Rule C031: Separate validation logic
*/
determineEnabledEngines(config) {
// If specific engine is requested via --engine option, use only that engine
if (this.options.engine) {
// Handle "auto" engine selection
if (this.options.engine === 'auto') {
// Auto-select best engines: default to heuristic for compatibility
const autoEngines = ['heuristic'];
// Add ESLint for JS/TS files if available
if (this.hasJavaScriptTypeScriptFiles() || config.eslint?.enabled !== false) {
autoEngines.push('eslint');
}
return autoEngines;
}
// Return specific engine as requested
return [this.options.engine];
}
const enabledEngines = [];
// Always enable heuristic engine for pattern-based rules
enabledEngines.push('heuristic');
// Enable ESLint engine for JS/TS files
if (this.hasJavaScriptTypeScriptFiles() || config.eslint?.enabled !== false) {
enabledEngines.push('eslint');
}
// Enable OpenAI engine if AI is configured and requested
if (this.options.ai || config.ai?.enabled) {
if (this.validateAIConfiguration(config)) {
enabledEngines.push('openai');
} else {
console.warn(chalk.yellow('⚠️ AI requested but not properly configured, skipping AI analysis'));
}
}
return enabledEngines;
}
/**
* Validate AI configuration
* Following Rule C006: Verb-noun naming
* Following Rule C031: Separate validation logic
*/
validateAIConfiguration(config) {
const aiConfig = config.ai || {};
// Check for API key
if (!aiConfig.apiKey && !process.env.OPENAI_API_KEY) {
console.warn(chalk.yellow('⚠️ No OpenAI API key found in config or environment'));
return false;
}
// Check for model configuration
if (!aiConfig.model && !process.env.OPENAI_MODEL) {
// Use default model
return true;
}
return true;
}
/**
* Check if target files contain JS/TS files
* Following Rule C006: Verb-noun naming
*/
hasJavaScriptTypeScriptFiles() {
if (!this.options.targetFiles) return false;
return this.options.targetFiles.some(file => {
const ext = require('path').extname(file).toLowerCase();
return ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].includes(ext);
});
}
/**
* Display modern banner with architecture info
* Following Rule C006: Verb-noun naming
*/
displayModernBanner() {
// Skip banner in quiet mode or JSON format
if (this.options.quiet || this.options.format === 'json') {
return;
}
const { version } = require('../package.json');
console.log(chalk.yellow.bold('☀️ Sun Lint - Modular Analysis Engine'));
console.log(chalk.gray(`Version: ${version} | Mode: ${this.isModernMode ? 'Modern' : 'Legacy'} | Sun* Engineering`));
if (this.options.debug) {
console.log(chalk.yellow('Debug mode enabled'));
console.log('Architecture:', this.isModernMode ? 'Plugin-based' : 'Legacy');
console.log('Options:', this.options);
}
console.log();
}
// Delegate methods to base functionality (same as original CliActionHandler)
/**
* Load configuration using existing config manager
* Following Rule C006: Verb-noun naming
*/
async loadConfiguration() {
this.configManager = new ConfigManager();
return await this.configManager.loadConfig(this.options.config, this.options);
}
/**
* Validate CLI input
* Following Rule C006: Verb-noun naming
* Following Rule C031: Separate validation logic
*/
/**
* Validate CLI input with priority system
* Priority: CLI > Config File > Default
* Following Rule C006: Verb-noun naming
* Following Rule C031: Separate validation logic
*/
validateInput(config) {
// Validate engine option if specified (check this first, always)
if (this.options.engine) {
const validEngines = ['auto', 'eslint', 'heuristic', 'openai'];
if (!validEngines.includes(this.options.engine)) {
throw new Error(
chalk.red(`❌ Invalid engine: ${this.options.engine}\n`) +
chalk.gray(`Valid engines: ${validEngines.join(', ')}`)
);
}
}
// Priority 1: CLI --input parameter (highest priority)
if (this.options.input) {
// Validate CLI input path exists
if (!fs.existsSync(this.options.input)) {
throw new Error(
chalk.red(`❌ Input path does not exist: ${this.options.input}\n`) +
chalk.gray('Please check the path and try again.')
);
}
return; // CLI input is valid, use it
}
// Priority 2: Config file 'include' field
if (config && config.include && Array.isArray(config.include) && config.include.length > 0) {
// Config provides include patterns, use current directory as base
// Let FileTargetingService handle the include patterns from config
this.options.input = '.';
if (this.options.verbose) {
console.log(chalk.gray(`ℹ️ Using config include patterns: ${config.include.join(', ')}`));
}
return;
}
// Priority 3: Default behavior (fallback)
if (!this.options.input && (!config || !config.include)) {
// Set default input directory instead of glob patterns
this.options.input = '.'; // Current directory, let FileTargetingService handle patterns
if (this.options.verbose) {
console.log(chalk.gray('ℹ️ Using default input: current directory with JS/TS file patterns'));
}
return;
}
}
/**
* Handle CLI shortcuts
* Following Rule C006: Verb-noun naming
*/
handleShortcuts() {
// Handle version shortcut
if (this.options.version) {
const { version } = require('../package.json');
console.log(version);
process.exit(0);
}
// Handle list-rules shortcut
if (this.options.listRules) {
// This could be enhanced to use the new orchestrator's rule discovery
console.log('Available rules will be listed here...');
process.exit(0);
}
}
/**
* Get preset name based on CLI options
*/
getPresetName() {
if (this.options.all) return 'all preset';
if (this.options.quality) return 'quality preset';
if (this.options.security) return 'security preset';
if (this.options.category) return `${this.options.category} preset`;
if (this.options.rule) return 'single rule';
if (this.options.rules) return 'multiple rules';
return 'config-based';
}
/**
* Show dry run preview
* Following Rule C006: Verb-noun naming
*/
async showDryRunPreview(config, rulesToRun = null) {
console.log(chalk.blue('🔍 Dry Run Preview'));
console.log(chalk.gray('This would analyze the following configuration:'));
let rulesInfo;
if (rulesToRun) {
rulesInfo = `${rulesToRun.length} rules (${this.getPresetName()})`;
} else {
rulesInfo = this.options.rules || 'config-based';
}
console.log(JSON.stringify({
rules: rulesInfo,
files: this.options.targetFiles?.length || 'auto-detected',
engines: this.determineEnabledEngines(config),
mode: this.isModernMode ? 'modern' : 'legacy'
}, null, 2));
}
/**
* Apply file targeting logic
* Following Rule C006: Verb-noun naming
*/
async applyFileTargeting(config) {
// Handle both string and array input patterns
const inputPaths = Array.isArray(this.options.input)
? this.options.input
: [this.options.input];
return await this.fileTargetingService.getTargetFiles(inputPaths, config, this.options);
}
/**
* Display analysis information
* Following Rule C006: Verb-noun naming
*/
displayAnalysisInfo(rulesToRun, targetingResult) {
if (this.options.quiet) return;
console.log(chalk.blue('📊 Analysis Configuration:'));
console.log(`• Rules: ${rulesToRun.length} selected`);
console.log(`• Files: ${targetingResult.files.length} targeted`);
console.log(`• Architecture: ${this.isModernMode ? 'Modern Plugin-based' : 'Legacy'}`);
if (this.options.debug) {
console.log(`• Rules: ${rulesToRun.map(r => r.id).join(', ')}`);
}
console.log();
}
/**
* Display targeting statistics
* Following Rule C006: Verb-noun naming
*/
displayTargetingStats(stats) {
if (this.options.quiet) return;
console.log(chalk.gray('Targeting Stats:'));
Object.entries(stats).forEach(([key, value]) => {
console.log(`• ${key}: ${value}`);
});
}
/**
* Handle process exit based on results
* Following Rule C006: Verb-noun naming
*/
handleExit(results) {
if (this.options.noExit) return;
// Check if any violations were found
const hasViolations = results.results?.some(result =>
result.violations && result.violations.length > 0
);
if (hasViolations && this.options.failOnViolations !== false) {
process.exit(1);
} else {
process.exit(0);
}
}
}
module.exports = CliActionHandler;