Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions crates/oxc_ast/src/ast/literal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,18 +159,22 @@ pub struct RegExp<'a> {
/// This pattern may or may not be parsed.
#[ast]
#[derive(Debug)]
#[generate_derive(CloneIn, Dummy, TakeIn, ESTree)]
#[estree(via = RegExpPatternConverter)]
pub enum RegExpPattern<'a> {
/// Unparsed pattern. Contains string slice of the pattern.
/// Pattern was not parsed, so may be valid or invalid.
Raw(&'a str) = 0,
/// An invalid pattern. Contains string slice of the pattern.
/// Pattern was parsed and found to be invalid.
Invalid(&'a str) = 1,
/// A parsed pattern. Read [Pattern] for more details.
/// Pattern was parsed and found to be valid.
Pattern(Box<'a, Pattern<'a>>) = 2,
#[generate_derive(CloneIn, Dummy, TakeIn, ContentEq, ESTree)]
#[estree(no_type, flatten)]
pub struct RegExpPattern<'a> {
/// The regexp's pattern as a string.
///
/// If `pattern` is defined, `pattern` and `text` must be in sync.
/// i.e. If you alter the regexp by mutating `pattern`, you must regenerate `text` to match it,
/// using `format_atom!("{}", &pattern)`.
///
/// `oxc_codegen` ignores `pattern` field, and prints `text`.
#[estree(rename = "pattern")]
pub text: Atom<'a>,
/// Parsed regexp pattern
#[content_eq(skip)]
#[estree(skip)]
pub pattern: Option<Box<'a, Pattern<'a>>>,
}

bitflags! {
Expand Down
73 changes: 1 addition & 72 deletions crates/oxc_ast/src/ast_impl/literal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use std::{

use oxc_allocator::{Allocator, CloneIn, Dummy};
use oxc_data_structures::inline_string::InlineString;
use oxc_regular_expression::ast::Pattern;
use oxc_span::ContentEq;

use crate::ast::*;
Expand Down Expand Up @@ -133,77 +132,7 @@ impl Display for BigIntLiteral<'_> {

impl Display for RegExp<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "/{}/{}", self.pattern, self.flags)
}
}

impl<'a> RegExpPattern<'a> {
/// Returns the number of characters in the pattern.
pub fn len(&self) -> usize {
match self {
Self::Raw(it) | Self::Invalid(it) => it.len(),
Self::Pattern(it) => it.span.size() as usize,
}
}

/// Returns `true` if the pattern is empty (i.e. has a
/// [len](RegExpPattern::len) of `0`).
pub fn is_empty(&self) -> bool {
self.len() == 0
}

/// Returns the string as this regular expression would appear in source code.
pub fn source_text(&self, source_text: &'a str) -> Cow<str> {
match self {
Self::Raw(raw) | Self::Invalid(raw) => Cow::Borrowed(raw),
Self::Pattern(pat) if pat.span.is_unspanned() => Cow::Owned(pat.to_string()),
Self::Pattern(pat) => Cow::Borrowed(pat.span.source_text(source_text)),
}
}

/// # Panics
/// If `self` is anything but `RegExpPattern::Pattern`.
pub fn require_pattern(&self) -> &Pattern<'a> {
if let Some(it) = self.as_pattern() {
it
} else {
unreachable!(
"Required `{}` to be `{}`",
stringify!(RegExpPattern),
stringify!(Pattern)
);
}
}

/// Flatten this regular expression into a compiled [`Pattern`], returning
/// [`None`] if the pattern is invalid or not parsed.
pub fn as_pattern(&self) -> Option<&Pattern<'a>> {
if let Self::Pattern(it) = self { Some(it.as_ref()) } else { None }
}
}

impl ContentEq for RegExpPattern<'_> {
fn content_eq(&self, other: &Self) -> bool {
let self_str = match self {
Self::Raw(s) | Self::Invalid(s) => *s,
Self::Pattern(p) => &p.to_string(),
};

let other_str = match other {
Self::Raw(s) | Self::Invalid(s) => *s,
Self::Pattern(p) => &p.to_string(),
};

self_str == other_str
}
}

impl Display for RegExpPattern<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Raw(it) | Self::Invalid(it) => it.fmt(f),
Self::Pattern(it) => it.fmt(f),
}
write!(f, "/{}/{}", self.pattern.text, self.flags)
}
}

Expand Down
4 changes: 4 additions & 0 deletions crates/oxc_ast/src/generated/assert_layouts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,8 @@ const _: () = {

assert!(size_of::<RegExpPattern>() == 24);
assert!(align_of::<RegExpPattern>() == 8);
assert!(offset_of!(RegExpPattern, text) == 0);
assert!(offset_of!(RegExpPattern, pattern) == 16);

assert!(size_of::<RegExpFlags>() == 1);
assert!(align_of::<RegExpFlags>() == 1);
Expand Down Expand Up @@ -2189,6 +2191,8 @@ const _: () = {

assert!(size_of::<RegExpPattern>() == 12);
assert!(align_of::<RegExpPattern>() == 4);
assert!(offset_of!(RegExpPattern, text) == 0);
assert!(offset_of!(RegExpPattern, pattern) == 8);

assert!(size_of::<RegExpFlags>() == 1);
assert!(align_of::<RegExpFlags>() == 1);
Expand Down
18 changes: 6 additions & 12 deletions crates/oxc_ast/src/generated/derive_clone_in.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4917,22 +4917,16 @@ impl<'new_alloc> CloneIn<'new_alloc> for RegExpPattern<'_> {
type Cloned = RegExpPattern<'new_alloc>;

fn clone_in(&self, allocator: &'new_alloc Allocator) -> Self::Cloned {
match self {
Self::Raw(it) => RegExpPattern::Raw(CloneIn::clone_in(it, allocator)),
Self::Invalid(it) => RegExpPattern::Invalid(CloneIn::clone_in(it, allocator)),
Self::Pattern(it) => RegExpPattern::Pattern(CloneIn::clone_in(it, allocator)),
RegExpPattern {
text: CloneIn::clone_in(&self.text, allocator),
pattern: CloneIn::clone_in(&self.pattern, allocator),
}
}

fn clone_in_with_semantic_ids(&self, allocator: &'new_alloc Allocator) -> Self::Cloned {
match self {
Self::Raw(it) => RegExpPattern::Raw(CloneIn::clone_in_with_semantic_ids(it, allocator)),
Self::Invalid(it) => {
RegExpPattern::Invalid(CloneIn::clone_in_with_semantic_ids(it, allocator))
}
Self::Pattern(it) => {
RegExpPattern::Pattern(CloneIn::clone_in_with_semantic_ids(it, allocator))
}
RegExpPattern {
text: CloneIn::clone_in_with_semantic_ids(&self.text, allocator),
pattern: CloneIn::clone_in_with_semantic_ids(&self.pattern, allocator),
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions crates/oxc_ast/src/generated/derive_content_eq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1496,6 +1496,12 @@ impl ContentEq for RegExp<'_> {
}
}

impl ContentEq for RegExpPattern<'_> {
fn content_eq(&self, other: &Self) -> bool {
ContentEq::content_eq(&self.text, &other.text)
}
}

impl ContentEq for JSXElement<'_> {
fn content_eq(&self, other: &Self) -> bool {
ContentEq::content_eq(&self.opening_element, &other.opening_element)
Expand Down
2 changes: 1 addition & 1 deletion crates/oxc_ast/src/generated/derive_dummy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1635,7 +1635,7 @@ impl<'a> Dummy<'a> for RegExpPattern<'a> {
///
/// Does not allocate any data into arena.
fn dummy(allocator: &'a Allocator) -> Self {
Self::Raw(Dummy::dummy(allocator))
Self { text: Dummy::dummy(allocator), pattern: Dummy::dummy(allocator) }
}
}

Expand Down
6 changes: 4 additions & 2 deletions crates/oxc_ast/src/generated/derive_estree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1973,15 +1973,17 @@ impl ESTree for RegExpLiteral<'_> {
impl ESTree for RegExp<'_> {
fn serialize<S: Serializer>(&self, serializer: S) {
let mut state = serializer.serialize_struct();
state.serialize_field("pattern", &self.pattern);
state.serialize_field("pattern", &self.pattern.text);
state.serialize_field("flags", &self.flags);
state.end();
}
}

impl ESTree for RegExpPattern<'_> {
fn serialize<S: Serializer>(&self, serializer: S) {
crate::serialize::RegExpPatternConverter(self).serialize(serializer)
let mut state = serializer.serialize_struct();
state.serialize_field("pattern", &self.text);
state.end();
}
}

Expand Down
10 changes: 0 additions & 10 deletions crates/oxc_ast/src/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -470,16 +470,6 @@ impl ESTree for RegExpLiteralValue<'_, '_> {
}
}

#[ast_meta]
#[estree(ts_type = "string")]
pub struct RegExpPatternConverter<'a, 'b>(pub &'b RegExpPattern<'a>);

impl ESTree for RegExpPatternConverter<'_, '_> {
fn serialize<S: Serializer>(&self, serializer: S) {
self.0.to_string().serialize(serializer);
}
}

#[ast_meta]
#[estree(
ts_type = "string",
Expand Down
18 changes: 10 additions & 8 deletions crates/oxc_codegen/src/gen.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::borrow::Cow;
use std::ops::Not;

use cow_utils::CowUtils;

use oxc_ast::ast::*;
use oxc_span::GetSpan;
use oxc_syntax::{
Expand Down Expand Up @@ -1353,19 +1353,21 @@ impl GenExpr for BigIntLiteral<'_> {
impl Gen for RegExpLiteral<'_> {
fn r#gen(&self, p: &mut Codegen, _ctx: Context) {
p.add_source_mapping(self.span);
let last = p.last_byte();
let pattern_text = p.source_text.map_or_else(
|| Cow::Owned(self.regex.pattern.to_string()),
|src| self.regex.pattern.source_text(src),
);
// Avoid forming a single-line comment or "</script" sequence
let last = p.last_byte();
if last == Some(b'/')
|| (last == Some(b'<') && pattern_text.cow_to_ascii_lowercase().starts_with("script"))
|| (last == Some(b'<')
&& self
.regex
.pattern
.text
.get(..6)
.is_some_and(|first_six| first_six.cow_to_ascii_lowercase() == "script"))
{
p.print_hard_space();
}
p.print_ascii_byte(b'/');
p.print_str(pattern_text.as_ref());
p.print_str(self.regex.pattern.text.as_str());
p.print_ascii_byte(b'/');
p.print_str(self.regex.flags.to_inline_string().as_str());
p.prev_reg_exp_end = p.code().len();
Expand Down
4 changes: 1 addition & 3 deletions crates/oxc_linter/src/ast_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -786,9 +786,7 @@ pub fn get_static_property_name<'a>(parent_node: &AstNode<'a>) -> Option<Cow<'a,
}

match key {
PropertyKey::RegExpLiteral(regex) => {
Some(Cow::Owned(format!("/{}/{}", regex.regex.pattern, regex.regex.flags)))
}
PropertyKey::RegExpLiteral(regex) => Some(Cow::Owned(regex.regex.to_string())),
PropertyKey::BigIntLiteral(bigint) => Some(Cow::Borrowed(bigint.raw.as_str())),
PropertyKey::TemplateLiteral(template) => {
if template.expressions.is_empty() && template.quasis.len() == 1 {
Expand Down
2 changes: 1 addition & 1 deletion crates/oxc_linter/src/rules/eslint/no_div_regex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ declare_oxc_lint!(
impl Rule for NoDivRegex {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
if let AstKind::RegExpLiteral(lit) = node.kind() {
let Some(pattern) = lit.regex.pattern.as_pattern() else { return };
let Some(pattern) = &lit.regex.pattern.pattern else { return };
if pattern
.body
.body
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,12 @@ declare_oxc_lint!(
impl Rule for NoEmptyCharacterClass {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
if let AstKind::RegExpLiteral(lit) = node.kind() {
let Some(pattern) = lit.regex.pattern.as_pattern() else {
let Some(pattern) = &lit.regex.pattern.pattern else {
return;
};

// Skip if the pattern doesn't contain a `[` or `]` character
if memchr2(b'[', b']', lit.regex.pattern.source_text(ctx.source_text()).as_bytes())
.is_none()
{
if memchr2(b'[', b']', lit.regex.pattern.text.as_bytes()).is_none() {
return;
}

Expand Down
9 changes: 4 additions & 5 deletions crates/oxc_linter/src/rules/eslint/no_regex_spaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ impl Rule for NoRegexSpaces {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
match node.kind() {
AstKind::RegExpLiteral(lit) => {
if let Some(span) = Self::find_literal_to_report(lit, ctx) {
if let Some(span) = Self::find_literal_to_report(lit) {
ctx.diagnostic(no_regex_spaces_diagnostic(span)); // /a b/
}
}
Expand All @@ -73,14 +73,13 @@ impl Rule for NoRegexSpaces {
}

impl NoRegexSpaces {
fn find_literal_to_report(literal: &RegExpLiteral, ctx: &LintContext) -> Option<Span> {
let pattern_text = literal.regex.pattern.source_text(ctx.source_text());
let pattern_text = pattern_text.as_ref();
fn find_literal_to_report(literal: &RegExpLiteral) -> Option<Span> {
let pattern_text = literal.regex.pattern.text.as_str();
if !Self::has_double_space(pattern_text) {
return None;
}

let pattern = literal.regex.pattern.as_pattern()?;
let pattern = literal.regex.pattern.pattern.as_deref()?;
find_consecutive_spaces(pattern)
}

Expand Down
4 changes: 2 additions & 2 deletions crates/oxc_linter/src/rules/eslint/no_useless_escape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ impl Rule for NoUselessEscape {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
match node.kind() {
AstKind::RegExpLiteral(literal)
if literal.regex.pattern.len() + literal.regex.flags.iter().count()
if literal.regex.pattern.text.len() + literal.regex.flags.iter().count()
!= literal.span.size() as usize =>
{
if let Some(pattern) = literal.regex.pattern.as_pattern() {
if let Some(pattern) = &literal.regex.pattern.pattern {
let mut finder = UselessEscapeFinder {
useless_escape_spans: vec![],
character_classes: vec![],
Expand Down
2 changes: 1 addition & 1 deletion crates/oxc_linter/src/rules/unicorn/no_hex_escape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ impl Rule for NoHexEscape {
});
}
AstKind::RegExpLiteral(regex) => {
let Some(pattern) = regex.regex.pattern.as_pattern() else {
let Some(pattern) = &regex.regex.pattern.pattern else {
return;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ impl Rule for PreferStringReplaceAll {
let pattern = &call_expr.arguments[0];
match method_name_str {
"replaceAll" => {
if let Some(k) = get_pattern_replacement(pattern, ctx) {
if let Some(k) = get_pattern_replacement(pattern) {
ctx.diagnostic_with_fix(string_literal(pattern.span(), &k), |fixer| {
// foo.replaceAll(/hello world/g, bar) => foo.replaceAll("hello world", bar)
fixer.replace(pattern.span(), format!("{k:?}"))
Expand Down Expand Up @@ -115,10 +115,7 @@ fn is_reg_exp_with_global_flag<'a>(expr: &'a Argument<'a>) -> bool {
false
}

fn get_pattern_replacement<'a>(
expr: &'a Argument<'a>,
ctx: &LintContext<'a>,
) -> Option<CompactStr> {
fn get_pattern_replacement<'a>(expr: &'a Argument<'a>) -> Option<CompactStr> {
let Argument::RegExpLiteral(reg_exp_literal) = expr else {
return None;
};
Expand All @@ -130,7 +127,8 @@ fn get_pattern_replacement<'a>(
let pattern_terms = reg_exp_literal
.regex
.pattern
.as_pattern()
.pattern
.as_deref()
.filter(|pattern| pattern.body.body.len() == 1)
.and_then(|pattern| pattern.body.body.first().map(|it| &it.body))?;
let is_simple_string = pattern_terms.iter().all(|term| matches!(term, Term::Character(_)));
Expand All @@ -139,10 +137,7 @@ fn get_pattern_replacement<'a>(
return None;
}

let pattern_text = reg_exp_literal.regex.pattern.source_text(ctx.source_text());
let pattern_text = pattern_text.as_ref();

Some(CompactStr::new(pattern_text))
Some(CompactStr::new(reg_exp_literal.regex.pattern.text.as_str()))
}

#[test]
Expand Down
Loading
Loading