UNPKG

smart-ast-analyzer

Version:

Advanced AST-based project analysis tool with deep complexity analysis, security scanning, and optional AI enhancement

934 lines (805 loc) 29.3 kB
const parser = require('@babel/parser'); const traverse = require('@babel/traverse').default; const t = require('@babel/types'); const path = require('path'); const fs = require('fs').promises; class DeepAnalysisEngine { constructor(options = {}) { this.options = options; this.analysisResults = { complexity: {}, dependencies: {}, security: {}, performance: {}, quality: {}, patterns: {}, architecture: {} }; } async analyzeProject(files) { console.log(`🔬 Starting deep analysis of ${files.length} files...`); const analyses = await Promise.all([ this.analyzeComplexity(files), this.analyzeDependencies(files), this.analyzeSecurityVulnerabilities(files), this.analyzePerformanceBottlenecks(files), this.analyzeCodeQuality(files), this.analyzeArchitecturalPatterns(files), this.analyzeDatabaseUsage(files) ]); // Merge all analyses const [complexity, dependencies, security, performance, quality, patterns, database] = analyses; return { complexity, dependencies, security, performance, quality, patterns, database, summary: this.generateSummary(analyses), recommendations: this.generateRecommendations(analyses) }; } async analyzeComplexity(files) { const complexity = { cyclomatic: {}, cognitive: {}, nesting: {}, functions: [], classes: [], overall: { score: 0, rating: 'unknown' } }; for (const file of files) { if (!this.isJavaScriptFile(file.path)) continue; try { const ast = await this.parseFile(file.content); const fileComplexity = this.calculateFileComplexity(ast, file.path); complexity.cyclomatic[file.path] = fileComplexity.cyclomatic; complexity.cognitive[file.path] = fileComplexity.cognitive; complexity.nesting[file.path] = fileComplexity.maxNesting; complexity.functions.push(...fileComplexity.functions); complexity.classes.push(...fileComplexity.classes); } catch (error) { console.warn(`Complexity analysis failed for ${file.path}:`, error.message); } } complexity.overall = this.calculateOverallComplexity(complexity); return complexity; } calculateFileComplexity(ast, filePath) { const result = { cyclomatic: 0, cognitive: 0, maxNesting: 0, functions: [], classes: [] }; const self = this; traverse(ast, { Function(path) { const complexity = self.calculateFunctionComplexity(path); result.functions.push({ name: self.getFunctionName(path), file: filePath, line: path.node.loc?.start.line || 1, cyclomatic: complexity.cyclomatic, cognitive: complexity.cognitive, nesting: complexity.maxNesting, params: path.node.params.length, lines: self.calculateLinesOfCode(path), warnings: self.generateComplexityWarnings(complexity) }); result.cyclomatic += complexity.cyclomatic; result.cognitive += complexity.cognitive; result.maxNesting = Math.max(result.maxNesting, complexity.maxNesting); }, ClassDeclaration(path) { const classComplexity = self.calculateClassComplexity(path); result.classes.push({ name: path.node.id.name, file: filePath, line: path.node.loc?.start.line || 1, methods: classComplexity.methods, properties: classComplexity.properties, complexity: classComplexity.totalComplexity, cohesion: self.calculateCohesion(path), warnings: classComplexity.warnings }); } }); return result; } calculateFunctionComplexity(path) { let cyclomatic = 1; // Base complexity let cognitive = 0; let maxNesting = 0; let currentNesting = 0; path.traverse({ enter(innerPath) { // Cyclomatic complexity if (t.isIfStatement(innerPath.node) || t.isWhileStatement(innerPath.node) || t.isForStatement(innerPath.node) || t.isForInStatement(innerPath.node) || t.isForOfStatement(innerPath.node) || t.isSwitchCase(innerPath.node) || t.isCatchClause(innerPath.node) || t.isConditionalExpression(innerPath.node) || t.isLogicalExpression(innerPath.node)) { cyclomatic++; } // Cognitive complexity if (t.isIfStatement(innerPath.node) || t.isSwitchStatement(innerPath.node) || t.isWhileStatement(innerPath.node) || t.isForStatement(innerPath.node) || t.isForInStatement(innerPath.node) || t.isForOfStatement(innerPath.node) || t.isCatchClause(innerPath.node)) { cognitive += (1 + currentNesting); currentNesting++; } maxNesting = Math.max(maxNesting, currentNesting); }, exit(innerPath) { if (t.isIfStatement(innerPath.node) || t.isSwitchStatement(innerPath.node) || t.isWhileStatement(innerPath.node) || t.isForStatement(innerPath.node) || t.isForInStatement(innerPath.node) || t.isForOfStatement(innerPath.node) || t.isCatchClause(innerPath.node)) { currentNesting--; } } }); return { cyclomatic, cognitive, maxNesting }; } async analyzeDependencies(files) { const dependencies = { imports: new Map(), exports: new Map(), graph: {}, cycles: [], unused: [], external: new Set(), internal: new Set() }; // First pass: collect all imports/exports for (const file of files) { if (!this.isJavaScriptFile(file.path)) continue; try { const ast = await this.parseFile(file.content); const fileDeps = this.extractDependencies(ast, file.path); dependencies.imports.set(file.path, fileDeps.imports); dependencies.exports.set(file.path, fileDeps.exports); fileDeps.imports.forEach(imp => { if (imp.source.startsWith('.')) { dependencies.internal.add(imp.source); } else { dependencies.external.add(imp.source); } }); } catch (error) { console.warn(`Dependency analysis failed for ${file.path}:`, error.message); } } // Build dependency graph dependencies.graph = this.buildDependencyGraph(dependencies.imports); // Detect circular dependencies dependencies.cycles = this.detectCircularDependencies(dependencies.graph); // Find unused dependencies dependencies.unused = this.findUnusedDependencies(dependencies.imports, dependencies.exports); return dependencies; } async analyzeSecurityVulnerabilities(files) { const security = { vulnerabilities: [], riskyPatterns: [], missingValidation: [], authIssues: [], dataExposure: [], score: 0 }; for (const file of files) { if (!this.isJavaScriptFile(file.path)) continue; try { const ast = await this.parseFile(file.content); const fileVulns = this.scanSecurityVulnerabilities(ast, file.path, file.content); security.vulnerabilities.push(...fileVulns.vulnerabilities); security.riskyPatterns.push(...fileVulns.riskyPatterns); security.missingValidation.push(...fileVulns.missingValidation); security.authIssues.push(...fileVulns.authIssues); security.dataExposure.push(...fileVulns.dataExposure); } catch (error) { console.warn(`Security analysis failed for ${file.path}:`, error.message); } } security.score = this.calculateSecurityScore(security); return security; } scanSecurityVulnerabilities(ast, filePath, content) { const vulnerabilities = []; const riskyPatterns = []; const missingValidation = []; const authIssues = []; const dataExposure = []; traverse(ast, { CallExpression(path) { const callee = path.node.callee; // Check for dangerous functions if (t.isIdentifier(callee)) { // eval() usage if (callee.name === 'eval') { vulnerabilities.push({ type: 'dangerous-eval', severity: 'critical', message: 'Use of eval() can lead to code injection', file: filePath, line: path.node.loc?.start.line, suggestion: 'Use JSON.parse() or safer alternatives' }); } // setTimeout/setInterval with string if (['setTimeout', 'setInterval'].includes(callee.name)) { const firstArg = path.node.arguments[0]; if (firstArg && (t.isStringLiteral(firstArg) || t.isIdentifier(firstArg))) { vulnerabilities.push({ type: 'string-execution', severity: 'high', message: `${callee.name}() with string argument acts like eval()`, file: filePath, line: path.node.loc?.start.line, suggestion: 'Use function references instead of strings' }); } } } // Check for SQL injection patterns if (t.isMemberExpression(callee)) { if (callee.property.name === 'query' || callee.property.name === 'execute') { const arg = path.node.arguments[0]; if (t.isTemplateLiteral(arg) || (t.isBinaryExpression(arg) && arg.operator === '+')) { vulnerabilities.push({ type: 'sql-injection', severity: 'critical', message: 'Potential SQL injection vulnerability', file: filePath, line: path.node.loc?.start.line, suggestion: 'Use parameterized queries' }); } } } }, AssignmentExpression(path) { // Check for innerHTML assignments if (t.isMemberExpression(path.node.left) && path.node.left.property.name === 'innerHTML') { vulnerabilities.push({ type: 'xss-innerHTML', severity: 'medium', message: 'innerHTML assignment can lead to XSS', file: filePath, line: path.node.loc?.start.line, suggestion: 'Use textContent or sanitize HTML' }); } }, MemberExpression(path) { // Check for process.env access without validation if (t.isIdentifier(path.node.object, { name: 'process' }) && t.isIdentifier(path.node.property, { name: 'env' })) { missingValidation.push({ type: 'env-validation', severity: 'low', message: 'Environment variable used without validation', file: filePath, line: path.node.loc?.start.line, suggestion: 'Validate environment variables' }); } } }); // Check for hardcoded secrets in content const secretPatterns = [ { regex: /password\s*[=:]\s*["']([^"']{8,})["']/gi, type: 'hardcoded-password' }, { regex: /api[_-]?key\s*[=:]\s*["']([^"']{20,})["']/gi, type: 'hardcoded-api-key' }, { regex: /secret\s*[=:]\s*["']([^"']{16,})["']/gi, type: 'hardcoded-secret' } ]; secretPatterns.forEach(({ regex, type }) => { let match; while ((match = regex.exec(content))) { dataExposure.push({ type, severity: 'high', message: `Hardcoded ${type.replace('hardcoded-', '')} found`, file: filePath, suggestion: 'Use environment variables or secure storage' }); } }); return { vulnerabilities, riskyPatterns, missingValidation, authIssues, dataExposure }; } async analyzePerformanceBottlenecks(files) { const performance = { bottlenecks: [], antiPatterns: [], optimizations: [], metrics: {} }; for (const file of files) { if (!this.isJavaScriptFile(file.path)) continue; try { const ast = await this.parseFile(file.content); const filePerf = this.scanPerformanceIssues(ast, file.path); performance.bottlenecks.push(...filePerf.bottlenecks); performance.antiPatterns.push(...filePerf.antiPatterns); performance.optimizations.push(...filePerf.optimizations); } catch (error) { console.warn(`Performance analysis failed for ${file.path}:`, error.message); } } return performance; } scanPerformanceIssues(ast, filePath) { const bottlenecks = []; const antiPatterns = []; const optimizations = []; traverse(ast, { ForStatement(path) { // Check if this for loop is nested inside another loop let parentPath = path.parentPath; let inLoop = false; while (parentPath) { if (t.isForStatement(parentPath.node) || t.isWhileStatement(parentPath.node) || t.isForInStatement(parentPath.node) || t.isForOfStatement(parentPath.node) || (t.isCallExpression(parentPath.node) && t.isMemberExpression(parentPath.node.callee) && ['forEach', 'map', 'filter'].includes(parentPath.node.callee.property.name))) { inLoop = true; break; } parentPath = parentPath.parentPath; } if (inLoop) { bottlenecks.push({ type: 'nested-iteration', severity: 'medium', message: 'Nested iteration detected (O(n²) complexity)', file: filePath, line: path.node.loc?.start.line, suggestion: 'Consider using more efficient algorithms or caching' }); } }, CallExpression(path) { const callee = path.node.callee; // Check for inefficient array operations if (t.isMemberExpression(callee)) { if (callee.property.name === 'forEach') { // Check if inside another loop let parentPath = path.parentPath; let inLoop = false; while (parentPath) { if (t.isForStatement(parentPath.node) || t.isWhileStatement(parentPath.node) || (t.isCallExpression(parentPath.node) && t.isMemberExpression(parentPath.node.callee) && ['forEach', 'map', 'filter'].includes(parentPath.node.callee.property.name))) { inLoop = true; break; } parentPath = parentPath.parentPath; } if (inLoop) { bottlenecks.push({ type: 'nested-iteration', severity: 'medium', message: 'Nested iteration detected (O(n²) complexity)', file: filePath, line: path.node.loc?.start.line, suggestion: 'Consider using more efficient algorithms or caching' }); } } // Check for DOM queries in loops if (['querySelector', 'getElementById', 'getElementsByClassName'].includes(callee.property.name)) { let parentPath = path.parentPath; let inLoop = false; while (parentPath) { if (t.isForStatement(parentPath.node) || t.isWhileStatement(parentPath.node)) { inLoop = true; break; } parentPath = parentPath.parentPath; } if (inLoop) { bottlenecks.push({ type: 'dom-query-in-loop', severity: 'high', message: 'DOM query inside loop', file: filePath, line: path.node.loc?.start.line, suggestion: 'Cache DOM queries outside the loop' }); } } } // Check for JSON.parse(JSON.stringify()) pattern if (t.isMemberExpression(callee) && callee.property.name === 'parse' && t.isIdentifier(callee.object, { name: 'JSON' })) { const arg = path.node.arguments[0]; if (t.isCallExpression(arg) && t.isMemberExpression(arg.callee) && arg.callee.property.name === 'stringify') { antiPatterns.push({ type: 'inefficient-cloning', severity: 'medium', message: 'Inefficient deep cloning with JSON.parse(JSON.stringify())', file: filePath, line: path.node.loc?.start.line, suggestion: 'Use structuredClone() or a proper deep clone library' }); } } }, // Check for memory leaks VariableDeclaration(path) { path.node.declarations.forEach(declarator => { if (t.isArrayExpression(declarator.init)) { // Large array initialization if (declarator.init.elements.length > 1000) { bottlenecks.push({ type: 'large-array-init', severity: 'low', message: 'Large array initialization may impact memory', file: filePath, line: path.node.loc?.start.line, suggestion: 'Consider lazy loading or chunking' }); } } }); } }); return { bottlenecks, antiPatterns, optimizations }; } // Helper methods isJavaScriptFile(filePath) { const ext = path.extname(filePath); return ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].includes(ext); } async parseFile(content) { return parser.parse(content, { sourceType: 'module', plugins: [ 'jsx', 'typescript', 'decorators-legacy', 'classProperties', 'dynamicImport', 'exportDefaultFrom', 'exportNamespaceFrom', 'asyncGenerators', 'functionBind', 'functionSent', 'objectRestSpread', 'optionalCatchBinding', 'optionalChaining', 'nullishCoalescingOperator' ] }); } getFunctionName(path) { if (path.node.id) return path.node.id.name; if (t.isVariableDeclarator(path.parent)) return path.parent.id.name; if (t.isProperty(path.parent)) return path.parent.key.name; return 'anonymous'; } calculateLinesOfCode(path) { const start = path.node.loc?.start.line || 0; const end = path.node.loc?.end.line || 0; return end - start + 1; } generateComplexityWarnings(complexity) { const warnings = []; if (complexity.cyclomatic > 10) { warnings.push('High cyclomatic complexity - consider refactoring'); } if (complexity.cognitive > 15) { warnings.push('High cognitive complexity - difficult to understand'); } if (complexity.maxNesting > 4) { warnings.push('Deep nesting detected - consider extracting functions'); } return warnings; } calculateOverallComplexity(complexity) { const totalFunctions = complexity.functions.length; if (totalFunctions === 0) return { score: 0, rating: 'unknown' }; const avgCyclomatic = complexity.functions.reduce((sum, f) => sum + f.cyclomatic, 0) / totalFunctions; const avgCognitive = complexity.functions.reduce((sum, f) => sum + f.cognitive, 0) / totalFunctions; const score = (avgCyclomatic + avgCognitive) / 2; let rating; if (score <= 5) rating = 'excellent'; else if (score <= 10) rating = 'good'; else if (score <= 15) rating = 'moderate'; else if (score <= 25) rating = 'complex'; else rating = 'very-complex'; return { score: Math.round(score), rating, totalFunctions }; } calculateSecurityScore(security) { const critical = security.vulnerabilities.filter(v => v.severity === 'critical').length; const high = security.vulnerabilities.filter(v => v.severity === 'high').length; const medium = security.vulnerabilities.filter(v => v.severity === 'medium').length; const low = security.vulnerabilities.filter(v => v.severity === 'low').length; // Score out of 100, deduct points for vulnerabilities let score = 100; score -= critical * 20; score -= high * 10; score -= medium * 5; score -= low * 2; return Math.max(0, score); } generateSummary(analyses) { return { totalFiles: this.analysisResults.totalFiles || 0, complexityRating: analyses[0].overall.rating, securityScore: analyses[2].score, vulnerabilitiesFound: analyses[2].vulnerabilities.length, performanceIssues: analyses[3].bottlenecks.length, circularDependencies: analyses[1].cycles.length }; } async analyzeCodeQuality(files) { const quality = { maintainability: {}, readability: {}, testability: {}, documentation: {}, metrics: {} }; for (const file of files) { if (!this.isJavaScriptFile(file.path)) continue; try { const ast = await this.parseFile(file.content); const fileQuality = this.assessCodeQuality(ast, file.path, file.content); Object.keys(fileQuality).forEach(key => { if (!quality[key]) quality[key] = {}; quality[key][file.path] = fileQuality[key]; }); } catch (error) { console.warn(`Code quality analysis failed for ${file.path}:`, error.message); } } return quality; } async analyzeArchitecturalPatterns(files) { const patterns = { designPatterns: [], antiPatterns: [], architecturalSmells: [], suggestions: [] }; // Analyze for common design patterns and anti-patterns for (const file of files) { if (!this.isJavaScriptFile(file.path)) continue; try { const ast = await this.parseFile(file.content); const filePatterns = this.detectArchitecturalPatterns(ast, file.path); patterns.designPatterns.push(...filePatterns.designPatterns); patterns.antiPatterns.push(...filePatterns.antiPatterns); patterns.architecturalSmells.push(...filePatterns.architecturalSmells); } catch (error) { console.warn(`Pattern analysis failed for ${file.path}:`, error.message); } } return patterns; } async analyzeDatabaseUsage(files) { const database = { queries: [], models: [], connections: [], migrations: [], issues: [] }; for (const file of files) { if (!this.isJavaScriptFile(file.path)) continue; try { const ast = await this.parseFile(file.content); const dbUsage = this.analyzeDbPatterns(ast, file.path, file.content); database.queries.push(...dbUsage.queries); database.models.push(...dbUsage.models); database.connections.push(...dbUsage.connections); database.issues.push(...dbUsage.issues); } catch (error) { console.warn(`Database analysis failed for ${file.path}:`, error.message); } } return database; } extractDependencies(ast, filePath) { const imports = []; const exports = []; traverse(ast, { ImportDeclaration(path) { imports.push({ source: path.node.source.value, specifiers: path.node.specifiers.map(spec => ({ type: spec.type, local: spec.local.name, imported: spec.imported?.name })), line: path.node.loc?.start.line }); }, ExportNamedDeclaration(path) { if (path.node.source) { exports.push({ type: 'reexport', source: path.node.source.value, specifiers: path.node.specifiers?.map(spec => spec.exported.name) || [], line: path.node.loc?.start.line }); } else { exports.push({ type: 'named', name: path.node.declaration?.id?.name || 'anonymous', line: path.node.loc?.start.line }); } }, ExportDefaultDeclaration(path) { exports.push({ type: 'default', name: path.node.declaration?.id?.name || 'default', line: path.node.loc?.start.line }); } }); return { imports, exports }; } buildDependencyGraph(importsMap) { const graph = {}; for (const [file, imports] of importsMap) { graph[file] = imports .filter(imp => imp.source.startsWith('.')) .map(imp => imp.source); } return graph; } detectCircularDependencies(graph) { const visited = new Set(); const recursionStack = new Set(); const cycles = []; const dfs = (node, path = []) => { if (recursionStack.has(node)) { const cycleStart = path.indexOf(node); cycles.push(path.slice(cycleStart).concat([node])); return; } if (visited.has(node)) return; visited.add(node); recursionStack.add(node); const dependencies = graph[node] || []; for (const dep of dependencies) { dfs(dep, [...path, node]); } recursionStack.delete(node); }; for (const node in graph) { if (!visited.has(node)) { dfs(node); } } return cycles; } findUnusedDependencies(importsMap, exportsMap) { // This is a simplified implementation // In reality, you'd need to track actual usage return []; } calculateClassComplexity(path) { const methods = []; const properties = []; let totalComplexity = 0; const warnings = []; const self = this; path.traverse({ ClassMethod(methodPath) { const complexity = self.calculateFunctionComplexity(methodPath); methods.push({ name: methodPath.node.key.name, complexity: complexity.cyclomatic, line: methodPath.node.loc?.start.line }); totalComplexity += complexity.cyclomatic; }, ClassProperty(propPath) { properties.push({ name: propPath.node.key.name, line: propPath.node.loc?.start.line }); } }); if (methods.length > 20) { warnings.push('Large class with many methods - consider splitting'); } return { methods, properties, totalComplexity, warnings }; } calculateCohesion(path) { // Simplified LCOM metric return 0.5; // Placeholder } assessCodeQuality(ast, filePath, content) { // Placeholder for code quality assessment return { maintainability: 0.7, readability: 0.8, testability: 0.6 }; } detectArchitecturalPatterns(ast, filePath) { // Placeholder for pattern detection return { designPatterns: [], antiPatterns: [], architecturalSmells: [] }; } analyzeDbPatterns(ast, filePath, content) { const queries = []; const models = []; const connections = []; const issues = []; traverse(ast, { CallExpression(path) { const callee = path.node.callee; // Check for SQL queries if (t.isMemberExpression(callee)) { if (['query', 'execute', 'run'].includes(callee.property.name)) { const arg = path.node.arguments[0]; if (t.isStringLiteral(arg) || t.isTemplateLiteral(arg)) { queries.push({ type: 'sql', query: t.isStringLiteral(arg) ? arg.value : 'template', file: filePath, line: path.node.loc?.start.line }); } } } }, // Check for ORM model definitions ClassDeclaration(path) { if (path.node.superClass && (path.node.superClass.name === 'Model' || (t.isMemberExpression(path.node.superClass) && path.node.superClass.property.name === 'Model'))) { models.push({ name: path.node.id.name, file: filePath, line: path.node.loc?.start.line }); } } }); return { queries, models, connections, issues }; } generateRecommendations(analyses) { const recommendations = []; const [complexity, dependencies, security, performance] = analyses; // Complexity recommendations if (complexity.overall.rating === 'very-complex') { recommendations.push('🔴 Critical: Refactor complex functions to improve maintainability'); } // Security recommendations if (security.vulnerabilities.length > 0) { recommendations.push(`🔴 Security: Fix ${security.vulnerabilities.length} security vulnerabilities`); } // Performance recommendations if (performance.bottlenecks.length > 0) { recommendations.push(`⚡ Performance: Address ${performance.bottlenecks.length} performance bottlenecks`); } // Dependency recommendations if (dependencies.cycles.length > 0) { recommendations.push(`🔄 Architecture: Resolve ${dependencies.cycles.length} circular dependencies`); } return recommendations; } } module.exports = DeepAnalysisEngine;