ricecoder_generation/
review_engine.rs

1//! Review engine for generated code
2//!
3//! Provides code review capabilities that check generated code against spec requirements,
4//! measure code quality metrics, and provide actionable feedback and suggestions.
5
6use crate::error::GenerationError;
7use crate::models::GeneratedFile;
8use ricecoder_specs::models::Spec;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12/// Result of code review
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ReviewResult {
15    /// Overall quality score (0.0 to 1.0)
16    pub quality_score: f32,
17    /// Spec compliance score (0.0 to 1.0)
18    pub compliance_score: f32,
19    /// Overall review score (0.0 to 1.0)
20    pub overall_score: f32,
21    /// Code quality metrics
22    pub quality_metrics: CodeQualityMetrics,
23    /// Spec compliance details
24    pub compliance_details: ComplianceDetails,
25    /// Suggestions for improvement
26    pub suggestions: Vec<Suggestion>,
27    /// Issues found during review
28    pub issues: Vec<ReviewIssue>,
29    /// Summary of the review
30    pub summary: String,
31}
32
33/// Code quality metrics
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct CodeQualityMetrics {
36    /// Average cyclomatic complexity
37    pub avg_complexity: f32,
38    /// Estimated test coverage percentage
39    pub estimated_coverage: f32,
40    /// Code style score (0.0 to 1.0)
41    pub style_score: f32,
42    /// Documentation score (0.0 to 1.0)
43    pub documentation_score: f32,
44    /// Error handling score (0.0 to 1.0)
45    pub error_handling_score: f32,
46    /// Total lines of code
47    pub total_lines: usize,
48    /// Lines of comments
49    pub comment_lines: usize,
50    /// Number of functions/methods
51    pub function_count: usize,
52    /// Number of public functions/methods
53    pub public_function_count: usize,
54}
55
56impl Default for CodeQualityMetrics {
57    fn default() -> Self {
58        Self {
59            avg_complexity: 0.0,
60            estimated_coverage: 0.0,
61            style_score: 1.0,
62            documentation_score: 0.0,
63            error_handling_score: 0.0,
64            total_lines: 0,
65            comment_lines: 0,
66            function_count: 0,
67            public_function_count: 0,
68        }
69    }
70}
71
72/// Spec compliance details
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct ComplianceDetails {
75    /// Total requirements in spec
76    pub total_requirements: usize,
77    /// Requirements addressed by generated code
78    pub addressed_requirements: usize,
79    /// Acceptance criteria coverage
80    pub criteria_coverage: f32,
81    /// Requirements not addressed
82    pub unaddressed_requirements: Vec<String>,
83    /// Acceptance criteria not met
84    pub unmet_criteria: Vec<String>,
85}
86
87impl Default for ComplianceDetails {
88    fn default() -> Self {
89        Self {
90            total_requirements: 0,
91            addressed_requirements: 0,
92            criteria_coverage: 0.0,
93            unaddressed_requirements: Vec::new(),
94            unmet_criteria: Vec::new(),
95        }
96    }
97}
98
99/// A suggestion for improvement
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct Suggestion {
102    /// Suggestion category
103    pub category: SuggestionCategory,
104    /// File path this suggestion applies to
105    pub file: Option<String>,
106    /// Line number this suggestion applies to
107    pub line: Option<usize>,
108    /// Suggestion message
109    pub message: String,
110    /// Suggested fix or action
111    pub action: String,
112    /// Priority of this suggestion (1-5, 5 being highest)
113    pub priority: u8,
114}
115
116/// Categories of suggestions
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
118pub enum SuggestionCategory {
119    /// Code quality improvement
120    CodeQuality,
121    /// Documentation improvement
122    Documentation,
123    /// Error handling improvement
124    ErrorHandling,
125    /// Testing improvement
126    Testing,
127    /// Performance improvement
128    Performance,
129    /// Security improvement
130    Security,
131    /// Spec compliance improvement
132    SpecCompliance,
133}
134
135/// An issue found during review
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct ReviewIssue {
138    /// Issue severity
139    pub severity: IssueSeverity,
140    /// File path where issue was found
141    pub file: String,
142    /// Line number where issue was found
143    pub line: Option<usize>,
144    /// Issue message
145    pub message: String,
146    /// Issue code (e.g., "REVIEW-001")
147    pub code: String,
148}
149
150/// Severity levels for review issues
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
152pub enum IssueSeverity {
153    /// Critical issue that must be fixed
154    Critical,
155    /// Major issue that should be fixed
156    Major,
157    /// Minor issue that could be improved
158    Minor,
159    /// Informational issue
160    Info,
161}
162
163/// Configuration for code review
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct ReviewConfig {
166    /// Whether to check code quality metrics
167    pub check_quality: bool,
168    /// Whether to check spec compliance
169    pub check_compliance: bool,
170    /// Whether to generate suggestions
171    pub generate_suggestions: bool,
172    /// Minimum quality score threshold (0.0 to 1.0)
173    pub min_quality_score: f32,
174    /// Minimum compliance score threshold (0.0 to 1.0)
175    pub min_compliance_score: f32,
176}
177
178impl Default for ReviewConfig {
179    fn default() -> Self {
180        Self {
181            check_quality: true,
182            check_compliance: true,
183            generate_suggestions: true,
184            min_quality_score: 0.6,
185            min_compliance_score: 0.8,
186        }
187    }
188}
189
190/// Review engine for generated code
191#[derive(Debug, Clone)]
192pub struct ReviewEngine {
193    config: ReviewConfig,
194}
195
196impl ReviewEngine {
197    /// Creates a new ReviewEngine with default configuration
198    pub fn new() -> Self {
199        Self {
200            config: ReviewConfig::default(),
201        }
202    }
203
204    /// Creates a new ReviewEngine with custom configuration
205    pub fn with_config(config: ReviewConfig) -> Self {
206        Self { config }
207    }
208
209    /// Reviews generated code against spec requirements
210    ///
211    /// # Arguments
212    ///
213    /// * `files` - Generated code files to review
214    /// * `spec` - Specification to review against
215    ///
216    /// # Returns
217    ///
218    /// A review result with scores, metrics, and suggestions
219    ///
220    /// # Errors
221    ///
222    /// Returns an error if review cannot be completed
223    pub fn review(
224        &self,
225        files: &[GeneratedFile],
226        spec: &Spec,
227    ) -> Result<ReviewResult, GenerationError> {
228        // Calculate code quality metrics
229        let quality_metrics = if self.config.check_quality {
230            self.calculate_quality_metrics(files)?
231        } else {
232            CodeQualityMetrics::default()
233        };
234
235        // Check spec compliance
236        let compliance_details = if self.config.check_compliance {
237            self.check_compliance(files, spec)?
238        } else {
239            ComplianceDetails::default()
240        };
241
242        // Generate suggestions
243        let suggestions = if self.config.generate_suggestions {
244            self.generate_suggestions(files, spec, &quality_metrics, &compliance_details)?
245        } else {
246            Vec::new()
247        };
248
249        // Calculate scores
250        let quality_score = self.calculate_quality_score(&quality_metrics);
251        let compliance_score = compliance_details.criteria_coverage;
252        let overall_score = (quality_score * 0.4) + (compliance_score * 0.6);
253
254        // Find issues
255        let issues = self.find_issues(files, spec)?;
256
257        // Generate summary
258        let summary = self.generate_summary(
259            &quality_metrics,
260            &compliance_details,
261            quality_score,
262            compliance_score,
263        );
264
265        Ok(ReviewResult {
266            quality_score,
267            compliance_score,
268            overall_score,
269            quality_metrics,
270            compliance_details,
271            suggestions,
272            issues,
273            summary,
274        })
275    }
276
277    /// Calculates code quality metrics for generated files
278    fn calculate_quality_metrics(
279        &self,
280        files: &[GeneratedFile],
281    ) -> Result<CodeQualityMetrics, GenerationError> {
282        let mut metrics = CodeQualityMetrics::default();
283
284        for file in files {
285            let lines: Vec<&str> = file.content.lines().collect();
286            metrics.total_lines += lines.len();
287
288            // Count comment lines
289            for line in &lines {
290                let trimmed = line.trim();
291                if trimmed.starts_with("//")
292                    || trimmed.starts_with("/*")
293                    || trimmed.starts_with("*")
294                {
295                    metrics.comment_lines += 1;
296                }
297            }
298
299            // Estimate function count and complexity
300            let function_count = self.count_functions(&file.content, &file.language);
301            metrics.function_count += function_count;
302
303            // Estimate public function count
304            let public_count = self.count_public_functions(&file.content, &file.language);
305            metrics.public_function_count += public_count;
306        }
307
308        // Calculate documentation score
309        if metrics.total_lines > 0 {
310            metrics.documentation_score =
311                (metrics.comment_lines as f32 / metrics.total_lines as f32).min(1.0);
312        }
313
314        // Estimate coverage based on test presence
315        metrics.estimated_coverage = self.estimate_coverage(files);
316
317        // Calculate style score
318        metrics.style_score = self.calculate_style_score(files)?;
319
320        // Calculate error handling score
321        metrics.error_handling_score = self.calculate_error_handling_score(files)?;
322
323        // Estimate average complexity
324        if metrics.function_count > 0 {
325            metrics.avg_complexity = 1.5; // Simplified estimate
326        }
327
328        Ok(metrics)
329    }
330
331    /// Counts functions in code
332    fn count_functions(&self, content: &str, language: &str) -> usize {
333        match language.to_lowercase().as_str() {
334            "rust" => content.matches("fn ").count(),
335            "typescript" | "javascript" => {
336                content.matches("function ").count() + content.matches("=>").count()
337            }
338            "python" => content.matches("def ").count(),
339            "go" => content.matches("func ").count(),
340            "java" => content.matches("public ").count() + content.matches("private ").count(),
341            _ => 0,
342        }
343    }
344
345    /// Counts public functions in code
346    fn count_public_functions(&self, content: &str, language: &str) -> usize {
347        match language.to_lowercase().as_str() {
348            "rust" => content.matches("pub fn ").count(),
349            "typescript" | "javascript" => content.matches("export ").count(),
350            "python" => {
351                // In Python, functions not starting with _ are public
352                content
353                    .lines()
354                    .filter(|l| l.trim().starts_with("def ") && !l.contains("_"))
355                    .count()
356            }
357            "go" => {
358                // In Go, functions starting with uppercase are public
359                content.matches("func (").count() + content.matches("func [A-Z]").count()
360            }
361            "java" => content.matches("public ").count(),
362            _ => 0,
363        }
364    }
365
366    /// Estimates test coverage based on test file presence
367    fn estimate_coverage(&self, files: &[GeneratedFile]) -> f32 {
368        let has_tests = files.iter().any(|f| {
369            f.path.contains("test") || f.path.contains("spec") || f.path.ends_with("_test.rs")
370        });
371
372        if has_tests {
373            0.6 // Estimate 60% coverage if tests are present
374        } else {
375            0.2 // Estimate 20% coverage if no tests
376        }
377    }
378
379    /// Calculates style score
380    fn calculate_style_score(&self, files: &[GeneratedFile]) -> Result<f32, GenerationError> {
381        let mut score: f32 = 1.0;
382
383        for file in files {
384            // Check for consistent indentation
385            let lines: Vec<&str> = file.content.lines().collect();
386            let mut indent_styles = HashMap::new();
387
388            for line in &lines {
389                if line.starts_with(' ') {
390                    let spaces = line.len() - line.trim_start().len();
391                    *indent_styles.entry(spaces % 4).or_insert(0) += 1;
392                }
393            }
394
395            // If multiple indentation styles, reduce score
396            if indent_styles.len() > 1 {
397                score -= 0.1;
398            }
399
400            // Check for trailing whitespace
401            let trailing_ws = lines
402                .iter()
403                .filter(|l| l.ends_with(' ') || l.ends_with('\t'))
404                .count();
405            if trailing_ws > 0 {
406                score -= 0.05;
407            }
408        }
409
410        Ok(score.max(0.0))
411    }
412
413    /// Calculates error handling score
414    fn calculate_error_handling_score(
415        &self,
416        files: &[GeneratedFile],
417    ) -> Result<f32, GenerationError> {
418        let mut total_score = 0.0;
419        let mut file_count = 0;
420
421        for file in files {
422            let content = &file.content;
423            let language = &file.language;
424
425            let error_patterns = match language.to_lowercase().as_str() {
426                "rust" => vec!["Result<", "?", "unwrap", "expect"],
427                "typescript" | "javascript" => vec!["try", "catch", "throw", "Error"],
428                "python" => vec!["try", "except", "raise"],
429                "go" => vec!["if err != nil", "error"],
430                "java" => vec!["try", "catch", "throw", "Exception"],
431                _ => vec![],
432            };
433
434            let error_count = error_patterns
435                .iter()
436                .map(|p| content.matches(p).count())
437                .sum::<usize>();
438            let lines = content.lines().count();
439
440            let score = if lines > 0 {
441                (error_count as f32 / lines as f32).min(1.0)
442            } else {
443                0.0
444            };
445
446            total_score += score;
447            file_count += 1;
448        }
449
450        if file_count > 0 {
451            Ok(total_score / file_count as f32)
452        } else {
453            Ok(0.0)
454        }
455    }
456
457    /// Checks spec compliance
458    fn check_compliance(
459        &self,
460        files: &[GeneratedFile],
461        spec: &Spec,
462    ) -> Result<ComplianceDetails, GenerationError> {
463        let mut details = ComplianceDetails {
464            total_requirements: spec.requirements.len(),
465            ..Default::default()
466        };
467
468        // Check if requirements are addressed in generated code
469        let combined_content = files
470            .iter()
471            .map(|f| f.content.as_str())
472            .collect::<Vec<_>>()
473            .join("\n");
474
475        for requirement in &spec.requirements {
476            let mut requirement_addressed = false;
477
478            // Check if requirement ID or keywords appear in code
479            if combined_content.contains(&requirement.id)
480                || combined_content.contains(&requirement.user_story)
481            {
482                requirement_addressed = true;
483                details.addressed_requirements += 1;
484            }
485
486            if !requirement_addressed {
487                details
488                    .unaddressed_requirements
489                    .push(requirement.id.clone());
490            }
491
492            // Check acceptance criteria
493            for criterion in &requirement.acceptance_criteria {
494                let criterion_text = format!("{} {}", criterion.when, criterion.then);
495                if !combined_content.contains(&criterion_text) {
496                    details.unmet_criteria.push(criterion_text);
497                }
498            }
499        }
500
501        // Calculate coverage
502        if details.total_requirements > 0 {
503            details.criteria_coverage = (details.addressed_requirements as f32
504                / details.total_requirements as f32)
505                .min(1.0);
506        }
507
508        Ok(details)
509    }
510
511    /// Generates suggestions for improvement
512    fn generate_suggestions(
513        &self,
514        _files: &[GeneratedFile],
515        _spec: &Spec,
516        metrics: &CodeQualityMetrics,
517        compliance: &ComplianceDetails,
518    ) -> Result<Vec<Suggestion>, GenerationError> {
519        let mut suggestions = Vec::new();
520
521        // Suggest improvements based on metrics
522        if metrics.documentation_score < 0.5 {
523            suggestions.push(Suggestion {
524                category: SuggestionCategory::Documentation,
525                file: None,
526                line: None,
527                message: "Code documentation is below recommended level".to_string(),
528                action: "Add doc comments to public functions and types".to_string(),
529                priority: 4,
530            });
531        }
532
533        if metrics.error_handling_score < 0.5 {
534            suggestions.push(Suggestion {
535                category: SuggestionCategory::ErrorHandling,
536                file: None,
537                line: None,
538                message: "Error handling coverage is low".to_string(),
539                action: "Add error handling for fallible operations".to_string(),
540                priority: 4,
541            });
542        }
543
544        if metrics.estimated_coverage < 0.5 {
545            suggestions.push(Suggestion {
546                category: SuggestionCategory::Testing,
547                file: None,
548                line: None,
549                message: "Test coverage is estimated to be low".to_string(),
550                action: "Add unit tests for public functions".to_string(),
551                priority: 3,
552            });
553        }
554
555        // Suggest improvements based on compliance
556        if compliance.criteria_coverage < 0.8 {
557            suggestions.push(Suggestion {
558                category: SuggestionCategory::SpecCompliance,
559                file: None,
560                line: None,
561                message: format!(
562                    "Only {:.0}% of spec requirements are addressed",
563                    compliance.criteria_coverage * 100.0
564                ),
565                action: "Review unaddressed requirements and implement missing functionality"
566                    .to_string(),
567                priority: 5,
568            });
569        }
570
571        // Suggest code quality improvements
572        if metrics.avg_complexity > 5.0 {
573            suggestions.push(Suggestion {
574                category: SuggestionCategory::CodeQuality,
575                file: None,
576                line: None,
577                message: "Average function complexity is high".to_string(),
578                action:
579                    "Consider breaking down complex functions into smaller, more focused functions"
580                        .to_string(),
581                priority: 3,
582            });
583        }
584
585        Ok(suggestions)
586    }
587
588    /// Finds issues in generated code
589    fn find_issues(
590        &self,
591        files: &[GeneratedFile],
592        _spec: &Spec,
593    ) -> Result<Vec<ReviewIssue>, GenerationError> {
594        let mut issues = Vec::new();
595
596        // Check for missing public documentation
597        for file in files {
598            let lines: Vec<&str> = file.content.lines().collect();
599            for (idx, line) in lines.iter().enumerate() {
600                let trimmed = line.trim();
601
602                // Check for public functions without doc comments
603                if trimmed.starts_with("pub fn ")
604                    || trimmed.starts_with("pub struct ")
605                    || trimmed.starts_with("pub enum ")
606                {
607                    // Check if previous line is a doc comment
608                    if idx == 0 || !lines[idx - 1].trim().starts_with("///") {
609                        issues.push(ReviewIssue {
610                            severity: IssueSeverity::Minor,
611                            file: file.path.clone(),
612                            line: Some(idx + 1),
613                            message: "Public item missing documentation comment".to_string(),
614                            code: "REVIEW-001".to_string(),
615                        });
616                    }
617                }
618            }
619        }
620
621        Ok(issues)
622    }
623
624    /// Calculates overall quality score
625    fn calculate_quality_score(&self, metrics: &CodeQualityMetrics) -> f32 {
626        let weights = [
627            (metrics.documentation_score, 0.25),
628            (metrics.error_handling_score, 0.25),
629            (metrics.style_score, 0.25),
630            (metrics.estimated_coverage, 0.25),
631        ];
632
633        weights.iter().map(|(score, weight)| score * weight).sum()
634    }
635
636    /// Generates a summary of the review
637    fn generate_summary(
638        &self,
639        metrics: &CodeQualityMetrics,
640        compliance: &ComplianceDetails,
641        quality_score: f32,
642        compliance_score: f32,
643    ) -> String {
644        format!(
645            "Code Review Summary:\n\
646             - Quality Score: {:.1}%\n\
647             - Compliance Score: {:.1}%\n\
648             - Total Lines: {}\n\
649             - Functions: {}\n\
650             - Public Functions: {}\n\
651             - Documentation Coverage: {:.1}%\n\
652             - Estimated Test Coverage: {:.1}%\n\
653             - Requirements Addressed: {}/{}\n\
654             - Unmet Criteria: {}",
655            quality_score * 100.0,
656            compliance_score * 100.0,
657            metrics.total_lines,
658            metrics.function_count,
659            metrics.public_function_count,
660            metrics.documentation_score * 100.0,
661            metrics.estimated_coverage * 100.0,
662            compliance.addressed_requirements,
663            compliance.total_requirements,
664            compliance.unmet_criteria.len()
665        )
666    }
667}
668
669impl Default for ReviewEngine {
670    fn default() -> Self {
671        Self::new()
672    }
673}
674
675#[cfg(test)]
676mod tests {
677    use super::*;
678
679    #[test]
680    fn test_review_engine_creation() {
681        let engine = ReviewEngine::new();
682        assert_eq!(engine.config.check_quality, true);
683        assert_eq!(engine.config.check_compliance, true);
684    }
685
686    #[test]
687    fn test_count_functions_rust() {
688        let engine = ReviewEngine::new();
689        let code = "fn foo() {}\nfn bar() {}";
690        assert_eq!(engine.count_functions(code, "rust"), 2);
691    }
692
693    #[test]
694    fn test_count_public_functions_rust() {
695        let engine = ReviewEngine::new();
696        let code = "pub fn foo() {}\nfn bar() {}";
697        assert_eq!(engine.count_public_functions(code, "rust"), 1);
698    }
699
700    #[test]
701    fn test_calculate_quality_score() {
702        let engine = ReviewEngine::new();
703        let metrics = CodeQualityMetrics {
704            documentation_score: 0.8,
705            error_handling_score: 0.7,
706            style_score: 0.9,
707            estimated_coverage: 0.6,
708            ..Default::default()
709        };
710        let score = engine.calculate_quality_score(&metrics);
711        assert!(score > 0.0 && score <= 1.0);
712    }
713}