diff --git a/Cargo.lock b/Cargo.lock index a2d6dd443dde0..1d49765b7644f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2061,6 +2061,7 @@ version = "0.0.0" dependencies = [ "convert_case", "project-root", + "rustc-hash", "syn", ] diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index 7c80e97f68f27..e6222a45b9d1d 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -1437,7 +1437,6 @@ impl RuleRunner for crate::rules::nextjs::no_async_client_component::NoAsyncClie impl RuleRunner for crate::rules::nextjs::no_before_interactive_script_outside_document::NoBeforeInteractiveScriptOutsideDocument { const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::from_types(&[AstType::JSXOpeningElement]); const ANY_NODE_TYPE: bool = false; - } impl RuleRunner for crate::rules::nextjs::no_css_tags::NoCssTags { @@ -1724,7 +1723,6 @@ impl RuleRunner for crate::rules::react::button_has_type::ButtonHasType { impl RuleRunner for crate::rules::react::checked_requires_onchange_or_readonly::CheckedRequiresOnchangeOrReadonly { const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new(); const ANY_NODE_TYPE: bool = true; - } impl RuleRunner for crate::rules::react::exhaustive_deps::ExhaustiveDeps { @@ -2131,13 +2129,11 @@ impl RuleRunner for crate::rules::typescript::no_namespace::NoNamespace { impl RuleRunner for crate::rules::typescript::no_non_null_asserted_nullish_coalescing::NoNonNullAssertedNullishCoalescing { const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new(); const ANY_NODE_TYPE: bool = true; - } impl RuleRunner for crate::rules::typescript::no_non_null_asserted_optional_chain::NoNonNullAssertedOptionalChain { const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new(); const ANY_NODE_TYPE: bool = true; - } impl RuleRunner for crate::rules::typescript::no_non_null_assertion::NoNonNullAssertion { @@ -2165,19 +2161,16 @@ impl RuleRunner for crate::rules::typescript::no_this_alias::NoThisAlias { impl RuleRunner for crate::rules::typescript::no_unnecessary_boolean_literal_compare::NoUnnecessaryBooleanLiteralCompare { const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new(); const ANY_NODE_TYPE: bool = true; - } impl RuleRunner for crate::rules::typescript::no_unnecessary_parameter_property_assignment::NoUnnecessaryParameterPropertyAssignment { const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new(); const ANY_NODE_TYPE: bool = true; - } impl RuleRunner for crate::rules::typescript::no_unnecessary_template_expression::NoUnnecessaryTemplateExpression { const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new(); const ANY_NODE_TYPE: bool = true; - } impl RuleRunner @@ -2393,7 +2386,6 @@ impl RuleRunner for crate::rules::typescript::unbound_method::UnboundMethod { impl RuleRunner for crate::rules::typescript::use_unknown_in_catch_callback_variable::UseUnknownInCatchCallbackVariable { const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new(); const ANY_NODE_TYPE: bool = true; - } impl RuleRunner for crate::rules::unicorn::catch_error_name::CatchErrorName { @@ -2787,7 +2779,6 @@ impl RuleRunner for crate::rules::unicorn::prefer_includes::PreferIncludes { impl RuleRunner for crate::rules::unicorn::prefer_logical_operator_over_ternary::PreferLogicalOperatorOverTernary { const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new(); const ANY_NODE_TYPE: bool = true; - } impl RuleRunner for crate::rules::unicorn::prefer_math_min_max::PreferMathMinMax { @@ -2924,7 +2915,6 @@ impl RuleRunner for crate::rules::unicorn::require_array_join_separator::Require impl RuleRunner for crate::rules::unicorn::require_number_to_fixed_digits_argument::RequireNumberToFixedDigitsArgument { const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new(); const ANY_NODE_TYPE: bool = true; - } impl RuleRunner @@ -2979,7 +2969,6 @@ impl RuleRunner for crate::rules::vitest::prefer_to_be_truthy::PreferToBeTruthy impl RuleRunner for crate::rules::vitest::require_local_test_context_for_concurrent_snapshots::RequireLocalTestContextForConcurrentSnapshots { const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new(); const ANY_NODE_TYPE: bool = true; - } impl RuleRunner for crate::rules::vue::valid_define_emits::ValidDefineEmits { diff --git a/tasks/linter_codegen/Cargo.toml b/tasks/linter_codegen/Cargo.toml index a7a8a4044b448..fc2a7a62eed6b 100644 --- a/tasks/linter_codegen/Cargo.toml +++ b/tasks/linter_codegen/Cargo.toml @@ -16,4 +16,5 @@ doctest = false [dependencies] convert_case = { workspace = true } project-root = { workspace = true } +rustc-hash = { workspace = true } syn = { workspace = true, features = ["full", "visit", "parsing"] } diff --git a/tasks/linter_codegen/src/main.rs b/tasks/linter_codegen/src/main.rs index 908ce34962214..4d7d57c272bfd 100644 --- a/tasks/linter_codegen/src/main.rs +++ b/tasks/linter_codegen/src/main.rs @@ -1,7 +1,6 @@ #![allow(clippy::print_stdout)] use std::{ - collections::BTreeSet, fmt::Write as _, fs, io::{self, Write as _}, @@ -10,6 +9,7 @@ use std::{ }; use convert_case::{Case, Casing}; +use rustc_hash::FxHashSet; use syn::{Expr, ExprIf, File, Pat, Path as SynPath, Stmt}; // keep syn in scope for parse_file used elsewhere fn main() -> io::Result<()> { @@ -35,32 +35,35 @@ pub fn generate_rule_runner_impls() -> io::Result<()> { for rule in &rule_entries { // Try to open the rule source file and use syn to detect node types - let mut detected_types: BTreeSet = BTreeSet::new(); + let mut detected_types: NodeTypeSet = NodeTypeSet::new(); if let Some(src_path) = find_rule_source_file(&root, rule) && let Ok(src_contents) = fs::read_to_string(&src_path) && let Ok(file) = syn::parse_file(&src_contents) - && let Some(bitset) = detect_top_level_node_types(&file, rule) + && let Some(node_types) = detect_top_level_node_types(&file, rule) { - detected_types.extend(bitset); + detected_types.extend(node_types); } let has_detected = !detected_types.is_empty(); let (node_types_init, any_node_type) = if has_detected { - // Map variant name to AstType constant path (AstType::Variant) - let type_idents: Vec = - detected_types.into_iter().map(|v| format!("AstType::{v}")).collect(); - (format!("AstTypesBitset::from_types(&[{}])", type_idents.join(", ")), false) + (detected_types.to_ast_type_bitset_string(), false) } else { ("AstTypesBitset::new()".to_string(), true) }; write!( out, - "impl RuleRunner for crate::rules::{plugin_module}::{rule_module}::{rule_struct} {{\n const NODE_TYPES: &AstTypesBitset = &{node_types_init};\n const ANY_NODE_TYPE: bool = {any_node_type};\n\n}}\n\n", + r" +impl RuleRunner for crate::rules::{plugin_module}::{rule_module}::{rule_struct} {{ + const NODE_TYPES: &AstTypesBitset = &{node_types_init}; + const ANY_NODE_TYPE: bool = {any_node_type}; +}} + ", plugin_module = rule.plugin_module_name, rule_module = rule.rule_module_name, rule_struct = rule.rule_struct_name(), - ).unwrap(); + ) + .unwrap(); } let formatted_out = rust_fmt(&out); @@ -147,23 +150,57 @@ fn get_all_rules(contents: &str) -> io::Result>> { Ok(rule_entries) } +/// A set of AstKind variants, used for storing the unique node types detected in a rule, +/// or a portion of the rule file. +struct NodeTypeSet { + node_types: FxHashSet, +} + +impl NodeTypeSet { + /// Create a new set of node variants + fn new() -> Self { + Self { node_types: FxHashSet::default() } + } + + /// Insert a variant into the set + fn insert(&mut self, node_type_variant: String) { + self.node_types.insert(node_type_variant); + } + + /// Returns `true` if there are no node types in the set. + fn is_empty(&self) -> bool { + self.node_types.is_empty() + } + + /// Extend the set with another set of node types. + fn extend(&mut self, other: NodeTypeSet) { + self.node_types.extend(other.node_types); + } + + /// Returns the generated code string to initialize an `AstTypesBitset` with the variants + /// in this set. + fn to_ast_type_bitset_string(&self) -> String { + let mut variants: Vec<&str> = + self.node_types.iter().map(std::string::String::as_str).collect(); + variants.sort_unstable(); + let type_idents: Vec = + variants.into_iter().map(|v| format!("AstType::{v}")).collect(); + format!("AstTypesBitset::from_types(&[{}])", type_idents.join(", ")) + } +} + /// 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) -> Option { let rule_impl = find_rule_impl_block(file, &rule.rule_struct_name())?; let run_func = find_impl_function(rule_impl, "run")?; - let variants: BTreeSet = if let Some(det) = IfElseKindDetector::from_run_func(run_func) - { - det.variants - } else { - return None; - }; - if variants.is_empty() { + let node_types = IfElseKindDetector::from_run_func(run_func)?; + if node_types.is_empty() { return None; } - Some(variants) + Some(node_types) } fn find_rule_impl_block<'a>(file: &'a File, rule_struct_name: &str) -> Option<&'a syn::ItemImpl> { @@ -194,11 +231,11 @@ fn find_impl_function<'a>(imp: &'a syn::ItemImpl, func_name: &str) -> Option<&'a /// Detects top-level `if let AstKind::... = node.kind()` patterns in the `run` method. struct IfElseKindDetector { - variants: BTreeSet, + node_types: NodeTypeSet, } impl IfElseKindDetector { - fn from_run_func(run_func: &syn::ImplItemFn) -> Option { + fn from_run_func(run_func: &syn::ImplItemFn) -> Option { // Only consider when the body has exactly one top-level statement and it's an `if`. let block = &run_func.block; if block.stmts.len() != 1 { @@ -206,15 +243,71 @@ impl IfElseKindDetector { } let stmt = &block.stmts[0]; let Stmt::Expr(Expr::If(ifexpr), _) = stmt else { return None }; - let mut variants = BTreeSet::new(); - let result = collect_if_chain_variants(ifexpr, &mut variants); - if result == CollectionResult::Incomplete || variants.is_empty() { + let mut detector = Self { node_types: NodeTypeSet::new() }; + let result = detector.collect_if_chain_variants(ifexpr); + if result == CollectionResult::Incomplete || detector.node_types.is_empty() { return None; } - Some(Self { variants }) + Some(detector.node_types) + } + + /// Collects AstKind variants from an if-else chain of `if let AstKind::Xxx(..) = node.kind()`. + /// Returns `true` if all syntax was recognized as supported, otherwise `false`, indicating that + /// the variants collected may be incomplete and should not be treated as valid. + fn collect_if_chain_variants(&mut self, ifexpr: &ExprIf) -> CollectionResult { + // Extract variants from condition like `if let AstKind::Xxx(..) = node.kind()`. + if self.extract_variants_from_if_let_condition(&ifexpr.cond) == CollectionResult::Incomplete + { + // If syntax is not recognized, return Incomplete. + return CollectionResult::Incomplete; + } + // Walk else-if chain. + if let Some((_, else_branch)) = &ifexpr.else_branch { + match &**else_branch { + Expr::If(nested) => self.collect_if_chain_variants(nested), + // plain `else { ... }` should default to any node type + _ => CollectionResult::Incomplete, + } + } else { + CollectionResult::Complete + } + } + + /// Extracts AstKind variants from an `if let` condition like `if let AstKind::Xxx(..) = node.kind()`. + fn extract_variants_from_if_let_condition(&mut self, cond: &Expr) -> CollectionResult { + let Expr::Let(let_expr) = cond else { return CollectionResult::Incomplete }; + // RHS must be `node.kind()` + if is_node_kind_call(&let_expr.expr) { + self.extract_variants_from_pat(&let_expr.pat) + } else { + CollectionResult::Incomplete + } + } + + fn extract_variants_from_pat(&mut self, pat: &Pat) -> CollectionResult { + match pat { + Pat::Or(orpat) => { + for p in &orpat.cases { + if self.extract_variants_from_pat(p) == CollectionResult::Incomplete { + return CollectionResult::Incomplete; + } + } + CollectionResult::Complete + } + Pat::TupleStruct(ts) => { + if let Some(variant) = astkind_variant_from_path(&ts.path) { + self.node_types.insert(variant); + CollectionResult::Complete + } else { + CollectionResult::Incomplete + } + } + _ => CollectionResult::Incomplete, + } } } +/// Result of attempting to collect node type variants. #[derive(Debug, PartialEq, Eq)] enum CollectionResult { /// All syntax recognized as supported, variants collected should be complete. @@ -224,37 +317,6 @@ enum CollectionResult { Incomplete, } -/// Collects AstKind variants from an if-else chain of `if let AstKind::Xxx(..) = node.kind()`. -/// Returns `true` if all syntax was recognized as supported, otherwise `false`, indicating that -/// the variants collected may be incomplete and should not be treated as valid. -fn collect_if_chain_variants(ifexpr: &ExprIf, out: &mut BTreeSet) -> CollectionResult { - // Extract variants from condition like `if let AstKind::Xxx(..) = node.kind()`. - if extract_variants_from_if_condition(&ifexpr.cond, out) == CollectionResult::Incomplete { - // If syntax is not recognized, return Incomplete. - return CollectionResult::Incomplete; - } - // Walk else-if chain. - if let Some((_, else_branch)) = &ifexpr.else_branch { - match &**else_branch { - Expr::If(nested) => collect_if_chain_variants(nested, out), - // plain `else { ... }` should default to any node type - _ => CollectionResult::Incomplete, - } - } else { - CollectionResult::Complete - } -} - -fn extract_variants_from_if_condition(cond: &Expr, out: &mut BTreeSet) -> CollectionResult { - let Expr::Let(let_expr) = cond else { return CollectionResult::Incomplete }; - // RHS must be `node.kind()` - if is_node_kind_call(&let_expr.expr) { - extract_variants_from_pat(&let_expr.pat, out) - } else { - CollectionResult::Incomplete - } -} - fn is_node_kind_call(expr: &Expr) -> bool { if let Expr::MethodCall(mc) = expr && mc.method == "kind" @@ -266,28 +328,6 @@ fn is_node_kind_call(expr: &Expr) -> bool { false } -fn extract_variants_from_pat(pat: &Pat, out: &mut BTreeSet) -> CollectionResult { - match pat { - Pat::Or(orpat) => { - for p in &orpat.cases { - if extract_variants_from_pat(p, out) == CollectionResult::Incomplete { - return CollectionResult::Incomplete; - } - } - CollectionResult::Complete - } - Pat::TupleStruct(ts) => { - if let Some(variant) = astkind_variant_from_path(&ts.path) { - out.insert(variant); - CollectionResult::Complete - } else { - CollectionResult::Incomplete - } - } - _ => CollectionResult::Incomplete, - } -} - /// Extract AstKind variant from something like `AstKind::Variant` fn astkind_variant_from_path(path: &SynPath) -> Option { // Expect `AstKind::Variant`