perf(linter/unicorn/prefer-string-starts-ends-with): narrow AST dispatch (#28350)

Start from regex literals and preserve member-call wrappers, optional
calls, and fixes. Reduce fixture callbacks from 62,606 to 209.

---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Cam McHenryandCopilot authored and GitHub committed 2026-10-28 22:28:44 -04:00
1 parent 142ef4f385
commit 84d974b9f7
3 files changed
+31 -18

No files matched your search

+1 -1
View File
@@ -3971,7 +3971,7 @@ impl RuleRunner
for crate::rules::unicorn::prefer_string_starts_ends_with::PreferStringStartsEndsWith
{
const NODE_TYPES: Option<&AstTypesBitset> =
Some(&AstTypesBitset::from_types(&[AstType::CallExpression]));
Some(&AstTypesBitset::from_types(&[AstType::RegExpLiteral]));
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run;
}
@@ -1,6 +1,6 @@
use oxc_ast::{
AstKind,
ast::{CallExpression, Expression, MemberExpression, RegExpFlags, RegExpLiteral},
ast::{CallExpression, Expression, RegExpFlags, RegExpLiteral},
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
@@ -9,6 +9,7 @@ use oxc_span::{GetSpan, Span};
use crate::{
AstNode,
ast_util::{iter_outer_expressions, outermost_paren_parent},
context::LintContext,
fixer::{RuleFix, RuleFixer},
rule::Rule,
@@ -62,30 +63,33 @@ declare_oxc_lint!(
impl Rule for PreferStringStartsEndsWith {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let AstKind::CallExpression(call_expr) = node.kind() else {
let AstKind::RegExpLiteral(regex) = node.kind() else {
return;
};
if call_expr.optional {
let Some(member_node) = outermost_paren_parent(node, ctx) else {
return;
};
let AstKind::StaticMemberExpression(member_expr) = member_node.kind() else {
return;
};
if member_expr.property.name != "test" {
return;
}
let Some(member_expr) = call_expr.callee.get_member_expr() else {
let Some(AstKind::CallExpression(call_expr)) =
iter_outer_expressions(ctx.nodes(), member_node.id())
.find(|kind| !matches!(kind, AstKind::ChainExpression(_)))
else {
return;
};
let MemberExpression::StaticMemberExpression(static_member_expr) = &member_expr else {
return;
};
if !matches!(static_member_expr.property.name.as_str(), "test") {
if call_expr.optional
|| call_expr
.callee
.get_member_expr()
.is_none_or(|callee| callee.span() != member_expr.span)
{
return;
}
let Expression::RegExpLiteral(regex) = &member_expr.object().without_parentheses() else {
return;
};
let Some(err_kind) = check_regex(regex) else {
return;
};
@@ -215,6 +219,11 @@ fn test() {
r#"startWith("bar")"#,
r"foo()()",
r"if (foo.match(/^foo/)) {}",
r"obj.test(/^foo/)",
r"fn(/^foo/.test)",
r"new (/^foo/.test)(bar)",
r"/^foo/.test?.(bar)",
r"(/^foo/ as RegExp).test(bar)",
r"if (/^foo/.exec(foo)) {}",
r"/foo/.test(bar)",
r"/^foo$/.test(bar)",
@@ -289,6 +298,10 @@ fn test() {
let fix = vec![
("/^foo/.test(x)", r"x.startsWith('foo')"),
("((/^foo/)).test(x)", r"x.startsWith('foo')"),
("(/^foo/.test)(x)", r"x.startsWith('foo')"),
("(/^foo/?.test)(x)", r"x.startsWith('foo')"),
("(/^foo/.test as Function)(x)", r"x.startsWith('foo')"),
("/foo$/.test(x)", r"x.endsWith('foo')"),
("/^foo/.test(x.y)", r"x.y.startsWith('foo')"),
("/foo$/.test(x.y)", r"x.y.endsWith('foo')"),
@@ -633,7 +633,7 @@ unicorn/prefer-spread | 62606
unicorn/prefer-string-raw | 62460
unicorn/prefer-string-replace-all | 100105
unicorn/prefer-string-slice | 62606
unicorn/prefer-string-starts-ends-with | 62606
unicorn/prefer-string-starts-ends-with | 209
unicorn/prefer-string-trim-start-end | 62606
unicorn/prefer-structured-clone | 62606
unicorn/prefer-ternary | 13190