mira-consciousness
Version:
Memory & Intelligence Retention Archive - Preserving The Spark
515 lines • 21.2 kB
JavaScript
/**
* 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