diff --git a/crates/oxc_linter/src/rules/eslint/no_regex_spaces.rs b/crates/oxc_linter/src/rules/eslint/no_regex_spaces.rs index e817f7c3c3c5af..ccc5f0592a39b8 100644 --- a/crates/oxc_linter/src/rules/eslint/no_regex_spaces.rs +++ b/crates/oxc_linter/src/rules/eslint/no_regex_spaces.rs @@ -1,3 +1,5 @@ +use aho_corasick::AhoCorasick; +use lazy_static::lazy_static; use oxc_allocator::{Allocator, Vec}; use oxc_ast::{ ast::{Argument, CallExpression, NewExpression, RegExpLiteral}, @@ -6,7 +8,7 @@ use oxc_ast::{ use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; use oxc_regular_expression::{ - ast::{Pattern, Term}, + ast::{Alternative, Disjunction, Pattern, Term}, Parser, ParserOptions, }; use oxc_span::Span; @@ -45,6 +47,11 @@ declare_oxc_lint!( pending // TODO: This is somewhat autofixable, but the fixer does not exist yet. ); +lazy_static! { + static ref DOUBLE_SPACE: AhoCorasick = + AhoCorasick::new([" "]).expect("no-regex-spaces: Unable to build AhoCorasick"); +} + impl Rule for NoRegexSpaces { fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { match node.kind() { @@ -75,7 +82,7 @@ impl NoRegexSpaces { fn find_literal_to_report(literal: &RegExpLiteral, ctx: &LintContext) -> Option { let pattern_text = literal.regex.pattern.source_text(ctx.source_text()); let pattern_text = pattern_text.as_ref(); - if Self::has_exempted_char_class(pattern_text) { + if !Self::has_double_space(pattern_text) { return None; } @@ -93,8 +100,8 @@ impl NoRegexSpaces { let Some(Argument::StringLiteral(pattern)) = args.first() else { return None; }; - if Self::has_exempted_char_class(&pattern.value) { - return None; // skip spaces inside char class, e.g. RegExp('[ ]') + if !Self::has_double_space(&pattern.value) { + return None; } let alloc = Allocator::default(); @@ -109,7 +116,7 @@ impl NoRegexSpaces { fn find_consecutive_spaces(pattern: &Pattern) -> Option { let mut last_space_span: Option = None; let mut in_quantifier = false; - pattern.visit_terms(&mut |term| { + visit_terms(pattern, &mut |term| { if let Term::Quantifier(_) = term { in_quantifier = true; return; @@ -121,7 +128,7 @@ impl NoRegexSpaces { in_quantifier = false; return; } - if ch.value != b' ' as u32 { + if ch.value != u32::from(b' ') { return; } if let Some(ref mut space_span) = last_space_span { @@ -154,23 +161,47 @@ impl NoRegexSpaces { expr.callee.is_specific_id("RegExp") && expr.arguments.len() > 0 } - /// Whether the input has a character class but no consecutive spaces - /// outside the character class. - fn has_exempted_char_class(input: &str) -> bool { - let mut inside_class = false; - - for (i, c) in input.chars().enumerate() { - match c { - '[' => inside_class = true, - ']' => inside_class = false, - ' ' if input.chars().nth(i + 1) == Some(' ') && !inside_class => { - return false; - } - _ => {} + // For skipping if there aren't any consecutive spaces in the source, to avoid reporting cases + // where the space is explicitly escaped, like: `RegExp(' \ ')``. + fn has_double_space(input: &str) -> bool { + DOUBLE_SPACE.is_match(input) + } +} + +/// Calls the given closure on every [`Term`] in the [`Pattern`]. +fn visit_terms<'a, F: FnMut(&'a Term<'a>)>(pattern: &'a Pattern, f: &mut F) { + visit_terms_disjunction(&pattern.body, f); +} + +/// Calls the given closure on every [`Term`] in the [`Disjunction`]. +fn visit_terms_disjunction<'a, F: FnMut(&'a Term<'a>)>(disjunction: &'a Disjunction, f: &mut F) { + for alternative in &disjunction.body { + visit_terms_alternative(alternative, f); + } +} + +/// Calls the given closure on every [`Term`] in the [`Alternative`]. +fn visit_terms_alternative<'a, F: FnMut(&'a Term<'a>)>(alternative: &'a Alternative, f: &mut F) { + for term in &alternative.body { + match term { + Term::LookAroundAssertion(lookaround) => { + f(term); + visit_terms_disjunction(&lookaround.body, f); } + Term::Quantifier(quant) => { + f(term); + f(&quant.body); + } + Term::CapturingGroup(group) => { + f(term); + visit_terms_disjunction(&group.body, f); + } + Term::IgnoreGroup(group) => { + f(term); + visit_terms_disjunction(&group.body, f); + } + _ => f(term), } - - true } }