rewriter -> js

This commit is contained in:
Toshit Chawda 2025-03-16 21:46:47 -07:00
parent 531cc7273c
commit e8f504f0dc
No known key found for this signature in database
GPG key ID: 91480ED99E2B3D9D
13 changed files with 22 additions and 22 deletions

16
rewriter/js/Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "js"
version = "0.1.0"
edition = "2021"
[lints]
workspace = true
[dependencies]
oxc = { workspace = true }
smallvec = "1.14.0"
thiserror = "2.0.12"
[features]
default = ["debug"]
debug = []

26
rewriter/js/src/cfg.rs Normal file
View file

@ -0,0 +1,26 @@
use oxc::allocator::{Allocator, String};
pub struct Config<'alloc, E>
where
E: Fn(&str, &'alloc Allocator) -> String<'alloc>,
{
pub prefix: &'alloc str,
pub sourcetag: &'alloc str,
pub base: &'alloc str,
pub wrapfn: &'alloc str,
pub wrapthisfn: &'alloc str,
pub importfn: &'alloc str,
pub rewritefn: &'alloc str,
pub setrealmfn: &'alloc str,
pub metafn: &'alloc str,
pub pushsourcemapfn: &'alloc str,
/// URL REWRITER IS RESPONSIBLE FOR ADDING BASE
pub urlrewriter: E,
pub capture_errors: bool,
pub scramitize: bool,
pub do_sourcemaps: bool,
pub strict_rewrites: bool,
}

517
rewriter/js/src/changes.rs Normal file
View file

@ -0,0 +1,517 @@
use std::cmp::Ordering;
use oxc::{
allocator::{Allocator, String, Vec},
ast::ast::AssignmentOperator,
span::{format_compact_str, Atom, Span},
};
use smallvec::{smallvec, SmallVec};
use crate::{cfg::Config, RewriterError};
// const STRICTCHECKER: &str = "(function(a){arguments[0]=false;return a})(true)";
const STRICTCHECKER: &str = "(function(){return !this;})()";
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Rewrite<'data> {
/// `(cfg.wrapfn(ident,strictchecker))` | `cfg.wrapfn(ident,strictchecker)`
WrapFn {
span: Span,
wrapped: bool,
},
/// `cfg.setrealmfn({}).ident`
SetRealmFn {
span: Span,
},
/// `cfg.wrapthis(this)`
WrapThisFn {
span: Span,
},
/// `(cfg.importfn("cfg.base"))`
ImportFn {
span: Span,
},
/// `cfg.metafn("cfg.base")`
MetaFn {
span: Span,
},
// dead code only if debug is disabled
#[allow(dead_code)]
/// `$scramerr(name)`
ScramErr {
span: Span,
ident: Atom<'data>,
},
/// `$scramitize(span)`
Scramitize {
span: Span,
},
/// `eval(cfg.rewritefn(inner))`
Eval {
span: Span,
inner: Span,
},
/// `((t)=>$scramjet$tryset(name,"op",t)||(name op t))(rhsspan)`
Assignment {
name: Atom<'data>,
entirespan: Span,
rhsspan: Span,
op: AssignmentOperator,
},
/// `ident,` -> `ident: cfg.wrapfn(ident),`
ShorthandObj {
span: Span,
name: Atom<'data>,
},
SourceTag {
span: Span,
},
// don't use for anything static, only use for stuff like rewriteurl
Replace {
span: Span,
text: String<'data>,
},
Delete {
span: Span,
},
}
impl<'data> Rewrite<'data> {
fn into_inner(self) -> SmallVec<[JsChange<'data>; 4]> {
match self {
Self::WrapFn {
wrapped: extra,
span,
} => {
let start = Span::new(span.start, span.start);
let end = Span::new(span.end, span.end);
smallvec![
JsChange::WrapFnLeft { span: start, extra },
JsChange::WrapFnRight { span: end, extra }
]
}
Self::SetRealmFn { span } => smallvec![JsChange::SetRealmFn { span }],
Self::WrapThisFn { span } => smallvec![
JsChange::WrapThisFn {
span: Span::new(span.start, span.start)
},
JsChange::ClosingParen {
span: Span::new(span.end, span.end),
semi: false,
}
],
Self::ImportFn { span } => smallvec![JsChange::ImportFn { span }],
Self::MetaFn { span } => smallvec![JsChange::MetaFn { span }],
Self::ScramErr { span, ident } => {
smallvec![JsChange::ScramErrFn {
span: Span::new(span.start, span.start),
ident,
},]
}
Self::Scramitize { span } => {
smallvec![
JsChange::ScramitizeFn {
span: Span::new(span.start, span.start)
},
JsChange::ClosingParen {
span: Span::new(span.end, span.end),
semi: false,
}
]
}
Self::Eval { inner, span } => smallvec![
JsChange::EvalRewriteFn {
span: Span::new(span.start, inner.start)
},
JsChange::ReplaceClosingParen {
span: Span::new(inner.end, span.end),
}
],
Self::Assignment {
name,
rhsspan,
op,
entirespan,
} => smallvec![
JsChange::AssignmentLeft {
name,
op,
span: Span::new(entirespan.start, rhsspan.start)
},
JsChange::ReplaceClosingParen {
span: Span::new(rhsspan.end, entirespan.end)
}
],
// maps to insert
Self::ShorthandObj { name, span } => smallvec![JsChange::ShorthandObj {
ident: name,
span: Span::new(span.end, span.end)
}],
// maps to insert
Self::SourceTag { span } => smallvec![JsChange::SourceTag { span }],
Self::Replace { text, span } => smallvec![JsChange::Replace { span, text }],
Self::Delete { span } => smallvec![JsChange::Delete { span }],
}
}
}
enum Change<'a> {
Str(&'a str),
U32(u32),
}
impl<'a> From<&'a str> for Change<'a> {
fn from(value: &'a str) -> Self {
Self::Str(value)
}
}
impl<'a> From<&'a String<'_>> for Change<'a> {
fn from(value: &'a String<'_>) -> Self {
Self::Str(value.as_str())
}
}
impl<'a> From<&'a Atom<'_>> for Change<'a> {
fn from(value: &'a Atom<'_>) -> Self {
Self::Str(value.as_str())
}
}
impl<'a> From<&'a AssignmentOperator> for Change<'a> {
fn from(value: &'a AssignmentOperator) -> Self {
Self::Str(value.as_str())
}
}
impl From<u32> for Change<'static> {
fn from(value: u32) -> Self {
Self::U32(value)
}
}
macro_rules! changes {
[$($change:expr),+] => {
smallvec![$(Change::from($change)),+]
};
}
type Changes<'a> = SmallVec<[Change<'a>; 8]>;
enum JsChangeInner<'a> {
Insert { loc: u32, str: Changes<'a> },
Replace { str: Changes<'a> },
}
#[derive(Debug, PartialEq, Eq)]
enum JsChange<'data> {
/// insert `${cfg.wrapfn}(`
WrapFnLeft { span: Span, extra: bool },
/// insert `,strictchecker)`
WrapFnRight { span: Span, extra: bool },
/// insert `${cfg.setrealmfn}({}).`
SetRealmFn { span: Span },
/// insert `${cfg.wrapthis}(`
WrapThisFn { span: Span },
/// insert `$scramerr(ident);`
ScramErrFn { span: Span, ident: Atom<'data> },
/// insert `$scramitize(`
ScramitizeFn { span: Span },
/// insert `eval(${cfg.rewritefn}(`
EvalRewriteFn { span: Span },
/// insert `: ${cfg.wrapfn}(ident)`
ShorthandObj { span: Span, ident: Atom<'data> },
/// insert scramtag
SourceTag { span: Span },
/// replace span with `${cfg.importfn}`
ImportFn { span: Span },
/// replace span with `${cfg.metafn}("${cfg.base}")`
MetaFn { span: Span },
/// replace span with `((t)=>$scramjet$tryset(${name},"${op}",t)||(${name}${op}t))(`
AssignmentLeft {
span: Span,
name: Atom<'data>,
op: AssignmentOperator,
},
/// replace span with `)`
ReplaceClosingParen { span: Span },
/// insert `)`
ClosingParen { span: Span, semi: bool },
/// replace span with text
Replace { span: Span, text: String<'data> },
/// replace span with ""
Delete { span: Span },
}
impl JsChange<'_> {
fn get_span(&self) -> &Span {
match self {
Self::WrapFnLeft { span, .. }
| Self::WrapFnRight { span, .. }
| Self::SetRealmFn { span }
| Self::WrapThisFn { span }
| Self::ScramErrFn { span, .. }
| Self::ScramitizeFn { span }
| Self::EvalRewriteFn { span }
| Self::ShorthandObj { span, .. }
| Self::SourceTag { span, .. }
| Self::ImportFn { span }
| Self::MetaFn { span }
| Self::AssignmentLeft { span, .. }
| Self::ReplaceClosingParen { span }
| Self::ClosingParen { span, .. }
| Self::Replace { span, .. }
| Self::Delete { span } => span,
}
}
fn to_inner<'alloc, 'change, E>(
&'change self,
cfg: &'change Config<'alloc, E>,
offset: u32,
) -> JsChangeInner<'change>
where
E: Fn(&str, &'alloc Allocator) -> String<'alloc>,
{
match self {
Self::WrapFnLeft { span, extra } => {
if *extra {
JsChangeInner::Insert {
loc: span.start,
str: changes!["(", cfg.wrapfn, "("],
}
} else {
JsChangeInner::Insert {
loc: span.start,
str: changes![cfg.wrapfn, "("],
}
}
}
Self::WrapFnRight { span, extra } => {
if *extra {
JsChangeInner::Insert {
loc: span.start,
str: changes![",", STRICTCHECKER, "))"],
}
} else {
JsChangeInner::Insert {
loc: span.start,
str: changes![",", STRICTCHECKER, ")"],
}
}
}
Self::SetRealmFn { span } => JsChangeInner::Insert {
loc: span.start,
str: changes![cfg.setrealmfn, "({})."],
},
Self::WrapThisFn { span } => JsChangeInner::Insert {
loc: span.start,
str: changes![cfg.wrapthisfn, "("],
},
Self::ScramErrFn { span, ident } => JsChangeInner::Insert {
loc: span.start,
str: changes!["$scramerr(", ident, ");"],
},
Self::ScramitizeFn { span } => JsChangeInner::Insert {
loc: span.start,
str: changes![" $scramitize("],
},
Self::EvalRewriteFn { .. } => JsChangeInner::Replace {
str: changes!["eval(", cfg.rewritefn, "("],
},
Self::ShorthandObj { span, ident } => JsChangeInner::Insert {
loc: span.start,
str: changes![":", cfg.wrapfn, "(", ident, ")"],
},
Self::SourceTag { span } => JsChangeInner::Insert {
loc: span.start,
str: changes!["/*scramtag ", span.start + offset, " ", cfg.sourcetag, "*/"],
},
Self::ImportFn { .. } => JsChangeInner::Replace {
str: changes![cfg.importfn, "(\"", cfg.base, "\","],
},
Self::MetaFn { .. } => JsChangeInner::Replace {
str: changes![cfg.metafn, "(\"", cfg.base, "\")"],
},
Self::AssignmentLeft { name, op, .. } => JsChangeInner::Replace {
str: changes![
"((t)=>$scramjet$tryset(",
name,
",\"",
op,
"\",t)||(",
name,
op,
"t))("
],
},
Self::ReplaceClosingParen { .. } => JsChangeInner::Replace { str: changes![")"] },
Self::ClosingParen { span, semi } => JsChangeInner::Insert {
loc: span.start,
str: if *semi { changes![");"] } else { changes![")"] },
},
Self::Replace { text, .. } => JsChangeInner::Replace {
str: changes![text],
},
Self::Delete { .. } => JsChangeInner::Replace { str: changes![""] },
}
}
}
impl PartialOrd for JsChange<'_> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for JsChange<'_> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match self.get_span().start.cmp(&other.get_span().start) {
Ordering::Equal => match (self, other) {
(Self::ScramErrFn { .. }, _) => Ordering::Less,
(_, Self::ScramErrFn { .. }) => Ordering::Greater,
_ => Ordering::Equal,
},
x => x,
}
}
}
pub(crate) struct JsChangeResult<'alloc> {
pub js: Vec<'alloc, u8>,
pub sourcemap: Vec<'alloc, u8>,
}
pub(crate) struct JsChanges<'alloc: 'data, 'data> {
alloc: &'alloc Allocator,
inner: Vec<'alloc, JsChange<'data>>,
}
impl<'alloc: 'data, 'data> JsChanges<'alloc, 'data> {
pub fn new(alloc: &'alloc Allocator, capacity: usize) -> Self {
Self {
inner: Vec::with_capacity_in(capacity, alloc),
alloc,
}
}
pub fn add(&mut self, rewrite: Rewrite<'data>) {
for change in rewrite.into_inner() {
self.inner.push(change);
}
}
pub fn perform<E>(
&mut self,
js: &str,
cfg: &Config<'alloc, E>,
) -> Result<JsChangeResult<'alloc>, RewriterError>
where
E: Fn(&str, &'alloc Allocator) -> String<'alloc>,
{
// using wrapping adds for perf, 4gb large files is really an edge case
let mut cursor = 0;
let mut offset = 0i32;
let mut buffer = Vec::with_capacity_in(js.len() * 2, self.alloc);
macro_rules! tryget {
($start:ident..$end:ident) => {
js.get($start as usize..$end as usize)
.ok_or_else(|| RewriterError::Oob($start, $end))?
};
}
macro_rules! eval {
($change:expr) => {
match $change {
Change::Str(x) => {
buffer.extend_from_slice(x.as_bytes());
x.len()
}
Change::U32(x) => {
let x = format_compact_str!("{}", x);
buffer.extend_from_slice(x.as_bytes());
x.len()
}
}
};
}
// insert has a 9 byte size, replace has a 13 byte minimum and usually it's like 5 bytes
// for the old str added on so use 16 as a really rough estimate
let mut map = Vec::with_capacity_in(self.inner.len() * 16, self.alloc);
map.extend_from_slice(&(self.inner.len() as u32).to_le_bytes());
self.inner.sort();
for change in &self.inner {
let span = change.get_span();
let start = span.start;
let end = span.end;
buffer.extend_from_slice(tryget!(cursor..start).as_bytes());
match change.to_inner(cfg, cursor) {
JsChangeInner::Insert { loc, str } => {
let mut len = 0u32;
buffer.extend_from_slice(tryget!(start..loc).as_bytes());
for str in &str {
len += eval!(str) as u32;
}
buffer.extend_from_slice(tryget!(loc..end).as_bytes());
// INSERT op
map.push(0);
// pos
map.extend_from_slice(&loc.wrapping_add_signed(offset).to_le_bytes());
// size
map.extend_from_slice(&len.to_le_bytes());
offset = offset.wrapping_add_unsigned(len);
}
JsChangeInner::Replace { str } => {
let mut len = 0u32;
for str in &str {
len += eval!(str) as u32;
}
// REPLACE op
map.push(1);
// len
map.extend_from_slice(&(span.end - span.start).to_le_bytes());
// start
map.extend_from_slice(&(span.start.wrapping_add_signed(offset)).to_le_bytes());
// end
map.extend_from_slice(
&((span.start + len).wrapping_add_signed(offset)).to_le_bytes(),
);
// oldstr
map.extend_from_slice(tryget!(start..end).as_bytes());
let len = i32::try_from(len).map_err(|_| RewriterError::AddedTooLarge)?;
let diff = len.wrapping_sub_unsigned(span.end - span.start);
offset = offset.wrapping_add(diff);
}
}
cursor = end;
}
let js_len = js.len() as u32;
buffer.extend_from_slice(tryget!(cursor..js_len).as_bytes());
Ok(JsChangeResult {
js: buffer,
sourcemap: map,
})
}
}

89
rewriter/js/src/lib.rs Normal file
View file

@ -0,0 +1,89 @@
use oxc::{
allocator::{Allocator, String, Vec},
ast_visit::Visit,
diagnostics::OxcDiagnostic,
parser::{ParseOptions, Parser},
span::SourceType,
};
use thiserror::Error;
pub mod cfg;
pub mod changes;
mod visitor;
use cfg::Config;
use changes::{JsChangeResult, JsChanges};
use visitor::Visitor;
#[derive(Error, Debug)]
pub enum RewriterError {
#[error("oxc panicked in parser: {0}")]
OxcPanicked(std::string::String),
#[error("out of bounds while applying range: {0}..{1})")]
Oob(u32, u32),
#[error("too much code added while applying changes")]
AddedTooLarge,
#[error("formatting error: {0}")]
Formatting(#[from] std::fmt::Error),
}
#[derive(Debug)]
pub struct RewriteResult<'alloc> {
pub js: Vec<'alloc, u8>,
pub sourcemap: Vec<'alloc, u8>,
pub errors: std::vec::Vec<OxcDiagnostic>,
}
pub fn rewrite<'alloc, 'data, E>(
alloc: &'alloc Allocator,
js: &'data str,
config: Config<'alloc, E>,
module: bool,
capacity: usize,
) -> Result<RewriteResult<'alloc>, RewriterError>
where
E: Fn(&str, &'alloc Allocator) -> String<'alloc>,
{
let source_type = SourceType::unambiguous()
.with_javascript(true)
.with_module(module)
.with_standard(true);
let ret = Parser::new(alloc, js, source_type)
.with_options(ParseOptions {
parse_regular_expression: false,
allow_v8_intrinsics: true,
allow_return_outside_function: true,
preserve_parens: true,
})
.parse();
if ret.panicked {
use std::fmt::Write;
let mut errors = std::string::String::new();
for error in ret.errors {
writeln!(errors, "{error}")?;
}
return Err(RewriterError::OxcPanicked(errors));
}
let mut visitor = Visitor {
jschanges: JsChanges::new(alloc, capacity),
config,
alloc,
};
visitor.visit_program(&ret.program);
let Visitor {
mut jschanges,
config,
alloc: _,
} = visitor;
let JsChangeResult { js, sourcemap } = jschanges.perform(js, &config)?;
Ok(RewriteResult {
js,
sourcemap,
errors: ret.errors,
})
}

313
rewriter/js/src/visitor.rs Normal file
View file

@ -0,0 +1,313 @@
use oxc::{
allocator::{Allocator, String},
ast::ast::{
AssignmentExpression, AssignmentTarget, CallExpression, DebuggerStatement,
ExportAllDeclaration, ExportNamedDeclaration, Expression, FunctionBody,
IdentifierReference, ImportDeclaration, ImportExpression, MemberExpression, MetaProperty,
NewExpression, ObjectExpression, ObjectPropertyKind, ReturnStatement, ThisExpression,
UnaryExpression, UnaryOperator, UpdateExpression,
},
ast_visit::{walk, Visit},
span::{Atom, GetSpan, Span},
};
use crate::{
cfg::Config,
changes::{JsChanges, Rewrite},
};
// js MUST not be able to get a reference to any of these because sbx
//
// maybe move this out of this lib?
const UNSAFE_GLOBALS: &[&str] = &[
"window",
"self",
"globalThis",
"this",
"parent",
"top",
"location",
"document",
"eval",
"frames",
];
pub struct Visitor<'alloc, 'data, E>
where
E: Fn(&str, &'alloc Allocator) -> String<'alloc>,
{
pub jschanges: JsChanges<'alloc, 'data>,
pub config: Config<'alloc, E>,
pub alloc: &'alloc Allocator,
}
impl<'alloc, 'data, E> Visitor<'alloc, 'data, E>
where
E: Fn(&str, &'alloc Allocator) -> String<'alloc>,
{
fn rewrite_url(&mut self, url: Atom<'data>) -> oxc::allocator::String<'alloc> {
let mut urlencoded = (self.config.urlrewriter)(&url, self.alloc);
urlencoded.insert_str(0, self.config.prefix);
urlencoded
}
fn rewrite_ident(&mut self, name: &Atom, span: Span) {
if UNSAFE_GLOBALS.contains(&name.as_str()) {
self.jschanges.add(Rewrite::WrapFn {
span,
wrapped: true,
});
}
}
fn walk_member_expression(&mut self, it: &Expression) -> bool {
match it {
Expression::Identifier(s) => {
self.rewrite_ident(&s.name, s.span);
true
}
Expression::StaticMemberExpression(s) => self.walk_member_expression(&s.object),
Expression::ComputedMemberExpression(s) => self.walk_member_expression(&s.object),
_ => false,
}
}
fn scramitize(&mut self, span: Span) {
self.jschanges.add(Rewrite::Scramitize { span });
}
}
impl<'alloc, 'data, E> Visit<'data> for Visitor<'alloc, 'data, E>
where
E: Fn(&str, &'alloc Allocator) -> String<'alloc>,
{
fn visit_identifier_reference(&mut self, it: &IdentifierReference) {
// if self.config.capture_errors {
// self.jschanges.insert(JsChange::GenericChange {
// span: it.span,
// text: format!(
// "{}({}, typeof arguments != 'undefined' && arguments)",
// self.config.wrapfn, it.name
// ),
// });
// } else {
//
if UNSAFE_GLOBALS.contains(&it.name.as_str()) {
self.jschanges.add(Rewrite::WrapFn {
span: it.span,
wrapped: false,
});
}
// }
}
fn visit_new_expression(&mut self, it: &NewExpression<'data>) {
self.walk_member_expression(&it.callee);
walk::walk_arguments(self, &it.arguments);
}
fn visit_member_expression(&mut self, it: &MemberExpression<'data>) {
// TODO
// you could break this with ["postMessage"] etc
// however this code only exists because of recaptcha whatever
// and it would slow down js execution a lot
if let MemberExpression::StaticMemberExpression(s) = it {
if s.property.name == "postMessage" {
self.jschanges.add(Rewrite::SetRealmFn {
span: s.property.span,
});
walk::walk_expression(self, &s.object);
return; // unwise to walk the rest of the tree
}
if !self.config.strict_rewrites && !UNSAFE_GLOBALS.contains(&s.property.name.as_str()) {
if let Expression::Identifier(_) | Expression::ThisExpression(_) = &s.object {
// cull tree - this should be safe
return;
}
}
if self.config.scramitize
&& !matches!(s.object, Expression::MetaProperty(_) | Expression::Super(_))
{
self.scramitize(s.object.span());
}
}
walk::walk_member_expression(self, it);
}
fn visit_this_expression(&mut self, it: &ThisExpression) {
self.jschanges.add(Rewrite::WrapThisFn { span: it.span });
}
fn visit_debugger_statement(&mut self, it: &DebuggerStatement) {
// delete debugger statements entirely. some sites will spam debugger as an anti-debugging measure, and we don't want that!
self.jschanges.add(Rewrite::Delete { span: it.span });
}
// we can't overwrite window.eval in the normal way because that would make everything an
// indirect eval, which could break things. we handle that edge case here
fn visit_call_expression(&mut self, it: &CallExpression<'data>) {
if let Expression::Identifier(s) = &it.callee {
// if it's optional that actually makes it an indirect eval which is handled separately
if s.name == "eval" && !it.optional {
self.jschanges.add(Rewrite::Eval {
span: it.span,
inner: Span::new(s.span.end + 1, it.span.end),
});
// then we walk the arguments, but not the callee, since we want it to resolve to
// the real eval
walk::walk_arguments(self, &it.arguments);
return;
}
}
if self.config.scramitize {
self.scramitize(it.span);
}
walk::walk_call_expression(self, it);
}
fn visit_import_declaration(&mut self, it: &ImportDeclaration<'data>) {
let text = self.rewrite_url(it.source.value);
self.jschanges.add(Rewrite::Replace {
span: it.source.span.shrink(1),
text,
});
walk::walk_import_declaration(self, it);
}
fn visit_import_expression(&mut self, it: &ImportExpression<'data>) {
self.jschanges.add(Rewrite::ImportFn {
span: Span::new(it.span.start, it.span.start + 7),
});
walk::walk_import_expression(self, it);
}
fn visit_export_all_declaration(&mut self, it: &ExportAllDeclaration<'data>) {
let text = self.rewrite_url(it.source.value);
self.jschanges.add(Rewrite::Replace {
span: it.source.span.shrink(1),
text,
});
}
fn visit_export_named_declaration(&mut self, it: &ExportNamedDeclaration<'data>) {
if let Some(source) = &it.source {
let text = self.rewrite_url(source.value);
self.jschanges.add(Rewrite::Replace {
span: source.span.shrink(1),
text,
});
}
// do not walk further, we don't want to rewrite the identifiers
}
#[cfg(feature = "debug")]
fn visit_try_statement(&mut self, it: &oxc::ast::ast::TryStatement<'data>) {
// for debugging we need to know what the error was
if self.config.capture_errors {
if let Some(h) = &it.handler {
if let Some(name) = &h.param {
if let Some(ident) = name.pattern.get_identifier_name() {
self.jschanges.add(Rewrite::ScramErr {
span: Span::new(h.body.span.start + 1, h.body.span.start + 1),
ident,
});
}
}
}
}
walk::walk_try_statement(self, it);
}
fn visit_object_expression(&mut self, it: &ObjectExpression<'data>) {
for prop in &it.properties {
if let ObjectPropertyKind::ObjectProperty(p) = prop {
if let Expression::Identifier(s) = &p.value {
if UNSAFE_GLOBALS.contains(&s.name.to_string().as_str()) && p.shorthand {
self.jschanges.add(Rewrite::ShorthandObj {
span: s.span,
name: s.name,
});
return;
}
}
}
}
walk::walk_object_expression(self, it);
}
fn visit_function_body(&mut self, it: &FunctionBody<'data>) {
// tag function for use in sourcemaps
if self.config.do_sourcemaps {
self.jschanges.add(Rewrite::SourceTag {
span: Span::new(it.span.start, it.span.start),
});
}
walk::walk_function_body(self, it);
}
fn visit_return_statement(&mut self, it: &ReturnStatement<'data>) {
// if let Some(arg) = &it.argument {
// self.jschanges.insert(JsChange::GenericChange {
// span: Span::new(it.span.start + 6, it.span.start + 6),
// text: format!(" $scramdbg((()=>{{ try {{return arguments}} catch(_){{}} }})(),("),
// });
// self.jschanges.insert(JsChange::GenericChange {
// span: Span::new(expression_span(arg).end, expression_span(arg).end),
// text: format!("))"),
// });
// }
walk::walk_return_statement(self, it);
}
fn visit_unary_expression(&mut self, it: &UnaryExpression<'data>) {
if matches!(it.operator, UnaryOperator::Typeof) {
// don't walk to identifier rewrites since it won't matter
return;
}
walk::walk_unary_expression(self, it);
}
fn visit_update_expression(&mut self, _it: &UpdateExpression<'data>) {
// then no, don't walk it, we don't care
}
fn visit_meta_property(&mut self, it: &MetaProperty<'data>) {
if it.meta.name == "import" {
self.jschanges.add(Rewrite::MetaFn { span: it.span });
}
}
fn visit_assignment_expression(&mut self, it: &AssignmentExpression<'data>) {
match &it.left {
AssignmentTarget::AssignmentTargetIdentifier(s) => {
if ["location"].contains(&s.name.to_string().as_str()) {
self.jschanges.add(Rewrite::Assignment {
name: s.name,
entirespan: it.span,
rhsspan: it.right.span(),
op: it.operator,
});
// avoid walking rest of tree, i would need to figure out nested rewrites
// somehow
return;
}
}
AssignmentTarget::ArrayAssignmentTarget(_) => {
// [location] = ["https://example.com"]
// this is such a ridiculously specific edge case. just ignore it
return;
}
_ => {
// only walk the left side if it isn't an identifier, we can't replace the
// identifier with a function obviously
walk::walk_assignment_target(self, &it.left);
}
}
walk::walk_expression(self, &it.right);
}
}