cargo_diff_tools/
diff.rs

1use anyhow::{Context, Result};
2use lazy_static::lazy_static;
3use regex::Regex;
4use std::collections::HashMap;
5
6pub type FileChanges = HashMap<String, Vec<(usize, usize)>>;
7
8/// Return for each file an ordered list of (start, len) intervals of modified lines.
9pub fn parse_diff(diff: &str) -> Result<FileChanges> {
10    lazy_static! {
11        static ref RE: Regex = Regex::new(
12            r"^\+\+\+ .?/(?P<filePath>.*)\s*$|^@@ -[0-9]+(,[0-9]+)? \+(?P<linesFrom>[0-9]+)(,(?P<linesLen>[0-9]+))? @@"
13        ).expect("Failed to parse regex");
14    }
15
16    let mut file_changes: FileChanges = HashMap::new();
17    let mut curr_file_path = None;
18    for line in diff.lines() {
19        if let Some(cap) = RE.captures(line) {
20            if let Some(file_path_match) = cap.name("filePath") {
21                let file_path = file_path_match.as_str().to_string();
22                file_changes.insert(file_path.clone(), vec![]);
23                curr_file_path = Some(file_path);
24            }
25            if let Some(lines_from_match) = cap.name("linesFrom") {
26                let from = lines_from_match
27                    .as_str()
28                    .parse::<usize>()
29                    .with_context(|| {
30                        format!("Failed to parse start of lines range (line: {:?})", line)
31                    })?;
32                let len = if let Some(lines_len_match) = cap.name("linesLen") {
33                    lines_len_match.as_str().parse::<usize>().with_context(|| {
34                        format!("Failed to parse length of lines range (line: {:?})", line)
35                    })?
36                } else {
37                    1
38                };
39                let curr_file_path_ref = curr_file_path
40                    .as_ref()
41                    .with_context(|| "Failed to retrieve current file path")?;
42                file_changes
43                    .get_mut(curr_file_path_ref)
44                    .with_context(|| {
45                        format!(
46                            "Failed to retrieve ranges of file path {:?}",
47                            curr_file_path_ref
48                        )
49                    })?
50                    .push((from, len));
51            }
52        }
53    }
54
55    Ok(file_changes)
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use indoc::indoc;
62
63    #[test]
64    fn test_parse_diff_1() {
65        let diff = indoc! {"
66            +++ b/Cargo.lock
67            @@ -3,0 +4,9 @@
68            @@ -5,0 +15,26 @@
69            +++ b/Cargo.toml
70            @@ -9,0 +10 @@
71            +++ b/src/main.rs
72            @@ -2,0 +3,3 @@
73            @@ -8 +11 @@
74            @@ -13,2 +16,15 @@
75        "};
76        let file_changes = parse_diff(diff).unwrap();
77        eprintln!("{:?}", file_changes);
78        assert_eq!(file_changes.len(), 3);
79        assert_eq!(&file_changes["Cargo.lock"], &[(4, 9), (15, 26)]);
80        assert_eq!(&file_changes["Cargo.toml"], &[(10, 1)]);
81        assert_eq!(&file_changes["src/main.rs"], &[(3, 3), (11, 1), (16, 15)]);
82    }
83
84    #[test]
85    fn test_parse_diff_2() {
86        let diff = indoc! {"
87            +++ b/prusti-viper/src/encoder/mir_encoder/mod.rs
88            @@ -98,5 +98,5 @@
89        "};
90        let file_changes = parse_diff(diff).unwrap();
91        eprintln!("{:?}", file_changes);
92        assert_eq!(file_changes.len(), 1);
93        assert_eq!(&file_changes["prusti-viper/src/encoder/mir_encoder/mod.rs"], &[(98, 5)]);
94    }
95}