Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support Hermes (react-native) SourceMaps #22

Merged
merged 13 commits into from
Feb 5, 2020
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.DS_Store
target
Cargo.lock
1 change: 1 addition & 0 deletions examples/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ fn load_from_reader<R: Read>(mut rdr: R) -> SourceMap {
..Default::default()
})
.unwrap(),
_ => panic!("unexpected sourcemap format"),
}
}

Expand Down
1 change: 1 addition & 0 deletions examples/rewrite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ fn load_from_reader<R: Read>(mut rdr: R) -> SourceMap {
..Default::default()
})
.unwrap(),
_ => panic!("unexpected sourcemap format"),
}
}

Expand Down
5 changes: 4 additions & 1 deletion src/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use serde_json;
use serde_json::Value;

use crate::errors::{Error, Result};
use crate::hermes::decode_hermes;
use crate::jsontypes::RawSourceMap;
use crate::types::{DecodedMap, RawToken, SourceMap, SourceMapIndex, SourceMapSection};
use crate::vlq::parse_vlq_segment;
Expand Down Expand Up @@ -121,7 +122,7 @@ pub fn strip_junk_header(slice: &[u8]) -> io::Result<&[u8]> {
Ok(&slice[slice.len()..])
}

fn decode_regular(rsm: RawSourceMap) -> Result<SourceMap> {
pub fn decode_regular(rsm: RawSourceMap) -> Result<SourceMap> {
let mut dst_col;
let mut src_id = 0;
let mut src_line = 0;
Expand Down Expand Up @@ -267,6 +268,8 @@ fn decode_index(rsm: RawSourceMap) -> Result<SourceMapIndex> {
fn decode_common(rsm: RawSourceMap) -> Result<DecodedMap> {
Ok(if rsm.sections.is_some() {
DecodedMap::Index(decode_index(rsm)?)
} else if rsm.x_facebook_sources.is_some() {
DecodedMap::Hermes(decode_hermes(rsm)?)
} else {
DecodedMap::Regular(decode_regular(rsm)?)
})
Expand Down
3 changes: 3 additions & 0 deletions src/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ impl Encodable for SourceMap {
mappings: Some(serialize_mappings(self)),
x_facebook_offsets: None,
x_metro_module_paths: None,
x_facebook_sources: None,
}
}
}
Expand Down Expand Up @@ -124,6 +125,7 @@ impl Encodable for SourceMapIndex {
mappings: None,
x_facebook_offsets: None,
x_metro_module_paths: None,
x_facebook_sources: None,
}
}
}
Expand All @@ -133,6 +135,7 @@ impl Encodable for DecodedMap {
match *self {
DecodedMap::Regular(ref sm) => sm.as_raw_sourcemap(),
DecodedMap::Index(ref smi) => smi.as_raw_sourcemap(),
DecodedMap::Hermes(ref smh) => smh.as_raw_sourcemap(),
}
}
}
173 changes: 173 additions & 0 deletions src/hermes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
use crate::decoder::{decode, decode_regular, decode_slice};
use crate::encoder::{encode, Encodable};
use crate::errors::{Error, Result};
use crate::jsontypes::{FacebookScopeMapping, FacebookSources, RawSourceMap};
use crate::types::{DecodedMap, RewriteOptions, SourceMap};
use crate::vlq::parse_vlq_segment;
use std::cmp::Ordering;
use std::io::{Read, Write};
use std::ops::{Deref, DerefMut};

/// These are starting locations of scopes.
/// The `name_index` represents the index into the `HermesFunctionMap.names` vec,
/// which represents the function names/scopes.
pub struct HermesScopeOffset {
line: u32,
column: u32,
name_index: u32,
}

pub struct HermesFunctionMap {
names: Vec<String>,
mappings: Vec<HermesScopeOffset>,
}

pub struct SourceMapHermes {
pub(crate) sm: SourceMap,
// There should be one `HermesFunctionMap` per each `sources` entry in the main SourceMap.
function_maps: Vec<Option<HermesFunctionMap>>,
raw_facebook_sources: FacebookSources,
}

impl Deref for SourceMapHermes {
type Target = SourceMap;

fn deref(&self) -> &Self::Target {
&self.sm
}
}

impl DerefMut for SourceMapHermes {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.sm
}
}

impl Encodable for SourceMapHermes {
fn as_raw_sourcemap(&self) -> RawSourceMap {
// TODO: need to serialize the `HermesFunctionMap` mappings
let mut rsm = self.sm.as_raw_sourcemap();
rsm.x_facebook_sources = self.raw_facebook_sources.clone();
rsm
}
}

impl SourceMapHermes {
pub fn from_reader<R: Read>(rdr: R) -> Result<Self> {
match decode(rdr)? {
DecodedMap::Hermes(sm) => Ok(sm),
_ => Err(Error::IndexedSourcemap),
}
}

pub fn from_slice(slice: &[u8]) -> Result<Self> {
match decode_slice(slice)? {
DecodedMap::Hermes(sm) => Ok(sm),
_ => Err(Error::IndexedSourcemap),
}
}

pub fn to_writer<W: Write>(&self, w: W) -> Result<()> {
encode(self, w)
}

pub fn get_original_function_name(&self, bytecode_offset: u32) -> Option<&str> {
let token = self.sm.lookup_token(0, bytecode_offset)?;

let function_map = &self.function_maps[token.get_src_id() as usize].as_ref()?;
Swatinem marked this conversation as resolved.
Show resolved Hide resolved

// Find the closest mapping, just like here:
// https://github.com/facebook/metro/blob/63b523eb20e7bdf62018aeaf195bb5a3a1a67f36/packages/metro-symbolicate/src/SourceMetadataMapConsumer.js#L204-L231
let mapping =
function_map
.mappings
.binary_search_by(|o| match o.line.cmp(&token.get_src_line()) {
Ordering::Equal => o.column.cmp(&token.get_src_col()),
x => x,
});
let name_index = function_map.mappings[match mapping {
Swatinem marked this conversation as resolved.
Show resolved Hide resolved
Ok(a) => a,
Err(a) => a.saturating_sub(1),
}]
.name_index;

function_map
.names
.get(name_index as usize)
.map(|n| n.as_str())
}

pub fn rewrite(self, options: &RewriteOptions<'_>) -> Result<Self> {
let Self {
sm,
function_maps,
raw_facebook_sources,
} = self;
let sm = sm.rewrite(options)?;
Ok(Self {
sm,
function_maps,
raw_facebook_sources,
})
}
}

pub fn decode_hermes(mut rsm: RawSourceMap) -> Result<SourceMapHermes> {
let x_facebook_sources = rsm
.x_facebook_sources
.take()
.expect("expected x_facebook_sources");

// This is basically the logic from here:
// https://github.com/facebook/metro/blob/63b523eb20e7bdf62018aeaf195bb5a3a1a67f36/packages/metro-symbolicate/src/SourceMetadataMapConsumer.js#L182-L202

let function_maps = x_facebook_sources
.iter()
.map(|v| {
let FacebookScopeMapping {
names,
mappings: raw_mappings,
} = v.as_ref()?.iter().next()?;

let mut mappings = vec![];
let mut line = 1;
let mut name_index = 0;

for line_mapping in raw_mappings.split(';') {
if line_mapping.is_empty() {
continue;
}

let mut column = 0;

for mapping in line_mapping.split(',') {
if mapping.is_empty() {
continue;
}

let mut nums = parse_vlq_segment(mapping).ok()?.into_iter();

column = (i64::from(column) + nums.next()?) as u32;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wonder if we want to deal with the overflow explicitly here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, the regular sourcemap code doesn’t either.

name_index = (i64::from(name_index) + nums.next().unwrap_or(0)) as u32;
line = (i64::from(line) + nums.next().unwrap_or(0)) as u32;
mappings.push(HermesScopeOffset {
column,
line,
name_index,
});
}
}
Some(HermesFunctionMap {
names: names.clone(),
mappings,
})
})
.collect();

let sm = decode_regular(rsm)?;
Ok(SourceMapHermes {
sm,
function_maps,
raw_facebook_sources: Some(x_facebook_sources),
})
}
14 changes: 14 additions & 0 deletions src/jsontypes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ pub struct RawSection {
pub map: Option<Box<RawSourceMap>>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct FacebookScopeMapping {
pub names: Vec<String>,
pub mappings: String,
}

// Each element here is matching the `sources` of the outer SourceMap.
// It has a list of metadata, the first one of which is a *function map*,
// containing scope information as a nested source map.
// See the decoder in `hermes.rs` for details.
pub type FacebookSources = Option<Vec<Option<Vec<FacebookScopeMapping>>>>;

#[derive(Serialize, Deserialize)]
pub struct RawSourceMap {
pub version: Option<u32>,
Expand All @@ -35,6 +47,8 @@ pub struct RawSourceMap {
pub x_facebook_offsets: Option<Vec<Option<u32>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub x_metro_module_paths: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub x_facebook_sources: FacebookSources,
}

#[derive(Deserialize)]
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ pub use crate::detector::{
SourceMapRef,
};
pub use crate::errors::{Error, Result};
pub use crate::hermes::SourceMapHermes;
pub use crate::sourceview::SourceView;
pub use crate::types::{
DecodedMap, RawToken, RewriteOptions, SourceMap, SourceMapIndex, SourceMapSection,
Expand All @@ -67,6 +68,7 @@ mod decoder;
mod detector;
mod encoder;
mod errors;
mod hermes;
mod jsontypes;
mod sourceview;
mod types;
Expand Down
2 changes: 1 addition & 1 deletion src/ram_bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ impl<'a> Iterator for SplitRamBundleModuleIter<'a> {
}
}

/// Decontstructs a RAM bundle into a sequence of sources and their sourcemaps
/// Deconstructs a RAM bundle into a sequence of sources and their sourcemaps
///
/// With the help of the RAM bundle's indexed sourcemap, the bundle is split into modules,
/// where each module is represented by its minified source and the corresponding sourcemap that
Expand Down
2 changes: 1 addition & 1 deletion src/sourceview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ impl<'a> Iterator for Lines<'a> {

/// Provides efficient access to minified sources.
///
/// This type is used to implement farily efficient source mapping
/// This type is used to implement fairly efficient source mapping
/// operations.
pub struct SourceView<'a> {
source: Cow<'a, str>,
Expand Down
17 changes: 12 additions & 5 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::builder::SourceMapBuilder;
use crate::decoder::{decode, decode_slice};
use crate::encoder::encode;
use crate::errors::{Error, Result};
use crate::hermes::SourceMapHermes;
use crate::sourceview::SourceView;
use crate::utils::find_common_prefix;

Expand Down Expand Up @@ -55,11 +56,14 @@ impl<'a> Default for RewriteOptions<'a> {
/// Usually the two things are too distinct to provide a common
/// interface however for token lookup and writing back into a writer
/// general methods are provided.
#[non_exhaustive]
Swatinem marked this conversation as resolved.
Show resolved Hide resolved
pub enum DecodedMap {
/// Indicates a regular sourcemap
Regular(SourceMap),
/// Indicates a sourcemap index
Index(SourceMapIndex),
/// Indicates a sourcemap as generated by Metro+Hermes, as used by react-native
Hermes(SourceMapHermes),
}

impl DecodedMap {
Expand All @@ -73,6 +77,7 @@ impl DecodedMap {
match *self {
DecodedMap::Regular(ref sm) => encode(sm, w),
DecodedMap::Index(ref smi) => encode(smi, w),
DecodedMap::Hermes(ref smh) => encode(smh, w),
}
}

Expand All @@ -84,6 +89,7 @@ impl DecodedMap {
match *self {
DecodedMap::Regular(ref sm) => sm.lookup_token(line, col),
DecodedMap::Index(ref smi) => smi.lookup_token(line, col),
DecodedMap::Hermes(ref smh) => smh.lookup_token(line, col),
}
}
}
Expand Down Expand Up @@ -445,7 +451,7 @@ impl SourceMap {
pub fn from_reader<R: Read>(rdr: R) -> Result<SourceMap> {
match decode(rdr)? {
DecodedMap::Regular(sm) => Ok(sm),
DecodedMap::Index(_) => Err(Error::IndexedSourcemap),
_ => Err(Error::IndexedSourcemap),
Swatinem marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down Expand Up @@ -491,7 +497,7 @@ impl SourceMap {
pub fn from_slice(slice: &[u8]) -> Result<SourceMap> {
match decode_slice(slice)? {
DecodedMap::Regular(sm) => Ok(sm),
DecodedMap::Index(_) => Err(Error::IndexedSourcemap),
_ => Err(Error::IndexedSourcemap),
}
}

Expand Down Expand Up @@ -698,7 +704,7 @@ impl SourceMap {
}
}

/// This rewrites the sourcemap accoridng to the provided rewrite
/// This rewrites the sourcemap according to the provided rewrite
/// options.
///
/// The default behavior is to just deduplicate the sourcemap, something
Expand Down Expand Up @@ -769,8 +775,8 @@ impl SourceMapIndex {
/// sourcemap is encountered an error is returned.
pub fn from_reader<R: Read>(rdr: R) -> Result<SourceMapIndex> {
match decode(rdr)? {
DecodedMap::Regular(_) => Err(Error::RegularSourcemap),
DecodedMap::Index(smi) => Ok(smi),
_ => Err(Error::RegularSourcemap),
}
}

Expand All @@ -785,8 +791,8 @@ impl SourceMapIndex {
/// sourcemap is encountered an error is returned.
pub fn from_slice(slice: &[u8]) -> Result<SourceMapIndex> {
match decode_slice(slice)? {
DecodedMap::Regular(_) => Err(Error::RegularSourcemap),
DecodedMap::Index(smi) => Ok(smi),
_ => Err(Error::RegularSourcemap),
Swatinem marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down Expand Up @@ -889,6 +895,7 @@ impl SourceMapIndex {
Some(map) => match map {
DecodedMap::Regular(sm) => Cow::Borrowed(sm),
DecodedMap::Index(idx) => Cow::Owned(idx.flatten()?),
DecodedMap::Hermes(smh) => Cow::Borrowed(&smh.sm),
},
None => {
return Err(Error::CannotFlatten(format!(
Expand Down
Loading