Skip to content

Commit 53f5218

Browse files
committed
refactor(linter_codegen): split into separate files (#13657)
Pure refactoring. Just splitting up the linter codegen into individual files to make it a little more organized. This will make it easier to add more "detectors" (I guess that's what we'll call them for now) for additional types of syntax for generating rule runner impls.
1 parent 09428f6 commit 53f5218

File tree

5 files changed

+269
-245
lines changed

5 files changed

+269
-245
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
use syn::{Expr, ExprIf, Pat, Stmt};
2+
3+
use crate::{
4+
CollectionResult, NodeTypeSet,
5+
utils::{astkind_variant_from_path, is_node_kind_call},
6+
};
7+
8+
/// Detects top-level `if let AstKind::... = node.kind()` patterns in the `run` method.
9+
pub struct IfElseKindDetector {
10+
node_types: NodeTypeSet,
11+
}
12+
13+
impl IfElseKindDetector {
14+
pub fn from_run_func(run_func: &syn::ImplItemFn) -> Option<NodeTypeSet> {
15+
// Only consider when the body has exactly one top-level statement and it's an `if`.
16+
let block = &run_func.block;
17+
if block.stmts.len() != 1 {
18+
return None;
19+
}
20+
let stmt = &block.stmts[0];
21+
let Stmt::Expr(Expr::If(ifexpr), _) = stmt else { return None };
22+
let mut detector = Self { node_types: NodeTypeSet::new() };
23+
let result = detector.collect_if_chain_variants(ifexpr);
24+
if result == CollectionResult::Incomplete || detector.node_types.is_empty() {
25+
return None;
26+
}
27+
Some(detector.node_types)
28+
}
29+
30+
/// Collects AstKind variants from an if-else chain of `if let AstKind::Xxx(..) = node.kind()`.
31+
/// Returns `true` if all syntax was recognized as supported, otherwise `false`, indicating that
32+
/// the variants collected may be incomplete and should not be treated as valid.
33+
fn collect_if_chain_variants(&mut self, ifexpr: &ExprIf) -> CollectionResult {
34+
// Extract variants from condition like `if let AstKind::Xxx(..) = node.kind()`.
35+
if self.extract_variants_from_if_let_condition(&ifexpr.cond) == CollectionResult::Incomplete
36+
{
37+
// If syntax is not recognized, return Incomplete.
38+
return CollectionResult::Incomplete;
39+
}
40+
// Walk else-if chain.
41+
if let Some((_, else_branch)) = &ifexpr.else_branch {
42+
match &**else_branch {
43+
Expr::If(nested) => self.collect_if_chain_variants(nested),
44+
// plain `else { ... }` should default to any node type
45+
_ => CollectionResult::Incomplete,
46+
}
47+
} else {
48+
CollectionResult::Complete
49+
}
50+
}
51+
52+
/// Extracts AstKind variants from an `if let` condition like `if let AstKind::Xxx(..) = node.kind()`.
53+
fn extract_variants_from_if_let_condition(&mut self, cond: &Expr) -> CollectionResult {
54+
let Expr::Let(let_expr) = cond else { return CollectionResult::Incomplete };
55+
// RHS must be `node.kind()`
56+
if is_node_kind_call(&let_expr.expr) {
57+
self.extract_variants_from_pat(&let_expr.pat)
58+
} else {
59+
CollectionResult::Incomplete
60+
}
61+
}
62+
63+
fn extract_variants_from_pat(&mut self, pat: &Pat) -> CollectionResult {
64+
match pat {
65+
Pat::Or(orpat) => {
66+
for p in &orpat.cases {
67+
if self.extract_variants_from_pat(p) == CollectionResult::Incomplete {
68+
return CollectionResult::Incomplete;
69+
}
70+
}
71+
CollectionResult::Complete
72+
}
73+
Pat::TupleStruct(ts) => {
74+
if let Some(variant) = astkind_variant_from_path(&ts.path) {
75+
self.node_types.insert(variant);
76+
CollectionResult::Complete
77+
} else {
78+
CollectionResult::Incomplete
79+
}
80+
}
81+
_ => CollectionResult::Incomplete,
82+
}
83+
}
84+
}

tasks/linter_codegen/src/main.rs

Lines changed: 11 additions & 245 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
#![allow(clippy::print_stdout)]
22

3+
use crate::{
4+
if_else_detector::IfElseKindDetector,
5+
node_type_set::NodeTypeSet,
6+
rules::{RuleEntry, find_rule_source_file, get_all_rules},
7+
utils::{find_impl_function, find_rule_impl_block},
8+
};
39
use std::{
410
fmt::Write as _,
511
fs,
612
io::{self, Write as _},
7-
path::Path,
813
process::{Command, Stdio},
914
};
15+
use syn::File;
1016

11-
use convert_case::{Case, Casing};
12-
use rustc_hash::FxHashSet;
13-
use syn::{Expr, ExprIf, File, Pat, Path as SynPath, Stmt}; // keep syn in scope for parse_file used elsewhere
17+
mod if_else_detector;
18+
mod node_type_set;
19+
mod rules;
20+
mod utils;
1421

1522
fn main() -> io::Result<()> {
1623
generate_rule_runner_impls()
@@ -73,120 +80,6 @@ impl RuleRunner for crate::rules::{plugin_module}::{rule_module}::{rule_struct}
7380
Ok(())
7481
}
7582

76-
/// Given a rule entry, attempt to find its corresponding source file path
77-
fn find_rule_source_file(root: &Path, rule: &RuleEntry) -> Option<std::path::PathBuf> {
78-
// A rule path corresponds to:
79-
// 1) `crates/oxc_linter/src/rules/<plugin>/<rule>.rs`
80-
// 2) `crates/oxc_linter/src/rules/<plugin>/<rule>/mod.rs`
81-
let rules_path = root.join("crates/oxc_linter/src/rules").join(rule.plugin_module_name);
82-
83-
let direct_path = rules_path.join(format!("{}.rs", rule.rule_module_name));
84-
if direct_path.exists() {
85-
return Some(direct_path);
86-
}
87-
88-
let mod_path = rules_path.join(rule.rule_module_name).join("mod.rs");
89-
if mod_path.exists() {
90-
return Some(mod_path);
91-
}
92-
93-
None
94-
}
95-
96-
/// Represents a lint rule entry in the `declare_all_lint_rules!` macro.
97-
#[derive(PartialEq, Eq, PartialOrd, Ord)]
98-
struct RuleEntry<'e> {
99-
/// The module name of the rule's plugin, like `eslint` in `eslint::no_debugger::NoDebugger`.
100-
plugin_module_name: &'e str,
101-
/// The rule's module name, like `no_debugger` in `eslint::no_debugger:NoDebugger`.
102-
rule_module_name: &'e str,
103-
}
104-
105-
impl RuleEntry<'_> {
106-
/// Get the rule's struct name, like `NoDebugger` in `eslint::no_debugger::NoDebugger`.
107-
fn rule_struct_name(&self) -> String {
108-
self.rule_module_name.to_case(Case::Pascal)
109-
}
110-
}
111-
112-
/// Parses `crates/oxc_linter/src/rules.rs` to extract all lint rule declarations into a list
113-
/// of `RuleEntry`.
114-
fn get_all_rules(contents: &str) -> io::Result<Vec<RuleEntry<'_>>> {
115-
let start_marker = "oxc_macros::declare_all_lint_rules!";
116-
let start = contents.find(start_marker).ok_or_else(|| {
117-
std::io::Error::other("could not find declare_all_lint_rules macro invocation")
118-
})?;
119-
120-
let body = &contents[start..];
121-
122-
// Collect (module path, struct name) pairs. Do NOT deduplicate by struct name because
123-
// different plugins may have rules with the same struct name.
124-
let mut rule_entries = Vec::new();
125-
for line in body.lines().skip(1) {
126-
let line = line.trim();
127-
if line.contains('}') {
128-
break;
129-
}
130-
if line.is_empty() || line.starts_with("//") {
131-
continue;
132-
}
133-
if !line.ends_with(',') {
134-
continue;
135-
}
136-
let path = &line[..line.len() - 1];
137-
let parts = path.split("::").collect::<Vec<_>>();
138-
if parts.len() != 2 {
139-
continue;
140-
}
141-
let Some(plugin_module_name) = parts.first() else { continue };
142-
let Some(rule_module_name) = parts.get(1) else { continue };
143-
rule_entries.push(RuleEntry { plugin_module_name, rule_module_name });
144-
}
145-
// Sort deterministically
146-
rule_entries.sort_unstable();
147-
148-
Ok(rule_entries)
149-
}
150-
151-
/// A set of AstKind variants, used for storing the unique node types detected in a rule,
152-
/// or a portion of the rule file.
153-
struct NodeTypeSet {
154-
node_types: FxHashSet<String>,
155-
}
156-
157-
impl NodeTypeSet {
158-
/// Create a new set of node variants
159-
fn new() -> Self {
160-
Self { node_types: FxHashSet::default() }
161-
}
162-
163-
/// Insert a variant into the set
164-
fn insert(&mut self, node_type_variant: String) {
165-
self.node_types.insert(node_type_variant);
166-
}
167-
168-
/// Returns `true` if there are no node types in the set.
169-
fn is_empty(&self) -> bool {
170-
self.node_types.is_empty()
171-
}
172-
173-
/// Extend the set with another set of node types.
174-
fn extend(&mut self, other: NodeTypeSet) {
175-
self.node_types.extend(other.node_types);
176-
}
177-
178-
/// Returns the generated code string to initialize an `AstTypesBitset` with the variants
179-
/// in this set.
180-
fn to_ast_type_bitset_string(&self) -> String {
181-
let mut variants: Vec<&str> =
182-
self.node_types.iter().map(std::string::String::as_str).collect();
183-
variants.sort_unstable();
184-
let type_idents: Vec<String> =
185-
variants.into_iter().map(|v| format!("AstType::{v}")).collect();
186-
format!("AstTypesBitset::from_types(&[{}])", type_idents.join(", "))
187-
}
188-
}
189-
19083
/// Detect the top-level node types used in a lint rule file by analyzing the Rust AST with `syn`.
19184
/// Returns `Some(bitset)` if at least one node type can be determined, otherwise `None`.
19285
fn detect_top_level_node_types(file: &File, rule: &RuleEntry) -> Option<NodeTypeSet> {
@@ -201,110 +94,6 @@ fn detect_top_level_node_types(file: &File, rule: &RuleEntry) -> Option<NodeType
20194
Some(node_types)
20295
}
20396

204-
fn find_rule_impl_block<'a>(file: &'a File, rule_struct_name: &str) -> Option<&'a syn::ItemImpl> {
205-
for item in &file.items {
206-
let syn::Item::Impl(imp) = item else { continue };
207-
let ident = match imp.self_ty.as_ref() {
208-
syn::Type::Path(p) => p.path.get_ident(),
209-
_ => None,
210-
};
211-
if ident.is_some_and(|id| id == rule_struct_name)
212-
&& imp.trait_.as_ref().is_some_and(|(_, path, _)| path.is_ident("Rule"))
213-
{
214-
return Some(imp);
215-
}
216-
}
217-
None
218-
}
219-
220-
fn find_impl_function<'a>(imp: &'a syn::ItemImpl, func_name: &str) -> Option<&'a syn::ImplItemFn> {
221-
for impl_item in &imp.items {
222-
let syn::ImplItem::Fn(func) = impl_item else { continue };
223-
if func.sig.ident == func_name {
224-
return Some(func);
225-
}
226-
}
227-
None
228-
}
229-
230-
/// Detects top-level `if let AstKind::... = node.kind()` patterns in the `run` method.
231-
struct IfElseKindDetector {
232-
node_types: NodeTypeSet,
233-
}
234-
235-
impl IfElseKindDetector {
236-
fn from_run_func(run_func: &syn::ImplItemFn) -> Option<NodeTypeSet> {
237-
// Only consider when the body has exactly one top-level statement and it's an `if`.
238-
let block = &run_func.block;
239-
if block.stmts.len() != 1 {
240-
return None;
241-
}
242-
let stmt = &block.stmts[0];
243-
let Stmt::Expr(Expr::If(ifexpr), _) = stmt else { return None };
244-
let mut detector = Self { node_types: NodeTypeSet::new() };
245-
let result = detector.collect_if_chain_variants(ifexpr);
246-
if result == CollectionResult::Incomplete || detector.node_types.is_empty() {
247-
return None;
248-
}
249-
Some(detector.node_types)
250-
}
251-
252-
/// Collects AstKind variants from an if-else chain of `if let AstKind::Xxx(..) = node.kind()`.
253-
/// Returns `true` if all syntax was recognized as supported, otherwise `false`, indicating that
254-
/// the variants collected may be incomplete and should not be treated as valid.
255-
fn collect_if_chain_variants(&mut self, ifexpr: &ExprIf) -> CollectionResult {
256-
// Extract variants from condition like `if let AstKind::Xxx(..) = node.kind()`.
257-
if self.extract_variants_from_if_let_condition(&ifexpr.cond) == CollectionResult::Incomplete
258-
{
259-
// If syntax is not recognized, return Incomplete.
260-
return CollectionResult::Incomplete;
261-
}
262-
// Walk else-if chain.
263-
if let Some((_, else_branch)) = &ifexpr.else_branch {
264-
match &**else_branch {
265-
Expr::If(nested) => self.collect_if_chain_variants(nested),
266-
// plain `else { ... }` should default to any node type
267-
_ => CollectionResult::Incomplete,
268-
}
269-
} else {
270-
CollectionResult::Complete
271-
}
272-
}
273-
274-
/// Extracts AstKind variants from an `if let` condition like `if let AstKind::Xxx(..) = node.kind()`.
275-
fn extract_variants_from_if_let_condition(&mut self, cond: &Expr) -> CollectionResult {
276-
let Expr::Let(let_expr) = cond else { return CollectionResult::Incomplete };
277-
// RHS must be `node.kind()`
278-
if is_node_kind_call(&let_expr.expr) {
279-
self.extract_variants_from_pat(&let_expr.pat)
280-
} else {
281-
CollectionResult::Incomplete
282-
}
283-
}
284-
285-
fn extract_variants_from_pat(&mut self, pat: &Pat) -> CollectionResult {
286-
match pat {
287-
Pat::Or(orpat) => {
288-
for p in &orpat.cases {
289-
if self.extract_variants_from_pat(p) == CollectionResult::Incomplete {
290-
return CollectionResult::Incomplete;
291-
}
292-
}
293-
CollectionResult::Complete
294-
}
295-
Pat::TupleStruct(ts) => {
296-
if let Some(variant) = astkind_variant_from_path(&ts.path) {
297-
self.node_types.insert(variant);
298-
CollectionResult::Complete
299-
} else {
300-
CollectionResult::Incomplete
301-
}
302-
}
303-
_ => CollectionResult::Incomplete,
304-
}
305-
}
306-
}
307-
30897
/// Result of attempting to collect node type variants.
30998
#[derive(Debug, PartialEq, Eq)]
31099
enum CollectionResult {
@@ -315,29 +104,6 @@ enum CollectionResult {
315104
Incomplete,
316105
}
317106

318-
fn is_node_kind_call(expr: &Expr) -> bool {
319-
if let Expr::MethodCall(mc) = expr
320-
&& mc.method == "kind"
321-
&& mc.args.is_empty()
322-
&& let Expr::Path(p) = &*mc.receiver
323-
{
324-
return p.path.is_ident("node");
325-
}
326-
false
327-
}
328-
329-
/// Extract AstKind variant from something like `AstKind::Variant`
330-
fn astkind_variant_from_path(path: &SynPath) -> Option<String> {
331-
// Expect `AstKind::Variant`
332-
if path.segments.len() != 2 {
333-
return None;
334-
}
335-
if path.segments[0].ident != "AstKind" {
336-
return None;
337-
}
338-
Some(path.segments[1].ident.to_string())
339-
}
340-
341107
/// Format Rust code with `rustfmt`.
342108
///
343109
/// Does not format on disk - interfaces with `rustfmt` via stdin/stdout.

0 commit comments

Comments
 (0)