From 73f2cbb247fc6a170e3fb8eb5e563c79e7a0cf46 Mon Sep 17 00:00:00 2001
From: camchenry <1514176+camchenry@users.noreply.github.com>
Date: Thu, 16 Oct 2025 01:57:10 +0000
Subject: [PATCH] perf(linter): support getting `as_member_expression_kind()`
variants (#14642)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Previously, we bailed out on `node.kind().as_member_expression_kind()` diverging `let` statements. Now, we can compute those member expression kinds as they should be relatively constant.
+1-3% on the linter benchmarks, plus we just dropped sub-millisecond on the Radix UI benchmark!
---
.../src/generated/rule_runner_impls.rs | 30 ++++++++--
tasks/linter_codegen/src/let_else_detector.rs | 54 +++++++++--------
tasks/linter_codegen/src/main.rs | 23 +++++++-
.../src/member_expression_kinds.rs | 58 +++++++++++++++++++
tasks/linter_codegen/src/node_type_set.rs | 1 +
5 files changed, 134 insertions(+), 32 deletions(-)
create mode 100644 tasks/linter_codegen/src/member_expression_kinds.rs
diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs
index 2b2302c37426b..84da3d38031bd 100644
--- a/crates/oxc_linter/src/generated/rule_runner_impls.rs
+++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs
@@ -446,7 +446,11 @@ impl RuleRunner for crate::rules::eslint::no_irregular_whitespace::NoIrregularWh
}
impl RuleRunner for crate::rules::eslint::no_iterator::NoIterator {
- const NODE_TYPES: Option<&AstTypesBitset> = None;
+ const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[
+ AstType::ComputedMemberExpression,
+ AstType::PrivateFieldExpression,
+ AstType::StaticMemberExpression,
+ ]));
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run;
}
@@ -572,7 +576,11 @@ impl RuleRunner for crate::rules::eslint::no_plusplus::NoPlusplus {
}
impl RuleRunner for crate::rules::eslint::no_proto::NoProto {
- const NODE_TYPES: Option<&AstTypesBitset> = None;
+ const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[
+ AstType::ComputedMemberExpression,
+ AstType::PrivateFieldExpression,
+ AstType::StaticMemberExpression,
+ ]));
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run;
}
@@ -1201,7 +1209,11 @@ impl RuleRunner for crate::rules::jest::no_confusing_set_timeout::NoConfusingSet
}
impl RuleRunner for crate::rules::jest::no_deprecated_functions::NoDeprecatedFunctions {
- const NODE_TYPES: Option<&AstTypesBitset> = None;
+ const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[
+ AstType::ComputedMemberExpression,
+ AstType::PrivateFieldExpression,
+ AstType::StaticMemberExpression,
+ ]));
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run;
}
@@ -2066,7 +2078,11 @@ impl RuleRunner for crate::rules::promise::prefer_catch::PreferCatch {
}
impl RuleRunner for crate::rules::promise::spec_only::SpecOnly {
- const NODE_TYPES: Option<&AstTypesBitset> = None;
+ const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[
+ AstType::ComputedMemberExpression,
+ AstType::PrivateFieldExpression,
+ AstType::StaticMemberExpression,
+ ]));
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run;
}
@@ -3011,7 +3027,11 @@ impl RuleRunner for crate::rules::unicorn::no_array_sort::NoArraySort {
}
impl RuleRunner for crate::rules::unicorn::no_await_expression_member::NoAwaitExpressionMember {
- const NODE_TYPES: Option<&AstTypesBitset> = None;
+ const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[
+ AstType::ComputedMemberExpression,
+ AstType::PrivateFieldExpression,
+ AstType::StaticMemberExpression,
+ ]));
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run;
}
diff --git a/tasks/linter_codegen/src/let_else_detector.rs b/tasks/linter_codegen/src/let_else_detector.rs
index 9e161f30627d0..3df1a59e61ede 100644
--- a/tasks/linter_codegen/src/let_else_detector.rs
+++ b/tasks/linter_codegen/src/let_else_detector.rs
@@ -1,18 +1,22 @@
use syn::{Expr, Pat, Stmt};
use crate::{
- CollectionResult,
+ CollectionResult, RuleRunnerData,
node_type_set::NodeTypeSet,
utils::{astkind_variant_from_path, is_node_kind_call},
};
/// Detects top-level `let AstKind::... = node.kind() else { return; }` patterns in the `run` method.
-pub struct LetElseDetector {
+pub struct LetElseDetector<'a> {
node_types: NodeTypeSet,
+ rule_runner_data: &'a RuleRunnerData,
}
-impl LetElseDetector {
- pub fn from_run_func(run_func: &syn::ImplItemFn) -> Option {
+impl<'a> LetElseDetector<'a> {
+ pub fn from_run_func(
+ run_func: &syn::ImplItemFn,
+ rule_runner_data: &'a RuleRunnerData,
+ ) -> Option {
// Only consider when the body's first statement is `let AstKind::... = node.kind() else { ... }`,
// and the body of the `else` is just `return`.
let block = &run_func.block;
@@ -37,14 +41,14 @@ impl LetElseDetector {
return None;
}
- let mut detector = Self { node_types: NodeTypeSet::new() };
+ let mut detector = Self { node_types: NodeTypeSet::new(), rule_runner_data };
if is_node_kind_as_call(&init.expr) {
// If the initializer is `node.kind().as_()`, extract that variant.
if let Expr::MethodCall(mc) = &*init.expr
- && let Some(variant) = extract_variant_from_as_call(mc)
+ && let Some(variants) = detector.extract_variants_from_as_call(mc)
{
- detector.node_types.insert(variant);
+ detector.node_types.extend(variants);
}
} else {
// Otherwise, the initializer is `node.kind()`, so extract from the pattern.
@@ -74,6 +78,25 @@ impl LetElseDetector {
_ => CollectionResult::Incomplete,
}
}
+
+ fn extract_variants_from_as_call(&self, mc: &syn::ExprMethodCall) -> Option {
+ // Looking for `node.kind().as_()`
+ let method_ident = mc.method.to_string();
+ if !method_ident.starts_with("as_") || !mc.args.is_empty() {
+ return None;
+ }
+ // Receiver must be `node.kind()`
+ if !is_node_kind_call(&mc.receiver) {
+ return None;
+ }
+ let snake_variant = &method_ident[3..]; // strip `as_`
+ if snake_variant == "member_expression_kind" {
+ return Some(self.rule_runner_data.member_expression_kinds.clone());
+ }
+ let mut node_type_set = NodeTypeSet::new();
+ node_type_set.insert(snake_to_pascal_case(snake_variant));
+ Some(node_type_set)
+ }
}
/// Checks if is `node.kind().as_some_ast_kind()`
@@ -88,23 +111,6 @@ pub fn is_node_kind_as_call(expr: &Expr) -> bool {
false
}
-fn extract_variant_from_as_call(mc: &syn::ExprMethodCall) -> Option {
- // Looking for `node.kind().as_()`
- let method_ident = mc.method.to_string();
- if !method_ident.starts_with("as_") || !mc.args.is_empty() {
- return None;
- }
- // Receiver must be `node.kind()`
- if !is_node_kind_call(&mc.receiver) {
- return None;
- }
- let snake_variant = &method_ident[3..]; // strip `as_`
- if snake_variant == "member_expression_kind" {
- return None;
- }
- Some(snake_to_pascal_case(snake_variant))
-}
-
fn snake_to_pascal_case(s: &str) -> String {
s.split('_')
.filter(|seg| !seg.is_empty())
diff --git a/tasks/linter_codegen/src/main.rs b/tasks/linter_codegen/src/main.rs
index 48b7168f0734d..e0eae9ee8e7eb 100644
--- a/tasks/linter_codegen/src/main.rs
+++ b/tasks/linter_codegen/src/main.rs
@@ -5,6 +5,7 @@ use crate::{
if_else_detector::IfElseKindDetector,
let_else_detector::LetElseDetector,
match_detector::MatchDetector,
+ member_expression_kinds::get_member_expression_kinds,
node_type_set::NodeTypeSet,
rules::{RuleEntry, find_rule_source_file, get_all_rules},
utils::{find_impl_function, find_rule_impl_block},
@@ -22,6 +23,7 @@ mod early_diverge_detector;
mod if_else_detector;
mod let_else_detector;
mod match_detector;
+mod member_expression_kinds;
mod node_type_set;
mod rules;
mod utils;
@@ -32,6 +34,8 @@ fn main() -> io::Result<()> {
/// # Errors
/// Returns `io::Error` if file operations fail.
+/// # Panics
+/// - Panics if member expression kinds could not be read from source
pub fn generate_rule_runner_impls() -> io::Result<()> {
let root = project_root::get_project_root()
.map_err(|e| std::io::Error::other(format!("could not find project root: {e}")))?;
@@ -39,6 +43,10 @@ pub fn generate_rule_runner_impls() -> io::Result<()> {
let rules_file_contents = fs::read_to_string(root.join("crates/oxc_linter/src/rules.rs"))?;
let rule_entries = get_all_rules(&rules_file_contents)?;
+ let member_expression_kinds =
+ get_member_expression_kinds().expect("Failed to get member expression kinds");
+ let rule_runner_data = RuleRunnerData { member_expression_kinds };
+
let mut out = String::new();
out.push_str("// Auto-generated code, DO NOT EDIT DIRECTLY!\n");
out.push_str("// To regenerate: `cargo run -p oxc_linter_codegen`\n\n");
@@ -56,7 +64,7 @@ pub fn generate_rule_runner_impls() -> io::Result<()> {
&& let Ok(src_contents) = fs::read_to_string(&src_path)
&& let Ok(file) = syn::parse_file(&src_contents)
{
- if let Some(node_types) = detect_top_level_node_types(&file, rule) {
+ if let Some(node_types) = detect_top_level_node_types(&file, rule, &rule_runner_data) {
detected_types.extend(node_types);
}
@@ -108,11 +116,15 @@ impl RuleRunner for crate::rules::{plugin_module}::{rule_module}::{rule_struct}
/// Detect the top-level node types used in a lint rule file by analyzing the Rust AST with `syn`.
/// Returns `Some(bitset)` if at least one node type can be determined, otherwise `None`.
-fn detect_top_level_node_types(file: &File, rule: &RuleEntry) -> Option {
+fn detect_top_level_node_types(
+ file: &File,
+ rule: &RuleEntry,
+ rule_runner_data: &RuleRunnerData,
+) -> Option {
let rule_impl = find_rule_impl_block(file, &rule.rule_struct_name())?;
let run_func = find_impl_function(rule_impl, "run")?;
- let node_types = LetElseDetector::from_run_func(run_func);
+ let node_types = LetElseDetector::from_run_func(run_func, rule_runner_data);
if let Some(node_types) = node_types
&& !node_types.is_empty()
{
@@ -184,6 +196,11 @@ enum CollectionResult {
Incomplete,
}
+/// Additional data collected for rule runner impl generation
+struct RuleRunnerData {
+ member_expression_kinds: NodeTypeSet,
+}
+
/// Format Rust code with `rustfmt`.
///
/// Does not format on disk - interfaces with `rustfmt` via stdin/stdout.
diff --git a/tasks/linter_codegen/src/member_expression_kinds.rs b/tasks/linter_codegen/src/member_expression_kinds.rs
new file mode 100644
index 0000000000000..0d348aad5c2e2
--- /dev/null
+++ b/tasks/linter_codegen/src/member_expression_kinds.rs
@@ -0,0 +1,58 @@
+use syn::{Expr, Pat, Stmt};
+
+use crate::{node_type_set::NodeTypeSet, utils::find_impl_function};
+
+/// Fetches the current list of variants that can be returned by `AstKind::as_member_expression_kind()`.
+/// We read the source file to avoid hardcoding the list here and ensure this will stay updated.
+pub fn get_member_expression_kinds() -> Option {
+ // Read crates/oxc_ast/src/ast_kind_impl.rs and extract all variants in `as_member_expression_kind` function
+ let ast_kind_impl_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
+ .parent()?
+ .parent()?
+ .join("crates")
+ .join("oxc_ast")
+ .join("src")
+ .join("ast_kind_impl.rs");
+ let content = std::fs::read_to_string(ast_kind_impl_path).ok()?;
+ let syntax = syn::parse_file(&content).ok()?;
+ let mut node_type_set = NodeTypeSet::new();
+ for item in syntax.items {
+ if let syn::Item::Impl(impl_block) = item
+ && let syn::Type::Path(type_path) = impl_block.self_ty.as_ref()
+ && type_path.path.segments.last()?.ident == "AstKind"
+ {
+ let impl_fn = find_impl_function(&impl_block, "as_member_expression_kind")
+ .expect("as_member_expression_kind function not found");
+
+ // Look for `match self { ... }` inside the function body
+ if impl_fn.block.stmts.len() != 1 {
+ return None;
+ }
+ let stmt = &impl_fn.block.stmts[0];
+ if let Stmt::Expr(Expr::Match(match_expr), _) = stmt {
+ for arm in &match_expr.arms {
+ if let Pat::TupleStruct(ts) = &arm.pat
+ && let Some(variant) = self_astkind_variant_from_path(&ts.path)
+ {
+ node_type_set.insert(variant);
+ }
+ }
+ if !node_type_set.is_empty() {
+ return Some(node_type_set);
+ }
+ }
+ }
+ }
+ None
+}
+
+fn self_astkind_variant_from_path(path: &syn::Path) -> Option {
+ // Expect `Self::Variant`
+ if path.segments.len() != 2 {
+ return None;
+ }
+ if path.segments[0].ident != "Self" {
+ return None;
+ }
+ Some(path.segments[1].ident.to_string())
+}
diff --git a/tasks/linter_codegen/src/node_type_set.rs b/tasks/linter_codegen/src/node_type_set.rs
index 9287d338be40a..95ef14c87579a 100644
--- a/tasks/linter_codegen/src/node_type_set.rs
+++ b/tasks/linter_codegen/src/node_type_set.rs
@@ -2,6 +2,7 @@ use rustc_hash::FxHashSet;
/// A set of AstKind variants, used for storing the unique node types detected in a rule,
/// or a portion of the rule file.
+#[derive(Clone)]
pub struct NodeTypeSet {
node_types: FxHashSet,
}