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! Screenshot 2025-10-15 at 5 11 00 PM --- .../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, }