use std::fmt;
use crate::Span;
#[derive(Clone, Default, Eq, PartialEq)]
pub struct SourceMap {
files: Vec<String>,
segments: Vec<Segment>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct Segment {
output_line: usize,
file: Option<u32>,
source_line: usize,
fidelity: Fidelity,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct SourceLine(pub Option<String>, pub usize);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Origin<'a> {
pub file: Option<&'a str>,
pub line: usize,
pub col: Option<usize>,
pub fidelity: Fidelity,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Fidelity {
Verbatim,
Transformed(Transform),
Synthetic(Transform),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Transform {
TabExpansion,
Reindent,
LevelOffsetWrapper,
SecureLinkRewrite,
UnresolvedDirective,
Rewritten,
}
impl SourceMap {
pub(crate) fn append(
&mut self,
output_line: usize,
file: Option<&str>,
source_line: usize,
fidelity: Fidelity,
) {
let file = self.intern(file);
debug_assert!(
self.segments
.last()
.is_none_or(|last| output_line >= last.output_line),
"SourceMap::append called out of order: output_line {output_line} follows {}",
self.segments.last().map_or(0, |last| last.output_line),
);
self.segments.push(Segment {
output_line,
file,
source_line,
fidelity,
});
}
fn intern(&mut self, file: Option<&str>) -> Option<u32> {
let name = file?;
if let Some(i) = self.files.iter().position(|f| f == name) {
return Some(i as u32);
}
let id = self.files.len() as u32;
self.files.push(name.to_owned());
Some(id)
}
fn resolve(&self, key: usize) -> (Option<u32>, usize, Fidelity) {
let segment = match self.segments.binary_search_by_key(&key, |s| s.output_line) {
Ok(i) => self.segments.get(i),
Err(0) => None,
Err(i) => self.segments.get(i - 1),
};
match segment {
Some(s) => (s.file, s.source_line + key - s.output_line, s.fidelity),
None => (None, key, Fidelity::Verbatim),
}
}
#[cfg(test)]
pub(crate) fn anchors(&self) -> impl Iterator<Item = (usize, Option<&str>, usize)> {
self.segments
.iter()
.map(|s| (s.output_line, self.file_name(s.file), s.source_line))
}
fn file_name(&self, id: Option<u32>) -> Option<&str> {
id.and_then(|i| self.files.get(i as usize))
.map(String::as_str)
}
pub fn original_file_and_line(&self, key: usize) -> Option<SourceLine> {
let (file, source_line, _) = self.resolve(key);
Some(SourceLine(
self.file_name(file).map(str::to_owned),
source_line,
))
}
pub fn origin_at(&self, line: usize, col: usize) -> Origin<'_> {
let (file, source_line, fidelity) = self.resolve(line);
Origin {
file: self.file_name(file),
line: source_line,
col: matches!(fidelity, Fidelity::Verbatim).then_some(col),
fidelity,
}
}
pub fn origin_of(&self, span: Span<'_>) -> Origin<'_> {
self.origin_at(span.line(), span.col())
}
}
impl fmt::Debug for SourceMap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("SourceMap(&")?;
f.debug_list()
.entries(self.segments.iter().map(|s| {
(
s.output_line,
SourceLine(self.file_name(s.file).map(str::to_owned), s.source_line),
s.fidelity,
)
}))
.finish()?;
f.write_str(")")
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use crate::parser::{
SourceLine, SourceMap,
source_map::{Fidelity, Transform},
};
fn append(sm: &mut SourceMap, output_line: usize, file: Option<&str>, source_line: usize) {
sm.append(output_line, file, source_line, Fidelity::Verbatim);
}
#[test]
fn empty() {
let sm = SourceMap::default();
assert_eq!(sm.original_file_and_line(1), Some(SourceLine(None, 1)));
}
#[test]
fn one_entry() {
let mut sm = SourceMap::default();
append(&mut sm, 1, None, 1);
assert_eq!(sm.original_file_and_line(0), Some(SourceLine(None, 0)));
assert_eq!(sm.original_file_and_line(1), Some(SourceLine(None, 1)));
assert_eq!(sm.original_file_and_line(4), Some(SourceLine(None, 4)));
assert_eq!(
sm.original_file_and_line(4000),
Some(SourceLine(None, 4000))
);
}
#[test]
fn multiple_entries() {
let mut sm = SourceMap::default();
append(&mut sm, 1, None, 1);
append(&mut sm, 10, Some("foo.adoc"), 1);
append(&mut sm, 20, Some("bar.adoc"), 18);
append(&mut sm, 30, None, 11);
assert_eq!(sm.original_file_and_line(1), Some(SourceLine(None, 1)));
assert_eq!(sm.original_file_and_line(4), Some(SourceLine(None, 4)));
assert_eq!(
sm.original_file_and_line(10),
Some(SourceLine(Some("foo.adoc".to_owned()), 1))
);
assert_eq!(
sm.original_file_and_line(19),
Some(SourceLine(Some("foo.adoc".to_owned()), 10))
);
assert_eq!(
sm.original_file_and_line(20),
Some(SourceLine(Some("bar.adoc".to_owned()), 18))
);
assert_eq!(
sm.original_file_and_line(21),
Some(SourceLine(Some("bar.adoc".to_owned()), 19))
);
assert_eq!(
sm.original_file_and_line(29),
Some(SourceLine(Some("bar.adoc".to_owned()), 27))
);
assert_eq!(sm.original_file_and_line(30), Some(SourceLine(None, 11)));
assert_eq!(sm.original_file_and_line(40), Some(SourceLine(None, 21)));
}
#[test]
fn interns_repeated_file_names() {
let mut sm = SourceMap::default();
append(&mut sm, 1, Some("foo.adoc"), 1);
append(&mut sm, 5, Some("foo.adoc"), 10);
append(&mut sm, 9, Some("foo.adoc"), 20);
assert_eq!(sm.files, vec!["foo.adoc".to_owned()]);
assert_eq!(
sm.original_file_and_line(6),
Some(SourceLine(Some("foo.adoc".to_owned()), 11))
);
}
#[test]
fn origin_at_reports_column_only_when_verbatim() {
let mut sm = SourceMap::default();
sm.append(1, None, 1, Fidelity::Verbatim);
sm.append(
2,
Some("inc.adoc"),
4,
Fidelity::Transformed(Transform::TabExpansion),
);
sm.append(3, Some("inc.adoc"), 5, Fidelity::Verbatim);
let verbatim = sm.origin_at(1, 7);
assert_eq!(verbatim.file, None);
assert_eq!(verbatim.line, 1);
assert_eq!(verbatim.col, Some(7));
assert_eq!(verbatim.fidelity, Fidelity::Verbatim);
let transformed = sm.origin_at(2, 7);
assert_eq!(transformed.file, Some("inc.adoc"));
assert_eq!(transformed.line, 4);
assert_eq!(transformed.col, None);
assert_eq!(
transformed.fidelity,
Fidelity::Transformed(Transform::TabExpansion)
);
let after = sm.origin_at(3, 2);
assert_eq!(after.file, Some("inc.adoc"));
assert_eq!(after.line, 5);
assert_eq!(after.col, Some(2));
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "SourceMap::append called out of order")]
fn append_out_of_order_panics_in_debug() {
let mut sm = SourceMap::default();
append(&mut sm, 10, None, 1);
append(&mut sm, 5, None, 1);
}
#[test]
fn append_equal_output_line_is_allowed() {
let mut sm = SourceMap::default();
append(&mut sm, 1, None, 1);
sm.append(1, None, 1, Fidelity::Verbatim);
}
#[test]
fn impl_debug() {
let mut sm = SourceMap::default();
append(&mut sm, 1, None, 1);
assert_eq!(
format!("{sm:#?}"),
"SourceMap(&[\n (\n 1,\n SourceLine(\n None,\n 1,\n ),\n Verbatim,\n ),\n])"
);
}
}