]> git.proxmox.com Git - rustc.git/blame - src/tools/rustfmt/src/config/file_lines.rs
New upstream version 1.61.0+dfsg1
[rustc.git] / src / tools / rustfmt / src / config / file_lines.rs
CommitLineData
f20569fa
XL
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;
f20569fa
XL
6use std::{cmp, fmt, iter, str};
7
cdc7bbd5 8use rustc_data_structures::sync::Lrc;
f20569fa
XL
9use rustc_span::{self, SourceFile};
10use serde::{ser, Deserialize, Deserializer, Serialize, Serializer};
11use serde_json as json;
12use thiserror::Error;
13
14/// A range of lines in a file, inclusive of both ends.
15pub struct LineRange {
a2a8927a
XL
16 pub(crate) file: Lrc<SourceFile>,
17 pub(crate) lo: usize,
18 pub(crate) hi: usize,
f20569fa
XL
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 {
17df50a5 31 rustc_span::FileName::Real(rustc_span::RealFileName::LocalPath(p)) => FileName::Real(p),
f20569fa
XL
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.to_str().unwrap()),
ee023bcb 42 FileName::Stdin => write!(f, "<stdin>"),
f20569fa
XL
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 {
a2a8927a 78 pub(crate) fn file_name(&self) -> FileName {
f20569fa
XL
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(crate) 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)?;
94222f64 308 m.entry(s).or_insert_with(Vec::new).push(r);
f20569fa
XL
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)
94222f64 325 .ok_or(FileLinesError::CannotCanonicalize(self.file))?;
f20569fa
XL
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}