Skip to main content

ephemeris_report/
diff.rs

1//! The converter for `cargo fmt --check` output.
2
3use std::path::Path;
4
5use crate::{ConvertError, Converted, Details, FileDiff};
6
7const HEADER: &str = "Diff in ";
8const AT_LINE: &str = " at line ";
9
10/// Convert `cargo fmt --check` output into file diffs.
11///
12/// The output is a sequence of `Diff in <path> at line <n>:` blocks. Blocks
13/// of one file merge into one item. A path under `root` becomes relative to it,
14/// both in `file` and in the block header.
15///
16/// # Errors
17///
18/// Returns an error when the input is not UTF-8.
19pub(crate) fn convert(bytes: &[u8], root: &Path) -> Result<Converted, ConvertError> {
20    let text = str::from_utf8(bytes)?;
21    let mut items: Vec<FileDiff> = Vec::new();
22    let mut current = None;
23    for line in text.split_inclusive('\n') {
24        if let Some(header) = line.strip_prefix(HEADER) {
25            let (path, rest) = split_header(header);
26            let file = relative(path, root);
27            let index = items
28                .iter()
29                .position(|item| item.file == file)
30                .unwrap_or(items.len());
31            if index == items.len() {
32                items.push(FileDiff {
33                    file: file.clone(),
34                    text: String::new(),
35                });
36            }
37            current = Some(index);
38            if let Some(item) = items.get_mut(index) {
39                item.text.extend([HEADER, &file, rest]);
40            }
41        } else if let Some(item) = current.and_then(|index| items.get_mut(index)) {
42            item.text.push_str(line);
43        }
44    }
45    Ok(Converted {
46        details: Details::Diff { items },
47        terminal: text.to_owned(),
48    })
49}
50
51/// Split a header into its path and the rest, which includes the line number.
52fn split_header(header: &str) -> (&str, &str) {
53    let body = header.trim_end();
54    let path = body.find(AT_LINE).map_or_else(
55        || {
56            body.strip_suffix(':')
57                .and_then(|body| body.rsplit_once(':'))
58                .map_or(body, |(path, _)| path)
59        },
60        |index| body.get(..index).unwrap_or(body),
61    );
62    header.split_at(path.len())
63}
64
65fn relative(path: &str, root: &Path) -> String {
66    Path::new(path).strip_prefix(root).map_or_else(
67        |_| path.to_owned(),
68        |relative| relative.display().to_string(),
69    )
70}
71
72#[cfg(test)]
73mod tests {
74    use std::path::Path;
75
76    use super::convert;
77    use crate::{ConvertError, Details};
78
79    fn items(input: &str) -> Vec<(String, String)> {
80        let Details::Diff { items } = convert(input.as_bytes(), Path::new("/work"))
81            .unwrap()
82            .details
83        else {
84            panic!("expected a diff");
85        };
86        items
87            .into_iter()
88            .map(|item| (item.file, item.text))
89            .collect()
90    }
91
92    #[test]
93    fn empty_output_has_no_items() {
94        assert_eq!(items("").len(), 0);
95    }
96
97    #[test]
98    fn two_files_become_two_items() {
99        let input = "Diff in /work/src/a.rs at line 1:\n-let  x=1;\n+let x = 1;\nDiff in \
100                     /work/src/b.rs at line 7:\n-fn  f(){}\n+fn f() {}\n";
101        let items = items(input);
102        assert_eq!(items.len(), 2);
103        assert_eq!(items[0].0, "src/a.rs");
104        assert_eq!(
105            items[0].1,
106            "Diff in src/a.rs at line 1:\n-let  x=1;\n+let x = 1;\n"
107        );
108        assert_eq!(items[1].0, "src/b.rs");
109    }
110
111    #[test]
112    fn hunks_of_one_file_merge_and_foreign_paths_stay() {
113        let input = "Diff in /work/a.rs at line 1:\n-a\n+b\nDiff in /other/c.rs at line \
114                     2:\n-c\n+d\nDiff in /work/a.rs at line 9:\n-e\n+f\n";
115        let items = items(input);
116        assert_eq!(items.len(), 2);
117        assert_eq!(items[0].0, "a.rs");
118        assert_eq!(items[0].1.matches("Diff in").count(), 2);
119        assert_eq!(items[1].0, "/other/c.rs");
120    }
121
122    #[test]
123    fn the_colon_header_form_is_understood() {
124        let items = items("Diff in /work/a.rs:12:\n-a\n+b\n");
125        assert_eq!(items[0].0, "a.rs");
126        assert_eq!(items[0].1, "Diff in a.rs:12:\n-a\n+b\n");
127    }
128
129    #[test]
130    fn invalid_utf8_is_an_error() {
131        assert!(matches!(
132            convert(b"\xff", Path::new("/")),
133            Err(ConvertError::Utf8(_))
134        ));
135    }
136}