@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
JavaScript
/**
* 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
};