]> git.lizzy.rs Git - rust.git/blob - src/rustfmt_diff.rs
Merge commit 'c4416f20dcaec5d93077f72470e83e150fb923b1' into sync-rustfmt
[rust.git] / src / rustfmt_diff.rs
1 use std::collections::VecDeque;
2 use std::fmt;
3 use std::io;
4 use std::io::Write;
5
6 use crate::config::{Color, Config, Verbosity};
7
8 #[derive(Debug, PartialEq)]
9 pub(crate) enum DiffLine {
10     Context(String),
11     Expected(String),
12     Resulting(String),
13 }
14
15 #[derive(Debug, PartialEq)]
16 pub(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
25 impl 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)]
38 pub 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)]
49 pub struct ModifiedLines {
50     /// The set of changed chunks.
51     pub chunks: Vec<ModifiedChunk>,
52 }
53
54 impl 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.
86 impl 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.
107 impl 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.
144 pub(crate) struct OutputWriter {
145     terminal: Option<Box<dyn term::Terminal<Output = io::Stdout>>>,
146 }
147
148 impl 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.
180 pub(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
248 pub(crate) fn print_diff<F>(diff: Vec<Mismatch>, get_section_title: F, config: &Config)
249 where
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) => writer.writeln(
275                     &format!("-{}{}", str, line_terminator),
276                     Some(term::color::RED),
277                 ),
278             }
279         }
280     }
281 }
282
283 #[cfg(test)]
284 mod test {
285     use super::DiffLine::*;
286     use super::{make_diff, Mismatch};
287     use super::{ModifiedChunk, ModifiedLines};
288
289     #[test]
290     fn diff_simple() {
291         let src = "one\ntwo\nthree\nfour\nfive\n";
292         let dest = "one\ntwo\ntrois\nfour\nfive\n";
293         let diff = make_diff(src, dest, 1);
294         assert_eq!(
295             diff,
296             vec![Mismatch {
297                 line_number: 2,
298                 line_number_orig: 2,
299                 lines: vec![
300                     Context("two".to_owned()),
301                     Resulting("three".to_owned()),
302                     Expected("trois".to_owned()),
303                     Context("four".to_owned()),
304                 ],
305             }]
306         );
307     }
308
309     #[test]
310     fn diff_simple2() {
311         let src = "one\ntwo\nthree\nfour\nfive\nsix\nseven\n";
312         let dest = "one\ntwo\ntrois\nfour\ncinq\nsix\nseven\n";
313         let diff = make_diff(src, dest, 1);
314         assert_eq!(
315             diff,
316             vec![
317                 Mismatch {
318                     line_number: 2,
319                     line_number_orig: 2,
320                     lines: vec![
321                         Context("two".to_owned()),
322                         Resulting("three".to_owned()),
323                         Expected("trois".to_owned()),
324                         Context("four".to_owned()),
325                     ],
326                 },
327                 Mismatch {
328                     line_number: 5,
329                     line_number_orig: 5,
330                     lines: vec![
331                         Resulting("five".to_owned()),
332                         Expected("cinq".to_owned()),
333                         Context("six".to_owned()),
334                     ],
335                 },
336             ]
337         );
338     }
339
340     #[test]
341     fn diff_zerocontext() {
342         let src = "one\ntwo\nthree\nfour\nfive\n";
343         let dest = "one\ntwo\ntrois\nfour\nfive\n";
344         let diff = make_diff(src, dest, 0);
345         assert_eq!(
346             diff,
347             vec![Mismatch {
348                 line_number: 3,
349                 line_number_orig: 3,
350                 lines: vec![Resulting("three".to_owned()), Expected("trois".to_owned())],
351             }]
352         );
353     }
354
355     #[test]
356     fn diff_trailing_newline() {
357         let src = "one\ntwo\nthree\nfour\nfive";
358         let dest = "one\ntwo\nthree\nfour\nfive\n";
359         let diff = make_diff(src, dest, 1);
360         assert_eq!(
361             diff,
362             vec![Mismatch {
363                 line_number: 5,
364                 line_number_orig: 5,
365                 lines: vec![Context("five".to_owned()), Expected("".to_owned())],
366             }]
367         );
368     }
369
370     #[test]
371     fn modified_lines_from_str() {
372         use std::str::FromStr;
373
374         let src = "1 6 2\nfn some() {}\nfn main() {}\n25 3 1\n  struct Test {}";
375         let lines = ModifiedLines::from_str(src).unwrap();
376         assert_eq!(
377             lines,
378             ModifiedLines {
379                 chunks: vec![
380                     ModifiedChunk {
381                         line_number_orig: 1,
382                         lines_removed: 6,
383                         lines: vec!["fn some() {}".to_owned(), "fn main() {}".to_owned(),]
384                     },
385                     ModifiedChunk {
386                         line_number_orig: 25,
387                         lines_removed: 3,
388                         lines: vec!["  struct Test {}".to_owned()]
389                     }
390                 ]
391             }
392         );
393
394         let src = "1 5 3";
395         assert_eq!(ModifiedLines::from_str(src), Err(()));
396
397         let src = "1 5 3\na\nb";
398         assert_eq!(ModifiedLines::from_str(src), Err(()));
399     }
400 }