From 84d974b9f7328b3397334c9d98a829805ac83ce1 Mon Sep 17 00:00:00 2001 From: Cam McHenry Date: Wed, 28 Oct 2026 22:28:44 -0400 Subject: [PATCH] perf(linter/unicorn/prefer-string-starts-ends-with): narrow AST dispatch (#28350) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Start from regex literals and preserve member-call wrappers, optional calls, and fixes. Reduce fixture callbacks from 62,606 to 209. --- Stack created with GitHub Stacks CLI • Give Feedback 💬 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/generated/rule_runner_impls.rs | 2 +- .../unicorn/prefer_string_starts_ends_with.rs | 45 ++++++++++++------- .../track_linter_timings/linter_timings.snap | 2 +- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index f22c5f63f1..32d70649d5 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -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; } diff --git a/crates/oxc_linter/src/rules/unicorn/prefer_string_starts_ends_with.rs b/crates/oxc_linter/src/rules/unicorn/prefer_string_starts_ends_with.rs index a42a5c961c..94bb8a4098 100644 --- a/crates/oxc_linter/src/rules/unicorn/prefer_string_starts_ends_with.rs +++ b/crates/oxc_linter/src/rules/unicorn/prefer_string_starts_ends_with.rs @@ -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')"), diff --git a/tasks/track_linter_timings/linter_timings.snap b/tasks/track_linter_timings/linter_timings.snap index 56194b3feb..028298471d 100644 --- a/tasks/track_linter_timings/linter_timings.snap +++ b/tasks/track_linter_timings/linter_timings.snap @@ -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