Skip to content

Commit

Permalink
fix(es/minifier): Bailout Regex optimization when facing invalid flags
Browse files Browse the repository at this point in the history
  • Loading branch information
Austaras committed Mar 6, 2023
1 parent 2a8f6ae commit 76aff8d
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 53 deletions.
107 changes: 56 additions & 51 deletions crates/swc_ecma_minifier/src/compress/pure/misc.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{fmt::Write, iter::once, num::FpCategory};

use swc_atoms::js_word;
use rustc_hash::FxHashSet;
use swc_atoms::{js_word, JsWord};
use swc_common::{iter::IdentifyLast, util::take::Take, Span, DUMMY_SP};
use swc_ecma_ast::*;
use swc_ecma_transforms_optimization::debug_assert_valid;
Expand Down Expand Up @@ -247,76 +248,80 @@ impl Pure<'_> {

/// `new RegExp("([Sap]+)", "ig")` => `/([Sap]+)/gi`
fn optimize_regex(&mut self, args: &mut Vec<ExprOrSpread>, span: &mut Span) -> Option<Expr> {
if args.is_empty() || args.len() > 2 {
return None;
fn valid_pattern(pattern: &Expr) -> Option<JsWord> {
if let Expr::Lit(Lit::Str(s)) = pattern {
if s.value.contains(|c: char| {
// whitelist
!c.is_ascii_alphanumeric()
&& !matches!(c, '$' | '[' | ']' | '(' | ')' | '{' | '}' | '-' | '+' | '_')
}) {
return None;
}
if s.value.contains("\\\0") || s.value.contains('/') {
return None;
}
Some(s.value.clone())
} else {
None
}
}
fn valid_flag(flag: &Expr, es_version: EsVersion) -> Option<JsWord> {
if let Expr::Lit(Lit::Str(s)) = flag {
let mut set = FxHashSet::default();
for c in s.value.chars() {
if !(matches!(c, 'g' | 'i' | 'm')
|| (es_version >= EsVersion::Es2015 && matches!(c, 'u' | 'y'))
|| (es_version >= EsVersion::Es2018 && matches!(c, 's')))
|| (es_version >= EsVersion::Es2022 && matches!(c, 'd'))
{
return None;
}

// We aborts the method if arguments are not literals.
if args.iter().any(|v| {
v.spread.is_some()
|| match &*v.expr {
Expr::Lit(Lit::Str(s)) => {
if s.value.contains(|c: char| {
// whitelist
!c.is_ascii_alphanumeric()
&& !matches!(c, '%' | '[' | ']' | '(' | ')' | '{' | '}' | '-' | '+')
}) {
return true;
}
if s.value.contains("\\\0") || s.value.contains('/') {
return true;
}

false
if !set.insert(c) {
return None;
}
_ => true,
}
}) {
return None;
}

let pattern = args[0].expr.take();

let pattern = match *pattern {
Expr::Lit(Lit::Str(s)) => s.value,
_ => {
unreachable!()
Some(s.value.clone())
} else {
None
}
}

let (pattern, flag) = match args.as_slice() {
[ExprOrSpread { spread: None, expr }] => (valid_pattern(expr)?, "".into()),
[ExprOrSpread {
spread: None,
expr: pattern,
}, ExprOrSpread {
spread: None,
expr: flag,
}] => (
valid_pattern(pattern)?,
valid_flag(flag, self.options.ecma)?,
),
_ => return None,
};

if pattern.is_empty() {
// For some expressions `RegExp()` and `RegExp("")`
// Theoretically we can use `/(?:)/` to achieve shorter code
// But some browsers released in 2015 don't support them yet.
args[0].expr = pattern.into();
return None;
}

let flags = args
.get_mut(1)
.map(|v| v.expr.take())
.map(|v| match *v {
Expr::Lit(Lit::Str(s)) => {
assert!(s.value.is_ascii());

let s = s.value.to_string();
let mut bytes = s.into_bytes();
bytes.sort_unstable();

String::from_utf8(bytes).unwrap().into()
}
_ => {
unreachable!()
}
})
.unwrap_or_default();

report_change!("Optimized regex");

Some(Expr::Lit(Lit::Regex(Regex {
span: *span,
exp: pattern.into(),
flags,
flags: {
let flag = flag.to_string();
let mut bytes = flag.into_bytes();
bytes.sort_unstable();

String::from_utf8(bytes).unwrap().into()
},
})))
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
bar(RegExp(""));
bar(RegExp("", "u"));
bar(/a/);
bar(/a/u);
bar(RegExp("a", "u"));
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/bar/gi;
RegExp(foo);
RegExp("bar", ig);
/should/afil;
RegExp("should", "fail");

0 comments on commit 76aff8d

Please sign in to comment.