Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions crates/oxc_linter/src/generated/rule_runner_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand Down
54 changes: 30 additions & 24 deletions tasks/linter_codegen/src/let_else_detector.rs
Original file line number Diff line number Diff line change
@@ -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<NodeTypeSet> {
impl<'a> LetElseDetector<'a> {
pub fn from_run_func(
run_func: &syn::ImplItemFn,
rule_runner_data: &'a RuleRunnerData,
) -> Option<NodeTypeSet> {
// 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;
Expand All @@ -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_<variant>()`, 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.
Expand Down Expand Up @@ -74,6 +78,25 @@ impl LetElseDetector {
_ => CollectionResult::Incomplete,
}
}

fn extract_variants_from_as_call(&self, mc: &syn::ExprMethodCall) -> Option<NodeTypeSet> {
// Looking for `node.kind().as_<snake_case_variant>()`
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()`
Expand All @@ -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<String> {
// Looking for `node.kind().as_<snake_case_variant>()`
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())
Expand Down
23 changes: 20 additions & 3 deletions tasks/linter_codegen/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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;
Expand All @@ -32,13 +34,19 @@ 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}")))?;

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");
Expand All @@ -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);
}

Expand Down Expand Up @@ -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<NodeTypeSet> {
fn detect_top_level_node_types(
file: &File,
rule: &RuleEntry,
rule_runner_data: &RuleRunnerData,
) -> Option<NodeTypeSet> {
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()
{
Expand Down Expand Up @@ -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.
Expand Down
58 changes: 58 additions & 0 deletions tasks/linter_codegen/src/member_expression_kinds.rs
Original file line number Diff line number Diff line change
@@ -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<NodeTypeSet> {
// 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<String> {
// 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())
}
1 change: 1 addition & 0 deletions tasks/linter_codegen/src/node_type_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
}
Expand Down
Loading