]> git.lizzy.rs Git - rust.git/blob - src/config/file_lines.rs
Fix bugs related to file-lines (#3684)
[rust.git] / src / config / file_lines.rs
1 //! This module contains types and functions to support formatting specific line ranges.
2
3 use itertools::Itertools;
4 use std::collections::HashMap;
5 use std::path::PathBuf;
6 use std::rc::Rc;
7 use std::{cmp, fmt, iter, str};
8
9 use serde::{ser, Deserialize, Deserializer, Serialize, Serializer};
10 use serde_json as json;
11
12 use syntax::source_map::{self, SourceFile, SourceMap, Span};
13
14 /// A range of lines in a file, inclusive of both ends.
15 pub struct LineRange {
16     pub file: Rc<SourceFile>,
17     pub lo: usize,
18     pub hi: usize,
19 }
20
21 /// Defines the name of an input - either a file or stdin.
22 #[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
23 pub enum FileName {
24     Real(PathBuf),
25     Stdin,
26 }
27
28 impl From<source_map::FileName> for FileName {
29     fn from(name: source_map::FileName) -> FileName {
30         match name {
31             source_map::FileName::Real(p) => FileName::Real(p),
32             source_map::FileName::Custom(ref f) if f == "stdin" => FileName::Stdin,
33             _ => unreachable!(),
34         }
35     }
36 }
37
38 impl fmt::Display for FileName {
39     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40         match self {
41             FileName::Real(p) => write!(f, "{}", p.to_str().unwrap()),
42             FileName::Stdin => write!(f, "stdin"),
43         }
44     }
45 }
46
47 impl<'de> Deserialize<'de> for FileName {
48     fn deserialize<D>(deserializer: D) -> Result<FileName, D::Error>
49     where
50         D: Deserializer<'de>,
51     {
52         let s = String::deserialize(deserializer)?;
53         if s == "stdin" {
54             Ok(FileName::Stdin)
55         } else {
56             Ok(FileName::Real(s.into()))
57         }
58     }
59 }
60
61 impl Serialize for FileName {
62     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
63     where
64         S: Serializer,
65     {
66         let s = match self {
67             FileName::Stdin => Ok("stdin"),
68             FileName::Real(path) => path
69                 .to_str()
70                 .ok_or_else(|| ser::Error::custom("path can't be serialized as UTF-8 string")),
71         };
72
73         s.and_then(|s| serializer.serialize_str(s))
74     }
75 }
76
77 impl LineRange {
78     pub fn file_name(&self) -> FileName {
79         self.file.name.clone().into()
80     }
81
82     pub(crate) fn from_span(source_map: &SourceMap, span: Span) -> LineRange {
83         let lo_char_pos = source_map.lookup_char_pos(span.lo());
84         let hi_char_pos = source_map.lookup_char_pos(span.hi());
85         debug_assert!(lo_char_pos.file.name == hi_char_pos.file.name);
86         LineRange {
87             file: lo_char_pos.file.clone(),
88             lo: lo_char_pos.line,
89             hi: hi_char_pos.line,
90         }
91     }
92 }
93
94 /// A range that is inclusive of both ends.
95 #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Deserialize)]
96 pub struct Range {
97     lo: usize,
98     hi: usize,
99 }
100
101 impl<'a> From<&'a LineRange> for Range {
102     fn from(range: &'a LineRange) -> Range {
103         Range::new(range.lo, range.hi)
104     }
105 }
106
107 impl fmt::Display for Range {
108     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109         write!(f, "{}..{}", self.lo, self.hi)
110     }
111 }
112
113 impl Range {
114     pub fn new(lo: usize, hi: usize) -> Range {
115         Range { lo, hi }
116     }
117
118     fn is_empty(self) -> bool {
119         self.lo > self.hi
120     }
121
122     #[allow(dead_code)]
123     fn contains(self, other: Range) -> bool {
124         if other.is_empty() {
125             true
126         } else {
127             !self.is_empty() && self.lo <= other.lo && self.hi >= other.hi
128         }
129     }
130
131     fn intersects(self, other: Range) -> bool {
132         if self.is_empty() || other.is_empty() {
133             false
134         } else {
135             (self.lo <= other.hi && other.hi <= self.hi)
136                 || (other.lo <= self.hi && self.hi <= other.hi)
137         }
138     }
139
140     fn adjacent_to(self, other: Range) -> bool {
141         if self.is_empty() || other.is_empty() {
142             false
143         } else {
144             self.hi + 1 == other.lo || other.hi + 1 == self.lo
145         }
146     }
147
148     /// Returns a new `Range` with lines from `self` and `other` if they were adjacent or
149     /// intersect; returns `None` otherwise.
150     fn merge(self, other: Range) -> Option<Range> {
151         if self.adjacent_to(other) || self.intersects(other) {
152             Some(Range::new(
153                 cmp::min(self.lo, other.lo),
154                 cmp::max(self.hi, other.hi),
155             ))
156         } else {
157             None
158         }
159     }
160 }
161
162 /// A set of lines in files.
163 ///
164 /// It is represented as a multimap keyed on file names, with values a collection of
165 /// non-overlapping ranges sorted by their start point. An inner `None` is interpreted to mean all
166 /// lines in all files.
167 #[derive(Clone, Debug, Default, PartialEq)]
168 pub struct FileLines(Option<HashMap<FileName, Vec<Range>>>);
169
170 impl fmt::Display for FileLines {
171     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172         match &self.0 {
173             None => write!(f, "None")?,
174             Some(map) => {
175                 for (file_name, ranges) in map.iter() {
176                     write!(f, "{}: ", file_name)?;
177                     write!(f, "{}\n", ranges.iter().format(", "))?;
178                 }
179             }
180         };
181         Ok(())
182     }
183 }
184
185 /// Normalizes the ranges so that the invariants for `FileLines` hold: ranges are non-overlapping,
186 /// and ordered by their start point.
187 fn normalize_ranges(ranges: &mut HashMap<FileName, Vec<Range>>) {
188     for ranges in ranges.values_mut() {
189         ranges.sort();
190         let mut result = vec![];
191         let mut iter = ranges.iter_mut().peekable();
192         while let Some(next) = iter.next() {
193             let mut next = *next;
194             while let Some(&&mut peek) = iter.peek() {
195                 if let Some(merged) = next.merge(peek) {
196                     iter.next().unwrap();
197                     next = merged;
198                 } else {
199                     break;
200                 }
201             }
202             result.push(next)
203         }
204         *ranges = result;
205     }
206 }
207
208 impl FileLines {
209     /// Creates a `FileLines` that contains all lines in all files.
210     pub(crate) fn all() -> FileLines {
211         FileLines(None)
212     }
213
214     /// Returns `true` if this `FileLines` contains all lines in all files.
215     pub(crate) fn is_all(&self) -> bool {
216         self.0.is_none()
217     }
218
219     pub fn from_ranges(mut ranges: HashMap<FileName, Vec<Range>>) -> FileLines {
220         normalize_ranges(&mut ranges);
221         FileLines(Some(ranges))
222     }
223
224     /// Returns an iterator over the files contained in `self`.
225     pub fn files(&self) -> Files<'_> {
226         Files(self.0.as_ref().map(HashMap::keys))
227     }
228
229     /// Returns JSON representation as accepted by the `--file-lines JSON` arg.
230     pub fn to_json_spans(&self) -> Vec<JsonSpan> {
231         match &self.0 {
232             None => vec![],
233             Some(file_ranges) => file_ranges
234                 .iter()
235                 .flat_map(|(file, ranges)| ranges.iter().map(move |r| (file, r)))
236                 .map(|(file, range)| JsonSpan {
237                     file: file.to_owned(),
238                     range: (range.lo, range.hi),
239                 })
240                 .collect(),
241         }
242     }
243
244     /// Returns `true` if `self` includes all lines in all files. Otherwise runs `f` on all ranges
245     /// in the designated file (if any) and returns true if `f` ever does.
246     fn file_range_matches<F>(&self, file_name: &FileName, f: F) -> bool
247     where
248         F: FnMut(&Range) -> bool,
249     {
250         let map = match self.0 {
251             // `None` means "all lines in all files".
252             None => return true,
253             Some(ref map) => map,
254         };
255
256         match canonicalize_path_string(file_name).and_then(|file| map.get(&file)) {
257             Some(ranges) => ranges.iter().any(f),
258             None => false,
259         }
260     }
261
262     /// Returns `true` if `range` is fully contained in `self`.
263     #[allow(dead_code)]
264     pub(crate) fn contains(&self, range: &LineRange) -> bool {
265         self.file_range_matches(&range.file_name(), |r| r.contains(Range::from(range)))
266     }
267
268     /// Returns `true` if any lines in `range` are in `self`.
269     pub(crate) fn intersects(&self, range: &LineRange) -> bool {
270         self.file_range_matches(&range.file_name(), |r| r.intersects(Range::from(range)))
271     }
272
273     /// Returns `true` if `line` from `file_name` is in `self`.
274     pub(crate) fn contains_line(&self, file_name: &FileName, line: usize) -> bool {
275         self.file_range_matches(file_name, |r| r.lo <= line && r.hi >= line)
276     }
277
278     /// Returns `true` if all the lines between `lo` and `hi` from `file_name` are in `self`.
279     pub(crate) fn contains_range(&self, file_name: &FileName, lo: usize, hi: usize) -> bool {
280         self.file_range_matches(file_name, |r| r.contains(Range::new(lo, hi)))
281     }
282 }
283
284 /// `FileLines` files iterator.
285 pub struct Files<'a>(Option<::std::collections::hash_map::Keys<'a, FileName, Vec<Range>>>);
286
287 impl<'a> iter::Iterator for Files<'a> {
288     type Item = &'a FileName;
289
290     fn next(&mut self) -> Option<&'a FileName> {
291         self.0.as_mut().and_then(Iterator::next)
292     }
293 }
294
295 fn canonicalize_path_string(file: &FileName) -> Option<FileName> {
296     match *file {
297         FileName::Real(ref path) => path.canonicalize().ok().map(FileName::Real),
298         _ => Some(file.clone()),
299     }
300 }
301
302 // This impl is needed for `Config::override_value` to work for use in tests.
303 impl str::FromStr for FileLines {
304     type Err = String;
305
306     fn from_str(s: &str) -> Result<FileLines, String> {
307         let v: Vec<JsonSpan> = json::from_str(s).map_err(|e| e.to_string())?;
308         let mut m = HashMap::new();
309         for js in v {
310             let (s, r) = JsonSpan::into_tuple(js)?;
311             m.entry(s).or_insert_with(|| vec![]).push(r);
312         }
313         Ok(FileLines::from_ranges(m))
314     }
315 }
316
317 // For JSON decoding.
318 #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]
319 pub struct JsonSpan {
320     file: FileName,
321     range: (usize, usize),
322 }
323
324 impl JsonSpan {
325     fn into_tuple(self) -> Result<(FileName, Range), String> {
326         let (lo, hi) = self.range;
327         let canonical = canonicalize_path_string(&self.file)
328             .ok_or_else(|| format!("Can't canonicalize {}", &self.file))?;
329         Ok((canonical, Range::new(lo, hi)))
330     }
331 }
332
333 // This impl is needed for inclusion in the `Config` struct. We don't have a toml representation
334 // for `FileLines`, so it will just panic instead.
335 impl<'de> ::serde::de::Deserialize<'de> for FileLines {
336     fn deserialize<D>(_: D) -> Result<Self, D::Error>
337     where
338         D: ::serde::de::Deserializer<'de>,
339     {
340         panic!(
341             "FileLines cannot be deserialized from a project rustfmt.toml file: please \
342              specify it via the `--file-lines` option instead"
343         );
344     }
345 }
346
347 // We also want to avoid attempting to serialize a FileLines to toml. The
348 // `Config` struct should ensure this impl is never reached.
349 impl ::serde::ser::Serialize for FileLines {
350     fn serialize<S>(&self, _: S) -> Result<S::Ok, S::Error>
351     where
352         S: ::serde::ser::Serializer,
353     {
354         unreachable!("FileLines cannot be serialized. This is a rustfmt bug.");
355     }
356 }
357
358 #[cfg(test)]
359 mod test {
360     use super::Range;
361
362     #[test]
363     fn test_range_intersects() {
364         assert!(Range::new(1, 2).intersects(Range::new(1, 1)));
365         assert!(Range::new(1, 2).intersects(Range::new(2, 2)));
366         assert!(!Range::new(1, 2).intersects(Range::new(0, 0)));
367         assert!(!Range::new(1, 2).intersects(Range::new(3, 10)));
368         assert!(!Range::new(1, 3).intersects(Range::new(5, 5)));
369     }
370
371     #[test]
372     fn test_range_adjacent_to() {
373         assert!(!Range::new(1, 2).adjacent_to(Range::new(1, 1)));
374         assert!(!Range::new(1, 2).adjacent_to(Range::new(2, 2)));
375         assert!(Range::new(1, 2).adjacent_to(Range::new(0, 0)));
376         assert!(Range::new(1, 2).adjacent_to(Range::new(3, 10)));
377         assert!(!Range::new(1, 3).adjacent_to(Range::new(5, 5)));
378     }
379
380     #[test]
381     fn test_range_contains() {
382         assert!(Range::new(1, 2).contains(Range::new(1, 1)));
383         assert!(Range::new(1, 2).contains(Range::new(2, 2)));
384         assert!(!Range::new(1, 2).contains(Range::new(0, 0)));
385         assert!(!Range::new(1, 2).contains(Range::new(3, 10)));
386     }
387
388     #[test]
389     fn test_range_merge() {
390         assert_eq!(None, Range::new(1, 3).merge(Range::new(5, 5)));
391         assert_eq!(None, Range::new(4, 7).merge(Range::new(0, 1)));
392         assert_eq!(
393             Some(Range::new(3, 7)),
394             Range::new(3, 5).merge(Range::new(4, 7))
395         );
396         assert_eq!(
397             Some(Range::new(3, 7)),
398             Range::new(3, 5).merge(Range::new(5, 7))
399         );
400         assert_eq!(
401             Some(Range::new(3, 7)),
402             Range::new(3, 5).merge(Range::new(6, 7))
403         );
404         assert_eq!(
405             Some(Range::new(3, 7)),
406             Range::new(3, 7).merge(Range::new(4, 5))
407         );
408     }
409
410     use super::json::{self, json};
411     use super::{FileLines, FileName};
412     use std::{collections::HashMap, path::PathBuf};
413
414     #[test]
415     fn file_lines_to_json() {
416         let ranges: HashMap<FileName, Vec<Range>> = [
417             (
418                 FileName::Real(PathBuf::from("src/main.rs")),
419                 vec![Range::new(1, 3), Range::new(5, 7)],
420             ),
421             (
422                 FileName::Real(PathBuf::from("src/lib.rs")),
423                 vec![Range::new(1, 7)],
424             ),
425         ]
426         .iter()
427         .cloned()
428         .collect();
429
430         let file_lines = FileLines::from_ranges(ranges);
431         let mut spans = file_lines.to_json_spans();
432         spans.sort();
433         let json = json::to_value(&spans).unwrap();
434         assert_eq!(
435             json,
436             json! {[
437                 {"file": "src/lib.rs",  "range": [1, 7]},
438                 {"file": "src/main.rs", "range": [1, 3]},
439                 {"file": "src/main.rs", "range": [5, 7]},
440             ]}
441         );
442     }
443 }