UNPKG

vibe-janitor

Version:

A CLI tool that cleans AI-generated JavaScript/TypeScript projects efficiently and intelligently

232 lines (231 loc) 8.58 kB
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; } }