import { DiffProviderPort, LLMPort, OutputPort, ReviewReport, ReviewComment, Diff } from '../ports'; import { ReviewCommentImpl } from '../entities/ReviewComment'; import { ReviewReportImpl } from '../entities/ReviewReport'; export interface ReviewCodeOptions { readonly baseRef?: string; readonly headRef?: string; readonly includeStaged?: boolean; readonly includeUnstaged?: boolean; readonly filePatterns?: string[]; readonly excludePatterns?: string[]; readonly outputFormat?: 'console' | 'markdown' | 'json' | 'html' | 'github' | 'file'; readonly outputPath?: string; readonly postToGitHub?: boolean; readonly githubOwner?: string; readonly githubRepo?: string; readonly pullRequestNumber?: number; } export interface ReviewCodeUseCase { execute(options?: ReviewCodeOptions): Promise; } export class ReviewCodeUseCaseImpl implements ReviewCodeUseCase { constructor( private readonly diffProvider: DiffProviderPort, private readonly llmPort: LLMPort, private readonly outputPort: OutputPort ) {} async execute(options: ReviewCodeOptions = {}): Promise { try { // Get diffs const diffs = await this.diffProvider.getDiffs({ baseRef: options.baseRef, headRef: options.headRef, includeStaged: options.includeStaged, includeUnstaged: options.includeUnstaged, filePatterns: options.filePatterns, excludePatterns: options.excludePatterns, }); if (diffs.length === 0) { throw new Error('No changes found to review'); } // Review each diff const allComments: ReviewComment[] = []; for (const diff of diffs) { const comments = await this.reviewDiff(diff); allComments.push(...comments); } // Generate summary and metrics const summary = await this.generateSummary(diffs, allComments); const metrics = this.calculateMetrics(diffs, allComments); const overallScore = this.calculateOverallScore(allComments); const recommendations = await this.generateRecommendations(allComments); // Create review report const report = new ReviewReportImpl() .id(`review-${Date.now()}`) .timestamp(new Date()) .summary(summary) .overallScore(overallScore) .comments(allComments) .metrics(metrics) .recommendations(recommendations) .build(); // Output the report await this.outputPort.displayReviewReport(report, { format: options.outputFormat || 'console', outputPath: options.outputPath, }); return report; } catch (error) { await this.outputPort.displayError(`Review failed: ${error instanceof Error ? error.message : 'Unknown error'}`); throw error; } } private async reviewDiff(diff: Diff): Promise { const comments: ReviewComment[] = []; // Skip binary files if (diff.isBinary) { return comments; } // Create prompt for LLM const prompt = this.createReviewPrompt(diff); const messages = [ { role: 'system' as const, content: 'You are an expert code reviewer. Analyze the code changes and provide constructive feedback focusing on code quality, security, performance, and maintainability.', }, { role: 'user' as const, content: prompt, }, ]; try { const response = await this.llmPort.generateResponse(messages, { temperature: 0.3, maxTokens: 2000, }); // Parse LLM response and create comments const parsedComments = this.parseLLMResponse(response.content, diff); comments.push(...parsedComments); } catch (error) { // Fallback: create a basic comment if LLM fails const fallbackComment = new ReviewCommentImpl() .id(`fallback-${Date.now()}`) .filePath(diff.filePath) .message('Unable to analyze this file due to LLM service issues') .severity('warning') .category('maintainability') .build(); comments.push(fallbackComment); } return comments; } private createReviewPrompt(diff: Diff): string { const changes = diff.hunks .map(hunk => hunk.lines .filter(line => line.type === 'added' || line.type === 'removed') .map(line => `${line.type === 'added' ? '+' : '-'}${line.content}`) .join('\n') ) .join('\n'); return ` Review the following code changes in ${diff.filePath}: ${changes} Please provide feedback on: 1. Code quality and style 2. Potential bugs or logical errors 3. Security concerns 4. Performance implications 5. Maintainability and readability 6. Missing documentation or tests Format your response as JSON with the following structure: [ { "message": "Description of the issue", "severity": "info|warning|error|suggestion", "category": "style|logic|security|performance|maintainability|documentation|testing", "lineNumber": 42, "suggestion": "How to fix this issue" } ] `; } private parseLLMResponse(response: string, diff: Diff): ReviewComment[] { const comments: ReviewComment[] = []; try { // Try to parse JSON response const parsed = JSON.parse(response); if (Array.isArray(parsed)) { for (const item of parsed) { if (item.message && item.severity && item.category) { const comment = new ReviewCommentImpl() .id(`review-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`) .filePath(diff.filePath) .message(item.message) .severity(item.severity) .category(item.category); if (item.lineNumber) { comment.lineNumber(item.lineNumber); } if (item.suggestion) { comment.suggestion(item.suggestion); } comments.push(comment.build()); } } } } catch (error) { // If JSON parsing fails, create a single comment with the raw response const comment = new ReviewCommentImpl() .id(`review-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`) .filePath(diff.filePath) .message(response) .severity('info') .category('maintainability') .build(); comments.push(comment); } return comments; } private async generateSummary(diffs: Diff[], comments: ReviewComment[]): Promise { const fileCount = diffs.length; const commentCount = comments.length; const errorCount = comments.filter(c => c.severity === 'error').length; const warningCount = comments.filter(c => c.severity === 'warning').length; return `Reviewed ${fileCount} files with ${commentCount} total comments (${errorCount} errors, ${warningCount} warnings).`; } private calculateMetrics(diffs: Diff[], comments: ReviewComment[]) { const linesAdded = diffs.reduce((sum, diff) => sum + diff.hunks.reduce((hunkSum, hunk) => hunkSum + hunk.lines.filter(line => line.type === 'added').length, 0), 0); const linesRemoved = diffs.reduce((sum, diff) => sum + diff.hunks.reduce((hunkSum, hunk) => hunkSum + hunk.lines.filter(line => line.type === 'removed').length, 0), 0); return { totalComments: comments.length, errorCount: comments.filter(c => c.severity === 'error').length, warningCount: comments.filter(c => c.severity === 'warning').length, infoCount: comments.filter(c => c.severity === 'info').length, suggestionCount: comments.filter(c => c.severity === 'suggestion').length, filesReviewed: diffs.length, linesAdded, linesRemoved, }; } private calculateOverallScore(comments: ReviewComment[]): number { const errorWeight = 10; const warningWeight = 5; const infoWeight = 1; const suggestionWeight = 2; const weightedScore = comments.reduce((score, comment) => { switch (comment.severity) { case 'error': return score - errorWeight; case 'warning': return score - warningWeight; case 'info': return score - infoWeight; case 'suggestion': return score - suggestionWeight; default: return score; } }, 100); return Math.max(0, Math.min(100, weightedScore)); } private async generateRecommendations(comments: ReviewComment[]): Promise { const recommendations: string[] = []; const errorCount = comments.filter(c => c.severity === 'error').length; const warningCount = comments.filter(c => c.severity === 'warning').length; if (errorCount > 0) { recommendations.push('Address all error-level issues before merging'); } if (warningCount > 5) { recommendations.push('Consider addressing warning-level issues to improve code quality'); } const securityIssues = comments.filter(c => c.category === 'security'); if (securityIssues.length > 0) { recommendations.push('Review security-related comments carefully'); } const testIssues = comments.filter(c => c.category === 'testing'); if (testIssues.length > 0) { recommendations.push('Consider adding or improving test coverage'); } return recommendations; } }