@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;