UNPKG

woaru

Version:

Universal Project Setup Autopilot - Analyze and automatically configure development tools for ANY programming language

679 lines 27.1 kB
import fs from 'fs-extra'; import * as path from 'path'; import { glob } from 'glob'; import { CodeSmellAnalyzer } from './CodeSmellAnalyzer.js'; import { t, initializeI18n } from '../config/i18n.js'; /** * Security constants for code analysis validation */ const SECURITY_LIMITS = { MAX_FILE_SIZE: 5 * 1024 * 1024, // 5MB MAX_FILES_ANALYZED: 1000, MAX_PATH_LENGTH: 500, MAX_ANALYSIS_TIME: 120000, // 2 minutes MAX_INSIGHTS_PER_TYPE: 50, }; /** * Sanitizes file paths to prevent information leakage * @param filePath - The file path to sanitize * @returns Sanitized file path safe for display */ function sanitizeFilePath(filePath) { if (typeof filePath !== 'string') { return 'unknown-file'; } const baseName = path.basename(filePath); return (baseName.replace(/[^a-zA-Z0-9._-]/g, '').substring(0, 100) || 'unknown-file'); } /** * Sanitizes project paths to prevent directory traversal * @param projectPath - The project path to sanitize * @returns Sanitized project path safe for processing */ function sanitizeProjectPath(projectPath) { if (typeof projectPath !== 'string') { return process.cwd(); } const normalized = path.normalize(projectPath); // Check for directory traversal if (normalized.includes('..') || normalized.includes('\x00')) { return process.cwd(); } if (normalized.length > SECURITY_LIMITS.MAX_PATH_LENGTH) { return process.cwd(); } return normalized; } /** * Sanitizes error messages to prevent information leakage * @param error - The error to sanitize * @returns Sanitized error message safe for display */ function sanitizeError(error) { if (typeof error === 'string') { return error.replace(/\/[^\s]*\/[^\s]*/g, '[PATH]').substring(0, 200); } if (error instanceof Error) { return sanitizeError(error.message); } return 'Unknown analysis error'; } /** * CodeAnalyzer - Advanced multi-language codebase analysis with security validation * * The CodeAnalyzer class provides comprehensive code quality analysis across multiple * programming languages including JavaScript/TypeScript, Python, and C#. It combines * static code analysis, pattern detection, and the specialized WOARU Code Smell Analyzer * to generate actionable insights for improving code quality, maintainability, and * development workflow optimization. * * Key features: * - Multi-language support (JS/TS, Python, C#) * - Security-first analysis with input validation * - Integration with WOARU Code Smell Analyzer * - Performance metrics and timeout protection * - Comprehensive tool recommendations * * @example * ```typescript * const analyzer = new CodeAnalyzer(); * * // Analyze a TypeScript project * const insights = await analyzer.analyzeCodebase('./my-project', 'TypeScript'); * * // Review insights * for (const [toolName, insight] of insights) { * console.log(`${toolName}: ${insight.reason}`); * console.log(`Severity: ${insight.severity}`); * insight.evidence.forEach(evidence => console.log(`- ${evidence}`)); * } * * // Get analysis metrics * const metrics = analyzer.getAnalysisMetrics(); * console.log(`Analyzed ${metrics.filesAnalyzed} files`); * console.log(`Generated ${metrics.insightsGenerated} insights`); * ``` * * @since 1.0.0 */ export class CodeAnalyzer { codeSmellAnalyzer; analysisMetrics = { filesAnalyzed: 0, insightsGenerated: 0, securityIssuesFound: 0, averageAnalysisTime: 0, }; constructor() { this.codeSmellAnalyzer = new CodeSmellAnalyzer(); } /** * Performs comprehensive codebase analysis with multi-language support and security validation * * Analyzes the entire codebase for code quality issues, potential improvements, and tool * recommendations. The analysis is performed with security-first principles, input validation, * and timeout protection. Supports JavaScript, TypeScript, Python, and C# projects. * * The method performs the following analysis types: * - Formatting consistency and style issues * - Missing type definitions and type safety * - Debug statements and console outputs * - Comprehensive WOARU Code Smell Analysis * - Missing testing infrastructure * - Git workflow and pre-commit hooks * - Language-specific best practices * * @param projectPath - Absolute path to the project directory to analyze * @param language - Primary programming language of the project ('JavaScript', 'TypeScript', 'Python', 'C#') * @returns Promise resolving to Map of tool names to CodeInsight recommendations * * @throws {Error} When project path is invalid or analysis encounters security issues * * @example * ```typescript * const analyzer = new CodeAnalyzer(); * * // Analyze a TypeScript project for code quality * const insights = await analyzer.analyzeCodebase('./my-app', 'TypeScript'); * * // Check for Prettier recommendations * if (insights.has('prettier')) { * const prettierInsight = insights.get('prettier'); * console.log(`Formatting issues: ${prettierInsight.evidence.length}`); * prettierInsight.files.forEach(file => console.log(`- ${file}`)); * } * * // Check for code smell findings * if (insights.has('code-smells')) { * const smellInsight = insights.get('code-smells'); * console.log(`Code smells found: ${smellInsight.patterns.length} types`); * console.log(`Severity: ${smellInsight.severity}`); * } * * // Analyze a Python project * const pythonInsights = await analyzer.analyzeCodebase('./python-app', 'Python'); * if (pythonInsights.has('black')) { * console.log('Python formatting issues detected'); * } * ``` * * @since 1.0.0 */ async analyzeCodebase(projectPath, language) { const startTime = Date.now(); try { await initializeI18n(); // Validate inputs const safePath = sanitizeProjectPath(projectPath); if (!this.validateLanguage(language)) { console.warn(t('code_analyzer.unsupported_language', { language: String(language), })); return new Map(); } const insights = new Map(); // Add timeout protection const analysisPromise = this.performAnalysisWithTimeout(safePath, language, insights); await analysisPromise; // Update metrics this.updateAnalysisMetrics(insights, Date.now() - startTime); return insights; } catch (error) { console.error(t('code_analyzer.analysis_error'), sanitizeError(error)); return new Map(); } } /** * Validate programming language */ validateLanguage(language) { if (typeof language !== 'string') { return false; } const supportedLanguages = ['JavaScript', 'TypeScript', 'Python', 'C#']; return supportedLanguages.includes(language); } /** * Perform analysis with timeout protection */ async performAnalysisWithTimeout(projectPath, language, insights) { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Code analysis timeout')); }, SECURITY_LIMITS.MAX_ANALYSIS_TIME); this.performAnalysis(projectPath, language, insights) .then(() => { clearTimeout(timeout); resolve(); }) .catch(error => { clearTimeout(timeout); reject(error); }); }); } /** * Main analysis method */ async performAnalysis(projectPath, language, insights) { switch (language) { case 'JavaScript': case 'TypeScript': await this.analyzeJavaScriptProject(projectPath, insights); break; case 'Python': await this.analyzePythonProject(projectPath, insights); break; case 'C#': await this.analyzeCSharpProject(projectPath, insights); break; } } /** * Update analysis performance metrics */ updateAnalysisMetrics(insights, duration) { this.analysisMetrics.filesAnalyzed++; this.analysisMetrics.insightsGenerated += insights.size; this.analysisMetrics.averageAnalysisTime = (this.analysisMetrics.averageAnalysisTime + duration) / 2; } async analyzeJavaScriptProject(projectPath, insights) { // Check for inconsistent code formatting const jsFiles = await glob('**/*.{js,jsx,ts,tsx}', { cwd: projectPath, ignore: ['node_modules/**', 'dist/**', 'build/**'], }); if (jsFiles.length > 0) { const formattingIssues = await this.checkFormattingConsistency(projectPath, jsFiles); if (formattingIssues.length > 0) { insights.set('prettier', { reason: t('code_analyzer.formatting_inconsistent'), evidence: formattingIssues.map(issue => sanitizeFilePath(issue)), files: formattingIssues.map(f => f.split(':')[0]), severity: 'medium', }); } } // Check for missing type definitions const hasTypeScript = await fs.pathExists(path.join(projectPath, 'tsconfig.json')); if (!hasTypeScript) { const complexFiles = await this.findComplexJavaScriptFiles(projectPath, jsFiles); if (complexFiles.length > 0) { insights.set('typescript', { reason: t('code_analyzer.typescript_missing'), evidence: complexFiles .map(f => t('code_analyzer.complex_file_without_types', { file: sanitizeFilePath(f), })) .slice(0, SECURITY_LIMITS.MAX_INSIGHTS_PER_TYPE), files: complexFiles, severity: 'high', }); } } // Check for console.log statements const debugStatements = await this.findDebugStatements(projectPath, jsFiles); if (debugStatements.length > 0) { insights.set('eslint', { reason: t('code_analyzer.debug_statements_found'), evidence: debugStatements .map(stmt => sanitizeFilePath(stmt)) .slice(0, SECURITY_LIMITS.MAX_INSIGHTS_PER_TYPE), files: debugStatements.map(f => sanitizeFilePath(f.split(':')[0])), severity: 'medium', }); } // Run comprehensive code smell analysis const codeSmellFindings = await this.runCodeSmellAnalysis(projectPath, jsFiles); if (codeSmellFindings.length > 0) { const groupedFindings = this.groupCodeSmellFindings(codeSmellFindings); // Add WOARU Code Smell Analysis insight const topIssues = Object.entries(groupedFindings) .sort(([, a], [, b]) => b.length - a.length) .slice(0, 3) .map(([type, findings]) => `${type}: ${findings.length} occurrences`); const criticalCount = codeSmellFindings.filter(f => f.severity === 'error').length; const warningCount = codeSmellFindings.filter(f => f.severity === 'warning').length; insights.set('code-smells', { reason: t('code_analyzer.woaru_analysis_found', { total: codeSmellFindings.length, critical: criticalCount, warnings: warningCount, }), evidence: topIssues.slice(0, SECURITY_LIMITS.MAX_INSIGHTS_PER_TYPE), files: [...new Set(codeSmellFindings.map(f => f.file))], severity: criticalCount > 0 ? 'high' : 'medium', patterns: Object.keys(groupedFindings), }); } // Check for missing tests const testFiles = jsFiles.filter(f => f.includes('.test.') || f.includes('.spec.')); const sourceFiles = jsFiles.filter(f => !f.includes('.test.') && !f.includes('.spec.')); if (sourceFiles.length > 5 && testFiles.length === 0) { insights.set('jest', { reason: t('code_analyzer.no_tests_found'), evidence: [ t('code_analyzer.source_files_without_tests', { count: sourceFiles.length, }), ], files: [], severity: 'high', }); } // Check for Git commits without hooks const hasGit = await fs.pathExists(path.join(projectPath, '.git')); const hasHusky = await fs.pathExists(path.join(projectPath, '.husky')); if (hasGit && !hasHusky && jsFiles.length > 3) { insights.set('husky', { reason: t('code_analyzer.git_without_hooks'), evidence: [ t('code_analyzer.no_husky_directory'), t('code_analyzer.git_repository_active'), ], files: [], severity: 'medium', }); } } async analyzePythonProject(projectPath, insights) { const pyFiles = await glob('**/*.py', { cwd: projectPath, ignore: ['venv/**', '__pycache__/**', '.venv/**'], }); if (pyFiles.length > 0) { // Check for PEP8 violations const styleIssues = await this.checkPythonStyle(projectPath, pyFiles); if (styleIssues.length > 0) { insights.set('black', { reason: t('code_analyzer.pep8_violations'), evidence: styleIssues .map(issue => sanitizeFilePath(issue)) .slice(0, SECURITY_LIMITS.MAX_INSIGHTS_PER_TYPE), files: styleIssues.map(f => sanitizeFilePath(f.split(':')[0])), severity: 'medium', }); } // Check for missing type hints const missingTypes = await this.checkPythonTypeHints(projectPath, pyFiles); if (missingTypes.length > 0) { insights.set('mypy', { reason: t('code_analyzer.missing_type_hints'), evidence: missingTypes .map(type => sanitizeFilePath(type)) .slice(0, SECURITY_LIMITS.MAX_INSIGHTS_PER_TYPE), files: missingTypes.map(f => sanitizeFilePath(f.split(':')[0])), severity: 'medium', }); } // Check for print statements const printStatements = await this.findPrintStatements(projectPath, pyFiles); if (printStatements.length > 0) { insights.set('ruff', { reason: t('code_analyzer.print_statements_found'), evidence: printStatements .map(stmt => sanitizeFilePath(stmt)) .slice(0, SECURITY_LIMITS.MAX_INSIGHTS_PER_TYPE), files: printStatements.map(f => sanitizeFilePath(f.split(':')[0])), severity: 'low', }); } } } /** * Analyze C# project with security validation */ async analyzeCSharpProject(projectPath, insights) { try { const csFiles = await glob('**/*.cs', { cwd: projectPath, ignore: ['bin/**', 'obj/**'], }).catch(() => []); // Limit number of files analyzed const limitedFiles = csFiles.slice(0, SECURITY_LIMITS.MAX_FILES_ANALYZED); if (limitedFiles.length > 0) { // Check for missing .editorconfig const hasEditorConfig = await fs.pathExists(path.join(projectPath, '.editorconfig')); if (!hasEditorConfig) { insights.set('editorconfig', { reason: t('code_analyzer.no_editorconfig'), evidence: [ t('code_analyzer.csharp_files_no_style', { count: limitedFiles.length, }), ], files: [], severity: 'medium', }); } // Check for async issues const asyncIssues = await this.checkCSharpAsyncPatterns(projectPath, limitedFiles); if (asyncIssues.length > 0) { insights.set('sonaranalyzer', { reason: t('code_analyzer.async_issues_found'), evidence: asyncIssues .map(issue => sanitizeFilePath(issue)) .slice(0, SECURITY_LIMITS.MAX_INSIGHTS_PER_TYPE), files: asyncIssues.map(f => sanitizeFilePath(f.split(':')[0])), severity: 'high', }); } } } catch (error) { console.error(t('code_analyzer.csharp_analysis_error'), sanitizeError(error)); } } /** * Get analysis metrics */ getAnalysisMetrics() { return { ...this.analysisMetrics }; } /** * Reset analysis metrics */ resetMetrics() { this.analysisMetrics = { filesAnalyzed: 0, insightsGenerated: 0, securityIssuesFound: 0, averageAnalysisTime: 0, }; } /** * Check formatting consistency with security validation */ async checkFormattingConsistency(projectPath, files) { const issues = []; const indentations = new Set(); // Limit files analyzed for security const limitedFiles = files.slice(0, Math.min(10, SECURITY_LIMITS.MAX_FILES_ANALYZED)); for (const file of limitedFiles) { try { const filePath = path.join(projectPath, file); // Check file size before reading const stats = await fs.stat(filePath).catch(() => null); if (!stats || stats.size > SECURITY_LIMITS.MAX_FILE_SIZE) { continue; } const content = await fs.readFile(filePath, 'utf-8'); const lines = content.split('\n').slice(0, 100); // Limit lines analyzed // Check indentation style for (const line of lines) { const match = line.match(/^(\s+)/); if (match) { if (match[1].includes('\t')) { indentations.add('tabs'); } else if (match[1].length % 2 === 0) { indentations.add('spaces-2'); } else if (match[1].length % 4 === 0) { indentations.add('spaces-4'); } } } // Check for trailing spaces lines.forEach((line, index) => { if (line.endsWith(' ') || line.endsWith('\t')) { issues.push(`${file}:${index + 1}: Trailing whitespace`); } }); } catch { // Skip files that can't be read } } if (indentations.size > 1) { issues.push(t('code_analyzer.inconsistent_indentation', { styles: Array.from(indentations).join(', '), })); } return issues; } async findComplexJavaScriptFiles(projectPath, files) { const complexFiles = []; for (const file of files) { try { const content = await fs.readFile(path.join(projectPath, file), 'utf-8'); // Simple complexity check: functions with many parameters or deep nesting const functionMatches = content.match(/function\s*\w*\s*\([^)]*\)/g) || []; const arrowMatches = content.match(/\([^)]*\)\s*=>/g) || []; for (const match of [...functionMatches, ...arrowMatches]) { const params = match.match(/\((.*?)\)/)?.[1] || ''; const paramCount = params.split(',').filter(p => p.trim()).length; if (paramCount > 3) { complexFiles.push(file); break; } } // Check for deeply nested code (more than 4 levels) const nestingLevel = this.checkNestingLevel(content); if (nestingLevel > 4) { complexFiles.push(file); } } catch { // Skip files that can't be read } } return [...new Set(complexFiles)]; } checkNestingLevel(content) { let maxLevel = 0; let currentLevel = 0; for (const char of content) { if (char === '{') { currentLevel++; maxLevel = Math.max(maxLevel, currentLevel); } else if (char === '}') { currentLevel--; } } return maxLevel; } async findDebugStatements(projectPath, files) { const statements = []; for (const file of files) { try { const content = await fs.readFile(path.join(projectPath, file), 'utf-8'); const lines = content.split('\n'); lines.forEach((line, index) => { if (line.includes('console.log') || line.includes('console.error') || line.includes('debugger')) { statements.push(`${file}:${index + 1}: ${line.trim()}`); } }); } catch { // Skip files that can't be read } } return statements; } async checkPythonStyle(projectPath, files) { const issues = []; for (const file of files.slice(0, 10)) { try { const content = await fs.readFile(path.join(projectPath, file), 'utf-8'); const lines = content.split('\n'); lines.forEach((line, index) => { // Check line length if (line.length > 79) { issues.push(`${file}:${index + 1}: Zeile zu lang (${line.length} > 79)`); } // Check for tabs if (line.includes('\t')) { issues.push(`${file}:${index + 1}: Tabs statt Spaces`); } }); } catch { // Skip files that can't be read } } return issues; } async checkPythonTypeHints(projectPath, files) { const issues = []; for (const file of files) { try { const content = await fs.readFile(path.join(projectPath, file), 'utf-8'); // Simple check for function definitions without type hints const functionDefs = content.match(/def\s+\w+\s*\([^)]*\):/g) || []; for (const def of functionDefs) { if (!def.includes('->') && !def.includes(':')) { issues.push(`${file}: Funktion ohne Return Type Hint`); } } } catch { // Skip files that can't be read } } return issues; } async findPrintStatements(projectPath, files) { const statements = []; for (const file of files) { try { const content = await fs.readFile(path.join(projectPath, file), 'utf-8'); const lines = content.split('\n'); lines.forEach((line, index) => { if (line.match(/print\s*\(/)) { statements.push(`${file}:${index + 1}: ${line.trim()}`); } }); } catch { // Skip files that can't be read } } return statements; } async checkCSharpAsyncPatterns(projectPath, files) { const issues = []; for (const file of files) { try { const content = await fs.readFile(path.join(projectPath, file), 'utf-8'); // Check for async void (bad practice except for event handlers) if (content.match(/async\s+void\s+\w+\s*\(/)) { issues.push(`${file}: async void Methode gefunden (sollte Task zurückgeben)`); } // Check for .Result or .Wait() (can cause deadlocks) if (content.match(/\.\s*(Result|Wait)\s*\(/)) { issues.push(`${file}: .Result oder .Wait() kann zu Deadlocks führen`); } } catch { // Skip files that can't be read } } return issues; } /** * Run code smell analysis on JavaScript/TypeScript files */ async runCodeSmellAnalysis(projectPath, files) { const allFindings = []; for (const file of files) { try { const filePath = path.join(projectPath, file); const ext = path.extname(file).toLowerCase(); const language = ['.ts', '.tsx'].includes(ext) ? 'typescript' : 'javascript'; const findings = await this.codeSmellAnalyzer.analyzeFile(filePath, language); // Add file path to each finding findings.forEach(finding => { allFindings.push({ ...finding, file, }); }); } catch (error) { console.warn(`Code smell analysis failed for ${file}:`, error); } } return allFindings; } /** * Group code smell findings by type */ groupCodeSmellFindings(findings) { return findings.reduce((acc, finding) => { if (!acc[finding.type]) { acc[finding.type] = []; } acc[finding.type].push(finding); return acc; }, {}); } } //# sourceMappingURL=CodeAnalyzer.js.map