Skip to content

Commit

Permalink
refactor(linter): Use parsed patterns in `unicorn/prefer-string-start…
Browse files Browse the repository at this point in the history
…s-ends-with` (#5949)

- part of #5416

This change enhances the accuracy of the `prefer_string_starts_ends_with` rule by using the parsed regex patterns for analysis. It allows for more precise detection of patterns that can be replaced with `startsWith()` and `endsWith()` methods, reducing false positives and improving the overall effectiveness of the linter.

### What changed?

- Replaced the simple string-based regex analysis with a more robust AST-based approach.
- Removed the `is_simple_string` function as it's no longer needed.
  • Loading branch information
camchenry committed Sep 21, 2024
1 parent 3273b64 commit 05f592b
Showing 1 changed file with 23 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use oxc_ast::{
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_regular_expression::ast::{BoundaryAssertionKind, Term};
use oxc_span::{GetSpan, Span};

use crate::{
Expand Down Expand Up @@ -146,24 +147,33 @@ fn check_regex(regexp_lit: &RegExpLiteral, pattern_text: &str) -> Option<ErrorKi
return None;
}

if pattern_text.starts_with('^')
&& is_simple_string(&pattern_text[1..regexp_lit.regex.pattern.len()])
{
return Some(ErrorKind::StartsWith);
let alternatives = regexp_lit.regex.pattern.as_pattern().map(|pattern| &pattern.body.body)?;
// Must not be something with multiple alternatives like `/^a|b/`
if alternatives.len() > 1 {
return None;
}
let pattern_terms = alternatives.first().map(|it| &it.body)?;

if pattern_text.ends_with('$')
&& is_simple_string(&pattern_text[0..regexp_lit.regex.pattern.len() - 1])
{
return Some(ErrorKind::EndsWith);
if let Some(Term::BoundaryAssertion(boundary_assert)) = pattern_terms.first() {
if boundary_assert.kind == BoundaryAssertionKind::Start
&& pattern_terms.iter().skip(1).all(|term| matches!(term, Term::Character(_)))
{
return Some(ErrorKind::StartsWith);
}
}

None
}
if let Some(Term::BoundaryAssertion(boundary_assert)) = pattern_terms.last() {
if boundary_assert.kind == BoundaryAssertionKind::End
&& pattern_terms
.iter()
.take(pattern_terms.len() - 1)
.all(|term| matches!(term, Term::Character(_)))
{
return Some(ErrorKind::EndsWith);
}
}

fn is_simple_string(str: &str) -> bool {
str.chars()
.all(|c| !matches!(c, '^' | '$' | '+' | '[' | '{' | '(' | '\\' | '.' | '?' | '*' | '|'))
None
}

// `/^#/i` => `true` (the `i` flag is useless)
Expand Down

0 comments on commit 05f592b

Please sign in to comment.