Skip to main content

vtcode_core/
review.rs

1use anyhow::{Result, bail};
2use std::fmt::Write as _;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum ReviewTarget {
6    CurrentDiff,
7    LastDiff,
8    Files(Vec<String>),
9    Custom(String),
10}
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct ReviewSpec {
14    pub target: ReviewTarget,
15    pub style: Option<String>,
16}
17
18pub fn build_review_spec(
19    last_diff: bool,
20    target: Option<String>,
21    files: Vec<String>,
22    style: Option<String>,
23) -> Result<ReviewSpec> {
24    if last_diff && target.is_some() {
25        bail!("--last-diff cannot be combined with --target");
26    }
27    if last_diff && !files.is_empty() {
28        bail!("--last-diff cannot be combined with explicit files");
29    }
30    if target.is_some() && !files.is_empty() {
31        bail!("--target cannot be combined with explicit files");
32    }
33
34    let style = style
35        .map(|value| value.trim().to_string())
36        .filter(|value| !value.is_empty());
37
38    let target = if last_diff {
39        ReviewTarget::LastDiff
40    } else if let Some(target) = target {
41        let target = target.trim();
42        if target.is_empty() {
43            bail!("--target cannot be empty");
44        }
45        ReviewTarget::Custom(target.to_string())
46    } else if !files.is_empty() {
47        let files = files
48            .into_iter()
49            .map(|value| value.trim().to_string())
50            .filter(|value| !value.is_empty())
51            .collect::<Vec<_>>();
52        if files.is_empty() {
53            bail!("review files cannot be empty");
54        }
55        ReviewTarget::Files(files)
56    } else {
57        ReviewTarget::CurrentDiff
58    };
59
60    Ok(ReviewSpec { target, style })
61}
62
63pub fn build_review_prompt(spec: &ReviewSpec) -> String {
64    let mut prompt = String::new();
65    prompt.push_str("Perform a code review.\n");
66    match &spec.target {
67        ReviewTarget::CurrentDiff => {
68            prompt.push_str("Target: review the current git diff in the workspace.\n");
69        }
70        ReviewTarget::LastDiff => {
71            prompt.push_str("Target: review the last committed git diff.\n");
72        }
73        ReviewTarget::Files(files) => {
74            prompt.push_str("Target: review these files:\n");
75            for file in files {
76                let _ = writeln!(prompt, "- {}", file);
77            }
78        }
79        ReviewTarget::Custom(target) => {
80            let _ = writeln!(prompt, "Target: review `{}`.", target);
81        }
82    }
83
84    if let Some(style) = &spec.style {
85        let _ = writeln!(prompt, "Style: {}.", style);
86    }
87
88    prompt.push_str(
89        "\nRequirements:\n\
90         - Review only. Do not modify files or run mutating commands.\n\
91         - Focus on bugs, regressions, security issues, performance issues, and missing tests.\n\
92         - Present findings first, ordered by severity.\n\
93         - Include concrete file paths and line numbers when possible.\n\
94         - If there are no findings, say that explicitly.\n",
95    );
96
97    prompt
98}
99
100#[cfg(test)]
101mod tests {
102    use super::{ReviewSpec, ReviewTarget, build_review_prompt, build_review_spec};
103
104    #[test]
105    fn review_spec_defaults_to_current_diff() {
106        let spec = build_review_spec(false, None, Vec::new(), None).expect("spec");
107        assert_eq!(
108            spec,
109            ReviewSpec {
110                target: ReviewTarget::CurrentDiff,
111                style: None,
112            }
113        );
114    }
115
116    #[test]
117    fn review_spec_rejects_conflicting_target_selectors() {
118        let err = build_review_spec(true, Some("HEAD~1..HEAD".to_string()), Vec::new(), None)
119            .expect_err("conflicting selectors should fail");
120
121        assert!(err.to_string().contains("--last-diff"));
122    }
123
124    #[test]
125    fn review_prompt_includes_files_and_style() {
126        let spec = build_review_spec(
127            false,
128            None,
129            vec!["src/main.rs".to_string(), "src/lib.rs".to_string()],
130            Some("security".to_string()),
131        )
132        .expect("spec");
133        let prompt = build_review_prompt(&spec);
134
135        assert!(prompt.contains("Target: review these files"));
136        assert!(prompt.contains("- src/main.rs"));
137        assert!(prompt.contains("Style: security."));
138        assert!(prompt.contains("Review only."));
139    }
140}