UNPKG

mira-consciousness

Version:

Memory & Intelligence Retention Archive - Preserving The Spark

515 lines 21.2 kB
/** * Unused Code Detectors * Contains all detection logic for different types of unused code */ import fs from 'fs-extra'; import * as path from 'path'; import { glob } from 'glob'; export class UnusedCodeDetectors { projectRoot; callGraph; fileMetadata; entryPoints; constructor(projectRoot, callGraph, fileMetadata, entryPoints) { this.projectRoot = projectRoot; this.callGraph = callGraph; this.fileMetadata = fileMetadata; this.entryPoints = entryPoints; } async findUnusedFiles() { const unusedFiles = []; const reachableFiles = new Set(); // Start from entry points and traverse const toVisit = Array.from(this.entryPoints); const visited = new Set(); while (toVisit.length > 0) { const file = toVisit.pop(); if (visited.has(file)) continue; visited.add(file); reachableFiles.add(file); // Add imported files to visit list const metadata = this.fileMetadata.get(file); if (metadata) { metadata.imports.forEach(importPath => { const resolvedPath = this.resolveImportPath(importPath, file); if (resolvedPath && !visited.has(resolvedPath)) { toVisit.push(resolvedPath); } }); } } // Find unreachable files for (const [file, metadata] of this.fileMetadata.entries()) { if (!reachableFiles.has(file)) { // Additional checks for potential entry points const potentialEntryPoints = this.findPotentialEntryPoints(file); const confidence = this.calculateFileUnusedConfidence(file, potentialEntryPoints); unusedFiles.push({ path: file, size: metadata.size, lastModified: metadata.lastModified, reason: this.getFileUnusedReason(file, potentialEntryPoints), confidence, potentialEntryPoints }); } } return unusedFiles.sort((a, b) => b.size - a.size); } async findUnusedFunctions() { const unusedFunctions = []; for (const [nodeId, node] of this.callGraph.entries()) { if (node.type === 'function' && node.calledBy.size === 0) { // Check if it's exported (might be used externally) const isExported = node.isExported; const isEntryPoint = this.isInEntryPoint(node.file); // Skip if it's in an entry point file or exported (unless we're sure it's unused) if (!isEntryPoint && !isExported) { const complexity = await this.calculateFunctionComplexity(node.file, node.line); const size = await this.calculateFunctionSize(node.file, node.line); unusedFunctions.push({ name: node.name, file: node.file, line: node.line, size, complexity, reason: this.getFunctionUnusedReason(node), confidence: this.calculateFunctionUnusedConfidence(node), calledBy: Array.from(node.calledBy), calls: Array.from(node.calls) }); } } } return unusedFunctions.sort((a, b) => b.size - a.size); } async findUnusedImports() { const unusedImports = []; for (const [file, metadata] of this.fileMetadata.entries()) { const filePath = path.join(this.projectRoot, file); try { const content = await fs.readFile(filePath, 'utf-8'); const imports = this.parseImportStatements(content, metadata.language); for (const importInfo of imports) { const isUsed = this.isImportUsed(content, importInfo.name); if (!isUsed) { unusedImports.push({ name: importInfo.name, file, line: importInfo.line, source: importInfo.source, type: importInfo.type, confidence: this.calculateImportUnusedConfidence(content, importInfo.name) }); } } } catch (error) { // Skip files that can't be read } } return unusedImports.sort((a, b) => a.file.localeCompare(b.file)); } async findUnusedAssets() { const unusedAssets = []; // Find all asset files const assetPatterns = [ '**/*.{png,jpg,jpeg,gif,svg,ico,webp}', '**/*.{woff,woff2,ttf,eot,otf}', '**/*.{mp4,avi,mov,wmv,flv,webm}', '**/*.{mp3,wav,ogg,m4a,aac}', '**/*.{pdf,doc,docx,xls,xlsx,ppt,pptx}', '**/*.{json,xml,csv,txt}' ]; const allAssets = []; for (const pattern of assetPatterns) { const assets = await glob(pattern, { cwd: this.projectRoot, ignore: ['node_modules/**', 'dist/**', 'build/**', '.git/**'], absolute: false }); allAssets.push(...assets); } // Check each asset for usage for (const asset of allAssets) { const references = await this.findAssetReferences(asset); if (references.length === 0) { const assetPath = path.join(this.projectRoot, asset); try { const stats = await fs.stat(assetPath); unusedAssets.push({ path: asset, size: stats.size, type: this.getAssetType(asset), lastModified: stats.mtime, referencedBy: references, confidence: this.calculateAssetUnusedConfidence(asset, references) }); } catch (error) { // Skip if can't read file stats } } } return unusedAssets.sort((a, b) => b.size - a.size); } async findUnusedStyles() { const unusedStyles = []; // Find all CSS/SCSS/LESS files const styleFiles = await glob('**/*.{css,scss,sass,less,styl}', { cwd: this.projectRoot, ignore: ['node_modules/**', 'dist/**', 'build/**'], absolute: false }); // Extract all selectors from style files for (const styleFile of styleFiles) { const filePath = path.join(this.projectRoot, styleFile); try { const content = await fs.readFile(filePath, 'utf-8'); const selectors = this.extractCSSSelectors(content); for (const selector of selectors) { const usage = await this.findSelectorUsage(selector.name); if (usage === 0) { unusedStyles.push({ selector: selector.name, file: styleFile, line: selector.line, type: this.getCSSelectorType(selector.name), usageCount: usage, confidence: this.calculateStyleUnusedConfidence(selector.name, usage) }); } } } catch (error) { // Skip files that can't be read } } return unusedStyles.sort((a, b) => a.file.localeCompare(b.file)); } resolveImportPath(importPath, fromFile) { // Simple import resolution - in practice, this would be much more sophisticated if (importPath.startsWith('.')) { // Relative import const dir = path.dirname(fromFile); const resolved = path.resolve(dir, importPath); // Try different extensions const extensions = ['.js', '.ts', '.jsx', '.tsx', '.json']; for (const ext of extensions) { const withExt = resolved + ext; if (this.fileMetadata.has(withExt)) { return withExt; } } // Try index files for (const ext of extensions) { const indexFile = path.join(resolved, 'index' + ext); if (this.fileMetadata.has(indexFile)) { return indexFile; } } } return null; } findPotentialEntryPoints(file) { const potentialEntryPoints = []; // Check if file matches common entry point patterns const entryPatterns = [ /index\.(js|ts|jsx|tsx)$/, /main\.(js|ts|jsx|tsx)$/, /app\.(js|ts|jsx|tsx)$/, /server\.(js|ts|jsx|tsx)$/ ]; for (const pattern of entryPatterns) { if (pattern.test(file)) { potentialEntryPoints.push(`File matches entry point pattern: ${pattern.source}`); } } // Check if file is in pages directory (Next.js) if (file.includes('/pages/') || file.startsWith('pages/')) { potentialEntryPoints.push('File is in pages directory (Next.js route)'); } // Check if file is in routes directory if (file.includes('/routes/') || file.startsWith('routes/')) { potentialEntryPoints.push('File is in routes directory (API route)'); } return potentialEntryPoints; } calculateFileUnusedConfidence(file, potentialEntryPoints) { let confidence = 0.9; // Base confidence // Reduce confidence if there are potential entry points confidence -= potentialEntryPoints.length * 0.2; // Increase confidence for certain file types if (file.endsWith('.test.js') || file.endsWith('.spec.js')) { confidence = 0.3; // Test files might be unused but still important } return Math.max(0.1, Math.min(0.95, confidence)); } getFileUnusedReason(file, potentialEntryPoints) { if (potentialEntryPoints.length > 0) { return `File appears unused but has potential entry points: ${potentialEntryPoints.join(', ')}`; } return 'File is not imported or referenced by any reachable code'; } isInEntryPoint(file) { return this.entryPoints.has(file); } async calculateFunctionComplexity(file, line) { // Simplified complexity calculation - count decision points try { const filePath = path.join(this.projectRoot, file); const content = await fs.readFile(filePath, 'utf-8'); const lines = content.split('\n'); // Get function body (simplified) let complexity = 1; // Base complexity let braceCount = 0; let inFunction = false; for (let i = line - 1; i < lines.length; i++) { const currentLine = lines[i]; if (!inFunction && currentLine.includes('function')) { inFunction = true; } if (inFunction) { braceCount += (currentLine.match(/\{/g) || []).length; braceCount -= (currentLine.match(/\}/g) || []).length; // Count decision points if (currentLine.includes('if') || currentLine.includes('while') || currentLine.includes('for') || currentLine.includes('switch') || currentLine.includes('catch') || currentLine.includes('&&') || currentLine.includes('||')) { complexity++; } if (braceCount === 0 && inFunction) { break; } } } return complexity; } catch (error) { return 1; } } async calculateFunctionSize(file, line) { try { const filePath = path.join(this.projectRoot, file); const content = await fs.readFile(filePath, 'utf-8'); const lines = content.split('\n'); // Find function boundaries let braceCount = 0; let inFunction = false; let size = 0; for (let i = line - 1; i < lines.length; i++) { const currentLine = lines[i]; if (!inFunction && currentLine.includes('function')) { inFunction = true; } if (inFunction) { size++; braceCount += (currentLine.match(/\{/g) || []).length; braceCount -= (currentLine.match(/\}/g) || []).length; if (braceCount === 0 && inFunction) { break; } } } return size; } catch (error) { return 0; } } getFunctionUnusedReason(node) { if (node.isExported) { return 'Function is exported but not called within the codebase'; } return 'Function is not called by any other function'; } calculateFunctionUnusedConfidence(node) { let confidence = 0.8; // Reduce confidence for exported functions if (node.isExported) { confidence -= 0.3; } // Reduce confidence for functions with common patterns that might be called externally if (node.name.startsWith('handle') || node.name.startsWith('on') || node.name.includes('Handler')) { confidence -= 0.2; } return Math.max(0.1, Math.min(0.95, confidence)); } parseImportStatements(content, language) { const imports = []; const lines = content.split('\n'); if (language === 'javascript') { lines.forEach((line, index) => { // ES6 imports const es6Match = line.match(/import\s+(.+?)\s+from\s+['"]([^'"]+)['"]/); if (es6Match) { const importClause = es6Match[1].trim(); const source = es6Match[2]; if (importClause.startsWith('{') && importClause.endsWith('}')) { // Named imports const names = importClause.slice(1, -1).split(',').map(n => n.trim()); names.forEach(name => { imports.push({ name, source, line: index + 1, type: 'named' }); }); } else { // Default import imports.push({ name: importClause, source, line: index + 1, type: 'default' }); } } // CommonJS require const requireMatch = line.match(/(?:const|let|var)\s+(.+?)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/); if (requireMatch) { imports.push({ name: requireMatch[1].trim(), source: requireMatch[2], line: index + 1, type: 'default' }); } }); } return imports; } isImportUsed(content, importName) { // Simple check - look for the import name being used in the code const regex = new RegExp(`\\b${importName}\\b`, 'g'); const matches = content.match(regex); // If it appears more than once (import statement + usage), it's used return matches ? matches.length > 1 : false; } calculateImportUnusedConfidence(content, importName) { // Check if import name appears in comments (might be planned usage) const commentPattern = new RegExp(`//.*${importName}|/\\*.*${importName}.*\\*/`, 'g'); if (commentPattern.test(content)) { return 0.7; // Lower confidence if mentioned in comments } return 0.9; } async findAssetReferences(asset) { const references = []; const assetName = path.basename(asset); const assetNameWithoutExt = path.basename(asset, path.extname(asset)); // Search in all source files const sourceFiles = await glob('**/*.{js,ts,jsx,tsx,css,scss,sass,less,html,vue}', { cwd: this.projectRoot, ignore: ['node_modules/**', 'dist/**', 'build/**'], absolute: false }); for (const file of sourceFiles) { try { const content = await fs.readFile(path.join(this.projectRoot, file), 'utf-8'); if (content.includes(assetName) || content.includes(assetNameWithoutExt)) { references.push(file); } } catch (error) { // Skip files that can't be read } } return references; } getAssetType(asset) { const ext = path.extname(asset).toLowerCase(); if (['.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.webp'].includes(ext)) { return 'image'; } if (['.woff', '.woff2', '.ttf', '.eot', '.otf'].includes(ext)) { return 'font'; } if (['.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm'].includes(ext)) { return 'video'; } if (['.mp3', '.wav', '.ogg', '.m4a', '.aac'].includes(ext)) { return 'audio'; } if (['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'].includes(ext)) { return 'document'; } return 'data'; } calculateAssetUnusedConfidence(asset, references) { let confidence = 0.9; // Reduce confidence for assets in public directories if (asset.includes('/public/') || asset.startsWith('public/')) { confidence -= 0.3; } // Reduce confidence for common asset names const commonNames = ['favicon', 'logo', 'icon', 'banner']; if (commonNames.some(name => asset.toLowerCase().includes(name))) { confidence -= 0.2; } return Math.max(0.1, Math.min(0.95, confidence)); } extractCSSSelectors(content) { const selectors = []; const lines = content.split('\n'); lines.forEach((line, index) => { // Class selectors const classMatches = line.match(/\.([a-zA-Z][a-zA-Z0-9_-]*)/g); if (classMatches) { classMatches.forEach(match => { selectors.push({ name: match, line: index + 1 }); }); } // ID selectors const idMatches = line.match(/#([a-zA-Z][a-zA-Z0-9_-]*)/g); if (idMatches) { idMatches.forEach(match => { selectors.push({ name: match, line: index + 1 }); }); } }); return selectors; } async findSelectorUsage(selector) { let usageCount = 0; // Search in all markup and script files const files = await glob('**/*.{html,htm,jsx,tsx,vue,js,ts}', { cwd: this.projectRoot, ignore: ['node_modules/**', 'dist/**', 'build/**'], absolute: false }); for (const file of files) { try { const content = await fs.readFile(path.join(this.projectRoot, file), 'utf-8'); // Count occurrences const selectorName = selector.replace(/^[.#]/, ''); // Remove . or # prefix const regex = new RegExp(`\\b${selectorName}\\b`, 'g'); const matches = content.match(regex); if (matches) { usageCount += matches.length; } } catch (error) { // Skip files that can't be read } } return usageCount; } getCSSelectorType(selector) { if (selector.startsWith('.')) { return 'class'; } if (selector.startsWith('#')) { return 'id'; } if (selector.startsWith('[')) { return 'attribute'; } return 'element'; } calculateStyleUnusedConfidence(selector, usageCount) { let confidence = 0.9; // Reduce confidence for utility classes const utilityPatterns = ['flex', 'grid', 'text-', 'bg-', 'p-', 'm-', 'w-', 'h-']; if (utilityPatterns.some(pattern => selector.includes(pattern))) { confidence -= 0.3; } return Math.max(0.1, Math.min(0.95, confidence)); } } //# sourceMappingURL=UnusedCodeDetectors.js.map