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