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