Skip to main content

solar_interface/source_map/
mod.rs

1//! SourceMap related types and operations.
2
3use crate::{BytePos, CharPos, Span};
4use once_map::OnceMap;
5use solar_data_structures::{
6    fmt,
7    map::FxBuildHasher,
8    sync::{RwLock, RwLockReadGuard},
9};
10use std::{
11    io::{self, Read},
12    ops::Range,
13    path::{Path, PathBuf},
14    sync::{Arc, OnceLock},
15};
16
17mod analyze;
18
19mod file;
20pub use file::*;
21
22mod file_resolver;
23pub use file_resolver::{FileResolver, ResolveError};
24
25#[cfg(test)]
26mod tests;
27
28#[derive(Clone, PartialEq, Eq, Debug)]
29pub enum SpanLinesError {
30    DistinctSources(Box<DistinctSources>),
31}
32
33/// An error that can occur when converting a `Span` to a snippet.
34///
35/// In general these errors only occur on malformed spans created by the user.
36/// The parser never creates a span that would cause these errors.
37#[derive(Clone, PartialEq, Eq, Debug)]
38pub enum SpanSnippetError {
39    IllFormedSpan(Span),
40    DistinctSources(Box<DistinctSources>),
41    MalformedForSourcemap(MalformedSourceMapPositions),
42    SourceNotAvailable { filename: FileName },
43}
44
45#[derive(Clone, PartialEq, Eq, Debug)]
46pub struct DistinctSources {
47    pub begin: (FileName, BytePos),
48    pub end: (FileName, BytePos),
49}
50
51#[derive(Clone, PartialEq, Eq, Debug)]
52pub struct MalformedSourceMapPositions {
53    pub name: FileName,
54    pub source_len: usize,
55    pub begin_pos: BytePos,
56    pub end_pos: BytePos,
57}
58
59/// A value paired with a source file.
60#[derive(Clone, Debug)]
61pub struct WithSourceFile<T> {
62    pub file: Arc<SourceFile>,
63    pub data: T,
64}
65
66impl<T> std::ops::Deref for WithSourceFile<T> {
67    type Target = T;
68
69    #[inline]
70    fn deref(&self) -> &Self::Target {
71        &self.data
72    }
73}
74
75impl<T> std::ops::DerefMut for WithSourceFile<T> {
76    #[inline]
77    fn deref_mut(&mut self) -> &mut Self::Target {
78        &mut self.data
79    }
80}
81
82/// A source code location used for error reporting.
83#[derive(Clone, Debug)]
84pub struct Loc {
85    /// The (1-based) line number.
86    pub line: usize,
87    /// The (0-based) column offset.
88    pub col: CharPos,
89    /// The (0-based) column offset when displayed.
90    pub col_display: usize,
91}
92
93impl Default for Loc {
94    fn default() -> Self {
95        Self { line: 0, col: CharPos(0), col_display: 0 }
96    }
97}
98
99/// A source code location used for error reporting.
100///
101/// This is like [`Loc`], but it includes the line and column offsets of the start and end of the
102/// span.
103#[derive(Clone, Debug, Default)]
104pub struct SpanLoc {
105    /// The location of the start of the span.
106    pub lo: Loc,
107    /// The location of the end of the span.
108    pub hi: Loc,
109}
110
111// Used to be structural records.
112#[derive(Debug)]
113pub struct SourceFileAndLine {
114    pub sf: Arc<SourceFile>,
115    /// Index of line, starting from 0.
116    pub line: usize,
117}
118
119#[derive(Debug)]
120pub struct SourceFileAndBytePos {
121    pub sf: Arc<SourceFile>,
122    pub pos: BytePos,
123}
124
125#[derive(Copy, Clone, Debug, PartialEq, Eq)]
126pub struct LineInfo {
127    /// Index of line, starting from 0.
128    pub line_index: usize,
129
130    /// Column in line where span begins, starting from 0.
131    pub start_col: CharPos,
132
133    /// Column in line where span ends, starting from 0, exclusive.
134    pub end_col: CharPos,
135}
136
137pub type FileLines = WithSourceFile<Vec<LineInfo>>;
138
139/// Abstraction over IO operations.
140///
141/// This is called by the file resolver and source map to access the file system.
142///
143/// The [default implementation][RealFileLoader] uses [`std::fs`].
144pub trait FileLoader: Send + Sync + 'static {
145    fn canonicalize_path(&self, path: &Path) -> io::Result<PathBuf>;
146    fn load_stdin(&self) -> io::Result<String>;
147    fn load_file(&self, path: &Path) -> io::Result<String>;
148    fn load_binary_file(&self, path: &Path) -> io::Result<Vec<u8>>;
149}
150
151/// Default file loader that uses [`std::fs`].
152pub struct RealFileLoader;
153
154#[allow(clippy::disallowed_methods)] // Only place that's allowed.
155impl FileLoader for RealFileLoader {
156    fn canonicalize_path(&self, path: &Path) -> io::Result<PathBuf> {
157        crate::canonicalize(path)
158    }
159
160    fn load_stdin(&self) -> io::Result<String> {
161        let mut src = String::new();
162        io::stdin().read_to_string(&mut src)?;
163        Ok(src)
164    }
165
166    fn load_file(&self, path: &Path) -> io::Result<String> {
167        std::fs::read_to_string(path)
168    }
169
170    fn load_binary_file(&self, path: &Path) -> io::Result<Vec<u8>> {
171        std::fs::read(path)
172    }
173}
174
175/// Stores all the sources of the current compilation session.
176#[derive(derive_more::Debug)]
177pub struct SourceMap {
178    // INVARIANT: `source_files` is monotonic.
179    source_files: RwLock<Vec<Arc<SourceFile>>>,
180    #[debug(skip)]
181    id_to_file: OnceMap<SourceFileId, Arc<SourceFile>, FxBuildHasher>,
182
183    base_path: RwLock<Option<PathBuf>>,
184    #[debug(skip)]
185    file_loader: OnceLock<Box<dyn FileLoader>>,
186}
187
188impl Default for SourceMap {
189    fn default() -> Self {
190        Self::empty()
191    }
192}
193
194impl SourceMap {
195    /// Creates a new empty source map.
196    pub fn empty() -> Self {
197        Self {
198            source_files: Default::default(),
199            id_to_file: Default::default(),
200            base_path: Default::default(),
201            file_loader: Default::default(),
202        }
203    }
204
205    /// Clears the source map.
206    pub fn clear(&mut self) {
207        let _ = self.take();
208    }
209
210    /// Clears the source map, returning all the contained `SourceFile`s.
211    #[must_use]
212    pub fn take(&mut self) -> Vec<Arc<SourceFile>> {
213        self.id_to_file.clear();
214        std::mem::take(self.source_files.get_mut())
215    }
216
217    /// Sets the file loader for the source map.
218    /// This may only be called once. Further calls will do nothing.
219    ///
220    /// See [its documentation][FileLoader] for more details.
221    pub fn set_file_loader(&self, file_loader: impl FileLoader) {
222        if let Err(_prev) = self.file_loader.set(Box::new(file_loader)) {
223            warn!("file loader already set");
224        }
225    }
226
227    /// Returns the file loader for the source map.
228    ///
229    /// See [its documentation][FileLoader] for more details.
230    pub fn file_loader(&self) -> &dyn FileLoader {
231        self.file_loader.get().map(std::ops::Deref::deref).unwrap_or(&RealFileLoader)
232    }
233
234    /// Sets the base path for the source map.
235    ///
236    /// This is currently only used for trimming diagnostics' paths.
237    pub(crate) fn set_base_path(&self, base_path: Option<PathBuf>) {
238        *self.base_path.write() = base_path;
239    }
240
241    pub(crate) fn base_path(&self) -> Option<PathBuf> {
242        self.base_path.read().as_ref().cloned()
243    }
244
245    /// Returns `true` if the source map is empty.
246    pub fn is_empty(&self) -> bool {
247        self.files().is_empty()
248    }
249
250    /// Parses a file name from a string.
251    pub fn parse_file_name(&self, s: &str) -> FileName {
252        match s {
253            "stdin" | "<stdin>" => FileName::Stdin,
254            s if s.starts_with('<') && s.ends_with('>') => {
255                FileName::Custom(s[1..s.len() - 1].to_string())
256            }
257            s => {
258                if let Some(file) = FileResolver::new(self).get_file(s.as_ref()) {
259                    file.name.clone()
260                } else {
261                    FileName::Custom(s.to_string())
262                }
263            }
264        }
265    }
266
267    /// Returns the source file with the given path, if it exists.
268    /// Does not attempt to load the file.
269    pub fn get_file(&self, path: impl Into<FileName>) -> Option<Arc<SourceFile>> {
270        self.get_file_ref(&path.into())
271    }
272
273    /// Returns the source file with the given path, if it exists.
274    /// Does not attempt to load the file.
275    pub fn get_file_ref(&self, filename: &FileName) -> Option<Arc<SourceFile>> {
276        self.id_to_file.get_cloned(&SourceFileId::new(filename))
277    }
278
279    /// Loads a file from the given path.
280    pub fn load_file(&self, path: &Path) -> io::Result<Arc<SourceFile>> {
281        self.load_file_with_name(path.into(), path)
282    }
283
284    /// Loads a file with the given name from the given path.
285    pub fn load_file_with_name(&self, name: FileName, path: &Path) -> io::Result<Arc<SourceFile>> {
286        self.new_source_file_with(name, || self.file_loader().load_file(path))
287    }
288
289    /// Loads `stdin`.
290    pub fn load_stdin(&self) -> io::Result<Arc<SourceFile>> {
291        self.new_source_file_with(FileName::Stdin, || self.file_loader().load_stdin())
292    }
293
294    /// Creates a new `SourceFile` with the given name and source string.
295    ///
296    /// See [`new_source_file_with`](Self::new_source_file_with) for more details.
297    pub fn new_source_file(
298        &self,
299        name: impl Into<FileName>,
300        src: impl Into<String>,
301    ) -> io::Result<Arc<SourceFile>> {
302        self.new_source_file_with(name.into(), || Ok(src.into()))
303    }
304
305    /// Creates a new `SourceFile` with the given name and source string closure.
306    ///
307    /// If a file already exists in the `SourceMap` with the same ID, that file is returned
308    /// unmodified, and `get_src` is not called.
309    ///
310    /// Returns an error if the file is larger than 4GiB or other errors occur while creating the
311    /// `SourceFile`.
312    ///
313    /// Note that the `FileLoader` is not used when calling this function.
314    #[instrument(level = "debug", skip_all, fields(filename = %filename.display()))]
315    pub fn new_source_file_with(
316        &self,
317        filename: FileName,
318        get_src: impl FnOnce() -> io::Result<String>,
319    ) -> io::Result<Arc<SourceFile>> {
320        let id = SourceFileId::new(&filename);
321        self.id_to_file.try_insert_cloned(id, |&id| {
322            let file = SourceFile::new(filename, id, get_src()?)?;
323            self.append_source_file(file)
324        })
325    }
326
327    fn append_source_file(&self, mut file: SourceFile) -> io::Result<Arc<SourceFile>> {
328        trace!(name=%file.name.display(), len=file.src.len(), loc=file.count_lines(), "adding to source map");
329
330        let source_files = &mut *self.source_files.write();
331        file.start_pos = BytePos(if let Some(last_file) = source_files.last() {
332            // Add one so there is some space between files. This lets us distinguish
333            // positions in the `SourceMap`, even in the presence of zero-length files.
334            last_file.end_position().0.checked_add(1).ok_or(OffsetOverflowError(()))?
335        } else {
336            0
337        });
338
339        let file = Arc::new(file);
340        source_files.push(file.clone());
341
342        Ok(file)
343    }
344
345    /// Returns a read guard to the source files in the source map.
346    pub fn files(&self) -> impl std::ops::Deref<Target = [Arc<SourceFile>]> + '_ {
347        RwLockReadGuard::map(self.source_files.read(), std::ops::Deref::deref)
348    }
349
350    /// Display the filename for diagnostics.
351    pub fn filename_for_diagnostics<'a>(&self, filename: &'a FileName) -> FileNameDisplay<'a> {
352        FileNameDisplay { inner: filename, base_path: self.base_path() }
353    }
354
355    /// Returns `true` if the given span is multi-line.
356    pub fn is_multiline(&self, span: Span) -> bool {
357        let lo = self.lookup_source_file_idx(span.lo());
358        let hi = self.lookup_source_file_idx(span.hi());
359        if lo != hi {
360            return true;
361        }
362        let f = self.files()[lo].clone();
363        let lo = f.relative_position(span.lo());
364        let hi = f.relative_position(span.hi());
365        f.lookup_line(lo) != f.lookup_line(hi)
366    }
367
368    /// Returns the source snippet as `String` corresponding to the given `Span`.
369    pub fn span_to_snippet(&self, span: Span) -> Result<String, SpanSnippetError> {
370        let WithSourceFile { file, data } = self.span_to_source(span)?;
371        file.src.get(data).map(|s| s.to_string()).ok_or(SpanSnippetError::IllFormedSpan(span))
372    }
373
374    /// Returns the source snippet as `String` before the given `Span`.
375    pub fn span_to_prev_source(&self, sp: Span) -> Result<String, SpanSnippetError> {
376        let WithSourceFile { file, data } = self.span_to_source(sp)?;
377        file.src.get(..data.start).map(|s| s.to_string()).ok_or(SpanSnippetError::IllFormedSpan(sp))
378    }
379
380    /// For a global `BytePos`, computes the local offset within the containing `SourceFile`.
381    pub fn lookup_byte_offset(&self, bpos: BytePos) -> SourceFileAndBytePos {
382        let sf = self.lookup_source_file(bpos);
383        let offset = bpos - sf.start_pos;
384        SourceFileAndBytePos { sf, pos: offset }
385    }
386
387    /// Returns the index of the [`SourceFile`] (in `self.files`) that contains `pos`.
388    ///
389    /// This index is guaranteed to be valid for the lifetime of this `SourceMap`.
390    pub fn lookup_source_file_idx(&self, pos: BytePos) -> usize {
391        Self::lookup_sf_idx(&self.files(), pos)
392    }
393
394    /// Return the SourceFile that contains the given `BytePos`.
395    pub fn lookup_source_file(&self, pos: BytePos) -> Arc<SourceFile> {
396        let files = &*self.files();
397        let idx = Self::lookup_sf_idx(files, pos);
398        files[idx].clone()
399    }
400
401    fn lookup_sf_idx(files: &[Arc<SourceFile>], pos: BytePos) -> usize {
402        assert!(!files.is_empty(), "attempted to lookup source file in empty `SourceMap`");
403        files.partition_point(|x| x.start_pos <= pos) - 1
404    }
405
406    /// Looks up source information about a `BytePos`.
407    pub fn lookup_char_pos(&self, pos: BytePos) -> WithSourceFile<Loc> {
408        let sf = self.lookup_source_file(pos);
409        let (line, col, col_display) = sf.lookup_file_pos_with_col_display(pos);
410        WithSourceFile { file: sf, data: Loc { line, col, col_display } }
411    }
412
413    /// If the corresponding `SourceFile` is empty, does not return a line number.
414    pub fn lookup_line(&self, pos: BytePos) -> Result<SourceFileAndLine, Arc<SourceFile>> {
415        let f = self.lookup_source_file(pos);
416        let pos = f.relative_position(pos);
417        match f.lookup_line(pos) {
418            Some(line) => Ok(SourceFileAndLine { sf: f, line }),
419            None => Err(f),
420        }
421    }
422
423    pub fn is_valid_span(&self, sp: Span) -> Result<WithSourceFile<SpanLoc>, SpanLinesError> {
424        let lo = self.lookup_char_pos(sp.lo());
425        let hi = self.lookup_char_pos(sp.hi());
426        if lo.file.start_pos != hi.file.start_pos {
427            return Err(SpanLinesError::DistinctSources(Box::new(DistinctSources {
428                begin: (lo.file.name.clone(), lo.file.start_pos),
429                end: (hi.file.name.clone(), hi.file.start_pos),
430            })));
431        }
432        Ok(WithSourceFile { file: lo.file, data: SpanLoc { lo: lo.data, hi: hi.data } })
433    }
434
435    pub fn is_line_before_span_empty(&self, sp: Span) -> bool {
436        match self.span_to_prev_source(sp) {
437            Ok(s) => s.rsplit_once('\n').unwrap_or(("", &s)).1.trim_start().is_empty(),
438            Err(_) => false,
439        }
440    }
441
442    /// Computes the [`FileLines`] for the given span.
443    pub fn span_to_lines(&self, sp: Span) -> Result<FileLines, SpanLinesError> {
444        let WithSourceFile { file, data: SpanLoc { lo, hi } } = self.is_valid_span(sp)?;
445        assert!(hi.line >= lo.line);
446
447        if sp.is_dummy() {
448            return Ok(FileLines { file, data: Vec::new() });
449        }
450
451        let mut lines = Vec::with_capacity(hi.line - lo.line + 1);
452
453        // The span starts partway through the first line,
454        // but after that it starts from offset 0.
455        let mut start_col = lo.col;
456
457        // For every line but the last, it extends from `start_col`
458        // and to the end of the line. Be careful because the line
459        // numbers in Loc are 1-based, so we subtract 1 to get 0-based
460        // lines.
461        //
462        // FIXME: now that we handle DUMMY_SP up above, we should consider
463        // asserting that the line numbers here are all indeed 1-based.
464        let hi_line = hi.line.saturating_sub(1);
465        for line_index in lo.line.saturating_sub(1)..hi_line {
466            let line_len = file.get_line(line_index).map_or(0, |s| s.chars().count());
467            lines.push(LineInfo { line_index, start_col, end_col: CharPos::from_usize(line_len) });
468            start_col = CharPos::from_usize(0);
469        }
470
471        // For the last line, it extends from `start_col` to `hi.col`:
472        lines.push(LineInfo { line_index: hi_line, start_col, end_col: hi.col });
473
474        Ok(FileLines { file, data: lines })
475    }
476
477    /// Returns the source file and the range of text corresponding to the given span.
478    ///
479    /// See [`span_to_source`](Self::span_to_source).
480    pub fn span_to_range(&self, sp: Span) -> Result<Range<usize>, SpanSnippetError> {
481        self.span_to_source(sp).map(|s| s.data)
482    }
483
484    /// Returns the source file and the range of text corresponding to the given span.
485    pub fn span_to_source(
486        &self,
487        sp: Span,
488    ) -> Result<WithSourceFile<Range<usize>>, SpanSnippetError> {
489        let local_begin = self.lookup_byte_offset(sp.lo());
490        let local_end = self.lookup_byte_offset(sp.hi());
491
492        if local_begin.sf.start_pos != local_end.sf.start_pos {
493            return Err(SpanSnippetError::DistinctSources(Box::new(DistinctSources {
494                begin: (local_begin.sf.name.clone(), local_begin.sf.start_pos),
495                end: (local_end.sf.name.clone(), local_end.sf.start_pos),
496            })));
497        }
498
499        let start_index = local_begin.pos.to_usize();
500        let end_index = local_end.pos.to_usize();
501        let source_len = local_begin.sf.source_len.to_usize();
502
503        if start_index > end_index || end_index > source_len {
504            return Err(SpanSnippetError::MalformedForSourcemap(MalformedSourceMapPositions {
505                name: local_begin.sf.name.clone(),
506                source_len,
507                begin_pos: local_begin.pos,
508                end_pos: local_end.pos,
509            }));
510        }
511
512        Ok(WithSourceFile { file: local_begin.sf, data: start_index..end_index })
513    }
514
515    /// Format the span location to be printed in diagnostics.
516    ///
517    /// Must not be emitted to build artifacts as this may leak local file paths.
518    pub fn span_to_diagnostic_string(&self, sp: Span) -> impl fmt::Display {
519        let (source_file, loc) = self.span_to_location_info(sp);
520        fmt::from_fn(move |f| {
521            let file_name = match &source_file {
522                Some(sf) => self.filename_for_diagnostics(&sf.name),
523                None => return f.write_str("no-location"),
524            };
525            let lo_line = loc.lo.line;
526            let lo_col = loc.lo.col.0 + 1;
527            let hi_line = loc.hi.line;
528            let hi_col = loc.hi.col.0 + 1;
529            write!(f, "{file_name}:{lo_line}:{lo_col}: {hi_line}:{hi_col}")
530        })
531    }
532
533    /// Returns the source file, line, and column information for the given span.
534    ///
535    /// This is similar to [`is_valid_span`](Self::is_valid_span).
536    pub fn span_to_location_info(&self, sp: Span) -> (Option<Arc<SourceFile>>, SpanLoc) {
537        if self.files().is_empty() || sp.is_dummy() {
538            return Default::default();
539        }
540        let Ok(WithSourceFile { file, data }) = self.is_valid_span(sp) else {
541            return Default::default();
542        };
543        (Some(file), data)
544    }
545}