solar_interface/source_map/
mod.rs

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