rustfmt_nightly/config/
file_lines.rs

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