vibe-janitor
Version:
A CLI tool that cleans AI-generated JavaScript/TypeScript projects efficiently and intelligently
232 lines (231 loc) • 8.58 kB
JavaScript
import fs from 'fs-extra';
import path from 'path';
import { Project } from 'ts-morph';
import { Logger } from '../utils/logger.js';
/**
* Analyzes code complexity and structure
*/
export class Analyzer {
project;
targetDir;
options;
constructor(targetDir, options = {}) {
this.targetDir = targetDir;
this.options = {
maxLineCount: options.maxLineCount ?? 500,
maxFunctionLength: options.maxFunctionLength ?? 50,
maxNestingDepth: options.maxNestingDepth ?? 4,
verbose: options.verbose ?? false,
};
try {
// Try to initialize with tsconfig if it exists
const tsConfigPath = this.findTsConfig();
if (tsConfigPath) {
this.project = new Project({
tsConfigFilePath: tsConfigPath,
skipAddingFilesFromTsConfig: true,
});
}
else {
// Fall back to basic compiler options
this.project = new Project({
compilerOptions: {
target: 99, // ScriptTarget.ES2020
module: 99, // ModuleKind.ESNext
moduleResolution: 2, // ModuleResolutionKind.NodeJs
esModuleInterop: true,
},
skipAddingFilesFromTsConfig: true,
});
}
}
catch (error) {
// If Project initialization fails, create with default settings
Logger.error(`Error initializing ts-morph Project: ${error instanceof Error ? error.message : String(error)}`);
this.project = new Project();
}
}
/**
* Finds the TypeScript config file for the target directory
*/
findTsConfig() {
const tsConfigPath = path.join(this.targetDir, 'tsconfig.json');
try {
if (fs.existsSync(tsConfigPath)) {
return tsConfigPath;
}
}
catch (error) {
// If we can't access the file system or the file doesn't exist,
// return undefined and let the Project initialize without a tsconfig
Logger.warn(`Could not access tsconfig at ${tsConfigPath}: ${error instanceof Error ? error.message : String(error)}`);
}
return undefined;
}
/**
* Add files to the project for analysis
*/
async addFilesToProject() {
try {
const filePatterns = ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'];
const ignorePatterns = [
'**/node_modules/**',
'**/dist/**',
'**/build/**',
'**/*.d.ts',
'**/public/**',
];
const files = await import('fast-glob').then((fg) => fg.default.sync(filePatterns, {
cwd: this.targetDir,
ignore: ignorePatterns,
absolute: true,
}));
if (this.options.verbose) {
Logger.info(`Found ${files.length} files to analyze for complexity`);
}
files.forEach((file) => {
this.project.addSourceFileAtPath(file);
});
}
catch (error) {
Logger.error(`Failed to add files to complexity analysis: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Calculate complexity metrics for a source file
*/
calculateFileComplexity(sourceFile) {
const filePath = sourceFile.getFilePath();
const text = sourceFile.getText();
const lines = text.split('\n');
const lineCount = lines.length;
const functions = sourceFile.getFunctions();
const methods = sourceFile.getClasses().flatMap((cls) => cls.getMethods());
const allFunctions = [...functions, ...methods];
// Find long functions
const longFunctions = allFunctions
.map((func) => {
const funcText = func.getText();
const funcLines = funcText.split('\n').length;
const name = func.getName() ?? 'anonymous';
return { name, lineCount: funcLines };
})
.filter((func) => func.lineCount > this.options.maxFunctionLength);
// Calculate nesting depth (simplified approach)
const deepNesting = [];
// This is a simplistic approach - a real implementation would use AST traversal
// to accurately determine nesting depth
const calculateNestingDepth = (code) => {
const lines = code.split('\n');
let currentDepth = 0;
let maxDepth = 0;
let maxDepthLine = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const openBraces = (line.match(/{/g) ?? []).length;
const closeBraces = (line.match(/}/g) ?? []).length;
currentDepth += openBraces - closeBraces;
if (currentDepth > maxDepth) {
maxDepth = currentDepth;
maxDepthLine = i + 1;
}
}
if (maxDepth > this.options.maxNestingDepth) {
deepNesting.push({
location: `line ${maxDepthLine}`,
depth: maxDepth,
});
}
};
// Calculate nesting depth for each function
allFunctions.forEach((func) => {
const funcText = func.getText();
calculateNestingDepth(funcText);
});
// Calculate overall complexity (a simple heuristic for now)
const complexity = Math.floor(lineCount / 100 + longFunctions.length * 5 + deepNesting.length * 10);
return {
filePath,
lineCount,
functionCount: allFunctions.length,
longFunctions,
deepNesting,
complexity,
};
}
/**
* Find large files based on line count
*/
findLargeFiles(sourceFiles) {
const largeFiles = [];
for (const sourceFile of sourceFiles) {
const complexity = this.calculateFileComplexity(sourceFile);
if (complexity.lineCount > this.options.maxLineCount) {
largeFiles.push(complexity);
}
}
return largeFiles.sort((a, b) => b.lineCount - a.lineCount);
}
/**
* Find complex functions
*/
findComplexFunctions(sourceFiles) {
const result = [];
for (const sourceFile of sourceFiles) {
const complexity = this.calculateFileComplexity(sourceFile);
if (complexity.longFunctions.length > 0) {
result.push({
filePath: complexity.filePath,
functions: complexity.longFunctions,
});
}
}
return result;
}
/**
* Find deeply nested code
*/
findDeeplyNestedCode(sourceFiles) {
const result = [];
for (const sourceFile of sourceFiles) {
const complexity = this.calculateFileComplexity(sourceFile);
if (complexity.deepNesting.length > 0) {
result.push({
filePath: complexity.filePath,
locations: complexity.deepNesting,
});
}
}
return result;
}
/**
* Find circular dependencies (placeholder - would use madge in a full implementation)
*/
findCircularDependencies() {
// This is a placeholder. In a real implementation, we would integrate with madge
// to detect circular dependencies in the import graph
return [];
}
/**
* Run the analysis process on the target directory
*/
async analyze() {
const result = {
largeFiles: [],
complexFunctions: [],
deeplyNested: [],
circularDependencies: [],
};
await this.addFilesToProject();
const sourceFiles = this.project.getSourceFiles();
if (this.options.verbose) {
Logger.info('Analyzing code complexity...');
}
// Run the analysis
result.largeFiles = this.findLargeFiles(sourceFiles);
result.complexFunctions = this.findComplexFunctions(sourceFiles);
result.deeplyNested = this.findDeeplyNestedCode(sourceFiles);
result.circularDependencies = this.findCircularDependencies();
return result;
}
}