use crate::{BytePos, CharPos, Span};
use once_map::OnceMap;
use solar_data_structures::{
fmt,
map::FxBuildHasher,
sync::{RwLock, RwLockReadGuard},
};
use std::{
io::{self, Read},
ops::Range,
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
mod analyze;
mod file;
pub use file::*;
mod file_resolver;
pub use file_resolver::{FileResolver, ResolveError};
#[cfg(test)]
mod tests;
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum SpanLinesError {
DistinctSources(Box<DistinctSources>),
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum SpanSnippetError {
IllFormedSpan(Span),
DistinctSources(Box<DistinctSources>),
MalformedForSourcemap(MalformedSourceMapPositions),
SourceNotAvailable { filename: FileName },
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct DistinctSources {
pub begin: (FileName, BytePos),
pub end: (FileName, BytePos),
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct MalformedSourceMapPositions {
pub name: FileName,
pub source_len: usize,
pub begin_pos: BytePos,
pub end_pos: BytePos,
}
#[derive(Clone, Debug)]
pub struct WithSourceFile<T> {
pub file: Arc<SourceFile>,
pub data: T,
}
impl<T> std::ops::Deref for WithSourceFile<T> {
type Target = T;
#[inline]
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl<T> std::ops::DerefMut for WithSourceFile<T> {
#[inline]
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.data
}
}
#[derive(Clone, Debug)]
pub struct Loc {
pub line: usize,
pub col: CharPos,
pub col_display: usize,
}
impl Default for Loc {
fn default() -> Self {
Self { line: 0, col: CharPos(0), col_display: 0 }
}
}
#[derive(Clone, Debug, Default)]
pub struct SpanLoc {
pub lo: Loc,
pub hi: Loc,
}
#[derive(Debug)]
pub struct SourceFileAndLine {
pub sf: Arc<SourceFile>,
pub line: usize,
}
#[derive(Debug)]
pub struct SourceFileAndBytePos {
pub sf: Arc<SourceFile>,
pub pos: BytePos,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct LineInfo {
pub line_index: usize,
pub start_col: CharPos,
pub end_col: CharPos,
}
pub type FileLines = WithSourceFile<Vec<LineInfo>>;
pub trait FileLoader: Send + Sync + 'static {
fn canonicalize_path(&self, path: &Path) -> io::Result<PathBuf>;
fn load_stdin(&self) -> io::Result<String>;
fn load_file(&self, path: &Path) -> io::Result<String>;
fn load_binary_file(&self, path: &Path) -> io::Result<Vec<u8>>;
}
pub struct RealFileLoader;
#[allow(clippy::disallowed_methods)] impl FileLoader for RealFileLoader {
fn canonicalize_path(&self, path: &Path) -> io::Result<PathBuf> {
crate::canonicalize(path)
}
fn load_stdin(&self) -> io::Result<String> {
let mut src = String::new();
io::stdin().read_to_string(&mut src)?;
Ok(src)
}
fn load_file(&self, path: &Path) -> io::Result<String> {
std::fs::read_to_string(path)
}
fn load_binary_file(&self, path: &Path) -> io::Result<Vec<u8>> {
std::fs::read(path)
}
}
#[derive(derive_more::Debug)]
pub struct SourceMap {
source_files: RwLock<Vec<Arc<SourceFile>>>,
#[debug(skip)]
id_to_file: OnceMap<SourceFileId, Arc<SourceFile>, FxBuildHasher>,
base_path: RwLock<Option<PathBuf>>,
#[debug(skip)]
file_loader: OnceLock<Box<dyn FileLoader>>,
}
impl Default for SourceMap {
fn default() -> Self {
Self::empty()
}
}
impl SourceMap {
pub fn empty() -> Self {
Self {
source_files: Default::default(),
id_to_file: Default::default(),
base_path: Default::default(),
file_loader: Default::default(),
}
}
pub fn clear(&mut self) {
let _ = self.take();
}
#[must_use]
pub fn take(&mut self) -> Vec<Arc<SourceFile>> {
self.id_to_file.clear();
std::mem::take(self.source_files.get_mut())
}
pub fn set_file_loader(&self, file_loader: impl FileLoader) {
if let Err(_prev) = self.file_loader.set(Box::new(file_loader)) {
warn!("file loader already set");
}
}
pub fn file_loader(&self) -> &dyn FileLoader {
self.file_loader.get().map(std::ops::Deref::deref).unwrap_or(&RealFileLoader)
}
pub(crate) fn set_base_path(&self, base_path: Option<PathBuf>) {
*self.base_path.write() = base_path;
}
pub(crate) fn base_path(&self) -> Option<PathBuf> {
self.base_path.read().as_ref().cloned()
}
pub fn is_empty(&self) -> bool {
self.files().is_empty()
}
pub fn get_file(&self, path: impl Into<FileName>) -> Option<Arc<SourceFile>> {
self.get_file_ref(&path.into())
}
pub fn get_file_ref(&self, filename: &FileName) -> Option<Arc<SourceFile>> {
self.id_to_file.get_cloned(&SourceFileId::new(filename))
}
pub fn load_file(&self, path: &Path) -> io::Result<Arc<SourceFile>> {
self.load_file_with_name(path.into(), path)
}
pub fn load_file_with_name(&self, name: FileName, path: &Path) -> io::Result<Arc<SourceFile>> {
self.new_source_file_with(name, || self.file_loader().load_file(path))
}
pub fn load_stdin(&self) -> io::Result<Arc<SourceFile>> {
self.new_source_file_with(FileName::Stdin, || self.file_loader().load_stdin())
}
pub fn new_source_file(
&self,
name: impl Into<FileName>,
src: impl Into<String>,
) -> io::Result<Arc<SourceFile>> {
self.new_source_file_with(name.into(), || Ok(src.into()))
}
#[instrument(level = "debug", skip_all, fields(filename = %filename.display()))]
pub fn new_source_file_with(
&self,
filename: FileName,
get_src: impl FnOnce() -> io::Result<String>,
) -> io::Result<Arc<SourceFile>> {
let id = SourceFileId::new(&filename);
self.id_to_file.try_insert_cloned(id, |&id| {
let file = SourceFile::new(filename, id, get_src()?)?;
self.append_source_file(file)
})
}
fn append_source_file(&self, mut file: SourceFile) -> io::Result<Arc<SourceFile>> {
trace!(name=%file.name.display(), len=file.src.len(), loc=file.count_lines(), "adding to source map");
let source_files = &mut *self.source_files.write();
file.start_pos = BytePos(if let Some(last_file) = source_files.last() {
last_file.end_position().0.checked_add(1).ok_or(OffsetOverflowError(()))?
} else {
0
});
let file = Arc::new(file);
source_files.push(file.clone());
Ok(file)
}
pub fn files(&self) -> impl std::ops::Deref<Target = [Arc<SourceFile>]> + '_ {
RwLockReadGuard::map(self.source_files.read(), std::ops::Deref::deref)
}
pub fn filename_for_diagnostics<'a>(&self, filename: &'a FileName) -> FileNameDisplay<'a> {
FileNameDisplay { inner: filename, base_path: self.base_path() }
}
pub fn is_multiline(&self, span: Span) -> bool {
let lo = self.lookup_source_file_idx(span.lo());
let hi = self.lookup_source_file_idx(span.hi());
if lo != hi {
return true;
}
let f = self.files()[lo].clone();
let lo = f.relative_position(span.lo());
let hi = f.relative_position(span.hi());
f.lookup_line(lo) != f.lookup_line(hi)
}
pub fn span_to_snippet(&self, span: Span) -> Result<String, SpanSnippetError> {
let WithSourceFile { file, data } = self.span_to_source(span)?;
file.src.get(data).map(|s| s.to_string()).ok_or(SpanSnippetError::IllFormedSpan(span))
}
pub fn span_to_prev_source(&self, sp: Span) -> Result<String, SpanSnippetError> {
let WithSourceFile { file, data } = self.span_to_source(sp)?;
file.src.get(..data.start).map(|s| s.to_string()).ok_or(SpanSnippetError::IllFormedSpan(sp))
}
pub fn lookup_byte_offset(&self, bpos: BytePos) -> SourceFileAndBytePos {
let sf = self.lookup_source_file(bpos);
let offset = bpos - sf.start_pos;
SourceFileAndBytePos { sf, pos: offset }
}
pub fn lookup_source_file_idx(&self, pos: BytePos) -> usize {
Self::lookup_sf_idx(&self.files(), pos)
}
pub fn lookup_source_file(&self, pos: BytePos) -> Arc<SourceFile> {
let files = &*self.files();
let idx = Self::lookup_sf_idx(files, pos);
files[idx].clone()
}
fn lookup_sf_idx(files: &[Arc<SourceFile>], pos: BytePos) -> usize {
assert!(!files.is_empty(), "attempted to lookup source file in empty `SourceMap`");
files.partition_point(|x| x.start_pos <= pos) - 1
}
pub fn lookup_char_pos(&self, pos: BytePos) -> WithSourceFile<Loc> {
let sf = self.lookup_source_file(pos);
let (line, col, col_display) = sf.lookup_file_pos_with_col_display(pos);
WithSourceFile { file: sf, data: Loc { line, col, col_display } }
}
pub fn lookup_line(&self, pos: BytePos) -> Result<SourceFileAndLine, Arc<SourceFile>> {
let f = self.lookup_source_file(pos);
let pos = f.relative_position(pos);
match f.lookup_line(pos) {
Some(line) => Ok(SourceFileAndLine { sf: f, line }),
None => Err(f),
}
}
pub fn is_valid_span(&self, sp: Span) -> Result<WithSourceFile<SpanLoc>, SpanLinesError> {
let lo = self.lookup_char_pos(sp.lo());
let hi = self.lookup_char_pos(sp.hi());
if lo.file.start_pos != hi.file.start_pos {
return Err(SpanLinesError::DistinctSources(Box::new(DistinctSources {
begin: (lo.file.name.clone(), lo.file.start_pos),
end: (hi.file.name.clone(), hi.file.start_pos),
})));
}
Ok(WithSourceFile { file: lo.file, data: SpanLoc { lo: lo.data, hi: hi.data } })
}
pub fn is_line_before_span_empty(&self, sp: Span) -> bool {
match self.span_to_prev_source(sp) {
Ok(s) => s.rsplit_once('\n').unwrap_or(("", &s)).1.trim_start().is_empty(),
Err(_) => false,
}
}
pub fn span_to_lines(&self, sp: Span) -> Result<FileLines, SpanLinesError> {
let WithSourceFile { file, data: SpanLoc { lo, hi } } = self.is_valid_span(sp)?;
assert!(hi.line >= lo.line);
if sp.is_dummy() {
return Ok(FileLines { file, data: Vec::new() });
}
let mut lines = Vec::with_capacity(hi.line - lo.line + 1);
let mut start_col = lo.col;
let hi_line = hi.line.saturating_sub(1);
for line_index in lo.line.saturating_sub(1)..hi_line {
let line_len = file.get_line(line_index).map_or(0, |s| s.chars().count());
lines.push(LineInfo { line_index, start_col, end_col: CharPos::from_usize(line_len) });
start_col = CharPos::from_usize(0);
}
lines.push(LineInfo { line_index: hi_line, start_col, end_col: hi.col });
Ok(FileLines { file, data: lines })
}
pub fn span_to_range(&self, sp: Span) -> Result<Range<usize>, SpanSnippetError> {
self.span_to_source(sp).map(|s| s.data)
}
pub fn span_to_source(
&self,
sp: Span,
) -> Result<WithSourceFile<Range<usize>>, SpanSnippetError> {
let local_begin = self.lookup_byte_offset(sp.lo());
let local_end = self.lookup_byte_offset(sp.hi());
if local_begin.sf.start_pos != local_end.sf.start_pos {
return Err(SpanSnippetError::DistinctSources(Box::new(DistinctSources {
begin: (local_begin.sf.name.clone(), local_begin.sf.start_pos),
end: (local_end.sf.name.clone(), local_end.sf.start_pos),
})));
}
let start_index = local_begin.pos.to_usize();
let end_index = local_end.pos.to_usize();
let source_len = local_begin.sf.source_len.to_usize();
if start_index > end_index || end_index > source_len {
return Err(SpanSnippetError::MalformedForSourcemap(MalformedSourceMapPositions {
name: local_begin.sf.name.clone(),
source_len,
begin_pos: local_begin.pos,
end_pos: local_end.pos,
}));
}
Ok(WithSourceFile { file: local_begin.sf, data: start_index..end_index })
}
pub fn span_to_diagnostic_string(&self, sp: Span) -> impl fmt::Display {
let (source_file, loc) = self.span_to_location_info(sp);
fmt::from_fn(move |f| {
let file_name = match &source_file {
Some(sf) => self.filename_for_diagnostics(&sf.name),
None => return f.write_str("no-location"),
};
let lo_line = loc.lo.line;
let lo_col = loc.lo.col.0 + 1;
let hi_line = loc.hi.line;
let hi_col = loc.hi.col.0 + 1;
write!(f, "{file_name}:{lo_line}:{lo_col}: {hi_line}:{hi_col}")
})
}
pub fn span_to_location_info(&self, sp: Span) -> (Option<Arc<SourceFile>>, SpanLoc) {
if self.files().is_empty() || sp.is_dummy() {
return Default::default();
}
let Ok(WithSourceFile { file, data }) = self.is_valid_span(sp) else {
return Default::default();
};
(Some(file), data)
}
}