mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-15 06:39:17 +00:00
fix(minifier): respect targets when folding RegExp source (#24790)
RegExp literal `.source` folding used a hard-coded approximation of ES2015 support. It rejected syntax that configured targets support, such as the `u` flag for ES2015, while allowing newer syntax without checking the configured engines. The same target compatibility mapping already exists in `oxc_ecmascript` for RegExp constructor analysis. Expose one parsed-syntax predicate from that layer and reuse it for `.source` folding. This removes the duplicate policy and the minifier's direct `oxc_regular_expression` dependency. An unspecified target remains conservative, while an explicit supporting target unlocks the fold. Covered by `test_fold_regex_source` with adjacent unsupported and supported targets for `y`, `u`, `s`, `d`, `v`, named groups, Unicode property escapes, lookbehind, duplicate named groups, and pattern modifiers. Verified with `cargo test -p oxc_ecmascript --features side_effects`, `cargo test -p oxc_minifier` (631 passed, 42 ignored), `just minsize` (no minsize or allocation snapshot changes), affected-crate clippy with warnings denied, and `just fmt`. Follow-up to #24712 and [its non-blocking review](https://github.com/oxc-project/oxc/pull/24712#discussion_r3619967432). Implemented with AI assistance (OpenAI Codex and Claude). The contributor remains responsible for reviewing, understanding, and submitting these changes under the repository AI usage policy.
This commit is contained in:
1 parent
70d6ecedb3
commit
fbe8d318ff
6 files changed
+44
-42
No files matched your search
Generated
-1
@@ -2322,7 +2322,6 @@ dependencies = [
|
||||
"oxc_index",
|
||||
"oxc_mangler",
|
||||
"oxc_parser",
|
||||
"oxc_regular_expression",
|
||||
"oxc_semantic",
|
||||
"oxc_sourcemap",
|
||||
"oxc_span",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_compat::ESFeature;
|
||||
use oxc_regular_expression::{
|
||||
LiteralParser, Options, RegexUnsupportedFlags, RegexUnsupportedPatterns,
|
||||
LiteralParser, Options, RegexUnsupportedFlags, RegexUnsupportedPatterns, ast::Pattern,
|
||||
has_unsupported_regular_expression_flags, has_unsupported_regular_expression_pattern,
|
||||
};
|
||||
|
||||
@@ -51,26 +51,32 @@ pub fn is_valid_regexp<'a>(
|
||||
},
|
||||
};
|
||||
|
||||
if has_unsupported_regular_expression_flags(
|
||||
flags.unwrap_or_default(),
|
||||
&unsupported_regexp_flags(ctx),
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if is_regexp_literal {
|
||||
// ES5 throws whenever a RegExp object and flags are both supplied.
|
||||
return flags.is_none()
|
||||
|| supports_es_feature(ctx, ESFeature::ES2015RegExpConstructorCanAlterFlags);
|
||||
return regexp_flags_are_supported(flags.unwrap_or_default(), ctx)
|
||||
&& (flags.is_none()
|
||||
|| supports_es_feature(ctx, ESFeature::ES2015RegExpConstructorCanAlterFlags));
|
||||
}
|
||||
|
||||
let unsupported_patterns = unsupported_regexp_patterns(ctx);
|
||||
|
||||
// The parser performs complete syntax validation beyond the compatibility checks above.
|
||||
let allocator = oxc_allocator::Allocator::default();
|
||||
LiteralParser::new(&allocator, pattern, flags, Options::default()).parse().is_ok_and(
|
||||
|pattern| !has_unsupported_regular_expression_pattern(&pattern, &unsupported_patterns),
|
||||
)
|
||||
LiteralParser::new(&allocator, pattern, flags, Options::default())
|
||||
.parse()
|
||||
.is_ok_and(|pattern| is_regexp_syntax_supported(&pattern, flags.unwrap_or_default(), ctx))
|
||||
}
|
||||
|
||||
/// Whether parsed RegExp syntax is supported by every configured target.
|
||||
pub fn is_regexp_syntax_supported<'a>(
|
||||
pattern: &Pattern<'_>,
|
||||
flags: &str,
|
||||
ctx: &impl MayHaveSideEffectsContext<'a>,
|
||||
) -> bool {
|
||||
regexp_flags_are_supported(flags, ctx)
|
||||
&& !has_unsupported_regular_expression_pattern(pattern, &unsupported_regexp_patterns(ctx))
|
||||
}
|
||||
|
||||
fn regexp_flags_are_supported<'a>(flags: &str, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
|
||||
!has_unsupported_regular_expression_flags(flags, &unsupported_regexp_flags(ctx))
|
||||
}
|
||||
|
||||
fn unsupported_regexp_flags<'a>(ctx: &impl MayHaveSideEffectsContext<'a>) -> RegexUnsupportedFlags {
|
||||
|
||||
@@ -5,7 +5,7 @@ mod pure_function;
|
||||
mod statements;
|
||||
|
||||
pub use context::{MayHaveSideEffectsContext, PropertyReadSideEffects};
|
||||
pub use known_globals::{is_typed_array_constructor, is_valid_regexp};
|
||||
pub use known_globals::{is_regexp_syntax_supported, is_typed_array_constructor, is_valid_regexp};
|
||||
pub use pure_function::is_pure_function;
|
||||
|
||||
/// Returns true if subtree changes application state.
|
||||
|
||||
@@ -29,7 +29,6 @@ oxc_ecmascript = { workspace = true, features = ["constant_evaluation"] }
|
||||
oxc_index = { workspace = true }
|
||||
oxc_mangler = { workspace = true }
|
||||
oxc_parser = { workspace = true }
|
||||
oxc_regular_expression = { workspace = true }
|
||||
oxc_semantic = { workspace = true }
|
||||
oxc_span = { workspace = true }
|
||||
oxc_str = { workspace = true }
|
||||
|
||||
@@ -8,10 +8,7 @@ use oxc_compat::ESFeature;
|
||||
use oxc_ecmascript::{
|
||||
StringCharAt, StringCharAtResult, ToBigInt, ToIntegerIndex,
|
||||
constant_evaluation::{ConstantEvaluation, DetermineValueType},
|
||||
side_effects::MayHaveSideEffects,
|
||||
};
|
||||
use oxc_regular_expression::{
|
||||
RegexUnsupportedPatterns, has_unsupported_regular_expression_pattern,
|
||||
side_effects::{MayHaveSideEffects, is_regexp_syntax_supported},
|
||||
};
|
||||
use oxc_span::SPAN;
|
||||
|
||||
@@ -447,33 +444,16 @@ impl<'a> PeepholeOptimizations {
|
||||
}
|
||||
Expression::RegExpLiteral(regex) => match name {
|
||||
"source" => {
|
||||
const ES2015_UNSUPPORTED_FLAGS: RegExpFlags = RegExpFlags::G
|
||||
.union(RegExpFlags::I)
|
||||
.union(RegExpFlags::M)
|
||||
.union(RegExpFlags::S)
|
||||
.union(RegExpFlags::Y)
|
||||
.complement();
|
||||
const ES2015_UNSUPPORTED_PATTERNS: RegexUnsupportedPatterns =
|
||||
RegexUnsupportedPatterns {
|
||||
look_behind_assertions: true,
|
||||
named_capture_groups: true,
|
||||
duplicate_named_capture_groups: true,
|
||||
unicode_property_escapes: true,
|
||||
pattern_modifiers: true,
|
||||
};
|
||||
|
||||
if regex.regex.pattern.pattern.is_none()
|
||||
&& let Ok(pattern) = regex.parse_pattern(ctx.allocator())
|
||||
{
|
||||
regex.regex.pattern.pattern = Some(ArenaBox::new_in(pattern, ctx));
|
||||
}
|
||||
if let Some(pattern) = ®ex.regex.pattern.pattern
|
||||
// for now, only replace regexes that are supported by ES2015 to preserve the syntax error
|
||||
// we can check whether each feature is supported for the target range to improve this
|
||||
&& regex.regex.flags.intersection(ES2015_UNSUPPORTED_FLAGS).is_empty()
|
||||
&& !has_unsupported_regular_expression_pattern(
|
||||
&& is_regexp_syntax_supported(
|
||||
pattern,
|
||||
&ES2015_UNSUPPORTED_PATTERNS,
|
||||
regex.regex.flags.to_inline_string().as_str(),
|
||||
ctx,
|
||||
)
|
||||
{
|
||||
Some(Expression::new_string_literal(
|
||||
|
||||
@@ -1246,4 +1246,22 @@ fn test_fold_regex_source() {
|
||||
test_same_value("/(/.source"); // this regex is invalid
|
||||
test_value("/\\u{}/.source", "'\\\\u{}'");
|
||||
test_same_value("/\\u{}/u.source"); // this regex is invalid, also u flag is not supported by ES2015
|
||||
|
||||
// Preserve newer RegExp syntax unless every configured target supports it.
|
||||
test_same_value("/a/u.source");
|
||||
for (source, expected, unsupported_target, supported_target) in [
|
||||
("x = /a/y.source", "x = 'a'", "chrome48", "es2015"),
|
||||
("x = /a/u.source", "x = 'a'", "chrome49", "es2015"),
|
||||
("x = /a/s.source", "x = 'a'", "es2017", "es2018"),
|
||||
("x = /a/d.source", "x = 'a'", "es2021", "es2022"),
|
||||
("x = /a/v.source", "x = 'a'", "es2023", "es2024"),
|
||||
("x = /(?<name>a)/.source", "x = '(?<name>a)'", "es2017", "es2018"),
|
||||
(r"x = /\p{Ll}/u.source", r"x = '\\p{Ll}'", "es2017", "es2018"),
|
||||
("x = /(?<=a)b/.source", "x = '(?<=a)b'", "es2017", "es2018"),
|
||||
("x = /(?<name>a)|(?<name>b)/.source", "x = '(?<name>a)|(?<name>b)'", "es2024", "es2025"),
|
||||
("x = /(?i:a)/.source", "x = '(?i:a)'", "es2024", "es2025"),
|
||||
] {
|
||||
test_target(source, source, unsupported_target);
|
||||
test_target(source, expected, supported_target);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user