solar_interface/source_map/
mod.rs1use 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#[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#[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#[derive(Clone, Debug)]
84pub struct Loc {
85 pub line: usize,
87 pub col: CharPos,
89 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#[derive(Clone, Debug, Default)]
104pub struct SpanLoc {
105 pub lo: Loc,
107 pub hi: Loc,
109}
110
111#[derive(Debug)]
113pub struct SourceFileAndLine {
114 pub sf: Arc<SourceFile>,
115 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 pub line_index: usize,
129
130 pub start_col: CharPos,
132
133 pub end_col: CharPos,
135}
136
137pub type FileLines = WithSourceFile<Vec<LineInfo>>;
138
139pub 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
151pub struct RealFileLoader;
153
154#[allow(clippy::disallowed_methods)] impl 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#[derive(derive_more::Debug)]
177pub struct SourceMap {
178 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 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 pub fn clear(&mut self) {
207 let _ = self.take();
208 }
209
210 #[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 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 pub fn file_loader(&self) -> &dyn FileLoader {
231 self.file_loader.get().map(std::ops::Deref::deref).unwrap_or(&RealFileLoader)
232 }
233
234 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 pub fn is_empty(&self) -> bool {
247 self.files().is_empty()
248 }
249
250 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 pub fn get_file(&self, path: impl Into<FileName>) -> Option<Arc<SourceFile>> {
270 self.get_file_ref(&path.into())
271 }
272
273 pub fn get_file_ref(&self, filename: &FileName) -> Option<Arc<SourceFile>> {
276 self.id_to_file.get_cloned(&SourceFileId::new(filename))
277 }
278
279 pub fn load_file(&self, path: &Path) -> io::Result<Arc<SourceFile>> {
281 self.load_file_with_name(path.into(), path)
282 }
283
284 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 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 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 #[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 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 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 pub fn filename_for_diagnostics<'a>(&self, filename: &'a FileName) -> FileNameDisplay<'a> {
352 FileNameDisplay { inner: filename, base_path: self.base_path() }
353 }
354
355 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 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 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 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 pub fn lookup_source_file_idx(&self, pos: BytePos) -> usize {
391 Self::lookup_sf_idx(&self.files(), pos)
392 }
393
394 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 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 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 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 let mut start_col = lo.col;
456
457 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 lines.push(LineInfo { line_index: hi_line, start_col, end_col: hi.col });
473
474 Ok(FileLines { file, data: lines })
475 }
476
477 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 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 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 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}