refactor(linter): Use parsed patterns in unicorn/prefer-string-starts-ends-with (#5949)

- part of https://github.com/oxc-project/oxc/issues/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.
This commit is contained in:
camchenry 2024-09-21 18:15:07 +00:00
parent 3273b64a0f
commit 05f592b834

View file

@ -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::{
@ -146,26 +147,35 @@ 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 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);
}
}
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.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);
}
}
None
}
fn is_simple_string(str: &str) -> bool {
str.chars()
.all(|c| !matches!(c, '^' | '$' | '+' | '[' | '{' | '(' | '\\' | '.' | '?' | '*' | '|'))
}
// `/^#/i` => `true` (the `i` flag is useless)
// `/^foo/i` => `false` (the `i` flag is not useless)
fn is_useless_case_sensitive_regex_flag(pattern_text: &str) -> bool {