use phf::phf_set;
use swc_atoms::Atom;
use swc_common::Spanned;
use swc_ecma_ast::{
ArrayLit, Expr, ExprOrSpread, IdentName, Lit, MemberExpr, MemberProp, ObjectLit, Prop,
PropOrSpread, SeqExpr, Str,
};
use swc_ecma_utils::{
number::{parse_canonical_index, ToJsString},
prop_name_eq, ExprExt, Known,
};
use super::Pure;
use crate::compress::pure::Ctx;
static ARRAY_SYMBOLS: phf::Set<&str> = phf_set!(
"constructor",
"length",
"at",
"concat",
"copyWithin",
"entries",
"every",
"fill",
"filter",
"find",
"findIndex",
"findLast",
"findLastIndex",
"flat",
"flatMap",
"forEach",
"includes",
"indexOf",
"join",
"keys",
"lastIndexOf",
"map",
"pop",
"push",
"reduce",
"reduceRight",
"reverse",
"shift",
"slice",
"some",
"sort",
"splice",
"toLocaleString",
"toReversed",
"toSorted",
"toSpliced",
"toString",
"unshift",
"values",
"with"
);
static STRING_SYMBOLS: phf::Set<&str> = phf_set!(
"constructor",
"length",
"anchor",
"at",
"big",
"blink",
"bold",
"charAt",
"charCodeAt",
"codePointAt",
"concat",
"endsWith",
"fixed",
"fontcolor",
"fontsize",
"includes",
"indexOf",
"isWellFormed",
"italics",
"lastIndexOf",
"link",
"localeCompare",
"match",
"matchAll",
"normalize",
"padEnd",
"padStart",
"repeat",
"replace",
"replaceAll",
"search",
"slice",
"small",
"split",
"startsWith",
"strike",
"sub",
"substr",
"substring",
"sup",
"toLocaleLowerCase",
"toLocaleUpperCase",
"toLowerCase",
"toString",
"toUpperCase",
"toWellFormed",
"trim",
"trimEnd",
"trimStart",
"valueOf"
);
static OBJECT_SYMBOLS: phf::Set<&str> = phf_set!(
"constructor",
"__proto__",
"__defineGetter__",
"__defineSetter__",
"__lookupGetter__",
"__lookupSetter__",
"hasOwnProperty",
"isPrototypeOf",
"propertyIsEnumerable",
"toLocaleString",
"toString",
"valueOf",
"watch",
"unwatch"
);
fn is_object_symbol(sym: &str) -> bool {
OBJECT_SYMBOLS.contains(sym)
}
fn is_array_symbol(sym: &str) -> bool {
ARRAY_SYMBOLS.contains(sym) || is_object_symbol(sym)
}
fn is_string_symbol(sym: &str) -> bool {
STRING_SYMBOLS.contains(sym) || is_object_symbol(sym)
}
struct KnownMemberKey {
property_key: Atom,
index: Option<usize>,
}
impl KnownMemberKey {
fn from_number(value: f64) -> Self {
Self::from_property_key(Atom::from(value.to_js_string()))
}
fn from_property_key(property_key: Atom) -> Self {
let index = parse_canonical_index(property_key.as_str());
Self {
property_key,
index,
}
}
}
fn does_key_exist(key: &str, props: &Vec<PropOrSpread>) -> Option<bool> {
for prop in props {
match prop {
PropOrSpread::Prop(prop) => match &**prop {
Prop::Shorthand(ident) => {
if ident.sym == key {
return Some(true);
}
}
Prop::KeyValue(prop) => {
if key != "__proto__" && prop_name_eq(&prop.key, "__proto__") {
if let Some(object) = prop.value.as_object() {
let exists = does_key_exist(key, &object.props);
if exists.is_none() {
return None;
} else if exists.is_some_and(|exists| exists) {
return Some(true);
}
} else {
return None;
}
} else {
if prop_name_eq(&prop.key, key) {
return Some(true);
}
}
}
Prop::Assign(_) => {
return None;
}
Prop::Getter(getter) => {
if prop_name_eq(&getter.key, key) {
return Some(true);
}
}
Prop::Setter(setter) => {
if prop_name_eq(&setter.key, key) {
return Some(true);
}
}
Prop::Method(method) => {
if prop_name_eq(&method.key, key) {
return Some(true);
}
}
#[cfg(swc_ast_unknown)]
_ => panic!("unable to access unknown nodes"),
},
_ => {
return None;
}
}
}
Some(false)
}
impl Pure<'_> {
pub(super) fn optimize_member_expr(
&mut self,
obj: &mut Expr,
prop: &MemberProp,
) -> Option<Expr> {
if !self.options.pristine_globals
|| self
.ctx
.intersects(Ctx::IS_CALLEE.union(Ctx::IS_LHS_OF_ASSIGN))
{
return None;
}
let op = match prop {
MemberProp::Ident(IdentName { sym, .. }) => {
KnownMemberKey::from_property_key(sym.clone())
}
MemberProp::Computed(c) => match &*c.expr {
Expr::Lit(Lit::Num(n)) => KnownMemberKey::from_number(n.value),
Expr::Ident(..) => {
return None;
}
_ => {
let Known(s) = c.expr.as_pure_string(self.expr_ctx) else {
return None;
};
KnownMemberKey::from_property_key(Atom::from(s))
}
},
_ => {
return None;
}
};
match obj {
Expr::Seq(SeqExpr { exprs, span }) => {
let replacement = self.optimize_member_expr(exprs.last_mut()?, prop)?;
let mut exprs: Vec<Box<Expr>> = exprs.drain(..(exprs.len() - 1)).collect();
exprs.push(Box::new(replacement));
Some(SeqExpr { span: *span, exprs }.into())
}
Expr::Lit(Lit::Str(Str { value, span, .. })) => {
match op.index {
Some(index) => {
let is_out_of_bounds = index >= value.len()
|| value.to_ill_formed_utf16().nth(index).is_none();
if is_out_of_bounds {
Some(*Expr::undefined(*span))
} else {
None
}
}
None => {
let key = op.property_key;
if key == "length" {
return None;
}
if is_string_symbol(key.as_str()) {
None
} else {
Some(*Expr::undefined(*span))
}
}
}
}
Expr::Array(ArrayLit { elems, span, .. }) => {
let has_spread = elems.iter().any(|elem| {
elem.as_ref()
.map(|elem| elem.spread.is_some())
.unwrap_or(false)
});
if has_spread {
return None;
}
match op.index {
Some(index) => {
if index < elems.len() {
return None;
}
let mut exprs = Vec::new();
elems.drain(..).flatten().for_each(|elem| {
self.expr_ctx.extract_side_effects_to(&mut exprs, *elem.expr);
});
Some(if exprs.is_empty() {
SeqExpr {
span: *span,
exprs: vec![0.into(), Expr::undefined(*span)]
}.into()
} else {
exprs.push(Expr::undefined(*span));
SeqExpr {
span: *span,
exprs
}.into()
})
}
None if op.property_key != "length" => {
let key = op.property_key;
let is_known_symbol = is_array_symbol(&key);
if is_known_symbol {
let optimized_len = elems
.iter()
.flatten()
.filter(|elem| elem.expr.may_have_side_effects(self.expr_ctx))
.count();
if optimized_len == elems.len() {
return None;
}
}
let mut exprs = Vec::new();
elems.drain(..).flatten().for_each(|elem| {
self.expr_ctx.extract_side_effects_to(&mut exprs, *elem.expr);
});
Some(if is_known_symbol {
MemberExpr {
span: *span,
obj: ArrayLit {
span: *span,
elems: exprs
.into_iter()
.map(|elem| Some(ExprOrSpread {
spread: None,
expr: elem,
}))
.collect()
}.into(),
prop: prop.clone(),
}.into()
} else {
let val = Expr::undefined(
*span);
if exprs.is_empty() {
SeqExpr {
span: val.span(),
exprs: vec![0.into(), val]
}.into()
} else {
exprs.push(val);
SeqExpr {
span: *span,
exprs
}.into()
}
})
}
_ => None
}
}
Expr::Object(ObjectLit { props, span }) => {
let contains_invalid_key = props
.iter()
.any(|prop| !matches!(prop, PropOrSpread::Prop(prop) if matches!(&**prop, Prop::KeyValue(kv) if kv.key.is_ident() || kv.key.is_str() || kv.key.is_num())));
if contains_invalid_key {
return None;
}
let key = op.property_key;
if key == "yield" {
return None;
}
let exists = does_key_exist(&key, props);
if exists.is_none() || exists.is_some_and(|exists| exists) {
return None;
}
let is_known_symbol = is_object_symbol(&key);
if is_known_symbol {
let optimized_len = props
.iter()
.filter(|prop| {
matches!(prop, PropOrSpread::Prop(prop) if matches!(&**prop, Prop::KeyValue(prop) if prop.value.may_have_side_effects(self.expr_ctx)))
})
.count();
if optimized_len == props.len() {
return None;
}
}
Some(*self.expr_ctx.preserve_effects(
*span,
if is_known_symbol {
MemberExpr {
span: *span,
obj: ObjectLit {
span: *span,
props: Vec::new(),
}
.into(),
prop: MemberProp::Ident(IdentName::new(key, *span)),
}
.into()
} else {
Expr::undefined(*span)
},
props.drain(..).map(|x| match x {
PropOrSpread::Prop(prop) => match *prop {
Prop::KeyValue(kv) => kv.value,
_ => unreachable!(),
},
_ => unreachable!(),
}),
))
}
_ => None,
}
}
}