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:
Dunqing committed 2026-07-28 03:37:44 +00:00
1 parent 70d6ecedb3
commit fbe8d318ff
6 files changed
+44 -42

No files matched your search

Generated
-1
View File
@@ -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.
-1
View File
@@ -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) = &regex.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);
}
}