UNPKG

@neurolint/cli

Version:

NeuroLint CLI for React/Next.js modernization with advanced 6-layer orchestration and intelligent AST transformations

745 lines (628 loc) 19.8 kB
const fs = require('fs-extra'); const path = require('path'); const crypto = require('crypto'); const { EventEmitter } = require('events'); /** * Performance Cache Manager for NeuroLint * Provides intelligent caching for analysis results, AST parsing, and transformations */ class CacheManager extends EventEmitter { constructor(config = {}) { super(); this.config = { cacheDir: config.cacheDir || path.join(process.cwd(), '.neurolint', 'cache'), maxCacheSize: config.maxCacheSize || 100 * 1024 * 1024, // 100MB maxAge: config.maxAge || 7 * 24 * 60 * 60 * 1000, // 7 days compressionEnabled: config.compressionEnabled !== false, persistentCache: config.persistentCache !== false, memoryCache: config.memoryCache !== false, maxMemoryItems: config.maxMemoryItems || 1000, hashAlgorithm: config.hashAlgorithm || 'sha256', version: config.version || '1.0.0' }; // In-memory cache for hot data this.memoryCache = new Map(); this.memoryStats = { hits: 0, misses: 0, size: 0 }; // Cache metadata this.metadata = { version: this.config.version, created: Date.now(), lastCleanup: Date.now(), totalFiles: 0, totalSize: 0 }; this.initialized = false; } /** * Initialize cache manager */ async initialize() { if (this.initialized) return; try { // Ensure cache directory exists if (this.config.persistentCache) { await fs.ensureDir(this.config.cacheDir); await this.loadMetadata(); await this.performMaintenance(); } this.initialized = true; this.emit('initialized'); // Schedule periodic maintenance this.setupMaintenance(); } catch (error) { console.warn('Cache initialization failed:', error.message); this.config.persistentCache = false; // Fallback to memory-only } } /** * Generate cache key from content and options */ generateKey(content, options = {}) { const hash = crypto.createHash(this.config.hashAlgorithm); // Include content hash hash.update(content); // Include options that affect the result const normalizedOptions = this.normalizeOptions(options); hash.update(JSON.stringify(normalizedOptions)); // Include version for cache invalidation hash.update(this.config.version); return hash.digest('hex'); } /** * Cache analysis results */ async cacheAnalysisResult(filePath, code, layers, result, options = {}) { const key = this.generateAnalysisKey(filePath, code, layers, options); const cacheData = { type: 'analysis', filePath, result, layers, options, timestamp: Date.now(), version: this.config.version, fileHash: this.generateContentHash(code), size: JSON.stringify(result).length }; await this.set(key, cacheData); this.emit('analysis-cached', { key, filePath, size: cacheData.size }); } /** * Get cached analysis result */ async getCachedAnalysisResult(filePath, code, layers, options = {}) { const key = this.generateAnalysisKey(filePath, code, layers, options); const cached = await this.get(key); if (!cached || cached.type !== 'analysis') { return null; } // Verify file hasn't changed const currentHash = this.generateContentHash(code); if (cached.fileHash !== currentHash) { await this.delete(key); this.emit('cache-invalidated', { key, reason: 'file-changed' }); return null; } this.emit('analysis-cache-hit', { key, filePath }); return cached.result; } /** * Cache AST parsing results */ async cacheAST(code, filename, ast, parseOptions = {}) { const key = this.generateASTKey(code, filename, parseOptions); const cacheData = { type: 'ast', filename, ast: this.serializeAST(ast), parseOptions, timestamp: Date.now(), version: this.config.version, codeHash: this.generateContentHash(code), size: JSON.stringify(ast).length }; await this.set(key, cacheData); this.emit('ast-cached', { key, filename, size: cacheData.size }); } /** * Get cached AST */ async getCachedAST(code, filename, parseOptions = {}) { const key = this.generateASTKey(code, filename, parseOptions); const cached = await this.get(key); if (!cached || cached.type !== 'ast') { return null; } // Verify code hasn't changed const currentHash = this.generateContentHash(code); if (cached.codeHash !== currentHash) { await this.delete(key); this.emit('cache-invalidated', { key, reason: 'code-changed' }); return null; } this.emit('ast-cache-hit', { key, filename }); return this.deserializeAST(cached.ast); } /** * Cache transformation results */ async cacheTransformations(ast, layerId, transformations, context = {}) { const key = this.generateTransformationKey(ast, layerId, context); const cacheData = { type: 'transformations', layerId, transformations, context, timestamp: Date.now(), version: this.config.version, astHash: this.generateASTHash(ast), size: JSON.stringify(transformations).length }; await this.set(key, cacheData); this.emit('transformations-cached', { key, layerId, count: transformations.length }); } /** * Get cached transformations */ async getCachedTransformations(ast, layerId, context = {}) { const key = this.generateTransformationKey(ast, layerId, context); const cached = await this.get(key); if (!cached || cached.type !== 'transformations') { return null; } // Verify AST hasn't changed (simplified check) const currentHash = this.generateASTHash(ast); if (cached.astHash !== currentHash) { await this.delete(key); this.emit('cache-invalidated', { key, reason: 'ast-changed' }); return null; } this.emit('transformations-cache-hit', { key, layerId }); return cached.transformations; } /** * Cache plugin results */ async cachePluginResult(pluginId, input, result, context = {}) { const key = this.generatePluginKey(pluginId, input, context); const cacheData = { type: 'plugin', pluginId, result, context, timestamp: Date.now(), version: this.config.version, inputHash: this.generateContentHash(JSON.stringify(input)), size: JSON.stringify(result).length }; await this.set(key, cacheData); this.emit('plugin-cached', { key, pluginId, size: cacheData.size }); } /** * Get cached plugin result */ async getCachedPluginResult(pluginId, input, context = {}) { const key = this.generatePluginKey(pluginId, input, context); const cached = await this.get(key); if (!cached || cached.type !== 'plugin') { return null; } // Verify input hasn't changed const currentHash = this.generateContentHash(JSON.stringify(input)); if (cached.inputHash !== currentHash) { await this.delete(key); this.emit('cache-invalidated', { key, reason: 'input-changed' }); return null; } this.emit('plugin-cache-hit', { key, pluginId }); return cached.result; } /** * Low-level cache operations */ async set(key, data) { // Store in memory cache if (this.config.memoryCache) { this.setMemoryCache(key, data); } // Store in persistent cache if (this.config.persistentCache) { await this.setPersistentCache(key, data); } } async get(key) { // Try memory cache first if (this.config.memoryCache) { const memoryResult = this.getMemoryCache(key); if (memoryResult) { this.memoryStats.hits++; return memoryResult; } this.memoryStats.misses++; } // Try persistent cache if (this.config.persistentCache) { const persistentResult = await this.getPersistentCache(key); if (persistentResult) { // Populate memory cache for next time if (this.config.memoryCache) { this.setMemoryCache(key, persistentResult); } return persistentResult; } } return null; } async delete(key) { // Remove from memory cache if (this.config.memoryCache) { this.memoryCache.delete(key); this.updateMemoryStats(); } // Remove from persistent cache if (this.config.persistentCache) { const filePath = this.getCacheFilePath(key); if (await fs.pathExists(filePath)) { await fs.remove(filePath); } } } /** * Memory cache operations */ setMemoryCache(key, data) { // Check if we need to evict items if (this.memoryCache.size >= this.config.maxMemoryItems) { this.evictOldestItems(Math.floor(this.config.maxMemoryItems * 0.1)); } this.memoryCache.set(key, { data, timestamp: Date.now(), accessCount: 1, lastAccess: Date.now() }); this.updateMemoryStats(); } getMemoryCache(key) { const cached = this.memoryCache.get(key); if (!cached) return null; // Check if expired if (Date.now() - cached.timestamp > this.config.maxAge) { this.memoryCache.delete(key); this.updateMemoryStats(); return null; } // Update access statistics cached.accessCount++; cached.lastAccess = Date.now(); return cached.data; } evictOldestItems(count) { const entries = Array.from(this.memoryCache.entries()) .sort((a, b) => a[1].lastAccess - b[1].lastAccess) .slice(0, count); entries.forEach(([key]) => { this.memoryCache.delete(key); }); this.updateMemoryStats(); } updateMemoryStats() { this.memoryStats.size = this.memoryCache.size; } /** * Persistent cache operations */ async setPersistentCache(key, data) { try { const filePath = this.getCacheFilePath(key); const serialized = this.config.compressionEnabled ? await this.compress(JSON.stringify(data)) : JSON.stringify(data); await fs.writeFile(filePath, serialized); this.metadata.totalFiles++; this.metadata.totalSize += serialized.length; } catch (error) { console.warn(`Failed to write cache file ${key}:`, error.message); } } async getPersistentCache(key) { try { const filePath = this.getCacheFilePath(key); if (!(await fs.pathExists(filePath))) { return null; } const stats = await fs.stat(filePath); // Check if expired if (Date.now() - stats.mtime.getTime() > this.config.maxAge) { await fs.remove(filePath); return null; } const content = await fs.readFile(filePath, 'utf8'); const data = this.config.compressionEnabled ? JSON.parse(await this.decompress(content)) : JSON.parse(content); return data; } catch (error) { console.warn(`Failed to read cache file ${key}:`, error.message); return null; } } getCacheFilePath(key) { // Create nested directory structure to avoid too many files in one dir const prefix = key.slice(0, 2); const dir = path.join(this.config.cacheDir, prefix); return path.join(dir, `${key}.json`); } /** * Cache key generation methods */ generateAnalysisKey(filePath, code, layers, options) { const content = JSON.stringify({ filePath, codeHash: this.generateContentHash(code), layers, options: this.normalizeOptions(options) }); return `analysis_${this.generateKey(content)}`; } generateASTKey(code, filename, parseOptions) { const content = JSON.stringify({ codeHash: this.generateContentHash(code), filename, parseOptions: this.normalizeOptions(parseOptions) }); return `ast_${this.generateKey(content)}`; } generateTransformationKey(ast, layerId, context) { const content = JSON.stringify({ astHash: this.generateASTHash(ast), layerId, context: this.normalizeOptions(context) }); return `transform_${this.generateKey(content)}`; } generatePluginKey(pluginId, input, context) { const content = JSON.stringify({ pluginId, inputHash: this.generateContentHash(JSON.stringify(input)), context: this.normalizeOptions(context) }); return `plugin_${this.generateKey(content)}`; } /** * Utility methods */ generateContentHash(content) { return crypto.createHash('md5').update(content).digest('hex'); } generateASTHash(ast) { // Generate hash from AST structure (simplified) try { return this.generateContentHash(JSON.stringify(ast, (key, value) => { // Exclude location information from hash if (key === 'loc' || key === 'start' || key === 'end') { return undefined; } return value; })); } catch (error) { return this.generateContentHash(String(ast)); } } normalizeOptions(options) { if (!options || typeof options !== 'object') { return {}; } // Sort keys for consistent hashing const sorted = {}; Object.keys(options).sort().forEach(key => { sorted[key] = options[key]; }); return sorted; } serializeAST(ast) { // In a real implementation, might use more efficient serialization return ast; } deserializeAST(data) { return data; } async compress(data) { // Simplified compression - in real implementation would use zlib return data; } async decompress(data) { return data; } /** * Maintenance operations */ async performMaintenance() { if (!this.config.persistentCache) return; try { await this.cleanExpiredFiles(); await this.enforceMaxCacheSize(); await this.saveMetadata(); this.metadata.lastCleanup = Date.now(); this.emit('maintenance-completed'); } catch (error) { console.warn('Cache maintenance failed:', error.message); } } async cleanExpiredFiles() { const cacheDir = this.config.cacheDir; const maxAge = this.config.maxAge; let cleanedCount = 0; let reclaimedSize = 0; const cleanDirectory = async (dir) => { const items = await fs.readdir(dir); for (const item of items) { const itemPath = path.join(dir, item); const stats = await fs.stat(itemPath); if (stats.isDirectory()) { await cleanDirectory(itemPath); } else if (stats.isFile() && item.endsWith('.json')) { const age = Date.now() - stats.mtime.getTime(); if (age > maxAge) { reclaimedSize += stats.size; await fs.remove(itemPath); cleanedCount++; } } } }; if (await fs.pathExists(cacheDir)) { await cleanDirectory(cacheDir); } this.metadata.totalFiles -= cleanedCount; this.metadata.totalSize -= reclaimedSize; if (cleanedCount > 0) { this.emit('cache-cleaned', { cleanedCount, reclaimedSize }); } } async enforceMaxCacheSize() { if (this.metadata.totalSize <= this.config.maxCacheSize) { return; } // Get all cache files with stats const files = []; const cacheDir = this.config.cacheDir; const collectFiles = async (dir) => { const items = await fs.readdir(dir); for (const item of items) { const itemPath = path.join(dir, item); const stats = await fs.stat(itemPath); if (stats.isDirectory()) { await collectFiles(itemPath); } else if (stats.isFile() && item.endsWith('.json')) { files.push({ path: itemPath, mtime: stats.mtime.getTime(), size: stats.size }); } } }; if (await fs.pathExists(cacheDir)) { await collectFiles(cacheDir); } // Sort by modification time (oldest first) files.sort((a, b) => a.mtime - b.mtime); // Remove files until under size limit let currentSize = this.metadata.totalSize; let removedCount = 0; for (const file of files) { if (currentSize <= this.config.maxCacheSize) { break; } await fs.remove(file.path); currentSize -= file.size; removedCount++; } this.metadata.totalFiles -= removedCount; this.metadata.totalSize = currentSize; if (removedCount > 0) { this.emit('cache-size-enforced', { removedCount, newSize: currentSize }); } } setupMaintenance() { // Run maintenance every hour setInterval(() => { this.performMaintenance().catch(error => { console.warn('Scheduled maintenance failed:', error.message); }); }, 60 * 60 * 1000); } /** * Metadata operations */ async loadMetadata() { const metadataFile = path.join(this.config.cacheDir, 'metadata.json'); try { if (await fs.pathExists(metadataFile)) { const data = await fs.readJson(metadataFile); this.metadata = { ...this.metadata, ...data }; } } catch (error) { console.warn('Failed to load cache metadata:', error.message); } } async saveMetadata() { const metadataFile = path.join(this.config.cacheDir, 'metadata.json'); try { await fs.writeJson(metadataFile, this.metadata, { spaces: 2 }); } catch (error) { console.warn('Failed to save cache metadata:', error.message); } } /** * Cache statistics and management */ getStatistics() { return { initialized: this.initialized, config: this.config, metadata: this.metadata, memoryStats: this.memoryStats, persistentCache: { enabled: this.config.persistentCache, directory: this.config.cacheDir, size: this.metadata.totalSize, files: this.metadata.totalFiles }, memoryCache: { enabled: this.config.memoryCache, size: this.memoryStats.size, maxItems: this.config.maxMemoryItems, hitRate: this.memoryStats.hits / (this.memoryStats.hits + this.memoryStats.misses) || 0 } }; } async clearCache() { // Clear memory cache this.memoryCache.clear(); this.updateMemoryStats(); // Clear persistent cache if (this.config.persistentCache && await fs.pathExists(this.config.cacheDir)) { await fs.remove(this.config.cacheDir); await fs.ensureDir(this.config.cacheDir); } // Reset metadata this.metadata = { version: this.config.version, created: Date.now(), lastCleanup: Date.now(), totalFiles: 0, totalSize: 0 }; await this.saveMetadata(); this.emit('cache-cleared'); } async warmupCache(filePaths = []) { // Pre-populate cache with commonly used files for (const filePath of filePaths) { try { if (await fs.pathExists(filePath)) { const code = await fs.readFile(filePath, 'utf8'); // Trigger parsing and caching // This would be called from the main engine } } catch (error) { console.warn(`Failed to warmup cache for ${filePath}:`, error.message); } } } /** * Cleanup resources */ async cleanup() { await this.saveMetadata(); this.memoryCache.clear(); this.emit('cleanup'); } } module.exports = { CacheManager };