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