Skip to main content

swc_sourcemap/
hermes.rs

1use std::{
2    io::{Read, Write},
3    ops::{Deref, DerefMut},
4};
5
6use bytes_str::BytesStr;
7
8use crate::{
9    decoder::{decode, decode_regular, decode_slice},
10    encoder::{encode, Encodable},
11    errors::{Error, Result},
12    jsontypes::{FacebookScopeMapping, FacebookSources, RawSourceMap},
13    types::{DecodedMap, RewriteOptions, SourceMap},
14    utils::greatest_lower_bound,
15    vlq::parse_vlq_segment_into,
16    Token,
17};
18
19/// These are starting locations of scopes.
20/// The `name_index` represents the index into the `HermesFunctionMap.names`
21/// vec, which represents the function names/scopes.
22#[derive(Debug, Clone, PartialEq)]
23pub struct HermesScopeOffset {
24    line: u32,
25    column: u32,
26    name_index: u32,
27}
28
29#[derive(Debug, Clone, PartialEq)]
30pub struct HermesFunctionMap {
31    names: Vec<BytesStr>,
32    mappings: Vec<HermesScopeOffset>,
33}
34
35/// Represents a `react-native`-style SourceMap, which has additional scope
36/// information embedded.
37#[derive(Debug, Clone, PartialEq)]
38pub struct SourceMapHermes {
39    pub(crate) sm: SourceMap,
40    // There should be one `HermesFunctionMap` per each `sources` entry in the main SourceMap.
41    function_maps: Vec<Option<HermesFunctionMap>>,
42    // XXX: right now, I am too lazy to actually serialize the above `function_maps`
43    // back into json types, so just keep the original json. Might be a bit inefficient, but meh.
44    raw_facebook_sources: FacebookSources,
45}
46
47impl Deref for SourceMapHermes {
48    type Target = SourceMap;
49
50    fn deref(&self) -> &Self::Target {
51        &self.sm
52    }
53}
54
55impl DerefMut for SourceMapHermes {
56    fn deref_mut(&mut self) -> &mut Self::Target {
57        &mut self.sm
58    }
59}
60
61impl Encodable for SourceMapHermes {
62    fn as_raw_sourcemap(&self) -> RawSourceMap {
63        // TODO: need to serialize the `HermesFunctionMap` mappings
64        let mut rsm = self.sm.as_raw_sourcemap();
65        rsm.x_facebook_sources
66            .clone_from(&self.raw_facebook_sources);
67        rsm
68    }
69}
70
71impl SourceMapHermes {
72    /// Creates a sourcemap from a reader over a JSON stream in UTF-8
73    /// format.
74    ///
75    /// See [`SourceMap::from_reader`](struct.SourceMap.html#method.from_reader)
76    pub fn from_reader<R: Read>(rdr: R) -> Result<Self> {
77        match decode(rdr)? {
78            DecodedMap::Hermes(sm) => Ok(sm),
79            _ => Err(Error::IncompatibleSourceMap),
80        }
81    }
82
83    /// Creates a sourcemap from a reader over a JSON byte slice in UTF-8
84    /// format.
85    ///
86    /// See [`SourceMap::from_slice`](struct.SourceMap.html#method.from_slice)
87    pub fn from_slice(slice: &[u8]) -> Result<Self> {
88        match decode_slice(slice)? {
89            DecodedMap::Hermes(sm) => Ok(sm),
90            _ => Err(Error::IncompatibleSourceMap),
91        }
92    }
93
94    /// Writes a sourcemap into a writer.
95    ///
96    /// See [`SourceMap::to_writer`](struct.SourceMap.html#method.to_writer)
97    pub fn to_writer<W: Write>(&self, w: W) -> Result<()> {
98        encode(self, w)
99    }
100
101    /// Given a bytecode offset, this will find the enclosing scopes function
102    /// name.
103    pub fn get_original_function_name(&self, bytecode_offset: u32) -> Option<&BytesStr> {
104        let token = self.sm.lookup_token(0, bytecode_offset)?;
105
106        self.get_scope_for_token(token)
107    }
108
109    /// Resolves the name of the enclosing function for the given [`Token`].
110    pub fn get_scope_for_token(&self, token: Token) -> Option<&BytesStr> {
111        let function_map = self
112            .function_maps
113            .get(token.get_src_id() as usize)?
114            .as_ref()?;
115
116        // Find the closest mapping, just like here:
117        // https://github.com/facebook/metro/blob/63b523eb20e7bdf62018aeaf195bb5a3a1a67f36/packages/metro-symbolicate/src/SourceMetadataMapConsumer.js#L204-L231
118        // Mappings use 1-based index for lines, and 0-based index for cols, as seen
119        // here: https://github.com/facebook/metro/blob/f2d80cebe66d3c64742f67259f41da26e83a0d8d/packages/metro/src/Server/symbolicate.js#L58-L60
120        let (_mapping_idx, mapping) = greatest_lower_bound(
121            &function_map.mappings,
122            &(token.get_src_line() + 1, token.get_src_col()),
123            |o| (o.line, o.column),
124        )?;
125        function_map.names.get(mapping.name_index as usize)
126    }
127
128    /// This rewrites the sourcemap according to the provided rewrite
129    /// options.
130    ///
131    /// See [`SourceMap::rewrite`](struct.SourceMap.html#method.rewrite)
132    pub fn rewrite(self, options: &RewriteOptions<'_>) -> Result<Self> {
133        let Self {
134            sm,
135            mut function_maps,
136            mut raw_facebook_sources,
137        } = self;
138
139        let (sm, mapping) = sm.rewrite_with_mapping(options)?;
140
141        if function_maps.len() >= mapping.len() {
142            function_maps = mapping
143                .iter()
144                .map(|idx| function_maps[*idx as usize].take())
145                .collect();
146            raw_facebook_sources = raw_facebook_sources.map(|mut sources| {
147                mapping
148                    .into_iter()
149                    .map(|idx| sources[idx as usize].take())
150                    .collect()
151            });
152        }
153
154        Ok(Self {
155            sm,
156            function_maps,
157            raw_facebook_sources,
158        })
159    }
160}
161
162pub fn decode_hermes(mut rsm: RawSourceMap) -> Result<SourceMapHermes> {
163    let x_facebook_sources = rsm
164        .x_facebook_sources
165        .take()
166        .ok_or(Error::IncompatibleSourceMap)?;
167
168    // This is basically the logic from here:
169    // https://github.com/facebook/metro/blob/63b523eb20e7bdf62018aeaf195bb5a3a1a67f36/packages/metro-symbolicate/src/SourceMetadataMapConsumer.js#L182-L202
170
171    let mut nums = Vec::with_capacity(4);
172
173    let function_maps = x_facebook_sources
174        .iter()
175        .map(|v| {
176            let FacebookScopeMapping {
177                names,
178                mappings: raw_mappings,
179            } = v.as_ref()?.iter().next()?;
180
181            let mut mappings = vec![];
182            let mut line = 1;
183            let mut name_index = 0;
184
185            for line_mapping in raw_mappings.split(';') {
186                if line_mapping.is_empty() {
187                    continue;
188                }
189
190                let mut column = 0;
191
192                for mapping in line_mapping.split(',') {
193                    if mapping.is_empty() {
194                        continue;
195                    }
196
197                    nums.clear();
198                    parse_vlq_segment_into(mapping, &mut nums).ok()?;
199                    let mut nums = nums.iter().copied();
200
201                    column = (i64::from(column) + nums.next()?) as u32;
202                    name_index = (i64::from(name_index) + nums.next().unwrap_or(0)) as u32;
203                    line = (i64::from(line) + nums.next().unwrap_or(0)) as u32;
204                    mappings.push(HermesScopeOffset {
205                        column,
206                        line,
207                        name_index,
208                    });
209                }
210            }
211            Some(HermesFunctionMap {
212                names: names.clone(),
213                mappings,
214            })
215        })
216        .collect();
217
218    let sm = decode_regular(rsm)?;
219    Ok(SourceMapHermes {
220        sm,
221        function_maps,
222        raw_facebook_sources: Some(x_facebook_sources),
223    })
224}