rustfmt_nightly/
rustfmt_diff.rs

1use std::collections::VecDeque;
2use std::fmt;
3use std::io;
4use std::io::Write;
5
6use crate::config::{Color, Config, Verbosity};
7
8#[derive(Debug, PartialEq)]
9pub(crate) enum DiffLine {
10    Context(String),
11    Expected(String),
12    Resulting(String),
13}
14
15#[derive(Debug, PartialEq)]
16pub(crate) struct Mismatch {
17    /// The line number in the formatted version.
18    pub(crate) line_number: u32,
19    /// The line number in the original version.
20    pub(crate) line_number_orig: u32,
21    /// The set of lines (context and old/new) in the mismatch.
22    pub(crate) lines: Vec<DiffLine>,
23}
24
25impl Mismatch {
26    fn new(line_number: u32, line_number_orig: u32) -> Mismatch {
27        Mismatch {
28            line_number,
29            line_number_orig,
30            lines: Vec::new(),
31        }
32    }
33}
34
35/// A single span of changed lines, with 0 or more removed lines
36/// and a vector of 0 or more inserted lines.
37#[derive(Debug, PartialEq, Eq)]
38pub struct ModifiedChunk {
39    /// The first to be removed from the original text
40    pub line_number_orig: u32,
41    /// The number of lines which have been replaced
42    pub lines_removed: u32,
43    /// The new lines
44    pub lines: Vec<String>,
45}
46
47/// Set of changed sections of a file.
48#[derive(Debug, PartialEq, Eq)]
49pub struct ModifiedLines {
50    /// The set of changed chunks.
51    pub chunks: Vec<ModifiedChunk>,
52}
53
54impl From<Vec<Mismatch>> for ModifiedLines {
55    fn from(mismatches: Vec<Mismatch>) -> ModifiedLines {
56        let chunks = mismatches.into_iter().map(|mismatch| {
57            let lines = mismatch.lines.iter();
58            let num_removed = lines
59                .filter(|line| matches!(line, DiffLine::Resulting(_)))
60                .count();
61
62            let new_lines = mismatch.lines.into_iter().filter_map(|line| match line {
63                DiffLine::Context(_) | DiffLine::Resulting(_) => None,
64                DiffLine::Expected(str) => Some(str),
65            });
66
67            ModifiedChunk {
68                line_number_orig: mismatch.line_number_orig,
69                lines_removed: num_removed as u32,
70                lines: new_lines.collect(),
71            }
72        });
73
74        ModifiedLines {
75            chunks: chunks.collect(),
76        }
77    }
78}
79
80// Converts a `Mismatch` into a serialized form, which just includes
81// enough information to modify the original file.
82// Each section starts with a line with three integers, space separated:
83//     lineno num_removed num_added
84// followed by (`num_added`) lines of added text. The line numbers are
85// relative to the original file.
86impl fmt::Display for ModifiedLines {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        for chunk in &self.chunks {
89            writeln!(
90                f,
91                "{} {} {}",
92                chunk.line_number_orig,
93                chunk.lines_removed,
94                chunk.lines.len()
95            )?;
96
97            for line in &chunk.lines {
98                writeln!(f, "{line}")?;
99            }
100        }
101
102        Ok(())
103    }
104}
105
106// Allows to convert `Display`ed `ModifiedLines` back to the structural data.
107impl std::str::FromStr for ModifiedLines {
108    type Err = ();
109
110    fn from_str(s: &str) -> Result<ModifiedLines, ()> {
111        let mut chunks = vec![];
112
113        let mut lines = s.lines();
114        while let Some(header) = lines.next() {
115            let mut header = header.split_whitespace();
116            let (orig, rem, new_lines) = match (header.next(), header.next(), header.next()) {
117                (Some(orig), Some(removed), Some(added)) => (orig, removed, added),
118                _ => return Err(()),
119            };
120            let (orig, rem, new_lines): (u32, u32, usize) =
121                match (orig.parse(), rem.parse(), new_lines.parse()) {
122                    (Ok(a), Ok(b), Ok(c)) => (a, b, c),
123                    _ => return Err(()),
124                };
125            let lines = lines.by_ref().take(new_lines);
126            let lines: Vec<_> = lines.map(ToOwned::to_owned).collect();
127            if lines.len() != new_lines {
128                return Err(());
129            }
130
131            chunks.push(ModifiedChunk {
132                line_number_orig: orig,
133                lines_removed: rem,
134                lines,
135            });
136        }
137
138        Ok(ModifiedLines { chunks })
139    }
140}
141
142// This struct handles writing output to stdout and abstracts away the logic
143// of printing in color, if it's possible in the executing environment.
144pub(crate) struct OutputWriter {
145    terminal: Option<Box<dyn term::Terminal<Output = io::Stdout>>>,
146}
147
148impl OutputWriter {
149    // Create a new OutputWriter instance based on the caller's preference
150    // for colorized output and the capabilities of the terminal.
151    pub(crate) fn new(color: Color) -> Self {
152        if let Some(t) = term::stdout() {
153            if color.use_colored_tty() && t.supports_color() {
154                return OutputWriter { terminal: Some(t) };
155            }
156        }
157        OutputWriter { terminal: None }
158    }
159
160    // Write output in the optionally specified color. The output is written
161    // in the specified color if this OutputWriter instance contains a
162    // Terminal in its `terminal` field.
163    pub(crate) fn writeln(&mut self, msg: &str, color: Option<term::color::Color>) {
164        match &mut self.terminal {
165            Some(ref mut t) => {
166                if let Some(color) = color {
167                    t.fg(color).unwrap();
168                }
169                writeln!(t, "{msg}").unwrap();
170                if color.is_some() {
171                    t.reset().unwrap();
172                }
173            }
174            None => println!("{msg}"),
175        }
176    }
177}
178
179// Produces a diff between the expected output and actual output of rustfmt.
180pub(crate) fn make_diff(expected: &str, actual: &str, context_size: usize) -> Vec<Mismatch> {
181    let mut line_number = 1;
182    let mut line_number_orig = 1;
183    let mut context_queue: VecDeque<&str> = VecDeque::with_capacity(context_size);
184    let mut lines_since_mismatch = context_size + 1;
185    let mut results = Vec::new();
186    let mut mismatch = Mismatch::new(0, 0);
187
188    for result in diff::lines(expected, actual) {
189        match result {
190            diff::Result::Left(str) => {
191                if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
192                    results.push(mismatch);
193                    mismatch = Mismatch::new(
194                        line_number - context_queue.len() as u32,
195                        line_number_orig - context_queue.len() as u32,
196                    );
197                }
198
199                while let Some(line) = context_queue.pop_front() {
200                    mismatch.lines.push(DiffLine::Context(line.to_owned()));
201                }
202
203                mismatch.lines.push(DiffLine::Resulting(str.to_owned()));
204                line_number_orig += 1;
205                lines_since_mismatch = 0;
206            }
207            diff::Result::Right(str) => {
208                if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
209                    results.push(mismatch);
210                    mismatch = Mismatch::new(
211                        line_number - context_queue.len() as u32,
212                        line_number_orig - context_queue.len() as u32,
213                    );
214                }
215
216                while let Some(line) = context_queue.pop_front() {
217                    mismatch.lines.push(DiffLine::Context(line.to_owned()));
218                }
219
220                mismatch.lines.push(DiffLine::Expected(str.to_owned()));
221                line_number += 1;
222                lines_since_mismatch = 0;
223            }
224            diff::Result::Both(str, _) => {
225                if context_queue.len() >= context_size {
226                    let _ = context_queue.pop_front();
227                }
228
229                if lines_since_mismatch < context_size {
230                    mismatch.lines.push(DiffLine::Context(str.to_owned()));
231                } else if context_size > 0 {
232                    context_queue.push_back(str);
233                }
234
235                line_number += 1;
236                line_number_orig += 1;
237                lines_since_mismatch += 1;
238            }
239        }
240    }
241
242    results.push(mismatch);
243    results.remove(0);
244
245    results
246}
247
248pub(crate) fn print_diff<F>(diff: Vec<Mismatch>, get_section_title: F, config: &Config)
249where
250    F: Fn(u32) -> String,
251{
252    let color = config.color();
253    let line_terminator = if config.verbose() == Verbosity::Verbose {
254        "⏎"
255    } else {
256        ""
257    };
258
259    let mut writer = OutputWriter::new(color);
260
261    for mismatch in diff {
262        let title = get_section_title(mismatch.line_number_orig);
263        writer.writeln(&title, None);
264
265        for line in mismatch.lines {
266            match line {
267                DiffLine::Context(ref str) => {
268                    writer.writeln(&format!(" {str}{line_terminator}"), None)
269                }
270                DiffLine::Expected(ref str) => writer.writeln(
271                    &format!("+{str}{line_terminator}"),
272                    Some(term::color::GREEN),
273                ),
274                DiffLine::Resulting(ref str) => {
275                    writer.writeln(&format!("-{str}{line_terminator}"), Some(term::color::RED))
276                }
277            }
278        }
279    }
280}
281
282#[cfg(test)]
283mod test {
284    use super::DiffLine::*;
285    use super::{Mismatch, make_diff};
286    use super::{ModifiedChunk, ModifiedLines};
287
288    #[test]
289    fn diff_simple() {
290        let src = "one\ntwo\nthree\nfour\nfive\n";
291        let dest = "one\ntwo\ntrois\nfour\nfive\n";
292        let diff = make_diff(src, dest, 1);
293        assert_eq!(
294            diff,
295            vec![Mismatch {
296                line_number: 2,
297                line_number_orig: 2,
298                lines: vec![
299                    Context("two".to_owned()),
300                    Resulting("three".to_owned()),
301                    Expected("trois".to_owned()),
302                    Context("four".to_owned()),
303                ],
304            }]
305        );
306    }
307
308    #[test]
309    fn diff_simple2() {
310        let src = "one\ntwo\nthree\nfour\nfive\nsix\nseven\n";
311        let dest = "one\ntwo\ntrois\nfour\ncinq\nsix\nseven\n";
312        let diff = make_diff(src, dest, 1);
313        assert_eq!(
314            diff,
315            vec![
316                Mismatch {
317                    line_number: 2,
318                    line_number_orig: 2,
319                    lines: vec![
320                        Context("two".to_owned()),
321                        Resulting("three".to_owned()),
322                        Expected("trois".to_owned()),
323                        Context("four".to_owned()),
324                    ],
325                },
326                Mismatch {
327                    line_number: 5,
328                    line_number_orig: 5,
329                    lines: vec![
330                        Resulting("five".to_owned()),
331                        Expected("cinq".to_owned()),
332                        Context("six".to_owned()),
333                    ],
334                },
335            ]
336        );
337    }
338
339    #[test]
340    fn diff_zerocontext() {
341        let src = "one\ntwo\nthree\nfour\nfive\n";
342        let dest = "one\ntwo\ntrois\nfour\nfive\n";
343        let diff = make_diff(src, dest, 0);
344        assert_eq!(
345            diff,
346            vec![Mismatch {
347                line_number: 3,
348                line_number_orig: 3,
349                lines: vec![Resulting("three".to_owned()), Expected("trois".to_owned())],
350            }]
351        );
352    }
353
354    #[test]
355    fn diff_trailing_newline() {
356        let src = "one\ntwo\nthree\nfour\nfive";
357        let dest = "one\ntwo\nthree\nfour\nfive\n";
358        let diff = make_diff(src, dest, 1);
359        assert_eq!(
360            diff,
361            vec![Mismatch {
362                line_number: 5,
363                line_number_orig: 5,
364                lines: vec![Context("five".to_owned()), Expected("".to_owned())],
365            }]
366        );
367    }
368
369    #[test]
370    fn modified_lines_from_str() {
371        use std::str::FromStr;
372
373        let src = "1 6 2\nfn some() {}\nfn main() {}\n25 3 1\n  struct Test {}";
374        let lines = ModifiedLines::from_str(src).unwrap();
375        assert_eq!(
376            lines,
377            ModifiedLines {
378                chunks: vec![
379                    ModifiedChunk {
380                        line_number_orig: 1,
381                        lines_removed: 6,
382                        lines: vec!["fn some() {}".to_owned(), "fn main() {}".to_owned(),]
383                    },
384                    ModifiedChunk {
385                        line_number_orig: 25,
386                        lines_removed: 3,
387                        lines: vec!["  struct Test {}".to_owned()]
388                    }
389                ]
390            }
391        );
392
393        let src = "1 5 3";
394        assert_eq!(ModifiedLines::from_str(src), Err(()));
395
396        let src = "1 5 3\na\nb";
397        assert_eq!(ModifiedLines::from_str(src), Err(()));
398    }
399}