Skip to main content
Glama
analysis.js29.4 kB
// import crypto from 'crypto'; // Commented out as unused export class AnalysisService { constructor() { // Initialize analysis patterns and rules this.securityPatterns = this.initializeSecurityPatterns(); this.codePatterns = this.initializeCodePatterns(); this.dependencyPatterns = this.initializeDependencyPatterns(); } /** * Analyze code quality metrics for changed files */ async analyzeCodeQuality(prDetails, filePaths = null) { // const filesToAnalyze = filePaths || prDetails.files.map(f => f.filename); // Commented out as unused const analysis = { overall_score: 0, files: [], summary: { high_complexity_files: [], maintainability_issues: [], code_smells: [], recommendations: [], }, }; for (const file of prDetails.files) { if (filePaths && !filePaths.includes(file.filename)) continue; const fileAnalysis = await this.analyzeFileQuality(file); analysis.files.push(fileAnalysis); } // Calculate overall score analysis.overall_score = this.calculateOverallScore(analysis.files); // Generate summary analysis.summary = this.generateQualitySummary(analysis.files); return analysis; } /** * Analyze individual file quality */ async analyzeFileQuality(file) { const analysis = { filename: file.filename, language: this.detectLanguage(file.filename), metrics: { cyclomatic_complexity: 0, lines_of_code: 0, maintainability_index: 0, technical_debt_ratio: 0, }, issues: [], suggestions: [], }; if (!file.patch) return analysis; // Analyze patch content const lines = file.patch.split('\n'); const addedLines = lines.filter(line => line.startsWith('+')).slice(1); // Remove first line which is file info // Calculate metrics analysis.metrics.lines_of_code = addedLines.length; analysis.metrics.cyclomatic_complexity = this.calculateComplexity( addedLines, analysis.language ); analysis.metrics.maintainability_index = this.calculateMaintainabilityIndex( addedLines, analysis.language ); analysis.metrics.technical_debt_ratio = this.calculateTechnicalDebt( addedLines, analysis.language ); // Detect issues analysis.issues = this.detectCodeIssues(addedLines, analysis.language); analysis.suggestions = this.generateCodeSuggestions( addedLines, analysis.language ); return analysis; } /** * Analyze diff impact and risk levels */ async analyzeDiffImpact(prDetails) { const analysis = { overall_risk: 'LOW', impact_categories: { breaking_changes: [], api_changes: [], database_changes: [], configuration_changes: [], security_sensitive: [], performance_critical: [], }, risk_factors: [], affected_areas: [], recommendations: [], }; for (const file of prDetails.files) { const fileImpact = this.analyzeFileImpact(file); this.mergeImpactAnalysis(analysis, fileImpact); } // Calculate overall risk analysis.overall_risk = this.calculateOverallRisk(analysis); // Generate recommendations analysis.recommendations = this.generateImpactRecommendations(analysis); return analysis; } /** * Detect security issues in code changes */ async detectSecurityIssues(prDetails) { const analysis = { security_score: 100, vulnerabilities: [], warnings: [], best_practices: [], compliance_issues: [], }; for (const file of prDetails.files) { if (!file.patch) continue; const fileSecurityIssues = this.scanFileForSecurity(file); analysis.vulnerabilities.push(...fileSecurityIssues.vulnerabilities); analysis.warnings.push(...fileSecurityIssues.warnings); analysis.best_practices.push(...fileSecurityIssues.best_practices); analysis.compliance_issues.push(...fileSecurityIssues.compliance_issues); } // Calculate security score analysis.security_score = this.calculateSecurityScore(analysis); return analysis; } /** * Detect code patterns and anti-patterns */ async detectCodePatterns(prDetails, language = null) { const analysis = { patterns_found: { good_patterns: [], anti_patterns: [], architectural_issues: [], design_patterns: [], }, recommendations: [], language: language || this.detectPrimaryLanguage(prDetails.files), }; for (const file of prDetails.files) { if (!file.patch) continue; const detectedLanguage = language || this.detectLanguage(file.filename); const filePatterns = this.analyzeFilePatterns(file, detectedLanguage); this.mergePatternAnalysis(analysis, filePatterns); } analysis.recommendations = this.generatePatternRecommendations(analysis); return analysis; } /** * Analyze dependency changes */ async analyzeDependencies(prDetails) { const analysis = { dependency_changes: { added: [], removed: [], updated: [], security_issues: [], }, impact_assessment: { risk_level: 'LOW', compatibility_issues: [], security_implications: [], performance_impact: [], }, recommendations: [], }; const dependencyFiles = prDetails.files.filter(file => this.isDependencyFile(file.filename) ); for (const file of dependencyFiles) { const depAnalysis = this.analyzeDependencyFile(file); this.mergeDependencyAnalysis(analysis, depAnalysis); } analysis.recommendations = this.generateDependencyRecommendations(analysis); return analysis; } /** * Analyze test coverage for changed code */ async analyzeTestCoverage(prDetails) { const analysis = { coverage_estimate: 0, test_files: [], production_files: [], missing_tests: [], test_quality: { unit_tests: 0, integration_tests: 0, edge_cases: 0, }, recommendations: [], }; // Categorize files for (const file of prDetails.files) { if (this.isTestFile(file.filename)) { analysis.test_files.push(file); } else if (this.isProductionFile(file.filename)) { analysis.production_files.push(file); } } // Analyze test coverage analysis.coverage_estimate = this.estimateTestCoverage( analysis.test_files, analysis.production_files ); analysis.missing_tests = this.identifyMissingTests( analysis.production_files, analysis.test_files ); analysis.test_quality = this.analyzeTestQuality(analysis.test_files); analysis.recommendations = this.generateTestRecommendations(analysis); return analysis; } /** * Generate code improvement suggestions */ async generateSuggestions(prDetails, filePath, focusAreas = []) { const file = prDetails.files.find(f => f.filename === filePath); if (!file) { throw new Error(`File ${filePath} not found in PR changes`); } const language = this.detectLanguage(filePath); const suggestions = { filename: filePath, language: language, categories: {}, priority_suggestions: [], implementation_examples: [], }; const areas = focusAreas.length > 0 ? focusAreas : [ 'performance', 'security', 'maintainability', 'readability', 'testing', ]; for (const area of areas) { suggestions.categories[area] = this.generateAreaSpecificSuggestions( file, area, language ); } // Prioritize suggestions suggestions.priority_suggestions = this.prioritizeSuggestions( suggestions.categories ); suggestions.implementation_examples = this.generateImplementationExamples( suggestions.priority_suggestions, language ); return suggestions; } // Helper methods for analysis detectLanguage(filename) { const extension = filename.split('.').pop().toLowerCase(); const languageMap = { js: 'javascript', jsx: 'javascript', ts: 'typescript', tsx: 'typescript', py: 'python', java: 'java', go: 'go', rs: 'rust', cpp: 'cpp', c: 'c', cs: 'csharp', php: 'php', rb: 'ruby', swift: 'swift', kt: 'kotlin', }; return languageMap[extension] || 'unknown'; } detectPrimaryLanguage(files) { const languageCounts = {}; for (const file of files) { const lang = this.detectLanguage(file.filename); languageCounts[lang] = (languageCounts[lang] || 0) + 1; } return ( Object.entries(languageCounts).sort((a, b) => b[1] - a[1])[0]?.[0] || 'unknown' ); } calculateComplexity(lines, language) { let complexity = 1; // Base complexity const complexityKeywords = { javascript: [ 'if', 'else', 'while', 'for', 'switch', 'case', 'catch', '&&', '||', '?', ], typescript: [ 'if', 'else', 'while', 'for', 'switch', 'case', 'catch', '&&', '||', '?', ], python: [ 'if', 'elif', 'else', 'while', 'for', 'try', 'except', 'and', 'or', ], java: [ 'if', 'else', 'while', 'for', 'switch', 'case', 'catch', '&&', '||', '?', ], }; const keywords = complexityKeywords[language] || complexityKeywords.javascript; for (const line of lines) { const trimmedLine = line.replace(/^\+\s*/, '').trim(); for (const keyword of keywords) { if (trimmedLine.includes(keyword)) { complexity++; } } } return Math.min(complexity, 20); // Cap at 20 } calculateMaintainabilityIndex(lines, language) { const loc = lines.length; const complexity = this.calculateComplexity(lines, language); // Simplified maintainability index calculation let maintainability = 100 - complexity * 2 - loc * 0.1; // Adjust based on code quality indicators const codeText = lines.join('\n'); if (codeText.includes('TODO') || codeText.includes('FIXME')) { maintainability -= 5; } if (codeText.match(/\w{30,}/)) { // Very long variable names maintainability -= 3; } return Math.max(0, Math.min(100, maintainability)); } calculateTechnicalDebt(lines, _language) { let debtScore = 0; const codeText = lines.join('\n'); // Technical debt indicators const debtIndicators = [ /TODO|FIXME|HACK|XXX/gi, /console\.log|print\(|println!/gi, // Debug statements /\/\*.*\*\//gs, // Commented out code blocks /\.length\s*>\s*\d{2,}/gi, // Magic numbers /function\s+\w+\([^)]{50,}\)/gi, // Long parameter lists /if\s*\([^)]{50,}\)/gi, // Complex conditions ]; for (const pattern of debtIndicators) { const matches = codeText.match(pattern); if (matches) { debtScore += matches.length * 5; } } return Math.min(100, debtScore); } calculateOverallScore(fileAnalyses) { if (fileAnalyses.length === 0) return 100; const avgMaintainability = fileAnalyses.reduce( (sum, f) => sum + f.metrics.maintainability_index, 0 ) / fileAnalyses.length; const avgComplexity = fileAnalyses.reduce( (sum, f) => sum + f.metrics.cyclomatic_complexity, 0 ) / fileAnalyses.length; const avgDebt = fileAnalyses.reduce((sum, f) => sum + f.metrics.technical_debt_ratio, 0) / fileAnalyses.length; return Math.round(avgMaintainability - avgComplexity * 2 - avgDebt * 0.5); } generateQualitySummary(fileAnalyses) { const summary = { high_complexity_files: [], maintainability_issues: [], code_smells: [], recommendations: [], }; for (const file of fileAnalyses) { if (file.metrics.cyclomatic_complexity > 10) { summary.high_complexity_files.push({ filename: file.filename, complexity: file.metrics.cyclomatic_complexity, }); } if (file.metrics.maintainability_index < 60) { summary.maintainability_issues.push({ filename: file.filename, score: file.metrics.maintainability_index, }); } if (file.metrics.technical_debt_ratio > 20) { summary.code_smells.push({ filename: file.filename, debt_ratio: file.metrics.technical_debt_ratio, }); } } // Generate recommendations if (summary.high_complexity_files.length > 0) { summary.recommendations.push( 'Consider breaking down complex functions into smaller, more manageable pieces' ); } if (summary.maintainability_issues.length > 0) { summary.recommendations.push( 'Focus on improving code readability and reducing complexity' ); } if (summary.code_smells.length > 0) { summary.recommendations.push( 'Address technical debt by removing TODO comments and cleaning up code' ); } return summary; } initializeSecurityPatterns() { return { sql_injection: [ /query\s*\+.*\+/gi, /execute\s*\(.*\+.*\)/gi, /SELECT.*\+.*FROM/gi, ], xss: [ /innerHTML\s*=.*\+/gi, /document\.write\s*\(/gi, /\.html\s*\(.*\+/gi, ], hardcoded_secrets: [ /password\s*=\s*["'][^"']+["']/gi, /api_key\s*=\s*["'][^"']+["']/gi, /secret\s*=\s*["'][^"']+["']/gi, /token\s*=\s*["'][^"']+["']/gi, ], insecure_random: [/Math\.random\(\)/gi, /rand\(\)/gi], eval_usage: [/eval\s*\(/gi, /exec\s*\(/gi, /setTimeout\s*\(.*string/gi], }; } initializeCodePatterns() { return { anti_patterns: [ { name: 'God Object', pattern: /class\s+\w+[\s\S]{1000,}/gi, description: 'Large classes that do too much', }, { name: 'Magic Numbers', pattern: /[^.\w]\d{2,}[^.\w]/g, description: 'Hardcoded numeric values without explanation', }, { name: 'Long Parameter List', pattern: /function\s+\w+\s*\([^)]{80,}\)/gi, description: 'Functions with too many parameters', }, ], good_patterns: [ { name: 'Single Responsibility', pattern: /class\s+\w+[\s\S]{50,200}/gi, description: 'Appropriately sized classes', }, { name: 'Constants Usage', pattern: /const\s+[A-Z_]+\s*=/gi, description: 'Use of constants instead of magic numbers', }, ], }; } initializeDependencyPatterns() { return { package_files: [ 'package.json', 'requirements.txt', 'Gemfile', 'pom.xml', 'Cargo.toml', 'go.mod', 'composer.json', ], lock_files: [ 'package-lock.json', 'yarn.lock', 'Pipfile.lock', 'Gemfile.lock', 'Cargo.lock', 'go.sum', ], }; } // Additional helper methods would continue here... // Due to length constraints, I'm showing the structure and key methods detectCodeIssues(lines, _language) { const issues = []; const codeText = lines.join('\n'); // Common code issues if (codeText.match(/console\.log|print\(/gi)) { issues.push({ type: 'debug_code', severity: 'medium', description: 'Debug statements found in code', }); } if (codeText.match(/TODO|FIXME/gi)) { issues.push({ type: 'incomplete_code', severity: 'low', description: 'TODO or FIXME comments found', }); } return issues; } generateCodeSuggestions(lines, language) { const suggestions = []; const codeText = lines.join('\n'); if (language === 'javascript' || language === 'typescript') { if (codeText.includes('var ')) { suggestions.push({ type: 'modernization', description: 'Consider using const/let instead of var', priority: 'medium', }); } if (codeText.match(/function\s*\(/)) { suggestions.push({ type: 'modernization', description: 'Consider using arrow functions for better readability', priority: 'low', }); } } return suggestions; } analyzeFileImpact(file) { const impact = { filename: file.filename, risk_level: 'LOW', categories: [], factors: [], }; // Analyze based on file type and changes if (file.filename.includes('config') || file.filename.includes('setting')) { impact.categories.push('configuration_changes'); impact.risk_level = 'MEDIUM'; } if (file.filename.includes('security') || file.filename.includes('auth')) { impact.categories.push('security_sensitive'); impact.risk_level = 'HIGH'; } if (file.status === 'removed') { impact.factors.push('file_deletion'); impact.risk_level = 'HIGH'; } return impact; } mergeImpactAnalysis(analysis, fileImpact) { for (const category of fileImpact.categories) { if (!analysis.impact_categories[category]) { analysis.impact_categories[category] = []; } analysis.impact_categories[category].push(fileImpact.filename); } analysis.risk_factors.push(...fileImpact.factors); } calculateOverallRisk(analysis) { let riskScore = 0; // Weight different categories if (analysis.impact_categories.security_sensitive.length > 0) riskScore += 30; if (analysis.impact_categories.breaking_changes.length > 0) riskScore += 25; if (analysis.impact_categories.database_changes.length > 0) riskScore += 20; if (analysis.impact_categories.api_changes.length > 0) riskScore += 15; if (riskScore >= 50) return 'HIGH'; if (riskScore >= 25) return 'MEDIUM'; return 'LOW'; } generateImpactRecommendations(analysis) { const recommendations = []; if (analysis.impact_categories.security_sensitive.length > 0) { recommendations.push('Conduct thorough security review and testing'); } if (analysis.overall_risk === 'HIGH') { recommendations.push( 'Consider staged deployment and increased monitoring' ); } return recommendations; } scanFileForSecurity(file) { const issues = { vulnerabilities: [], warnings: [], best_practices: [], compliance_issues: [], }; if (!file.patch) return issues; const addedLines = file.patch .split('\n') .filter(line => line.startsWith('+')); const codeText = addedLines.join('\n'); // Check for security patterns for (const [category, patterns] of Object.entries(this.securityPatterns)) { for (const pattern of patterns) { if (pattern.test(codeText)) { issues.vulnerabilities.push({ type: category, file: file.filename, severity: this.getSecuritySeverity(category), description: this.getSecurityDescription(category), }); } } } return issues; } getSecuritySeverity(category) { const severityMap = { sql_injection: 'critical', xss: 'high', hardcoded_secrets: 'high', insecure_random: 'medium', eval_usage: 'high', }; return severityMap[category] || 'medium'; } getSecurityDescription(category) { const descriptions = { sql_injection: 'Potential SQL injection vulnerability detected', xss: 'Potential XSS vulnerability detected', hardcoded_secrets: 'Hardcoded secrets or credentials found', insecure_random: 'Insecure random number generation', eval_usage: 'Dangerous eval() or similar function usage', }; return descriptions[category] || 'Security issue detected'; } calculateSecurityScore(analysis) { let score = 100; for (const vuln of analysis.vulnerabilities) { switch (vuln.severity) { case 'critical': score -= 25; break; case 'high': score -= 15; break; case 'medium': score -= 5; break; case 'low': score -= 2; break; } } return Math.max(0, score); } analyzeFilePatterns(file, _language) { const patterns = { good_patterns: [], anti_patterns: [], architectural_issues: [], design_patterns: [], }; if (!file.patch) return patterns; const codeText = file.patch; // Check for anti-patterns for (const antiPattern of this.codePatterns.anti_patterns) { if (antiPattern.pattern.test(codeText)) { patterns.anti_patterns.push({ name: antiPattern.name, file: file.filename, description: antiPattern.description, }); } } // Check for good patterns for (const goodPattern of this.codePatterns.good_patterns) { if (goodPattern.pattern.test(codeText)) { patterns.good_patterns.push({ name: goodPattern.name, file: file.filename, description: goodPattern.description, }); } } return patterns; } mergePatternAnalysis(analysis, filePatterns) { analysis.patterns_found.good_patterns.push(...filePatterns.good_patterns); analysis.patterns_found.anti_patterns.push(...filePatterns.anti_patterns); analysis.patterns_found.architectural_issues.push( ...filePatterns.architectural_issues ); analysis.patterns_found.design_patterns.push( ...filePatterns.design_patterns ); } generatePatternRecommendations(analysis) { const recommendations = []; if (analysis.patterns_found.anti_patterns.length > 0) { recommendations.push( 'Address identified anti-patterns to improve code maintainability' ); } if (analysis.patterns_found.good_patterns.length > 0) { recommendations.push( 'Continue using the identified good design patterns' ); } return recommendations; } isDependencyFile(filename) { return ( this.dependencyPatterns.package_files.includes(filename) || this.dependencyPatterns.lock_files.includes(filename) ); } analyzeDependencyFile(_file) { // Implementation for dependency analysis return { added: [], removed: [], updated: [], security_issues: [], }; } mergeDependencyAnalysis(analysis, depAnalysis) { analysis.dependency_changes.added.push(...depAnalysis.added); analysis.dependency_changes.removed.push(...depAnalysis.removed); analysis.dependency_changes.updated.push(...depAnalysis.updated); analysis.dependency_changes.security_issues.push( ...depAnalysis.security_issues ); } generateDependencyRecommendations(analysis) { const recommendations = []; if (analysis.dependency_changes.added.length > 0) { recommendations.push( 'Review new dependencies for security and licensing compliance' ); } return recommendations; } isTestFile(filename) { return ( filename.includes('test') || filename.includes('spec') || filename.includes('__tests__') || filename.endsWith('.test.js') || filename.endsWith('.spec.js') ); } isProductionFile(filename) { return ( !this.isTestFile(filename) && !filename.includes('config') && !filename.includes('README') && !filename.includes('documentation') ); } estimateTestCoverage(testFiles, productionFiles) { if (productionFiles.length === 0) return 100; // Simple heuristic: ratio of test files to production files const ratio = testFiles.length / productionFiles.length; return Math.min(100, Math.round(ratio * 80)); // Cap at 80% since it's an estimate } identifyMissingTests(productionFiles, testFiles) { const missingTests = []; for (const prodFile of productionFiles) { const baseName = prodFile.filename.replace(/\.[^.]+$/, ''); const hasTest = testFiles.some( testFile => testFile.filename.includes(baseName) || testFile.filename.includes(prodFile.filename.replace(/\.[^.]+$/, '')) ); if (!hasTest) { missingTests.push({ filename: prodFile.filename, suggested_test_file: `${baseName}.test.js`, }); } } return missingTests; } analyzeTestQuality(testFiles) { let unitTests = 0; let integrationTests = 0; let edgeCases = 0; for (const testFile of testFiles) { if (!testFile.patch) continue; const content = testFile.patch; // Count test types (simple heuristics) unitTests += (content.match(/describe|it|test\(/g) || []).length; integrationTests += ( content.match(/request|supertest|integration/gi) || [] ).length; edgeCases += (content.match(/edge|boundary|null|undefined|empty/gi) || []) .length; } return { unit_tests: unitTests, integration_tests: integrationTests, edge_cases: edgeCases, }; } generateTestRecommendations(analysis) { const recommendations = []; if (analysis.coverage_estimate < 70) { recommendations.push( 'Consider adding more test coverage for the changed code' ); } if (analysis.missing_tests.length > 0) { recommendations.push( `Add tests for: ${analysis.missing_tests.map(t => t.filename).join(', ')}` ); } if (analysis.test_quality.edge_cases === 0) { recommendations.push('Consider adding edge case testing'); } return recommendations; } generateAreaSpecificSuggestions(file, area, _language) { const suggestions = []; if (!file.patch) return suggestions; const codeText = file.patch; switch (area) { case 'performance': if (codeText.includes('for (') && codeText.includes('length')) { suggestions.push({ type: 'performance', description: 'Cache array length in loop conditions for better performance', example: 'for (let i = 0, len = array.length; i < len; i++)', }); } break; case 'security': if (codeText.includes('innerHTML')) { suggestions.push({ type: 'security', description: 'Consider using textContent instead of innerHTML to prevent XSS', example: 'element.textContent = userInput;', }); } break; case 'maintainability': if (codeText.match(/function\s+\w+\s*\([^)]{50,}\)/)) { suggestions.push({ type: 'maintainability', description: 'Consider breaking down functions with many parameters', example: 'Use configuration objects instead of long parameter lists', }); } break; case 'readability': if (codeText.includes('var ')) { suggestions.push({ type: 'readability', description: 'Use const/let instead of var for better scoping', example: 'const value = ...; // or let if reassignment needed', }); } break; case 'testing': if (!this.isTestFile(file.filename) && file.status === 'added') { suggestions.push({ type: 'testing', description: 'Consider adding unit tests for new functionality', example: `Create ${file.filename.replace(/\.[^.]+$/, '.test.js')}`, }); } break; } return suggestions; } prioritizeSuggestions(categories) { const allSuggestions = []; for (const [area, suggestions] of Object.entries(categories)) { for (const suggestion of suggestions) { allSuggestions.push({ ...suggestion, area, priority: this.getSuggestionPriority(suggestion.type), }); } } return allSuggestions .sort( (a, b) => this.getPriorityValue(a.priority) - this.getPriorityValue(b.priority) ) .slice(0, 10); // Top 10 suggestions } getSuggestionPriority(type) { const priorities = { security: 'high', performance: 'medium', maintainability: 'medium', readability: 'low', testing: 'medium', }; return priorities[type] || 'low'; } getPriorityValue(priority) { const values = { high: 1, medium: 2, low: 3 }; return values[priority] || 3; } generateImplementationExamples(suggestions, language) { return suggestions.slice(0, 5).map(suggestion => ({ suggestion: suggestion.description, example: suggestion.example || `// ${suggestion.description}`, language, })); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/heruujoko/github-review-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server