UNPKG

@sun-asterisk/sunlint

Version:

โ˜€๏ธ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards

376 lines (326 loc) โ€ข 10.7 kB
/** * Semantic Engine Manager * Manages multiple language analyzers and provides unified access to symbol tables * Following Rule C005: Single responsibility - manage language analyzers * Following Rule C006: Verb-noun naming pattern * Following Rule C014: Dependency injection - uses language analyzer interface * * This manager enables multi-language semantic analysis by: * 1. Registering language-specific analyzers (TypeScript, Dart, etc.) * 2. Routing file analysis to the appropriate analyzer * 3. Providing unified access to symbol tables across languages */ const path = require('path'); const { getRegistry } = require('./interfaces/language-analyzer.interface'); class SemanticEngineManager { constructor(options = {}) { this.options = { verbose: options.verbose || false, maxSemanticFiles: options.maxSemanticFiles, ...options }; // Get the singleton registry this.registry = getRegistry(); this.initialized = false; this.projectPath = null; this.targetFiles = null; // Statistics this.stats = { totalFilesAnalyzed: 0, filesByLanguage: {}, analysisTime: 0, initializationTime: 0 }; } /** * Register a language analyzer * @param {ILanguageAnalyzer} analyzer - Analyzer instance */ registerAnalyzer(analyzer) { this.registry.register(analyzer); if (this.options.verbose) { console.log(`๐Ÿ“ SemanticEngineManager: Registered ${analyzer.name} analyzer`); } } /** * Unregister a language analyzer * @param {string} name - Analyzer name */ unregisterAnalyzer(name) { this.registry.unregister(name); if (this.options.verbose) { console.log(`๐Ÿ“ SemanticEngineManager: Unregistered ${name} analyzer`); } } /** * Initialize all registered analyzers * @param {string} projectPath - Project root path * @param {string[]} [targetFiles] - Specific files to analyze * @returns {Promise<boolean>} - True if at least one analyzer initialized successfully */ async initialize(projectPath, targetFiles = null) { const startTime = Date.now(); this.projectPath = projectPath; this.targetFiles = targetFiles; if (this.options.verbose) { console.log(`๐Ÿš€ SemanticEngineManager: Initializing with ${this.registry.getNames().length} analyzers`); } // Group target files by language for each analyzer const filesByAnalyzer = this.groupFilesByAnalyzer(targetFiles); // Initialize each analyzer with its relevant files const config = { projectPath, verbose: this.options.verbose, maxSemanticFiles: this.options.maxSemanticFiles, compilerOptions: this.options.compilerOptions }; let anySuccess = false; for (const [analyzerName, files] of filesByAnalyzer) { const analyzer = this.registry.get(analyzerName); if (!analyzer) continue; try { const analyzerConfig = { ...config, targetFiles: files }; const success = await analyzer.initialize(analyzerConfig); if (success) { anySuccess = true; if (this.options.verbose) { console.log(`โœ… ${analyzerName} analyzer initialized with ${files.length} files`); } } } catch (error) { console.error(`โŒ Failed to initialize ${analyzerName} analyzer:`, error.message); } } // Also initialize analyzers that don't have matching files // (they might be needed for on-demand analysis) for (const analyzer of this.registry.getAll()) { if (!filesByAnalyzer.has(analyzer.name)) { try { const success = await analyzer.initialize(config); if (success) { anySuccess = true; } } catch (error) { // Silently skip - no files for this analyzer } } } this.initialized = anySuccess; this.stats.initializationTime = Date.now() - startTime; if (this.options.verbose) { console.log(`๐Ÿ”ง SemanticEngineManager initialized in ${this.stats.initializationTime}ms`); console.log(` ๐Ÿ“Š Active analyzers: ${this.registry.getNames().filter(n => this.registry.get(n)?.isReady()).join(', ')}`); } return anySuccess; } /** * Group files by their corresponding analyzer * @param {string[]|null} files - Files to group * @returns {Map<string, string[]>} - Map of analyzer name to files */ groupFilesByAnalyzer(files) { const grouped = new Map(); if (!files) { return grouped; } for (const file of files) { const analyzer = this.registry.getForFile(file); if (analyzer) { if (!grouped.has(analyzer.name)) { grouped.set(analyzer.name, []); } grouped.get(analyzer.name).push(file); } } return grouped; } /** * Get the analyzer for a specific file * @param {string} filePath - Path to the file * @returns {ILanguageAnalyzer|null} - Analyzer or null */ getAnalyzerForFile(filePath) { return this.registry.getForFile(filePath); } /** * Get the Symbol Table for a file * Routes to the appropriate language analyzer * @param {string} filePath - Absolute path to the file * @returns {Promise<Object|null>} - Symbol table or null */ async getSymbolTable(filePath) { const analyzer = this.registry.getForFile(filePath); if (!analyzer || !analyzer.isReady()) { if (this.options.verbose) { console.warn(`โš ๏ธ No ready analyzer for: ${path.basename(filePath)}`); } return null; } try { return await analyzer.getSymbolTable(filePath); } catch (error) { console.error(`โŒ Error getting symbol table for ${filePath}:`, error.message); return null; } } /** * Analyze a single file * @param {string} filePath - Path to the file * @param {Object[]} rules - Rules to apply * @param {Object} [options] - Analysis options * @returns {Promise<Object[]>} - Array of violations */ async analyzeFile(filePath, rules, options = {}) { const analyzer = this.registry.getForFile(filePath); if (!analyzer || !analyzer.isReady()) { if (this.options.verbose) { console.warn(`โš ๏ธ No ready analyzer for: ${path.basename(filePath)}`); } return []; } try { const startTime = Date.now(); const violations = await analyzer.analyzeFile(filePath, rules, options); this.stats.totalFilesAnalyzed++; this.stats.filesByLanguage[analyzer.name] = (this.stats.filesByLanguage[analyzer.name] || 0) + 1; this.stats.analysisTime += Date.now() - startTime; return violations; } catch (error) { console.error(`โŒ Error analyzing ${filePath}:`, error.message); return []; } } /** * Analyze multiple files * Routes each file to its appropriate analyzer * @param {string[]} files - Files to analyze * @param {Object[]} rules - Rules to apply * @param {Object} [options] - Analysis options * @returns {Promise<Object[]>} - Array of violations */ async analyzeFiles(files, rules, options = {}) { const allViolations = []; const filesByAnalyzer = this.groupFilesByAnalyzer(files); for (const [analyzerName, analyzerFiles] of filesByAnalyzer) { const analyzer = this.registry.get(analyzerName); if (!analyzer || !analyzer.isReady()) { continue; } try { const violations = await analyzer.analyzeFiles(analyzerFiles, rules, options); allViolations.push(...violations); this.stats.filesByLanguage[analyzerName] = (this.stats.filesByLanguage[analyzerName] || 0) + analyzerFiles.length; } catch (error) { console.error(`โŒ Error analyzing files with ${analyzerName}:`, error.message); } } this.stats.totalFilesAnalyzed += files.length; return allViolations; } /** * Check if the manager is ready for analysis * @returns {boolean} - True if at least one analyzer is ready */ isReady() { return this.initialized && this.registry.getAll().some(a => a.isReady()); } /** * Check if a file is supported * @param {string} filePath - Path to the file * @returns {boolean} - True if supported */ supportsFile(filePath) { return this.registry.supportsFile(filePath); } /** * Get all supported extensions * @returns {string[]} - Array of extensions */ getSupportedExtensions() { return this.registry.getSupportedExtensions(); } /** * Get analyzer by name * @param {string} name - Analyzer name * @returns {ILanguageAnalyzer|null} - Analyzer or null */ getAnalyzer(name) { return this.registry.get(name); } /** * Get all registered analyzer names * @returns {string[]} - Analyzer names */ getAnalyzerNames() { return this.registry.getNames(); } /** * Get statistics * @returns {Object} - Statistics object */ getStats() { return { ...this.stats, analyzers: this.registry.getCombinedStats() }; } /** * Check if Symbol Table analysis is available * (Backward compatibility with SemanticEngine) * @returns {boolean} - True if available */ isSymbolEngineReady() { return this.isReady(); } /** * Cleanup all analyzers * @returns {Promise<void>} */ async cleanup() { if (this.options.verbose) { console.log(`๐Ÿงน SemanticEngineManager: Cleaning up ${this.registry.getNames().length} analyzers`); } await this.registry.disposeAll(); // Print final stats if (this.options.verbose) { console.log(`๐Ÿ“Š SemanticEngineManager Stats:`); console.log(` ๐Ÿ“„ Files analyzed: ${this.stats.totalFilesAnalyzed}`); console.log(` โฑ๏ธ Total analysis time: ${this.stats.analysisTime}ms`); for (const [lang, count] of Object.entries(this.stats.filesByLanguage)) { console.log(` ๐Ÿ“ ${lang}: ${count} files`); } } this.initialized = false; } } // Singleton instance let managerInstance = null; /** * Get the singleton manager instance * @param {Object} [options] - Options for initialization * @returns {SemanticEngineManager} */ function getManager(options = {}) { if (!managerInstance) { managerInstance = new SemanticEngineManager(options); } return managerInstance; } /** * Reset the singleton manager (for testing) */ function resetManager() { if (managerInstance) { managerInstance.cleanup().catch(console.error); } managerInstance = null; } module.exports = { SemanticEngineManager, getManager, resetManager };