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