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