blob: e33fe9bb283644b89480c1c1b603d75acaf53a86 [file] [log] [blame]
//! This module contains types and functions to support formatting specific line ranges.
use itertools::Itertools;
use std::collections::HashMap;
use std::path::PathBuf;
use std::{cmp, fmt, iter, str};
use rustc_data_structures::sync::Lrc;
use rustc_span::{self, SourceFile};
use serde::{ser, Deserialize, Deserializer, Serialize, Serializer};
use serde_json as json;
use thiserror::Error;
/// A range of lines in a file, inclusive of both ends.
pub struct LineRange {
pub(crate) file: Lrc<SourceFile>,
pub(crate) lo: usize,
pub(crate) hi: usize,
}
/// Defines the name of an input - either a file or stdin.
#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub enum FileName {
Real(PathBuf),
Stdin,
}
impl From<rustc_span::FileName> for FileName {
fn from(name: rustc_span::FileName) -> FileName {
match name {
rustc_span::FileName::Real(rustc_span::RealFileName::LocalPath(p)) => FileName::Real(p),
rustc_span::FileName::Custom(ref f) if f == "stdin" => FileName::Stdin,
_ => unreachable!(),
}
}
}
impl fmt::Display for FileName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FileName::Real(p) => write!(f, "{}", p.to_str().unwrap()),
FileName::Stdin => write!(f, "<stdin>"),
}
}
}
impl<'de> Deserialize<'de> for FileName {
fn deserialize<D>(deserializer: D) -> Result<FileName, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
if s == "stdin" {
Ok(FileName::Stdin)
} else {
Ok(FileName::Real(s.into()))
}
}
}
impl Serialize for FileName {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = match self {
FileName::Stdin => Ok("stdin"),
FileName::Real(path) => path
.to_str()
.ok_or_else(|| ser::Error::custom("path can't be serialized as UTF-8 string")),
};
s.and_then(|s| serializer.serialize_str(s))
}
}
impl LineRange {
pub(crate) fn file_name(&self) -> FileName {
self.file.name.clone().into()
}
}
/// A range that is inclusive of both ends.
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Deserialize)]
pub struct Range {
lo: usize,
hi: usize,
}
impl<'a> From<&'a LineRange> for Range {
fn from(range: &'a LineRange) -> Range {
Range::new(range.lo, range.hi)
}
}
impl fmt::Display for Range {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}..{}", self.lo, self.hi)
}
}
impl Range {
pub fn new(lo: usize, hi: usize) -> Range {
Range { lo, hi }
}
fn is_empty(self) -> bool {
self.lo > self.hi
}
#[allow(dead_code)]
fn contains(self, other: Range) -> bool {
if other.is_empty() {
true
} else {
!self.is_empty() && self.lo <= other.lo && self.hi >= other.hi
}
}
fn intersects(self, other: Range) -> bool {
if self.is_empty() || other.is_empty() {
false
} else {
(self.lo <= other.hi && other.hi <= self.hi)
|| (other.lo <= self.hi && self.hi <= other.hi)
}
}
fn adjacent_to(self, other: Range) -> bool {
if self.is_empty() || other.is_empty() {
false
} else {
self.hi + 1 == other.lo || other.hi + 1 == self.lo
}
}
/// Returns a new `Range` with lines from `self` and `other` if they were adjacent or
/// intersect; returns `None` otherwise.
fn merge(self, other: Range) -> Option<Range> {
if self.adjacent_to(other) || self.intersects(other) {
Some(Range::new(
cmp::min(self.lo, other.lo),
cmp::max(self.hi, other.hi),
))
} else {
None
}
}
}
/// A set of lines in files.
///
/// It is represented as a multimap keyed on file names, with values a collection of
/// non-overlapping ranges sorted by their start point. An inner `None` is interpreted to mean all
/// lines in all files.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct FileLines(Option<HashMap<FileName, Vec<Range>>>);
impl fmt::Display for FileLines {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 {
None => write!(f, "None")?,
Some(map) => {
for (file_name, ranges) in map.iter() {
write!(f, "{file_name}: ")?;
write!(f, "{}\n", ranges.iter().format(", "))?;
}
}
};
Ok(())
}
}
/// Normalizes the ranges so that the invariants for `FileLines` hold: ranges are non-overlapping,
/// and ordered by their start point.
fn normalize_ranges(ranges: &mut HashMap<FileName, Vec<Range>>) {
for ranges in ranges.values_mut() {
ranges.sort();
let mut result = vec![];
let mut iter = ranges.iter_mut().peekable();
while let Some(next) = iter.next() {
let mut next = *next;
while let Some(&&mut peek) = iter.peek() {
if let Some(merged) = next.merge(peek) {
iter.next().unwrap();
next = merged;
} else {
break;
}
}
result.push(next)
}
*ranges = result;
}
}
impl FileLines {
/// Creates a `FileLines` that contains all lines in all files.
pub(crate) fn all() -> FileLines {
FileLines(None)
}
/// Returns `true` if this `FileLines` contains all lines in all files.
pub(crate) fn is_all(&self) -> bool {
self.0.is_none()
}
pub fn from_ranges(mut ranges: HashMap<FileName, Vec<Range>>) -> FileLines {
normalize_ranges(&mut ranges);
FileLines(Some(ranges))
}
/// Returns an iterator over the files contained in `self`.
pub fn files(&self) -> Files<'_> {
Files(self.0.as_ref().map(HashMap::keys))
}
/// Returns JSON representation as accepted by the `--file-lines JSON` arg.
pub fn to_json_spans(&self) -> Vec<JsonSpan> {
match &self.0 {
None => vec![],
Some(file_ranges) => file_ranges
.iter()
.flat_map(|(file, ranges)| ranges.iter().map(move |r| (file, r)))
.map(|(file, range)| JsonSpan {
file: file.to_owned(),
range: (range.lo, range.hi),
})
.collect(),
}
}
/// Returns `true` if `self` includes all lines in all files. Otherwise runs `f` on all ranges
/// in the designated file (if any) and returns true if `f` ever does.
fn file_range_matches<F>(&self, file_name: &FileName, f: F) -> bool
where
F: FnMut(&Range) -> bool,
{
let map = match self.0 {
// `None` means "all lines in all files".
None => return true,
Some(ref map) => map,
};
match canonicalize_path_string(file_name).and_then(|file| map.get(&file)) {
Some(ranges) => ranges.iter().any(f),
None => false,
}
}
/// Returns `true` if `range` is fully contained in `self`.
#[allow(dead_code)]
pub(crate) fn contains(&self, range: &LineRange) -> bool {
self.file_range_matches(&range.file_name(), |r| r.contains(Range::from(range)))
}
/// Returns `true` if any lines in `range` are in `self`.
pub(crate) fn intersects(&self, range: &LineRange) -> bool {
self.file_range_matches(&range.file_name(), |r| r.intersects(Range::from(range)))
}
/// Returns `true` if `line` from `file_name` is in `self`.
pub(crate) fn contains_line(&self, file_name: &FileName, line: usize) -> bool {
self.file_range_matches(file_name, |r| r.lo <= line && r.hi >= line)
}
/// Returns `true` if all the lines between `lo` and `hi` from `file_name` are in `self`.
pub(crate) fn contains_range(&self, file_name: &FileName, lo: usize, hi: usize) -> bool {
self.file_range_matches(file_name, |r| r.contains(Range::new(lo, hi)))
}
}
/// `FileLines` files iterator.
pub struct Files<'a>(Option<::std::collections::hash_map::Keys<'a, FileName, Vec<Range>>>);
impl<'a> iter::Iterator for Files<'a> {
type Item = &'a FileName;
fn next(&mut self) -> Option<&'a FileName> {
self.0.as_mut().and_then(Iterator::next)
}
}
fn canonicalize_path_string(file: &FileName) -> Option<FileName> {
match *file {
FileName::Real(ref path) => path.canonicalize().ok().map(FileName::Real),
_ => Some(file.clone()),
}
}
#[derive(Error, Debug)]
pub enum FileLinesError {
#[error("{0}")]
Json(json::Error),
#[error("Can't canonicalize {0}")]
CannotCanonicalize(FileName),
}
// This impl is needed for `Config::override_value` to work for use in tests.
impl str::FromStr for FileLines {
type Err = FileLinesError;
fn from_str(s: &str) -> Result<FileLines, Self::Err> {
let v: Vec<JsonSpan> = json::from_str(s).map_err(FileLinesError::Json)?;
let mut m = HashMap::new();
for js in v {
let (s, r) = JsonSpan::into_tuple(js)?;
m.entry(s).or_insert_with(Vec::new).push(r);
}
Ok(FileLines::from_ranges(m))
}
}
// For JSON decoding.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]
pub struct JsonSpan {
file: FileName,
range: (usize, usize),
}
impl JsonSpan {
fn into_tuple(self) -> Result<(FileName, Range), FileLinesError> {
let (lo, hi) = self.range;
let canonical = canonicalize_path_string(&self.file)
.ok_or(FileLinesError::CannotCanonicalize(self.file))?;
Ok((canonical, Range::new(lo, hi)))
}
}
// This impl is needed for inclusion in the `Config` struct. We don't have a toml representation
// for `FileLines`, so it will just panic instead.
impl<'de> ::serde::de::Deserialize<'de> for FileLines {
fn deserialize<D>(_: D) -> Result<Self, D::Error>
where
D: ::serde::de::Deserializer<'de>,
{
panic!(
"FileLines cannot be deserialized from a project rustfmt.toml file: please \
specify it via the `--file-lines` option instead"
);
}
}
// We also want to avoid attempting to serialize a FileLines to toml. The
// `Config` struct should ensure this impl is never reached.
impl ::serde::ser::Serialize for FileLines {
fn serialize<S>(&self, _: S) -> Result<S::Ok, S::Error>
where
S: ::serde::ser::Serializer,
{
unreachable!("FileLines cannot be serialized. This is a rustfmt bug.");
}
}
#[cfg(test)]
mod test {
use super::Range;
#[test]
fn test_range_intersects() {
assert!(Range::new(1, 2).intersects(Range::new(1, 1)));
assert!(Range::new(1, 2).intersects(Range::new(2, 2)));
assert!(!Range::new(1, 2).intersects(Range::new(0, 0)));
assert!(!Range::new(1, 2).intersects(Range::new(3, 10)));
assert!(!Range::new(1, 3).intersects(Range::new(5, 5)));
}
#[test]
fn test_range_adjacent_to() {
assert!(!Range::new(1, 2).adjacent_to(Range::new(1, 1)));
assert!(!Range::new(1, 2).adjacent_to(Range::new(2, 2)));
assert!(Range::new(1, 2).adjacent_to(Range::new(0, 0)));
assert!(Range::new(1, 2).adjacent_to(Range::new(3, 10)));
assert!(!Range::new(1, 3).adjacent_to(Range::new(5, 5)));
}
#[test]
fn test_range_contains() {
assert!(Range::new(1, 2).contains(Range::new(1, 1)));
assert!(Range::new(1, 2).contains(Range::new(2, 2)));
assert!(!Range::new(1, 2).contains(Range::new(0, 0)));
assert!(!Range::new(1, 2).contains(Range::new(3, 10)));
}
#[test]
fn test_range_merge() {
assert_eq!(None, Range::new(1, 3).merge(Range::new(5, 5)));
assert_eq!(None, Range::new(4, 7).merge(Range::new(0, 1)));
assert_eq!(
Some(Range::new(3, 7)),
Range::new(3, 5).merge(Range::new(4, 7))
);
assert_eq!(
Some(Range::new(3, 7)),
Range::new(3, 5).merge(Range::new(5, 7))
);
assert_eq!(
Some(Range::new(3, 7)),
Range::new(3, 5).merge(Range::new(6, 7))
);
assert_eq!(
Some(Range::new(3, 7)),
Range::new(3, 7).merge(Range::new(4, 5))
);
}
use super::json::{self, json};
use super::{FileLines, FileName};
use std::{collections::HashMap, path::PathBuf};
#[test]
fn file_lines_to_json() {
let ranges: HashMap<FileName, Vec<Range>> = [
(
FileName::Real(PathBuf::from("src/main.rs")),
vec![Range::new(1, 3), Range::new(5, 7)],
),
(
FileName::Real(PathBuf::from("src/lib.rs")),
vec![Range::new(1, 7)],
),
]
.iter()
.cloned()
.collect();
let file_lines = FileLines::from_ranges(ranges);
let mut spans = file_lines.to_json_spans();
spans.sort();
let json = json::to_value(&spans).unwrap();
assert_eq!(
json,
json! {[
{"file": "src/lib.rs", "range": [1, 7]},
{"file": "src/main.rs", "range": [1, 3]},
{"file": "src/main.rs", "range": [5, 7]},
]}
);
}
}