use alloc::boxed::Box;
use alloc::vec::Vec;
use span_lang::{BytePos, LineCol, Span};
use crate::{SourceFile, SourceId, SourceMapError};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SourceMap {
files: Vec<SourceFile>,
next_base: u32,
max_source_len: u32,
}
impl Default for SourceMap {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl SourceMap {
#[inline]
#[must_use]
pub const fn new() -> Self {
Self {
files: Vec::new(),
next_base: 0,
max_source_len: u32::MAX,
}
}
#[inline]
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
files: Vec::with_capacity(capacity),
next_base: 0,
max_source_len: u32::MAX,
}
}
#[inline]
#[must_use]
pub const fn max_source_len(&self) -> u32 {
self.max_source_len
}
#[inline]
pub fn set_max_source_len(&mut self, max: u32) {
self.max_source_len = max;
}
pub fn add(
&mut self,
name: impl Into<Box<str>>,
text: impl Into<Box<str>>,
) -> Result<SourceId, SourceMapError> {
self.push(name.into(), text.into())
}
pub fn add_bytes(
&mut self,
name: impl Into<Box<str>>,
bytes: &[u8],
) -> Result<SourceId, SourceMapError> {
let name = name.into();
match core::str::from_utf8(bytes) {
Ok(text) => self.push(name, Box::from(text)),
Err(_) => Err(SourceMapError::NotUtf8 { name }),
}
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub fn add_file(
&mut self,
path: impl AsRef<std::path::Path>,
) -> Result<SourceId, SourceMapError> {
use std::io::Read;
let path = path.as_ref();
let name: Box<str> = Box::from(path.to_string_lossy().as_ref());
let io_err = |e: std::io::Error| SourceMapError::Io {
name: name.clone(),
kind: e.kind(),
};
let mut file = std::fs::File::open(path).map_err(io_err)?;
if let Ok(meta) = file.metadata() {
if meta.len() > u64::from(self.max_source_len) {
return Err(SourceMapError::Oversize {
name,
len: meta.len(),
});
}
}
let mut bytes = Vec::new();
let _ = file.read_to_end(&mut bytes).map_err(io_err)?;
match core::str::from_utf8(&bytes) {
Ok(text) => self.push(name, Box::from(text)),
Err(_) => Err(SourceMapError::NotUtf8 { name }),
}
}
fn push(&mut self, name: Box<str>, text: Box<str>) -> Result<SourceId, SourceMapError> {
let needed = text.len() as u64;
if needed > u64::from(self.max_source_len) {
return Err(SourceMapError::Oversize { name, len: needed });
}
let base = self.next_base;
let available = u64::from(u32::MAX - base);
let index = u32::try_from(self.files.len());
let (len, index) = match (needed <= available, index) {
(true, Ok(index)) => (needed as u32, index),
_ => return Err(SourceMapError::SpaceExhausted { needed, available }),
};
let end = base + len;
let span = Span::new(base, end);
let id = SourceId::from_index(index);
self.files.push(SourceFile::new(name, text, span));
self.next_base = end;
Ok(id)
}
#[must_use]
pub fn locate(&self, pos: BytePos) -> Option<(SourceId, BytePos)> {
let at = pos.to_u32();
let after = self
.files
.partition_point(|f| f.span().start().to_u32() <= at);
let index = after.checked_sub(1)?;
let file = &self.files[index];
if file.span().contains(pos) {
let local = at - file.span().start().to_u32();
Some((SourceId::from_index(index as u32), BytePos::new(local)))
} else {
None
}
}
#[must_use]
pub fn line_col(&self, pos: BytePos) -> Option<(SourceId, LineCol)> {
let (id, local) = self.locate(pos)?;
let line_col = self.files[id.to_u32() as usize]
.line_index()
.line_col(local);
Some((id, line_col))
}
#[inline]
#[must_use]
pub fn source(&self, id: SourceId) -> Option<&SourceFile> {
self.files.get(id.to_u32() as usize)
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.files.len()
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
pub fn iter(&self) -> impl ExactSizeIterator<Item = (SourceId, &SourceFile)> + '_ {
self.files
.iter()
.enumerate()
.map(|(i, file)| (SourceId::from_index(i as u32), file))
}
}
#[cfg(feature = "serde")]
mod serde_support {
use alloc::string::String;
use alloc::vec::Vec;
use serde::de::Error as _;
use serde::ser::SerializeStruct;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::SourceMap;
#[derive(Serialize)]
struct SourceRef<'a> {
name: &'a str,
text: &'a str,
}
#[derive(Deserialize)]
struct SourceOwned {
name: String,
text: String,
}
fn unbounded() -> u32 {
u32::MAX
}
#[derive(Deserialize)]
struct MapData {
sources: Vec<SourceOwned>,
#[serde(default = "unbounded")]
max_source_len: u32,
}
impl Serialize for SourceMap {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let sources: Vec<SourceRef<'_>> = self
.files
.iter()
.map(|f| SourceRef {
name: f.name(),
text: f.text(),
})
.collect();
let mut state = serializer.serialize_struct("SourceMap", 2)?;
state.serialize_field("sources", &sources)?;
state.serialize_field("max_source_len", &self.max_source_len)?;
state.end()
}
}
impl<'de> Deserialize<'de> for SourceMap {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let data = MapData::deserialize(deserializer)?;
let mut map = SourceMap::with_capacity(data.sources.len());
for source in data.sources {
let _ = map
.push(source.name.into(), source.text.into())
.map_err(D::Error::custom)?;
}
map.max_source_len = data.max_source_len;
Ok(map)
}
}
}
#[cfg(test)]
mod tests {
extern crate alloc;
use alloc::format;
use alloc::vec::Vec;
use super::*;
#[test]
fn test_add_assigns_sequential_stable_ids() {
let mut map = SourceMap::new();
let a = map.add("a", "x").expect("fits");
let b = map.add("b", "yy").expect("fits");
let c = map.add("c", "zzz").expect("fits");
assert_eq!((a.to_u32(), b.to_u32(), c.to_u32()), (0, 1, 2));
assert_eq!(map.source(a).unwrap().name(), "a");
assert_eq!(map.source(b).unwrap().name(), "b");
}
#[test]
fn test_layout_is_contiguous_and_non_overlapping() {
let mut map = SourceMap::new();
map.add("a", "abc").expect("fits"); map.add("b", "de").expect("fits"); map.add("c", "fghi").expect("fits"); let spans: Vec<_> = map.iter().map(|(_, f)| f.span()).collect();
assert_eq!(spans[0], Span::new(0, 3));
assert_eq!(spans[1], Span::new(3, 5));
assert_eq!(spans[2], Span::new(5, 9));
}
#[test]
fn test_locate_at_boundaries_zero_one_and_past_end() {
let mut map = SourceMap::new();
let a = map.add("a", "abc").expect("fits"); let b = map.add("b", "de").expect("fits"); assert_eq!(map.locate(BytePos::new(0)), Some((a, BytePos::new(0))));
assert_eq!(map.locate(BytePos::new(2)), Some((a, BytePos::new(2))));
assert_eq!(map.locate(BytePos::new(3)), Some((b, BytePos::new(0))));
assert_eq!(map.locate(BytePos::new(4)), Some((b, BytePos::new(1))));
assert_eq!(map.locate(BytePos::new(5)), None);
assert_eq!(map.locate(BytePos::new(6)), None);
}
#[test]
fn test_locate_on_empty_map_is_none() {
let map = SourceMap::new();
assert_eq!(map.locate(BytePos::new(0)), None);
}
#[test]
fn test_empty_source_does_not_advance_space_and_is_unlocatable() {
let mut map = SourceMap::new();
let a = map.add("a", "ab").expect("fits"); let empty = map.add("empty", "").expect("fits"); let b = map.add("b", "cd").expect("fits");
assert!(map.source(empty).unwrap().span().is_empty());
assert_eq!(map.locate(BytePos::new(2)), Some((b, BytePos::new(0))));
assert_eq!(map.locate(BytePos::new(1)), Some((a, BytePos::new(1))));
}
#[test]
fn test_source_rejects_foreign_id() {
let mut map = SourceMap::new();
let _ = map.add("a", "x").expect("fits");
let mut other = SourceMap::new();
let foreign = other.add("b", "y").expect("fits");
let beyond = other.add("c", "z").expect("fits");
assert!(map.source(beyond).is_none());
assert!(map.source(foreign).is_some());
}
#[test]
fn test_add_at_space_boundary_accepts_exact_fit() {
let mut map = SourceMap::new();
map.next_base = u32::MAX - 4;
let id = map.add("edge", "abcd").expect("exactly fills the space");
assert_eq!(
map.source(id).unwrap().span(),
Span::new(u32::MAX - 4, u32::MAX)
);
assert_eq!(map.next_base, u32::MAX);
}
#[test]
fn test_add_past_space_boundary_is_rejected() {
let mut map = SourceMap::new();
map.next_base = u32::MAX - 4;
let err = map.add("edge", "abcde").expect_err("one byte too many");
assert_eq!(
err,
SourceMapError::SpaceExhausted {
needed: 5,
available: 4,
},
);
assert!(map.is_empty());
assert_eq!(map.next_base, u32::MAX - 4);
}
#[test]
fn test_add_empty_source_at_full_space_still_succeeds() {
let mut map = SourceMap::new();
map.next_base = u32::MAX;
let empty = map.add("nothing", "").expect("zero bytes always fit");
assert!(map.source(empty).unwrap().span().is_empty());
assert!(map.add("one", "x").is_err());
}
#[test]
fn test_iter_reports_exact_len_and_pairs_ids_in_order() {
let mut map = SourceMap::new();
for i in 0..5 {
map.add(format!("f{i}"), "..").expect("fits");
}
let mut iter = map.iter();
assert_eq!(iter.len(), 5);
let collected: Vec<_> = iter.by_ref().map(|(id, _)| id.to_u32()).collect();
assert_eq!(collected, [0, 1, 2, 3, 4]);
}
#[test]
fn test_add_bytes_stores_valid_utf8() {
let mut map = SourceMap::new();
let id = map
.add_bytes("greeting", "héllo".as_bytes())
.expect("valid");
assert_eq!(map.source(id).unwrap().text(), "héllo");
}
#[test]
fn test_add_bytes_rejects_invalid_utf8_and_leaves_map_unchanged() {
let mut map = SourceMap::new();
let err = map.add_bytes("blob", &[0x68, 0xff, 0x69]).unwrap_err();
match err {
SourceMapError::NotUtf8 { name } => assert_eq!(&*name, "blob"),
other => panic!("expected NotUtf8, got {other:?}"),
}
assert!(map.is_empty());
assert_eq!(map.next_base, 0);
}
#[test]
fn test_add_bytes_empty_is_a_zero_width_source() {
let mut map = SourceMap::new();
let id = map
.add_bytes("empty", b"")
.expect("zero bytes are valid utf8");
assert!(map.source(id).unwrap().span().is_empty());
}
#[test]
fn test_max_source_len_rejects_at_the_byte_boundary() {
let mut map = SourceMap::new();
map.set_max_source_len(4);
assert_eq!(map.max_source_len(), 4);
let ok = map.add("ok", "abcd").expect("exactly the limit");
assert_eq!(map.source(ok).unwrap().span().len(), 4);
let err = map.add("big", "abcde").unwrap_err();
match err {
SourceMapError::Oversize { name, len } => {
assert_eq!(&*name, "big");
assert_eq!(len, 5);
}
other => panic!("expected Oversize, got {other:?}"),
}
assert_eq!(map.next_base, 4);
}
#[test]
fn test_oversize_is_checked_before_space_exhaustion() {
let mut map = SourceMap::new();
map.set_max_source_len(2);
map.next_base = u32::MAX; let err = map.add("x", "abc").unwrap_err();
assert!(matches!(err, SourceMapError::Oversize { len: 3, .. }));
}
#[test]
fn test_line_col_resolves_across_files_and_lines() {
let mut map = SourceMap::new();
let a = map.add("a", "ab\ncd").expect("fits"); let b = map.add("b", "wx\nyz").expect("fits");
assert_eq!(map.line_col(BytePos::new(0)), Some((a, LineCol::new(1, 1))));
assert_eq!(map.line_col(BytePos::new(4)), Some((a, LineCol::new(2, 2))));
assert_eq!(map.line_col(BytePos::new(5)), Some((b, LineCol::new(1, 1))));
assert_eq!(map.line_col(BytePos::new(8)), Some((b, LineCol::new(2, 1))));
}
#[test]
fn test_line_col_counts_characters_not_bytes() {
let mut map = SourceMap::new();
let id = map.add("greek", "αβ").expect("fits");
assert_eq!(
map.line_col(BytePos::new(2)),
Some((id, LineCol::new(1, 2)))
);
}
#[test]
fn test_line_col_out_of_range_is_none() {
let mut map = SourceMap::new();
map.add("a", "abc").expect("fits"); assert_eq!(map.line_col(BytePos::new(3)), None);
assert_eq!(map.line_col(BytePos::new(99)), None);
}
}