1use crate::error::GenerationError;
7use crate::models::GeneratedFile;
8use ricecoder_specs::models::Spec;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ReviewResult {
15 pub quality_score: f32,
17 pub compliance_score: f32,
19 pub overall_score: f32,
21 pub quality_metrics: CodeQualityMetrics,
23 pub compliance_details: ComplianceDetails,
25 pub suggestions: Vec<Suggestion>,
27 pub issues: Vec<ReviewIssue>,
29 pub summary: String,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct CodeQualityMetrics {
36 pub avg_complexity: f32,
38 pub estimated_coverage: f32,
40 pub style_score: f32,
42 pub documentation_score: f32,
44 pub error_handling_score: f32,
46 pub total_lines: usize,
48 pub comment_lines: usize,
50 pub function_count: usize,
52 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#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct ComplianceDetails {
75 pub total_requirements: usize,
77 pub addressed_requirements: usize,
79 pub criteria_coverage: f32,
81 pub unaddressed_requirements: Vec<String>,
83 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#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct Suggestion {
102 pub category: SuggestionCategory,
104 pub file: Option<String>,
106 pub line: Option<usize>,
108 pub message: String,
110 pub action: String,
112 pub priority: u8,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
118pub enum SuggestionCategory {
119 CodeQuality,
121 Documentation,
123 ErrorHandling,
125 Testing,
127 Performance,
129 Security,
131 SpecCompliance,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct ReviewIssue {
138 pub severity: IssueSeverity,
140 pub file: String,
142 pub line: Option<usize>,
144 pub message: String,
146 pub code: String,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
152pub enum IssueSeverity {
153 Critical,
155 Major,
157 Minor,
159 Info,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct ReviewConfig {
166 pub check_quality: bool,
168 pub check_compliance: bool,
170 pub generate_suggestions: bool,
172 pub min_quality_score: f32,
174 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#[derive(Debug, Clone)]
192pub struct ReviewEngine {
193 config: ReviewConfig,
194}
195
196impl ReviewEngine {
197 pub fn new() -> Self {
199 Self {
200 config: ReviewConfig::default(),
201 }
202 }
203
204 pub fn with_config(config: ReviewConfig) -> Self {
206 Self { config }
207 }
208
209 pub fn review(
224 &self,
225 files: &[GeneratedFile],
226 spec: &Spec,
227 ) -> Result<ReviewResult, GenerationError> {
228 let quality_metrics = if self.config.check_quality {
230 self.calculate_quality_metrics(files)?
231 } else {
232 CodeQualityMetrics::default()
233 };
234
235 let compliance_details = if self.config.check_compliance {
237 self.check_compliance(files, spec)?
238 } else {
239 ComplianceDetails::default()
240 };
241
242 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 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 let issues = self.find_issues(files, spec)?;
256
257 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 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 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 let function_count = self.count_functions(&file.content, &file.language);
301 metrics.function_count += function_count;
302
303 let public_count = self.count_public_functions(&file.content, &file.language);
305 metrics.public_function_count += public_count;
306 }
307
308 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 metrics.estimated_coverage = self.estimate_coverage(files);
316
317 metrics.style_score = self.calculate_style_score(files)?;
319
320 metrics.error_handling_score = self.calculate_error_handling_score(files)?;
322
323 if metrics.function_count > 0 {
325 metrics.avg_complexity = 1.5; }
327
328 Ok(metrics)
329 }
330
331 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 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 content
353 .lines()
354 .filter(|l| l.trim().starts_with("def ") && !l.contains("_"))
355 .count()
356 }
357 "go" => {
358 content.matches("func (").count() + content.matches("func [A-Z]").count()
360 }
361 "java" => content.matches("public ").count(),
362 _ => 0,
363 }
364 }
365
366 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 } else {
375 0.2 }
377 }
378
379 fn calculate_style_score(&self, files: &[GeneratedFile]) -> Result<f32, GenerationError> {
381 let mut score: f32 = 1.0;
382
383 for file in files {
384 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 indent_styles.len() > 1 {
397 score -= 0.1;
398 }
399
400 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 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 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 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 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 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 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 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 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 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 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 fn find_issues(
590 &self,
591 files: &[GeneratedFile],
592 _spec: &Spec,
593 ) -> Result<Vec<ReviewIssue>, GenerationError> {
594 let mut issues = Vec::new();
595
596 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 if trimmed.starts_with("pub fn ")
604 || trimmed.starts_with("pub struct ")
605 || trimmed.starts_with("pub enum ")
606 {
607 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 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 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}