vibe-janitor
Version:
A CLI tool that cleans AI-generated JavaScript/TypeScript projects efficiently and intelligently
913 lines (912 loc) • 42.2 kB
JavaScript
import fs from 'fs-extra';
import path from 'path';
import glob from 'fast-glob';
import { Project, Node, SyntaxKind } from 'ts-morph';
import { Logger } from '../utils/logger.js';
/**
* Handles detecting and removing unused code, imports, and files
*/
export class Cleaner {
project;
targetDir;
options;
constructor(targetDir, options = {}) {
this.targetDir = targetDir;
this.options = options;
this.project = new Project({
tsConfigFilePath: this.findTsConfig(),
skipAddingFilesFromTsConfig: true,
});
}
/**
* Finds the TypeScript config file for the target directory
*/
findTsConfig() {
const tsConfigPath = path.join(this.targetDir, 'tsconfig.json');
if (fs.existsSync(tsConfigPath)) {
return tsConfigPath;
}
return undefined;
}
/**
* Adds all TypeScript and JavaScript files to the project for analysis
*/
async addFilesToProject() {
const filePatterns = ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'];
const ignorePatterns = [
'**/node_modules/**',
'**/dist/**',
'**/build/**',
'**/*.d.ts',
'**/public/**',
];
try {
const files = await glob(filePatterns, {
cwd: this.targetDir,
ignore: ignorePatterns,
absolute: true,
});
if (this.options.verbose) {
Logger.info(`Found ${files.length} files to analyze`);
}
files.forEach((file) => {
this.project.addSourceFileAtPath(file);
});
}
catch (error) {
Logger.error(`Failed to add files to project: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Finds unused imports in the codebase
*/
findUnusedImports() {
const result = [];
if (this.options.verbose) {
Logger.info('Analyzing unused imports...');
}
const sourceFiles = this.project.getSourceFiles();
for (const sourceFile of sourceFiles) {
try {
const unusedImports = [];
const fileText = sourceFile.getText();
const importDeclarations = sourceFile.getImportDeclarations();
for (const importDecl of importDeclarations) {
try {
// Handle named imports (e.g., import { useState, useEffect } from 'react')
const namedImports = importDecl.getNamedImports();
for (const namedImport of namedImports) {
try {
const importName = namedImport.getName();
// Skip imports that have aliases - these are harder to track
if (namedImport.getAliasNode()) {
continue;
}
// Improved regex pattern to match whole words only
const importNameRegex = new RegExp(`\\b${this.escapeRegExp(importName)}\\b`, 'g');
const matches = fileText.match(importNameRegex) ?? [];
// The first match is the import declaration itself
// If there's only 1 or fewer occurrences, it's unused
if (matches.length <= 1) {
unusedImports.push(importName);
}
}
catch (error) {
console.error(`Error processing named import: ${error}`);
// Skip this named import if there's an error
if (this.options.verbose) {
Logger.info(`Error processing named import in ${sourceFile.getFilePath()}`);
}
continue;
}
}
// Handle default imports (e.g., import React from 'react')
try {
const defaultImport = importDecl.getDefaultImport();
if (defaultImport) {
const importName = defaultImport.getText();
// Improved regex pattern to match whole words only
const importNameRegex = new RegExp(`\\b${this.escapeRegExp(importName)}\\b`, 'g');
const matches = fileText.match(importNameRegex) ?? [];
// The first match is the import declaration itself
// If there's only 1 or fewer occurrences, it's unused
if (matches.length <= 1) {
unusedImports.push(importName);
}
}
}
catch (error) {
console.error(`Error processing default import: ${error}`);
// Skip this default import if there's an error
if (this.options.verbose) {
Logger.info(`Error processing default import in ${sourceFile.getFilePath()}`);
}
}
// Handle namespace imports (e.g., import * as React from 'react')
try {
const namespaceImport = importDecl.getNamespaceImport();
if (namespaceImport) {
const importName = namespaceImport.getText();
// Improved regex pattern to match whole words only
const importNameRegex = new RegExp(`\\b${this.escapeRegExp(importName)}\\b`, 'g');
const matches = fileText.match(importNameRegex) ?? [];
// The first match is the import declaration itself
// If there's only 1 or fewer occurrences, it's unused
if (matches.length <= 1) {
unusedImports.push(importName);
}
}
}
catch (error) {
console.error(`Error processing namespace import: ${error}`);
// Skip this namespace import if there's an error
if (this.options.verbose) {
Logger.info(`Error processing namespace import in ${sourceFile.getFilePath()}`);
}
}
}
catch (error) {
console.error(`Error processing import declaration: ${error}`);
// Skip this import declaration if there's an error
if (this.options.verbose) {
Logger.info(`Error processing import declaration in ${sourceFile.getFilePath()}`);
}
continue;
}
}
if (unusedImports.length > 0) {
result.push({
file: sourceFile.getFilePath(),
imports: unusedImports,
});
}
}
catch (error) {
console.error(`Error processing source file: ${error}`);
// Skip this file if there's an error processing it
Logger.info(`Skipping import analysis for file due to error: ${sourceFile.getFilePath()}`);
continue;
}
}
return result;
}
/**
* Escape special characters for use in a regular expression
*/
escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
/**
* Finds unused variables in the codebase
*/
findUnusedVariables() {
const result = [];
if (this.options.verbose) {
Logger.info('Analyzing unused variables...');
}
const sourceFiles = this.project.getSourceFiles();
for (const sourceFile of sourceFiles) {
try {
const unusedVariables = [];
// Find variable declarations
const variableDeclarations = sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration);
for (const varDecl of variableDeclarations) {
try {
const varName = varDecl.getName();
// Skip destructuring patterns (more complex to analyze)
if (!varName || varName.includes('{') || varName.includes('[')) {
continue;
}
// Skip variables in loops, which may be used implicitly
if (varDecl.getFirstAncestorByKind(SyntaxKind.ForStatement) ||
varDecl.getFirstAncestorByKind(SyntaxKind.ForOfStatement) ||
varDecl.getFirstAncestorByKind(SyntaxKind.ForInStatement)) {
continue;
}
// Count references by using variable name text search
const text = sourceFile.getText();
const varNameRegex = new RegExp(`\\b${varName}\\b`, 'g');
const occurrences = (text.match(varNameRegex) ?? []).length;
// Check if the variable is only declared but never used (allowing for one reference)
if (occurrences <= 1) {
const parent = varDecl.getParent();
const grandparent = parent?.getParent();
// Skip exported variables as they might be used elsewhere
if (grandparent && !this.hasExportModifier(grandparent)) {
unusedVariables.push(varName);
}
}
}
catch {
// Skip this variable if there's an error processing it
if (this.options.verbose) {
Logger.info(`Error processing variable in ${sourceFile.getFilePath()}`);
}
continue;
}
}
if (unusedVariables.length > 0) {
result.push({
file: sourceFile.getFilePath(),
variables: unusedVariables,
});
}
}
catch {
// Skip this file if there's an error processing it
Logger.info(`Skipping variable analysis for file due to error: ${sourceFile.getFilePath()}`);
continue;
}
}
return result;
}
/**
* Helper method to check if a node has export modifier
*/
hasExportModifier(node) {
if (Node.isVariableStatement(node)) {
return (node.getModifiers()?.some((mod) => mod.getKind() === SyntaxKind.ExportKeyword) ?? false);
}
return false;
}
/**
* Finds unused functions in the codebase
*/
findUnusedFunctions() {
const result = [];
if (this.options.verbose) {
Logger.info('Analyzing unused functions...');
}
const sourceFiles = this.project.getSourceFiles();
for (const sourceFile of sourceFiles) {
try {
const unusedFunctions = [];
// Check for function declarations
const functionDeclarations = sourceFile.getFunctions();
for (const funcDecl of functionDeclarations) {
try {
const funcName = funcDecl.getName();
// Skip anonymous functions
if (!funcName) {
continue;
}
// Count references by using function name text search
const text = sourceFile.getText();
const funcNameRegex = new RegExp(`\\b${funcName}\\b`, 'g');
const occurrences = (text.match(funcNameRegex) ?? []).length;
// If the function is only declared but never called (allowing for definition)
if (occurrences <= 1) {
// Skip exported functions as they might be used elsewhere
if (!funcDecl.isExported()) {
unusedFunctions.push(funcName);
}
}
}
catch {
// Skip this function if there's an error processing it
if (this.options.verbose) {
Logger.info(`Error processing function in ${sourceFile.getFilePath()}`);
}
continue;
}
}
// Check for method declarations in classes
const classDeclarations = sourceFile.getClasses();
for (const classDecl of classDeclarations) {
try {
// Skip checking methods in exported classes
if (classDecl.isExported()) {
continue;
}
const methods = classDecl.getMethods();
for (const method of methods) {
try {
const methodName = method.getName();
// Skip private methods, getters, setters, and constructor
if (method.hasModifier(SyntaxKind.PrivateKeyword) ||
method.hasModifier(SyntaxKind.GetKeyword) ||
method.hasModifier(SyntaxKind.SetKeyword) ||
methodName === 'constructor') {
continue;
}
// Count references by using method name text search
const text = sourceFile.getText();
const methodNameRegex = new RegExp(`\\b${methodName}\\b`, 'g');
const occurrences = (text.match(methodNameRegex) ?? []).length;
// If the method is only declared but never called (allowing for definition)
if (occurrences <= 1) {
const className = classDecl.getName();
if (className) {
unusedFunctions.push(`${className}.${methodName}`);
}
}
}
catch {
// Skip this method if there's an error processing it
if (this.options.verbose) {
Logger.info(`Error processing method in ${sourceFile.getFilePath()}`);
}
continue;
}
}
}
catch {
// Skip this class if there's an error processing it
if (this.options.verbose) {
Logger.info(`Error processing class in ${sourceFile.getFilePath()}`);
}
continue;
}
}
if (unusedFunctions.length > 0) {
result.push({
file: sourceFile.getFilePath(),
functions: unusedFunctions,
});
}
}
catch {
// Skip this file if there's an error processing it
Logger.info(`Skipping file due to error: ${sourceFile.getFilePath()}`);
continue;
}
}
return result;
}
/**
* Finds potentially unused files based on import references
*/
findUnusedFiles() {
if (this.options.verbose) {
Logger.info('Analyzing potentially unused files...');
}
const sourceFiles = this.project.getSourceFiles();
const allFiles = new Set(sourceFiles.map((file) => file.getFilePath()));
const referencedFiles = new Set();
// First, collect all files referenced by imports
for (const sourceFile of sourceFiles) {
const sourceFilePath = sourceFile.getFilePath();
// Skip imports from test files when determining used components
const fileName = path.basename(sourceFilePath).toLowerCase();
const dirName = path.dirname(sourceFilePath);
const isTestFile = fileName.includes('test.') ||
fileName.includes('spec.') ||
fileName.includes('jest.') ||
fileName.includes('test-') ||
dirName.includes('/test') ||
dirName.includes('/tests') ||
dirName.includes('/__tests__') ||
dirName.includes('/__mocks__') ||
dirName.includes('/fixtures') ||
dirName.includes('/mocks');
// If this is a test file and we're in deep scrub mode with deleteUnusedFiles,
// don't count its imports as references
if (isTestFile && this.options.deepScrub && this.options.deleteUnusedFiles) {
if (this.options.verbose) {
Logger.info(`Skipping imports from test file: ${sourceFilePath}`);
}
continue;
}
const importDeclarations = sourceFile.getImportDeclarations();
for (const importDecl of importDeclarations) {
try {
const moduleSpecifier = importDecl.getModuleSpecifierValue();
// Try to resolve the imported file path
let resolvedPath = '';
// Handle relative imports
if (moduleSpecifier.startsWith('.')) {
const sourceDir = path.dirname(sourceFilePath);
resolvedPath = path.resolve(sourceDir, moduleSpecifier);
// Try to find the actual file (might need to add extensions)
if (!fs.existsSync(resolvedPath)) {
for (const ext of ['.ts', '.tsx', '.js', '.jsx']) {
const pathWithExt = `${resolvedPath}${ext}`;
if (fs.existsSync(pathWithExt)) {
resolvedPath = pathWithExt;
break;
}
// Also check for index files
const indexPath = path.join(resolvedPath, `index${ext}`);
if (fs.existsSync(indexPath)) {
resolvedPath = indexPath;
break;
}
}
}
if (fs.existsSync(resolvedPath)) {
referencedFiles.add(resolvedPath);
}
}
// Skip node_modules or other non-relative imports
}
catch {
// Skip errors in resolving imports
}
}
}
// Find entry point files that shouldn't be removed even if "unused"
const entryPoints = new Set();
const importantFiles = new Set();
// Add special entry points to the important files set
const findEntryPoints = () => {
// Check for package.json main field
const packageJsonPath = path.join(this.targetDir, 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const packageJson = fs.readJsonSync(packageJsonPath);
importantFiles.add(packageJsonPath); // Add package.json itself
if (packageJson.main) {
const mainPath = path.resolve(this.targetDir, packageJson.main);
entryPoints.add(mainPath);
// Also add without extension (to match both .js and .ts files)
const mainPathNoExt = mainPath.replace(/\.[^/.]+$/, '');
entryPoints.add(mainPathNoExt);
}
// Check for bin entries
if (packageJson.bin) {
if (typeof packageJson.bin === 'string') {
entryPoints.add(path.resolve(this.targetDir, packageJson.bin));
}
else if (typeof packageJson.bin === 'object') {
Object.values(packageJson.bin).forEach((binPath) => {
if (typeof binPath === 'string') {
entryPoints.add(path.resolve(this.targetDir, binPath));
}
});
}
}
}
catch {
// Ignore package.json parsing errors
}
}
// Add tsconfig.json to important files
const tsConfigPath = path.join(this.targetDir, 'tsconfig.json');
if (fs.existsSync(tsConfigPath)) {
importantFiles.add(tsConfigPath);
}
// Add README.md to important files
const readmePath = path.join(this.targetDir, 'README.md');
if (fs.existsSync(readmePath)) {
importantFiles.add(readmePath);
}
};
findEntryPoints();
// Files that might be unused (no references and not entry points or important files)
const potentiallyUnusedFiles = Array.from(allFiles).filter((file) => !referencedFiles.has(file) &&
!entryPoints.has(file) &&
!importantFiles.has(file) &&
!this.isFileProtected(file));
if (this.options.verbose && potentiallyUnusedFiles.length > 0) {
Logger.info(`Found ${potentiallyUnusedFiles.length} potentially unused files`);
}
return potentiallyUnusedFiles;
}
/**
* Check if a file should be protected from deletion
*/
isFileProtected(filePath) {
const fileName = path.basename(filePath).toLowerCase();
const fileExt = path.extname(filePath).toLowerCase();
const dirName = path.dirname(filePath);
// First check: Absolute protection for static directory and files
// Directly check if the path contains '/static/' to protect all files in static directories
if (filePath.includes('/static/') ||
filePath.includes('\\static\\') ||
filePath.includes('/public/') ||
filePath.includes('\\public\\') ||
/[\/\\]static$/.test(dirName)) {
return true;
}
// Protect global.css files
if (fileName === 'global.css') {
return true;
}
// Skip test files - more comprehensive check
if (fileName.includes('test.') ||
fileName.includes('spec.') ||
fileName.includes('jest.') ||
fileName.includes('test-') ||
dirName.includes('/test') ||
dirName.includes('/tests') ||
dirName.includes('/__tests__') ||
dirName.includes('/__mocks__') ||
dirName.includes('/fixtures') ||
dirName.includes('/mocks')) {
return true;
}
// Skip TypeScript declaration files (.d.ts)
if (fileName.endsWith('.d.ts') || fileExt === '.d.ts') {
return true;
}
// Skip config files
if (fileName === 'package.json' ||
fileName === 'tsconfig.json' ||
fileName === 'jest.config.js' ||
fileName === '.eslintrc.js' ||
fileName === '.prettierrc' ||
fileName.startsWith('.') ||
fileName.endsWith('rc') ||
fileName.endsWith('rc.js') ||
fileName.endsWith('rc.json')) {
return true;
}
// Skip documentation files
if (fileExt === '.md' ||
fileExt === '.mdx' ||
fileName === 'license' ||
fileName === 'changelog' ||
fileName === 'contributing') {
return true;
}
// Skip files in special directories (including nested ones)
const normalizedPath = filePath.toLowerCase().replace(/\\/g, '/');
const dirParts = normalizedPath.split('/');
// Check for special directories anywhere in the path
const specialDirs = [
'node_modules',
'dist',
'build',
'.git',
'docs',
'examples',
'scripts',
'public',
'static',
'.next',
'.nuxt',
'.output',
'out',
'.vercel',
];
for (const dir of specialDirs) {
// Check both for directory parts and paths that contain the directory name
// This ensures we protect things like /static/, /static-assets/, etc.
if (dirParts.includes(dir) ||
normalizedPath.includes(`/${dir}/`) ||
normalizedPath.startsWith(`${dir}/`)) {
return true;
}
}
// Protect framework-specific directories and files
const frameworkPatterns = [
'/pages/',
'/src/pages/', // Next.js pages
'/app/',
'/src/app/', // Next.js App Router
];
if (frameworkPatterns.some((pattern) => normalizedPath.includes(pattern))) {
return true;
}
// Protect files in the root directory - they're often configuration
if (path.dirname(filePath) === this.targetDir) {
return true;
}
// Also check using the relative path approach as a fallback
const relativePath = path.relative(this.targetDir, filePath).toLowerCase();
if (relativePath.startsWith('node_modules') ||
relativePath.startsWith('dist') ||
relativePath.startsWith('build') ||
relativePath.startsWith('.git') ||
relativePath.startsWith('public') ||
relativePath.startsWith('static') ||
relativePath.includes('/docs') ||
relativePath.includes('/examples') ||
relativePath.includes('/scripts') ||
relativePath.includes('/public') ||
relativePath.includes('/static')) {
return true;
}
// Additional check for common static assets like favicon
if (fileName === 'favicon.ico' ||
fileName.startsWith('favicon-') ||
fileName.startsWith('favicon.') ||
fileName.startsWith('icon-') ||
fileName.includes('.svg') ||
fileName.includes('.ico') ||
fileName.includes('.png') ||
fileName.includes('.jpg') ||
fileName.includes('.jpeg') ||
fileName.includes('.gif')) {
return true;
}
return false;
}
/**
* Delete unused files from the file system
* @param unusedFiles List of file paths to delete
* @returns Array of successfully deleted file paths
*/
async deleteUnusedFiles(unusedFiles) {
const deletedFiles = [];
if (unusedFiles.length === 0 || this.options.dryRun) {
return deletedFiles;
}
for (const filePath of unusedFiles) {
try {
// Final safety check before deletion
if (this.isFileProtected(filePath)) {
if (this.options.verbose) {
Logger.warn(`Skipping protected file: ${filePath}`);
}
continue;
}
// Delete the file
await fs.remove(filePath);
deletedFiles.push(filePath);
if (this.options.verbose) {
Logger.success(`Deleted unused file: ${filePath}`);
}
}
catch (error) {
Logger.error(`Failed to delete file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
}
}
return deletedFiles;
}
/**
* Clean the project by removing unused code and files
*/
async clean() {
const result = {
unusedFiles: [],
unusedImports: [],
unusedVariables: [],
unusedFunctions: [],
modifiedFiles: [],
deletedFiles: [],
unusedFilesSize: 0,
};
try {
await this.addFilesToProject();
// Find unused code
result.unusedImports = this.findUnusedImports();
if (this.options.deepScrub) {
try {
result.unusedVariables = this.findUnusedVariables();
}
catch (error) {
Logger.error(`Error finding unused variables: ${error instanceof Error ? error.message : String(error)}`);
}
try {
result.unusedFunctions = this.findUnusedFunctions();
}
catch (error) {
Logger.error(`Error finding unused functions: ${error instanceof Error ? error.message : String(error)}`);
}
try {
result.unusedFiles = this.findUnusedFiles();
// Calculate total size of unused files
result.unusedFilesSize = await this.calculateUnusedFilesSize(result.unusedFiles);
}
catch (error) {
Logger.error(`Error finding unused files: ${error instanceof Error ? error.message : String(error)}`);
}
}
// If not a dry run and removeUnused is enabled, actually remove the unused code
if (!this.options.dryRun && this.options.removeUnused) {
const sourceFiles = this.project.getSourceFiles();
// Process each file
for (const sourceFile of sourceFiles) {
try {
const filePath = sourceFile.getFilePath();
let fileModified = false;
// Remove unused imports
try {
const unusedImportsInFile = result.unusedImports.find((item) => item.file === filePath);
if (unusedImportsInFile) {
fileModified =
this.removeUnusedImports(sourceFile, unusedImportsInFile.imports) || fileModified;
}
}
catch (error) {
Logger.error(`Error removing unused imports in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
}
// Remove unused variables if deep scrub is enabled
if (this.options.deepScrub) {
try {
const unusedVarsInFile = result.unusedVariables.find((item) => item.file === filePath);
if (unusedVarsInFile) {
fileModified =
this.removeUnusedVariables(sourceFile, unusedVarsInFile.variables) ||
fileModified;
}
}
catch (error) {
Logger.error(`Error removing unused variables in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
}
// Remove unused functions
try {
const unusedFuncsInFile = result.unusedFunctions.find((item) => item.file === filePath);
if (unusedFuncsInFile) {
fileModified =
this.removeUnusedFunctions(sourceFile, unusedFuncsInFile.functions) ||
fileModified;
}
}
catch (error) {
Logger.error(`Error removing unused functions in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Save changes if the file was modified
if (fileModified) {
try {
await sourceFile.save();
result.modifiedFiles.push(filePath);
if (this.options.verbose) {
Logger.success(`Cleaned file: ${filePath}`);
}
}
catch (error) {
Logger.error(`Failed to save changes to ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
catch (error) {
// Skip this file if there's an error processing it
Logger.error(`Error processing file: ${sourceFile.getFilePath()}: ${error instanceof Error ? error.message : String(error)}`);
continue;
}
}
// Remove unused files if deep scrub is enabled and deleteUnusedFiles is true
if (this.options.deepScrub &&
result.unusedFiles.length > 0 &&
(this.options.deleteUnusedFiles || this.options.removeUnused)) {
result.deletedFiles = await this.deleteUnusedFiles(result.unusedFiles);
if (result.deletedFiles.length > 0 && this.options.verbose) {
Logger.success(`Deleted ${result.deletedFiles.length} unused files (${this.formatSize(result.unusedFilesSize)})`);
}
else if (result.unusedFiles.length > 0 &&
result.deletedFiles.length === 0 &&
this.options.verbose) {
Logger.info(`Found ${result.unusedFiles.length} potential unused files, but all were protected from deletion`);
}
}
}
// Report summary
const totalImports = result.unusedImports.reduce((acc, item) => acc + item.imports.length, 0);
const totalVars = result.unusedVariables.reduce((acc, item) => acc + item.variables.length, 0);
const totalFuncs = result.unusedFunctions.reduce((acc, item) => acc + item.functions.length, 0);
if (this.options.verbose) {
Logger.info(`Found ${totalImports} unused imports across ${result.unusedImports.length} files`);
Logger.info(`Found ${totalVars} unused variables across ${result.unusedVariables.length} files`);
Logger.info(`Found ${totalFuncs} unused functions across ${result.unusedFunctions.length} files`);
Logger.info(`Found ${result.unusedFiles.length} potentially unused files (${this.formatSize(result.unusedFilesSize)})`);
if (result.modifiedFiles.length > 0) {
Logger.info(`Modified ${result.modifiedFiles.length} files`);
}
}
}
catch (error) {
// Catch any unexpected errors in the main clean method
Logger.error(`Error during cleanup process: ${error instanceof Error ? error.message : String(error)}`);
}
return result;
}
/**
* Remove unused imports from a source file
*/
removeUnusedImports(sourceFile, unusedImports) {
const importDeclarations = sourceFile.getImportDeclarations();
let fileModified = false;
for (const importDecl of importDeclarations) {
const namedImports = importDecl.getNamedImports();
for (const namedImport of namedImports) {
if (unusedImports.includes(namedImport.getName())) {
namedImport.remove();
fileModified = true;
}
}
// Check for unused default imports
const defaultImport = importDecl.getDefaultImport();
if (defaultImport && unusedImports.includes(defaultImport.getText())) {
importDecl.removeDefaultImport();
fileModified = true;
}
}
return fileModified;
}
/**
* Remove unused variables from a source file
*/
removeUnusedVariables(sourceFile, unusedVariables) {
// Find variable declarations
const variableDeclarations = sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration);
let fileModified = false;
for (const varDecl of variableDeclarations) {
const varName = varDecl.getName();
// Skip destructuring patterns (more complex to analyze)
if (!varName || varName.includes('{') || varName.includes('[')) {
continue;
}
// Skip variables in loops, which may be used implicitly
if (varDecl.getFirstAncestorByKind(SyntaxKind.ForStatement) ||
varDecl.getFirstAncestorByKind(SyntaxKind.ForOfStatement) ||
varDecl.getFirstAncestorByKind(SyntaxKind.ForInStatement)) {
continue;
}
if (unusedVariables.includes(varName)) {
varDecl.remove();
fileModified = true;
}
}
return fileModified;
}
/**
* Remove unused functions from a source file
*/
removeUnusedFunctions(sourceFile, unusedFunctions) {
// Check for function declarations
const functionDeclarations = sourceFile.getFunctions();
let fileModified = false;
for (const funcDecl of functionDeclarations) {
const funcName = funcDecl.getName();
// Skip anonymous functions
if (!funcName) {
continue;
}
if (unusedFunctions.includes(funcName)) {
funcDecl.remove();
fileModified = true;
}
}
// Check for method declarations in classes
const classDeclarations = sourceFile.getClasses();
for (const classDecl of classDeclarations) {
// Skip checking methods in exported classes
if (classDecl.isExported()) {
continue;
}
const methods = classDecl.getMethods();
for (const method of methods) {
const methodName = method.getName();
// Skip private methods, getters, setters, and constructor
if (method.hasModifier(SyntaxKind.PrivateKeyword) ||
method.hasModifier(SyntaxKind.GetKeyword) ||
method.hasModifier(SyntaxKind.SetKeyword) ||
methodName === 'constructor') {
continue;
}
if (unusedFunctions.includes(methodName)) {
method.remove();
fileModified = true;
}
}
}
return fileModified;
}
/**
* Calculate the size of all unused files
*/
async calculateUnusedFilesSize(unusedFiles) {
let totalSize = 0;
if (unusedFiles.length === 0) {
return totalSize;
}
for (const filePath of unusedFiles) {
try {
const stats = await fs.stat(filePath);
totalSize += stats.size;
}
catch (error) {
if (this.options.verbose) {
Logger.info(`Error getting file size for ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
return totalSize;
}
/**
* Format file size in a human-readable way
*/
formatSize(bytes) {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(1)} ${units[unitIndex]}`;
}
}