solar_interface/source_map/
mod.rs

1//! SourceMap related types and operations.
2
3use crate::{BytePos, CharPos, Span};
4use solar_data_structures::{
5    map::FxBuildHasher,
6    sync::{ReadGuard, RwLock},
7};
8use std::{
9    io::{self, Read},
10    path::{Path, PathBuf},
11    sync::Arc,
12};
13
14mod analyze;
15
16mod file;
17pub use file::*;
18
19mod file_resolver;
20pub use file_resolver::{FileResolver, ResolveError};
21
22#[cfg(test)]
23mod tests;
24
25pub type FileLinesResult = Result<FileLines, SpanLinesError>;
26
27#[derive(Clone, PartialEq, Eq, Debug)]
28pub enum SpanLinesError {
29    DistinctSources(Box<DistinctSources>),
30}
31
32#[derive(Clone, PartialEq, Eq, Debug)]
33pub enum SpanSnippetError {
34    IllFormedSpan(Span),
35    DistinctSources(Box<DistinctSources>),
36    MalformedForSourcemap(MalformedSourceMapPositions),
37    SourceNotAvailable { filename: FileName },
38}
39
40#[derive(Clone, PartialEq, Eq, Debug)]
41pub struct DistinctSources {
42    pub begin: (FileName, BytePos),
43    pub end: (FileName, BytePos),
44}
45
46#[derive(Clone, PartialEq, Eq, Debug)]
47pub struct MalformedSourceMapPositions {
48    pub name: FileName,
49    pub source_len: usize,
50    pub begin_pos: BytePos,
51    pub end_pos: BytePos,
52}
53
54/// A source code location used for error reporting.
55#[derive(Clone, Debug)]
56pub struct Loc {
57    /// Information about the original source.
58    pub file: Arc<SourceFile>,
59    /// The (1-based) line number.
60    pub line: usize,
61    /// The (0-based) column offset.
62    pub col: CharPos,
63    /// The (0-based) column offset when displayed.
64    pub col_display: usize,
65}
66
67// Used to be structural records.
68#[derive(Debug)]
69pub struct SourceFileAndLine {
70    pub sf: Arc<SourceFile>,
71    /// Index of line, starting from 0.
72    pub line: usize,
73}
74
75#[derive(Debug)]
76pub struct SourceFileAndBytePos {
77    pub sf: Arc<SourceFile>,
78    pub pos: BytePos,
79}
80
81#[derive(Copy, Clone, Debug, PartialEq, Eq)]
82pub struct LineInfo {
83    /// Index of line, starting from 0.
84    pub line_index: usize,
85
86    /// Column in line where span begins, starting from 0.
87    pub start_col: CharPos,
88
89    /// Column in line where span ends, starting from 0, exclusive.
90    pub end_col: CharPos,
91}
92
93pub struct FileLines {
94    pub file: Arc<SourceFile>,
95    pub lines: Vec<LineInfo>,
96}
97
98pub struct SourceMap {
99    // INVARIANT: The only operation allowed on `source_files` is `push`.
100    source_files: RwLock<Vec<Arc<SourceFile>>>,
101    stable_id_to_source_file: scc::HashIndex<StableSourceFileId, Arc<SourceFile>, FxBuildHasher>,
102    hash_kind: SourceFileHashAlgorithm,
103}
104
105impl Default for SourceMap {
106    fn default() -> Self {
107        Self::empty()
108    }
109}
110
111impl SourceMap {
112    /// Creates a new empty source map with the given hash algorithm.
113    pub fn new(hash_kind: SourceFileHashAlgorithm) -> Self {
114        Self {
115            source_files: RwLock::new(Vec::new()),
116            stable_id_to_source_file: Default::default(),
117            hash_kind,
118        }
119    }
120
121    /// Creates a new empty source map.
122    pub fn empty() -> Self {
123        Self::new(SourceFileHashAlgorithm::default())
124    }
125
126    /// Loads a file from the given path.
127    pub fn load_file(&self, path: &Path) -> io::Result<Arc<SourceFile>> {
128        let filename = path.to_owned().into();
129        self.new_source_file(filename, || std::fs::read_to_string(path))
130    }
131
132    /// Loads `stdin`.
133    pub fn load_stdin(&self) -> io::Result<Arc<SourceFile>> {
134        self.new_source_file(FileName::Stdin, || {
135            let mut src = String::new();
136            io::stdin().read_to_string(&mut src)?;
137            Ok(src)
138        })
139    }
140
141    /// Loads a file with the given source string.
142    ///
143    /// This is useful for testing.
144    pub fn new_dummy_source_file(&self, path: PathBuf, src: String) -> io::Result<Arc<SourceFile>> {
145        self.new_source_file(path.into(), || Ok(src))
146    }
147
148    /// Creates a new `SourceFile`.
149    ///
150    /// If a file already exists in the `SourceMap` with the same ID, that file is returned
151    /// unmodified.
152    ///
153    /// Returns an error if the file is larger than 4GiB or other errors occur while creating the
154    /// `SourceFile`.
155    #[instrument(level = "debug", skip_all, fields(filename = %filename.display()))]
156    pub fn new_source_file(
157        &self,
158        filename: FileName,
159        get_src: impl FnOnce() -> io::Result<String>,
160    ) -> io::Result<Arc<SourceFile>> {
161        let stable_id = StableSourceFileId::from_filename_in_current_crate(&filename);
162        match self.stable_id_to_source_file.entry(stable_id) {
163            scc::hash_index::Entry::Occupied(entry) => Ok(entry.get().clone()),
164            scc::hash_index::Entry::Vacant(entry) => {
165                let file = SourceFile::new(filename, get_src()?, self.hash_kind)?;
166                let file = self.new_source_file_inner(file, stable_id)?;
167                entry.insert_entry(file.clone());
168                Ok(file)
169            }
170        }
171    }
172
173    fn new_source_file_inner(
174        &self,
175        mut file: SourceFile,
176        stable_id: StableSourceFileId,
177    ) -> io::Result<Arc<SourceFile>> {
178        // Let's make sure the file_id we generated above actually matches
179        // the ID we generate for the SourceFile we just created.
180        debug_assert_eq!(file.stable_id, stable_id);
181
182        trace!(name=%file.name.display(), len=file.src.len(), loc=file.count_lines(), "adding to source map");
183
184        let mut source_files = self.source_files.write();
185
186        file.start_pos = BytePos(if let Some(last_file) = source_files.last() {
187            // Add one so there is some space between files. This lets us distinguish
188            // positions in the `SourceMap`, even in the presence of zero-length files.
189            last_file.end_position().0.checked_add(1).ok_or(OffsetOverflowError(()))?
190        } else {
191            0
192        });
193
194        let file = Arc::new(file);
195        source_files.push(file.clone());
196
197        Ok(file)
198    }
199
200    pub fn files(&self) -> ReadGuard<'_, Vec<Arc<SourceFile>>> {
201        self.source_files.read()
202    }
203
204    pub fn source_file_by_file_name(&self, filename: &FileName) -> Option<Arc<SourceFile>> {
205        let stable_id = StableSourceFileId::from_filename_in_current_crate(filename);
206        self.source_file_by_stable_id(stable_id)
207    }
208
209    pub fn source_file_by_stable_id(
210        &self,
211        stable_id: StableSourceFileId,
212    ) -> Option<Arc<SourceFile>> {
213        self.stable_id_to_source_file.get(&stable_id).as_deref().cloned()
214    }
215
216    pub fn filename_for_diagnostics<'a>(&self, filename: &'a FileName) -> FileNameDisplay<'a> {
217        filename.display()
218    }
219
220    /// Returns `true` if the given span is multi-line.
221    pub fn is_multiline(&self, span: Span) -> bool {
222        let lo = self.lookup_source_file_idx(span.lo());
223        let hi = self.lookup_source_file_idx(span.hi());
224        if lo != hi {
225            return true;
226        }
227        let f = self.files()[lo].clone();
228        let lo = f.relative_position(span.lo());
229        let hi = f.relative_position(span.hi());
230        f.lookup_line(lo) != f.lookup_line(hi)
231    }
232
233    /// Returns the source snippet as `String` corresponding to the given `Span`.
234    pub fn span_to_snippet(&self, span: Span) -> Result<String, SpanSnippetError> {
235        self.span_to_source(span, |src, start_index, end_index| {
236            src.get(start_index..end_index)
237                .map(|s| s.to_string())
238                .ok_or(SpanSnippetError::IllFormedSpan(span))
239        })
240    }
241
242    /// For a global `BytePos`, computes the local offset within the containing `SourceFile`.
243    pub fn lookup_byte_offset(&self, bpos: BytePos) -> SourceFileAndBytePos {
244        let idx = self.lookup_source_file_idx(bpos);
245        let sf = self.files()[idx].clone();
246        let offset = bpos - sf.start_pos;
247        SourceFileAndBytePos { sf, pos: offset }
248    }
249
250    /// Returns the index of the [`SourceFile`] (in `self.files`) that contains `pos`.
251    ///
252    /// This index is guaranteed to be valid for the lifetime of this `SourceMap`.
253    pub fn lookup_source_file_idx(&self, pos: BytePos) -> usize {
254        self.files().partition_point(|x| x.start_pos <= pos) - 1
255    }
256
257    /// Return the SourceFile that contains the given `BytePos`.
258    pub fn lookup_source_file(&self, pos: BytePos) -> Arc<SourceFile> {
259        let idx = self.lookup_source_file_idx(pos);
260        self.files()[idx].clone()
261    }
262
263    /// Looks up source information about a `BytePos`.
264    pub fn lookup_char_pos(&self, pos: BytePos) -> Loc {
265        let sf = self.lookup_source_file(pos);
266        let (line, col, col_display) = sf.lookup_file_pos_with_col_display(pos);
267        Loc { file: sf, line, col, col_display }
268    }
269
270    /// If the corresponding `SourceFile` is empty, does not return a line number.
271    pub fn lookup_line(&self, pos: BytePos) -> Result<SourceFileAndLine, Arc<SourceFile>> {
272        let f = self.lookup_source_file(pos);
273        let pos = f.relative_position(pos);
274        match f.lookup_line(pos) {
275            Some(line) => Ok(SourceFileAndLine { sf: f, line }),
276            None => Err(f),
277        }
278    }
279
280    /// Returns the source snippet as `String` before the given `Span`.
281    pub fn span_to_prev_source(&self, sp: Span) -> Result<String, SpanSnippetError> {
282        self.span_to_source(sp, |src, start_index, _| {
283            src.get(..start_index).map(|s| s.to_string()).ok_or(SpanSnippetError::IllFormedSpan(sp))
284        })
285    }
286
287    pub fn is_valid_span(&self, sp: Span) -> Result<(Loc, Loc), SpanLinesError> {
288        let lo = self.lookup_char_pos(sp.lo());
289        let hi = self.lookup_char_pos(sp.hi());
290        if lo.file.start_pos != hi.file.start_pos {
291            return Err(SpanLinesError::DistinctSources(Box::new(DistinctSources {
292                begin: (lo.file.name.clone(), lo.file.start_pos),
293                end: (hi.file.name.clone(), hi.file.start_pos),
294            })));
295        }
296        Ok((lo, hi))
297    }
298
299    pub fn is_line_before_span_empty(&self, sp: Span) -> bool {
300        match self.span_to_prev_source(sp) {
301            Ok(s) => s.rsplit_once('\n').unwrap_or(("", &s)).1.trim_start().is_empty(),
302            Err(_) => false,
303        }
304    }
305
306    pub fn span_to_lines(&self, sp: Span) -> FileLinesResult {
307        let (lo, hi) = self.is_valid_span(sp)?;
308        assert!(hi.line >= lo.line);
309
310        if sp.is_dummy() {
311            return Ok(FileLines { file: lo.file, lines: Vec::new() });
312        }
313
314        let mut lines = Vec::with_capacity(hi.line - lo.line + 1);
315
316        // The span starts partway through the first line,
317        // but after that it starts from offset 0.
318        let mut start_col = lo.col;
319
320        // For every line but the last, it extends from `start_col`
321        // and to the end of the line. Be careful because the line
322        // numbers in Loc are 1-based, so we subtract 1 to get 0-based
323        // lines.
324        //
325        // FIXME: now that we handle DUMMY_SP up above, we should consider
326        // asserting that the line numbers here are all indeed 1-based.
327        let hi_line = hi.line.saturating_sub(1);
328        for line_index in lo.line.saturating_sub(1)..hi_line {
329            let line_len = lo.file.get_line(line_index).map_or(0, |s| s.chars().count());
330            lines.push(LineInfo { line_index, start_col, end_col: CharPos::from_usize(line_len) });
331            start_col = CharPos::from_usize(0);
332        }
333
334        // For the last line, it extends from `start_col` to `hi.col`:
335        lines.push(LineInfo { line_index: hi_line, start_col, end_col: hi.col });
336
337        Ok(FileLines { file: lo.file, lines })
338    }
339
340    /// Extracts the source surrounding the given `Span` using the `extract_source` function. The
341    /// extract function takes three arguments: a string slice containing the source, an index in
342    /// the slice for the beginning of the span and an index in the slice for the end of the span.
343    fn span_to_source<F, T>(&self, sp: Span, extract_source: F) -> Result<T, SpanSnippetError>
344    where
345        F: Fn(&str, usize, usize) -> Result<T, SpanSnippetError>,
346    {
347        let local_begin = self.lookup_byte_offset(sp.lo());
348        let local_end = self.lookup_byte_offset(sp.hi());
349
350        if local_begin.sf.start_pos != local_end.sf.start_pos {
351            Err(SpanSnippetError::DistinctSources(Box::new(DistinctSources {
352                begin: (local_begin.sf.name.clone(), local_begin.sf.start_pos),
353                end: (local_end.sf.name.clone(), local_end.sf.start_pos),
354            })))
355        } else {
356            // self.ensure_source_file_source_present(&local_begin.sf);
357
358            let start_index = local_begin.pos.to_usize();
359            let end_index = local_end.pos.to_usize();
360            let source_len = local_begin.sf.source_len.to_usize();
361
362            if start_index > end_index || end_index > source_len {
363                return Err(SpanSnippetError::MalformedForSourcemap(MalformedSourceMapPositions {
364                    name: local_begin.sf.name.clone(),
365                    source_len,
366                    begin_pos: local_begin.pos,
367                    end_pos: local_end.pos,
368                }));
369            }
370
371            extract_source(&local_begin.sf.src, start_index, end_index)
372        }
373    }
374
375    /// Format the span location to be printed in diagnostics. Must not be emitted
376    /// to build artifacts as this may leak local file paths. Use span_to_embeddable_string
377    /// for string suitable for embedding.
378    pub fn span_to_diagnostic_string(&self, sp: Span) -> String {
379        self.span_to_string(sp)
380    }
381
382    pub fn span_to_string(&self, sp: Span) -> String {
383        let (source_file, lo_line, lo_col, hi_line, hi_col) = self.span_to_location_info(sp);
384
385        let file_name = match source_file {
386            Some(sf) => sf.name.display().to_string(),
387            None => return "no-location".to_string(),
388        };
389
390        format!("{file_name}:{lo_line}:{lo_col}: {hi_line}:{hi_col}")
391    }
392
393    pub fn span_to_location_info(
394        &self,
395        sp: Span,
396    ) -> (Option<Arc<SourceFile>>, usize, usize, usize, usize) {
397        if self.files().is_empty() || sp.is_dummy() {
398            return (None, 0, 0, 0, 0);
399        }
400
401        let lo = self.lookup_char_pos(sp.lo());
402        let hi = self.lookup_char_pos(sp.hi());
403        (Some(lo.file), lo.line, lo.col.to_usize() + 1, hi.line, hi.col.to_usize() + 1)
404    }
405}