@sun-asterisk/sunlint
Version: 
☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards
449 lines (379 loc) • 13.5 kB
JavaScript
/**
 * Plugin Manager
 * Manages rule plugins lifecycle and loading
 * Following Rule C005: Single responsibility - Plugin management
 */
const fs = require('fs');
const path = require('path');
const { RulePluginInterface, SemanticRuleInterface, CustomRuleInterface } = require('./interfaces/rule-plugin.interface');
const { isValidCategory, getValidCategories, getDefaultCategory, normalizeCategory } = require('./constants/categories');
class PluginManager {
  constructor() {
    this.plugins = new Map();
    this.customRules = new Map();
    this.loadedEngines = new Set();
    this.verbose = false;
  }
  /**
   * Initialize plugin manager
   * @param {Object} config - Configuration options
   */
  async initialize(config = {}) {
    this.verbose = config.verbose || false;
    
    // Load core rules first (always loaded)
    await this.loadCoreRules(config);
    
    // Load custom rules from .sunlint.json (additional support)
    await this.loadCustomRules(config);
    
    if (this.verbose) {
      console.log(`🔌 Plugin Manager initialized: ${this.plugins.size} rules loaded`);
    }
  }
  /**
   * Load core rules from rules directory
   * @param {Object} config - Configuration options
   */
  async loadCoreRules(config = {}) {
    const rulesDir = path.resolve(__dirname, '../../rules');
    
    if (!fs.existsSync(rulesDir)) {
      console.warn('⚠️ Rules directory not found');
      return;
    }
    const categories = fs.readdirSync(rulesDir, { withFileTypes: true })
      .filter(dirent => dirent.isDirectory())
      .filter(dirent => !['tests', 'docs', 'utils', 'migration'].includes(dirent.name))
      .map(dirent => dirent.name);
    for (const category of categories) {
      await this.loadCategoryRules(category, rulesDir, config);
    }
  }
  /**
   * Load rules from a category directory
   * @param {string} category - Category name
   * @param {string} rulesDir - Rules directory path
   * @param {Object} config - Configuration options
   */
  async loadCategoryRules(category, rulesDir, config) {
    const categoryPath = path.join(rulesDir, category);
    
    const ruleFolders = fs.readdirSync(categoryPath, { withFileTypes: true })
      .filter(dirent => dirent.isDirectory())
      .map(dirent => dirent.name);
    for (const ruleFolder of ruleFolders) {
      const rulePath = path.join(categoryPath, ruleFolder);
      await this.loadRulePlugin(ruleFolder, rulePath, category, config);
    }
  }
  /**
   * Load a single rule plugin
   * @param {string} ruleId - Rule identifier
   * @param {string} rulePath - Path to rule directory
   * @param {string} category - Rule category
   * @param {Object} config - Configuration options
   */
  async loadRulePlugin(ruleId, rulePath, category, config) {
    try {
      // Try to load semantic rule first
      const semanticPath = path.join(rulePath, 'semantic-analyzer.js');
      if (fs.existsSync(semanticPath)) {
        await this.loadSemanticRule(ruleId, semanticPath, category, config);
        return;
      }
      // Try AST analyzer
      const astPath = path.join(rulePath, 'ast-analyzer.js');
      if (fs.existsSync(astPath)) {
        await this.loadPluginRule(ruleId, astPath, category, 'ast', config);
        return;
      }
      // Try regex analyzer
      const regexPath = path.join(rulePath, 'analyzer.js');
      if (fs.existsSync(regexPath)) {
        await this.loadPluginRule(ruleId, regexPath, category, 'regex', config);
        return;
      }
      if (this.verbose) {
        console.warn(`⚠️ No analyzer found for rule ${ruleId}`);
      }
    } catch (error) {
      if (this.verbose) {
        console.warn(`⚠️ Failed to load rule ${ruleId}:`, error.message);
      }
    }
  }
  /**
   * Load semantic rule plugin
   * @param {string} ruleId - Rule identifier
   * @param {string} analyzerPath - Path to analyzer
   * @param {string} category - Rule category
   * @param {Object} config - Configuration options
   */
  async loadSemanticRule(ruleId, analyzerPath, category, config) {
    try {
      const AnalyzerClass = require(analyzerPath);
      const metadata = await this.loadRuleMetadata(ruleId, path.dirname(analyzerPath));
      
      const plugin = new AnalyzerClass(ruleId, { ...metadata, category, type: 'semantic' });
      
      if (plugin instanceof SemanticRuleInterface) {
        this.registerPlugin(ruleId, plugin, 'semantic');
        
        if (this.verbose) {
          console.log(`🧠 Loaded semantic rule: ${ruleId}`);
        }
      } else {
        throw new Error(`${ruleId} does not implement SemanticRuleInterface`);
      }
    } catch (error) {
      if (this.verbose) {
        console.warn(`⚠️ Failed to load semantic rule ${ruleId}:`, error.message);
      }
    }
  }
  /**
   * Load standard plugin rule
   * @param {string} ruleId - Rule identifier
   * @param {string} analyzerPath - Path to analyzer
   * @param {string} category - Rule category
   * @param {string} type - Analyzer type
   * @param {Object} config - Configuration options
   */
  async loadPluginRule(ruleId, analyzerPath, category, type, config) {
    try {
      const analyzerModule = require(analyzerPath);
      const AnalyzerClass = analyzerModule.default || analyzerModule;
      const metadata = await this.loadRuleMetadata(ruleId, path.dirname(analyzerPath));
      
      // Create plugin wrapper for legacy analyzers
      const plugin = this.createLegacyPluginWrapper(ruleId, AnalyzerClass, {
        ...metadata,
        category,
        type
      });
      
      this.registerPlugin(ruleId, plugin, type);
      
      if (this.verbose) {
        console.log(`🔧 Loaded ${type} rule: ${ruleId}`);
      }
    } catch (error) {
      if (this.verbose) {
        console.warn(`⚠️ Failed to load ${type} rule ${ruleId}:`, error.message);
      }
    }
  }
  /**
   * Create plugin wrapper for legacy analyzers
   * @param {string} ruleId - Rule identifier
   * @param {Function|Object} analyzer - Analyzer class or instance
   * @param {Object} metadata - Rule metadata
   * @returns {RulePluginInterface} Plugin wrapper
   */
  createLegacyPluginWrapper(ruleId, analyzer, metadata) {
    return new class extends RulePluginInterface {
      constructor() {
        super(ruleId, metadata);
        this.analyzer = typeof analyzer === 'function' ? new analyzer() : analyzer;
      }
      async initialize(config = {}) {
        if (this.analyzer.initialize) {
          await this.analyzer.initialize(config);
        }
      }
      async analyze(files, language, options = {}) {
        if (!this.analyzer.analyze) {
          throw new Error(`Analyzer for ${ruleId} missing analyze method`);
        }
        
        return await this.analyzer.analyze(files, language, options);
      }
    }();
  }
  /**
   * Load rule metadata from config.json
   * @param {string} ruleId - Rule identifier
   * @param {string} rulePath - Rule directory path
   * @returns {Object} Rule metadata
   */
  async loadRuleMetadata(ruleId, rulePath) {
    const configPath = path.join(rulePath, 'config.json');
    
    if (fs.existsSync(configPath)) {
      try {
        return require(configPath);
      } catch (error) {
        if (this.verbose) {
          console.warn(`⚠️ Failed to load config for ${ruleId}:`, error.message);
        }
      }
    }
    return {
      name: ruleId,
      description: `Analysis for rule ${ruleId}`,
      severity: 'warning'
    };
  }
  /**
   * Load custom rules from .sunlint.json
   * @param {Object} config - Configuration options
   */
  async loadCustomRules(config = {}) {
    const configPath = path.resolve(process.cwd(), '.sunlint.json');
    
    if (!fs.existsSync(configPath)) {
      return;
    }
    try {
      const sunlintConfig = require(configPath);
      
      // Support both new and legacy config formats
      const customRules = sunlintConfig.customRules || 
                         sunlintConfig.custom || 
                         {}; // Default to empty if no custom rules
      for (const [ruleId, ruleConfig] of Object.entries(customRules)) {
        await this.loadCustomRule(ruleId, ruleConfig, config);
      }
      if (this.verbose && Object.keys(customRules).length > 0) {
        console.log(`📋 Loaded ${Object.keys(customRules).length} custom rules from config`);
      }
    } catch (error) {
      if (this.verbose) {
        console.warn(`⚠️ Failed to load custom rules config:`, error.message);
      }
    }
  }
  /**
   * Load a custom rule
   * @param {string} ruleId - Rule identifier
   * @param {Object} ruleConfig - Rule configuration
   * @param {Object} config - Global configuration
   */
  async loadCustomRule(ruleId, ruleConfig, config) {
    try {
      if (!ruleConfig.path) {
        throw new Error(`Custom rule ${ruleId} missing path`);
      }
      // Validate and normalize category
      const originalCategory = ruleConfig.category;
      ruleConfig.category = normalizeCategory(ruleConfig.category);
      
      if (originalCategory && originalCategory !== ruleConfig.category) {
        console.warn(`⚠️ Invalid category '${originalCategory}' for rule ${ruleId}. Valid categories: ${getValidCategories().join(', ')}`);
        console.warn(`   Auto-corrected to: '${ruleConfig.category}'`);
      }
      const rulePath = path.resolve(process.cwd(), ruleConfig.path);
      
      if (!fs.existsSync(rulePath)) {
        throw new Error(`Custom rule file not found: ${rulePath}`);
      }
      const CustomRuleClass = require(rulePath);
      const plugin = new CustomRuleClass(ruleId, ruleConfig);
      if (!(plugin instanceof CustomRuleInterface)) {
        throw new Error(`Custom rule ${ruleId} must extend CustomRuleInterface`);
      }
      this.registerPlugin(ruleId, plugin, 'custom');
      this.customRules.set(ruleId, { path: rulePath, config: ruleConfig });
      if (this.verbose) {
        console.log(`🎨 Loaded custom rule: ${ruleId} (category: ${ruleConfig.category || 'common'})`);
      }
    } catch (error) {
      if (this.verbose) {
        console.warn(`⚠️ Failed to load custom rule ${ruleId}:`, error.message);
      }
    }
  }
  /**
   * Register a plugin
   * @param {string} ruleId - Rule identifier
   * @param {RulePluginInterface} plugin - Plugin instance
   * @param {string} type - Plugin type
   */
  registerPlugin(ruleId, plugin, type) {
    this.plugins.set(ruleId, {
      plugin,
      type,
      engines: [], // Will be populated when engines request rules
      metadata: plugin.getMetadata()
    });
  }
  /**
   * Get rules for a specific engine
   * @param {string} engineName - Engine name
   * @param {Object} config - Configuration options
   * @returns {Map} Map of rule ID to plugin info
   */
  async loadRulesForEngine(engineName, config = {}) {
    const engineRules = new Map();
    for (const [ruleId, pluginInfo] of this.plugins) {
      // Check if rule is compatible with engine
      if (this.isRuleCompatibleWithEngine(ruleId, engineName, pluginInfo)) {
        engineRules.set(ruleId, {
          plugin: pluginInfo.plugin,
          type: pluginInfo.type,
          metadata: pluginInfo.metadata
        });
        // Track which engines use this rule
        if (!pluginInfo.engines.includes(engineName)) {
          pluginInfo.engines.push(engineName);
        }
      }
    }
    this.loadedEngines.add(engineName);
    return engineRules;
  }
  /**
   * Check if rule is compatible with engine
   * @param {string} ruleId - Rule identifier
   * @param {string} engineName - Engine name
   * @param {Object} pluginInfo - Plugin information
   * @returns {boolean} True if compatible
   */
  isRuleCompatibleWithEngine(ruleId, engineName, pluginInfo) {
    const { type, plugin } = pluginInfo;
    // Engine compatibility rules
    const compatibility = {
      heuristic: ['semantic', 'ast', 'regex', 'custom'],
      eslint: ['eslint', 'custom'],
      openai: ['semantic', 'custom']
    };
    return compatibility[engineName]?.includes(type) || false;
  }
  /**
   * Get plugin information
   * @param {string} ruleId - Rule identifier
   * @returns {Object|null} Plugin information
   */
  getPlugin(ruleId) {
    return this.plugins.get(ruleId) || null;
  }
  /**
   * Get all loaded plugins
   * @returns {Map} All plugins
   */
  getAllPlugins() {
    return this.plugins;
  }
  /**
   * Reload custom rules (for hot-reload during development)
   * @param {Object} config - Configuration options
   */
  async reloadCustomRules(config = {}) {
    // Clear existing custom rules
    for (const [ruleId, pluginInfo] of this.plugins) {
      if (pluginInfo.type === 'custom') {
        this.plugins.delete(ruleId);
      }
    }
    this.customRules.clear();
    // Reload custom rules
    await this.loadCustomRules(config);
    if (this.verbose) {
      const customCount = Array.from(this.plugins.values()).filter(p => p.type === 'custom').length;
      console.log(`🔄 Reloaded ${customCount} custom rules`);
    }
  }
  /**
   * Cleanup plugin manager
   */
  async cleanup() {
    for (const [ruleId, pluginInfo] of this.plugins) {
      try {
        await pluginInfo.plugin.cleanup();
      } catch (error) {
        // Ignore cleanup errors
      }
    }
    this.plugins.clear();
    this.customRules.clear();
    this.loadedEngines.clear();
    if (this.verbose) {
      console.log('🔌 Plugin Manager cleanup completed');
    }
  }
}
module.exports = PluginManager;