rw_deno_core/
source_map.rs

1// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2
3//! This mod provides functions to remap a `JsError` based on a source map.
4
5use crate::resolve_url;
6pub use sourcemap::SourceMap;
7use std::borrow::Cow;
8use std::collections::HashMap;
9use std::rc::Rc;
10use std::str;
11
12pub trait SourceMapGetter {
13  /// Returns the raw source map file.
14  fn get_source_map(&self, file_name: &str) -> Option<Vec<u8>>;
15  fn get_source_line(
16    &self,
17    file_name: &str,
18    line_number: usize,
19  ) -> Option<String>;
20}
21
22impl<T> SourceMapGetter for Rc<T>
23where
24  T: SourceMapGetter + ?Sized,
25{
26  fn get_source_map(&self, file_name: &str) -> Option<Vec<u8>> {
27    (**self).get_source_map(file_name)
28  }
29
30  fn get_source_line(
31    &self,
32    file_name: &str,
33    line_number: usize,
34  ) -> Option<String> {
35    (**self).get_source_line(file_name, line_number)
36  }
37}
38
39pub enum SourceMapApplication {
40  /// No mapping was applied, the location is unchanged.
41  Unchanged,
42  /// Line and column were mapped to a new location.
43  LineAndColumn {
44    line_number: u32,
45    column_number: u32,
46  },
47  /// Line, column and file name were mapped to a new location.
48  LineAndColumnAndFileName {
49    file_name: String,
50    line_number: u32,
51    column_number: u32,
52  },
53}
54
55pub type SourceMapData = Cow<'static, [u8]>;
56
57pub struct SourceMapper<G: SourceMapGetter> {
58  maps: HashMap<String, Option<SourceMap>>,
59  source_lines: HashMap<(String, i64), Option<String>>,
60  getter: Option<G>,
61  pub(crate) ext_source_maps: HashMap<String, SourceMapData>,
62  // This is not the right place for this, but it's the easiest way to make
63  // op_apply_source_map a fast op. This stashing should happen in #[op2].
64  pub(crate) stashed_file_name: Option<String>,
65}
66
67impl<G: SourceMapGetter> SourceMapper<G> {
68  pub fn new(getter: Option<G>) -> Self {
69    Self {
70      maps: Default::default(),
71      source_lines: Default::default(),
72      ext_source_maps: Default::default(),
73      getter,
74      stashed_file_name: Default::default(),
75    }
76  }
77
78  pub fn has_user_sources(&self) -> bool {
79    self.getter.is_some()
80  }
81
82  /// Apply a source map to the passed location. If there is no source map for
83  /// this location, or if the location remains unchanged after mapping, the
84  /// changed values are returned.
85  ///
86  /// Line and column numbers are 1-based.
87  pub fn apply_source_map(
88    &mut self,
89    file_name: &str,
90    line_number: u32,
91    column_number: u32,
92  ) -> SourceMapApplication {
93    // Lookup expects 0-based line and column numbers, but ours are 1-based.
94    let line_number = line_number - 1;
95    let column_number = column_number - 1;
96
97    let getter = self.getter.as_ref();
98    let maybe_source_map =
99      self.maps.entry(file_name.to_owned()).or_insert_with(|| {
100        None
101          .or_else(|| {
102            SourceMap::from_slice(self.ext_source_maps.get(file_name)?).ok()
103          })
104          .or_else(|| {
105            SourceMap::from_slice(&getter?.get_source_map(file_name)?).ok()
106          })
107      });
108
109    let Some(source_map) = maybe_source_map.as_ref() else {
110      return SourceMapApplication::Unchanged;
111    };
112
113    let Some(token) = source_map.lookup_token(line_number, column_number)
114    else {
115      return SourceMapApplication::Unchanged;
116    };
117
118    let new_line_number = token.get_src_line() + 1;
119    let new_column_number = token.get_src_col() + 1;
120
121    let new_file_name = match token.get_source() {
122      Some(source_file_name) => {
123        if source_file_name == file_name {
124          None
125        } else {
126          // The `source_file_name` written by tsc in the source map is
127          // sometimes only the basename of the URL, or has unwanted `<`/`>`
128          // around it. Use the `file_name` we get from V8 if
129          // `source_file_name` does not parse as a URL.
130          match resolve_url(source_file_name) {
131            Ok(m) if m.scheme() == "blob" => None,
132            Ok(m) => Some(m.to_string()),
133            Err(_) => None,
134          }
135        }
136      }
137      None => None,
138    };
139
140    match new_file_name {
141      None => SourceMapApplication::LineAndColumn {
142        line_number: new_line_number,
143        column_number: new_column_number,
144      },
145      Some(file_name) => SourceMapApplication::LineAndColumnAndFileName {
146        file_name,
147        line_number: new_line_number,
148        column_number: new_column_number,
149      },
150    }
151  }
152
153  const MAX_SOURCE_LINE_LENGTH: usize = 150;
154
155  pub fn get_source_line(
156    &mut self,
157    file_name: &str,
158    line_number: i64,
159  ) -> Option<String> {
160    let getter = self.getter.as_ref()?;
161    self
162      .source_lines
163      .entry((file_name.to_string(), line_number))
164      .or_insert_with(|| {
165        // Source lookup expects a 0-based line number, ours are 1-based.
166        let s = getter.get_source_line(file_name, (line_number - 1) as usize);
167        s.filter(|s| s.len() <= Self::MAX_SOURCE_LINE_LENGTH)
168      })
169      .clone()
170  }
171}