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