-// Copyright 2016 The Rust Project Developers. See the COPYRIGHT
-// file at the top-level directory of this distribution and at
-// http://rust-lang.org/COPYRIGHT.
-//
-// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
-// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
-// option. This file may not be copied, modified, or distributed
-// except according to those terms.
-
//! This module contains types and functions to support formatting specific line ranges.
-use std::{cmp, iter, str};
+use itertools::Itertools;
use std::collections::HashMap;
+use std::path::PathBuf;
use std::rc::Rc;
+use std::{cmp, fmt, iter, str};
-use serde::de::{Deserialize, Deserializer};
+use serde::{ser, Deserialize, Deserializer, Serialize, Serializer};
use serde_json as json;
-use syntax::codemap::{FileMap, FileName};
+use syntax::source_map::{self, SourceFile};
/// A range of lines in a file, inclusive of both ends.
pub struct LineRange {
- pub file: Rc<FileMap>,
+ pub file: Rc<SourceFile>,
pub lo: usize,
pub 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<source_map::FileName> for FileName {
+ fn from(name: source_map::FileName) -> FileName {
+ match name {
+ source_map::FileName::Real(p) => FileName::Real(p),
+ source_map::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 fn file_name(&self) -> &FileName {
- &self.file.name
+ pub fn file_name(&self) -> FileName {
+ self.file.name.clone().into()
}
}
}
}
+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 }
self.lo > self.hi
}
+ #[allow(dead_code)]
fn contains(self, other: Range) -> bool {
if other.is_empty() {
true
/// 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)]
+#[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.into_iter().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;
- }
+ 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)
}
+ result.push(next)
}
*ranges = result;
}
impl FileLines {
/// Creates a `FileLines` that contains all lines in all files.
- pub fn all() -> FileLines {
+ pub(crate) fn all() -> FileLines {
FileLines(None)
}
- /// Returns true if this `FileLines` contains all lines in all files.
- pub fn is_all(&self) -> bool {
+ /// Returns `true` if this `FileLines` contains all lines in all files.
+ pub(crate) fn is_all(&self) -> bool {
self.0.is_none()
}
}
/// Returns an iterator over the files contained in `self`.
- pub fn files(&self) -> Files {
- Files(self.0.as_ref().map(|m| m.keys()))
+ pub fn files(&self) -> Files<'_> {
+ Files(self.0.as_ref().map(HashMap::keys))
}
- /// 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.
+ /// 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,
}
}
- /// Returns true if `range` is fully contained in `self`.
- pub fn contains(&self, range: &LineRange) -> bool {
- self.file_range_matches(range.file_name(), |r| r.contains(Range::from(range)))
+ /// 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 fn intersects(&self, range: &LineRange) -> bool {
- self.file_range_matches(range.file_name(), |r| r.intersects(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 fn contains_line(&self, file_name: &FileName, line: usize) -> bool {
+ /// 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 any of the lines between `lo` and `hi` from `file_name` are in `self`.
- pub fn intersects_range(&self, file_name: &FileName, lo: usize, hi: usize) -> bool {
- self.file_range_matches(file_name, |r| r.intersects(Range::new(lo, hi)))
+ /// 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)))
}
}
}
// For JSON decoding.
-#[derive(Clone, Debug, Deserialize)]
-struct JsonSpan {
- #[serde(deserialize_with = "deserialize_filename")]
+#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]
+pub struct JsonSpan {
file: FileName,
range: (usize, usize),
}
-fn deserialize_filename<'de, D: Deserializer<'de>>(d: D) -> Result<FileName, D::Error> {
- let s = String::deserialize(d)?;
- if s == "stdin" {
- Ok(FileName::Custom(s))
- } else {
- Ok(FileName::Real(s.into()))
- }
-}
-
impl JsonSpan {
fn into_tuple(self) -> Result<(FileName, Range), String> {
let (lo, hi) = self.range;
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]},
+ ]}
+ );
+ }
}