]>
Commit | Line | Data |
---|---|---|
f20569fa XL |
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; | |
f20569fa XL |
6 | use std::{cmp, fmt, iter, str}; |
7 | ||
cdc7bbd5 | 8 | use rustc_data_structures::sync::Lrc; |
4b012472 | 9 | use rustc_span::SourceFile; |
f20569fa XL |
10 | use serde::{ser, Deserialize, Deserializer, Serialize, Serializer}; |
11 | use serde_json as json; | |
12 | use thiserror::Error; | |
13 | ||
14 | /// A range of lines in a file, inclusive of both ends. | |
15 | pub 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)] | |
23 | pub enum FileName { | |
24 | Real(PathBuf), | |
25 | Stdin, | |
26 | } | |
27 | ||
28 | impl 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 | ||
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()), | |
5e7ed085 | 42 | FileName::Stdin => write!(f, "<stdin>"), |
f20569fa XL |
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 { | |
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)] | |
85 | pub struct Range { | |
86 | lo: usize, | |
87 | hi: usize, | |
88 | } | |
89 | ||
90 | impl<'a> From<&'a LineRange> for Range { | |
91 | fn from(range: &'a LineRange) -> Range { | |
92 | Range::new(range.lo, range.hi) | |
93 | } | |
94 | } | |
95 | ||
96 | impl fmt::Display for Range { | |
97 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
98 | write!(f, "{}..{}", self.lo, self.hi) | |
99 | } | |
100 | } | |
101 | ||
102 | impl 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)] | |
157 | pub struct FileLines(Option<HashMap<FileName, Vec<Range>>>); | |
158 | ||
159 | impl 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() { | |
ed00b5ec | 165 | write!(f, "{file_name}: ")?; |
f20569fa XL |
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. | |
176 | fn 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 | ||
197 | impl 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. | |
274 | pub struct Files<'a>(Option<::std::collections::hash_map::Keys<'a, FileName, Vec<Range>>>); | |
275 | ||
276 | impl<'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 | ||
284 | fn 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)] | |
292 | pub 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. | |
300 | impl 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)] | |
316 | pub struct JsonSpan { | |
317 | file: FileName, | |
318 | range: (usize, usize), | |
319 | } | |
320 | ||
321 | impl 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. | |
332 | impl<'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. | |
346 | impl ::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)] | |
356 | mod 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 | } |