UNPKG

@sun-asterisk/sunlint

Version:

☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards

394 lines (351 loc) 11.1 kB
/** * Language Analyzer Interface * Defines the contract for all language-specific analyzers in SunLint * Following Rule C014: Dependency injection - Plugin interface * Following Rule C015: Use domain language - clear interface naming * * This interface enables multi-language support by allowing different * analyzers (TypeScript, Dart, etc.) to be registered and used interchangeably. */ /** * @typedef {Object} AnalyzerConfig * @property {string} projectPath - Root path of the project * @property {string[]} [targetFiles] - Specific files to analyze * @property {number} [maxFiles] - Maximum files to load for analysis * @property {boolean} [verbose] - Enable verbose logging * @property {Object} [compilerOptions] - Language-specific compiler options */ /** * @typedef {Object} Violation * @property {string} ruleId - The rule ID that was violated * @property {string} filePath - Absolute path to the file * @property {number} line - Line number (1-based) * @property {number} column - Column number (1-based) * @property {string} message - Human-readable violation message * @property {string} [severity] - 'error' | 'warning' | 'info' * @property {string} [analysisMethod] - 'ast' | 'regex' | 'semantic' * @property {number} [confidence] - Confidence score (0-100) */ /** * @typedef {Object} SymbolTable * @property {string} filePath - Absolute path to the file * @property {string} fileName - Base name of the file * @property {Array} imports - Import declarations * @property {Array} exports - Export declarations * @property {Array} functions - Function declarations * @property {Array} classes - Class declarations * @property {Array} interfaces - Interface declarations * @property {Array} variables - Variable declarations * @property {Array} constants - Constant declarations * @property {Array} functionCalls - Function call expressions * @property {Array} methodCalls - Method call expressions * @property {number} lastModified - Timestamp * @property {number} analysisTime - Analysis duration in ms */ /** * ILanguageAnalyzer Interface * All language analyzers must implement this interface */ class ILanguageAnalyzer { /** * Constructor for Language Analyzer * @param {string} name - Analyzer name (e.g., 'typescript', 'dart') * @param {string[]} extensions - Supported file extensions (e.g., ['.ts', '.tsx']) */ constructor(name, extensions = []) { if (this.constructor === ILanguageAnalyzer) { throw new Error('ILanguageAnalyzer is abstract and cannot be instantiated directly'); } this.name = name; this.extensions = extensions; this.initialized = false; this.stats = { filesAnalyzed: 0, cacheHits: 0, cacheMisses: 0, totalAnalysisTime: 0 }; } /** * Initialize the language analyzer * @param {AnalyzerConfig} config - Analyzer configuration * @returns {Promise<boolean>} - True if initialization succeeded */ async initialize(config) { throw new Error(`Method initialize() must be implemented by ${this.constructor.name}`); } /** * Analyze a single file and return violations * @param {string} filePath - Absolute path to the file * @param {Object[]} rules - Array of rule objects to apply * @param {Object} [options] - Analysis options * @returns {Promise<Violation[]>} - Array of violations found */ async analyzeFile(filePath, rules, options = {}) { throw new Error(`Method analyzeFile() must be implemented by ${this.constructor.name}`); } /** * Analyze multiple files * @param {string[]} files - Array of file paths * @param {Object[]} rules - Array of rule objects to apply * @param {Object} [options] - Analysis options * @returns {Promise<Violation[]>} - Array of violations found */ async analyzeFiles(files, rules, options = {}) { const violations = []; for (const file of files) { if (this.supportsFile(file)) { const fileViolations = await this.analyzeFile(file, rules, options); violations.push(...fileViolations); } } return violations; } /** * Get the Symbol Table for a file * Used by semantic rules for cross-file analysis * @param {string} filePath - Absolute path to the file * @returns {Promise<SymbolTable|null>} - Symbol table or null if not available */ async getSymbolTable(filePath) { throw new Error(`Method getSymbolTable() must be implemented by ${this.constructor.name}`); } /** * Check if analyzer is ready for analysis * @returns {boolean} - True if initialized and ready */ isReady() { return this.initialized; } /** * Check if this analyzer supports a specific file * @param {string} filePath - Path to the file * @returns {boolean} - True if file is supported */ supportsFile(filePath) { const ext = require('path').extname(filePath).toLowerCase(); return this.extensions.includes(ext); } /** * Check if this analyzer supports a specific file extension * @param {string} extension - File extension (e.g., '.ts') * @returns {boolean} - True if extension is supported */ supportsExtension(extension) { return this.extensions.includes(extension.toLowerCase()); } /** * Get analyzer metadata * @returns {Object} - Analyzer metadata */ getInfo() { return { name: this.name, extensions: this.extensions, initialized: this.initialized, stats: { ...this.stats } }; } /** * Get analysis statistics * @returns {Object} - Statistics object */ getStats() { return { ...this.stats }; } /** * Reset statistics */ resetStats() { this.stats = { filesAnalyzed: 0, cacheHits: 0, cacheMisses: 0, totalAnalysisTime: 0 }; } /** * Cleanup resources * Called when the analyzer is no longer needed * @returns {Promise<void>} */ async dispose() { this.initialized = false; } } /** * Language Analyzer Registry * Manages registration and lookup of language analyzers */ class LanguageAnalyzerRegistry { constructor() { this.analyzers = new Map(); this.extensionMap = new Map(); // extension -> analyzer name } /** * Register a language analyzer * @param {ILanguageAnalyzer} analyzer - Analyzer instance */ register(analyzer) { if (!(analyzer instanceof ILanguageAnalyzer)) { throw new Error('Analyzer must extend ILanguageAnalyzer'); } this.analyzers.set(analyzer.name, analyzer); // Map extensions to analyzer for (const ext of analyzer.extensions) { this.extensionMap.set(ext.toLowerCase(), analyzer.name); } // Silent registration - only log in verbose mode } /** * Unregister a language analyzer * @param {string} name - Analyzer name */ unregister(name) { const analyzer = this.analyzers.get(name); if (analyzer) { // Remove extension mappings for (const ext of analyzer.extensions) { if (this.extensionMap.get(ext) === name) { this.extensionMap.delete(ext); } } this.analyzers.delete(name); } } /** * Get analyzer by name * @param {string} name - Analyzer name * @returns {ILanguageAnalyzer|null} - Analyzer instance or null */ get(name) { return this.analyzers.get(name) || null; } /** * Get analyzer for a file * @param {string} filePath - Path to the file * @returns {ILanguageAnalyzer|null} - Analyzer instance or null */ getForFile(filePath) { const ext = require('path').extname(filePath).toLowerCase(); const analyzerName = this.extensionMap.get(ext); return analyzerName ? this.analyzers.get(analyzerName) : null; } /** * Get analyzer for an extension * @param {string} extension - File extension * @returns {ILanguageAnalyzer|null} - Analyzer instance or null */ getForExtension(extension) { const analyzerName = this.extensionMap.get(extension.toLowerCase()); return analyzerName ? this.analyzers.get(analyzerName) : null; } /** * Get all registered analyzers * @returns {ILanguageAnalyzer[]} - Array of analyzer instances */ getAll() { return Array.from(this.analyzers.values()); } /** * Get all registered analyzer names * @returns {string[]} - Array of analyzer names */ getNames() { return Array.from(this.analyzers.keys()); } /** * Check if an analyzer is registered * @param {string} name - Analyzer name * @returns {boolean} - True if registered */ has(name) { return this.analyzers.has(name); } /** * Check if a file is supported by any analyzer * @param {string} filePath - Path to the file * @returns {boolean} - True if supported */ supportsFile(filePath) { const ext = require('path').extname(filePath).toLowerCase(); return this.extensionMap.has(ext); } /** * Get all supported extensions * @returns {string[]} - Array of supported extensions */ getSupportedExtensions() { return Array.from(this.extensionMap.keys()); } /** * Initialize all registered analyzers * @param {AnalyzerConfig} config - Configuration for all analyzers * @returns {Promise<Map<string, boolean>>} - Map of analyzer name -> initialization success */ async initializeAll(config) { const results = new Map(); for (const [name, analyzer] of this.analyzers) { try { const success = await analyzer.initialize(config); results.set(name, success); } catch (error) { console.error(`Failed to initialize ${name} analyzer:`, error.message); results.set(name, false); } } return results; } /** * Dispose all registered analyzers * @returns {Promise<void>} */ async disposeAll() { for (const analyzer of this.analyzers.values()) { try { await analyzer.dispose(); } catch (error) { console.error(`Error disposing ${analyzer.name} analyzer:`, error.message); } } } /** * Get combined statistics from all analyzers * @returns {Object} - Combined statistics */ getCombinedStats() { const combined = { totalFilesAnalyzed: 0, totalCacheHits: 0, totalCacheMisses: 0, totalAnalysisTime: 0, byAnalyzer: {} }; for (const [name, analyzer] of this.analyzers) { const stats = analyzer.getStats(); combined.totalFilesAnalyzed += stats.filesAnalyzed; combined.totalCacheHits += stats.cacheHits; combined.totalCacheMisses += stats.cacheMisses; combined.totalAnalysisTime += stats.totalAnalysisTime; combined.byAnalyzer[name] = stats; } return combined; } } // Singleton instance of the registry let registryInstance = null; /** * Get the singleton registry instance * @returns {LanguageAnalyzerRegistry} */ function getRegistry() { if (!registryInstance) { registryInstance = new LanguageAnalyzerRegistry(); } return registryInstance; } module.exports = { ILanguageAnalyzer, LanguageAnalyzerRegistry, getRegistry };