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