strategies.ts.broken•5.78 kB
/**
* Cleanup Strategies
*
* Analysis strategies for detecting cleanup opportunities:
* - Dead code detection (unused imports, unreferenced files)
* - Duplicate file detection (hash-based comparison)
*/
import { createHash } from 'crypto';
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
import { stat } from 'fs/promises';
import { join, relative } from 'path';
export interface CleanupAction {
type: 'remove_unused_imports' | 'duplicate_files' | 'unreferenced_file';
file?: string;
files?: string[];
details?: string[];
impact: 'low' | 'medium' | 'high';
bytesFreed?: number;
}
export interface CleanupStrategy {
name: string;
analyze: (projectPath: string, exclusions: string[]) => Promise<CleanupAction[]>;
}
/**
* Find unused imports in GDScript source code
*/
function findUnusedImports(source: string): string[] {
const unusedImports: string[] = [];
const lines = source.split('\n');
// Pattern: const/var NAME = preload("...")
const importPattern = /^(?:const|var)\s+(\w+)\s*=\s*(?:preload|load)\(/;
for (const line of lines) {
const match = line.match(importPattern);
if (match) {
const varName = match[1];
// Check if variable is used elsewhere in the code
const usageCount = source.split(varName).length - 1;
if (usageCount === 1) {
// Only appears once (in the import line itself)
unusedImports.push(varName);
}
}
}
return unusedImports;
}
/**
* Dead code detection strategy
*/
export const deadCodeStrategy: CleanupStrategy = {
name: 'dead_code',
analyze: async (projectPath: string, exclusions: string[]) => {
const actions: CleanupAction[] = [];
// Find all .gd files
const gdFiles = await findGDScriptFiles(projectPath, exclusions);
for (const file of gdFiles) {
const filePath = join(projectPath, file);
try {
const source = readFileSync(filePath, 'utf-8');
const unusedImports = findUnusedImports(source);
if (unusedImports.length > 0) {
actions.push({
type: 'remove_unused_imports',
file,
details: unusedImports,
impact: 'low',
});
}
} catch (error) {
// Skip files that can't be read
continue;
}
}
return actions;
},
};
/**
* Duplicate file detection strategy
*/
export const duplicateStrategy: CleanupStrategy = {
name: 'duplicates',
analyze: async (projectPath: string, exclusions: string[]) => {
const actions: CleanupAction[] = [];
const hashMap = new Map<string, string[]>();
// Find all files
const allFiles = await findAllFiles(projectPath, exclusions);
// Hash all files
for (const file of allFiles) {
const filePath = join(projectPath, file);
try {
const content = readFileSync(filePath);
const hash = createHash('sha256').update(content).digest('hex');
if (!hashMap.has(hash)) {
hashMap.set(hash, []);
}
hashMap.get(hash)!.push(file);
} catch (error) {
// Skip files that can't be read
continue;
}
}
// Find duplicates (more than one file with same hash)
for (const [hash, paths] of hashMap) {
if (paths.length > 1) {
// Calculate bytes that could be freed
const firstFile = join(projectPath, paths[0]);
const fileStats = statSync(firstFile).catch(() => ({ size: 0 }));
const bytesFreed = fileStats.size * (paths.length - 1);
actions.push({
type: 'duplicate_files',
files: paths,
impact: 'medium',
bytesFreed,
});
}
}
return actions;
},
};
/**
* Recursively find all GDScript files
*/
async function findGDScriptFiles(
dir: string,
exclusions: string[]
): Promise<string[]> {
const files: string[] = [];
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
const relativePath = relative(process.cwd(), fullPath);
// Skip excluded paths
if (shouldExclude(relativePath, exclusions)) {
continue;
}
if (entry.isDirectory()) {
const subFiles = await findGDScriptFiles(fullPath, exclusions);
files.push(...subFiles);
} else if (entry.name.endsWith('.gd')) {
files.push(relative(dir, fullPath));
}
}
return files;
}
/**
* Recursively find all files (for duplicate detection)
*/
async function findAllFiles(
dir: string,
exclusions: string[]
): Promise<string[]> {
const files: string[] = [];
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
const relativePath = relative(process.cwd(), fullPath);
// Skip excluded paths
if (shouldExclude(relativePath, exclusions)) {
continue;
}
if (entry.isDirectory()) {
const subFiles = await findAllFiles(fullPath, exclusions);
files.push(...subFiles);
} else {
files.push(relative(dir, fullPath));
}
}
return files;
}
/**
* Check if path should be excluded
*/
function shouldExclude(path: string, exclusions: string[]): boolean {
// Always exclude hidden directories and common build artifacts
const alwaysExclude = ['.godot', '.git', 'node_modules', '.cleanup_trash'];
if (alwaysExclude.some((dir) => path.includes(dir))) {
return true;
}
// Check custom exclusions (simple string matching for now)
return exclusions.some((pattern) => path.includes(pattern));
}
/**
* Get all available strategies
*/
export const STRATEGIES: Record<string, CleanupStrategy> = {
dead_code: deadCodeStrategy,
duplicates: duplicateStrategy,
};