Skip to content

Commit cd42793

Browse files
committed
refactor(linter_codegen): improve code style for collecting nodes
1 parent 9fa551a commit cd42793

File tree

4 files changed

+120
-86
lines changed

4 files changed

+120
-86
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/oxc_linter/src/generated/rule_runner_impls.rs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1437,7 +1437,6 @@ impl RuleRunner for crate::rules::nextjs::no_async_client_component::NoAsyncClie
14371437
impl RuleRunner for crate::rules::nextjs::no_before_interactive_script_outside_document::NoBeforeInteractiveScriptOutsideDocument {
14381438
const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::from_types(&[AstType::JSXOpeningElement]);
14391439
const ANY_NODE_TYPE: bool = false;
1440-
14411440
}
14421441

14431442
impl RuleRunner for crate::rules::nextjs::no_css_tags::NoCssTags {
@@ -1724,7 +1723,6 @@ impl RuleRunner for crate::rules::react::button_has_type::ButtonHasType {
17241723
impl RuleRunner for crate::rules::react::checked_requires_onchange_or_readonly::CheckedRequiresOnchangeOrReadonly {
17251724
const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new();
17261725
const ANY_NODE_TYPE: bool = true;
1727-
17281726
}
17291727

17301728
impl RuleRunner for crate::rules::react::exhaustive_deps::ExhaustiveDeps {
@@ -2131,13 +2129,11 @@ impl RuleRunner for crate::rules::typescript::no_namespace::NoNamespace {
21312129
impl RuleRunner for crate::rules::typescript::no_non_null_asserted_nullish_coalescing::NoNonNullAssertedNullishCoalescing {
21322130
const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new();
21332131
const ANY_NODE_TYPE: bool = true;
2134-
21352132
}
21362133

21372134
impl RuleRunner for crate::rules::typescript::no_non_null_asserted_optional_chain::NoNonNullAssertedOptionalChain {
21382135
const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new();
21392136
const ANY_NODE_TYPE: bool = true;
2140-
21412137
}
21422138

21432139
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 {
21652161
impl RuleRunner for crate::rules::typescript::no_unnecessary_boolean_literal_compare::NoUnnecessaryBooleanLiteralCompare {
21662162
const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new();
21672163
const ANY_NODE_TYPE: bool = true;
2168-
21692164
}
21702165

21712166
impl RuleRunner for crate::rules::typescript::no_unnecessary_parameter_property_assignment::NoUnnecessaryParameterPropertyAssignment {
21722167
const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new();
21732168
const ANY_NODE_TYPE: bool = true;
2174-
21752169
}
21762170

21772171
impl RuleRunner for crate::rules::typescript::no_unnecessary_template_expression::NoUnnecessaryTemplateExpression {
21782172
const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new();
21792173
const ANY_NODE_TYPE: bool = true;
2180-
21812174
}
21822175

21832176
impl RuleRunner
@@ -2393,7 +2386,6 @@ impl RuleRunner for crate::rules::typescript::unbound_method::UnboundMethod {
23932386
impl RuleRunner for crate::rules::typescript::use_unknown_in_catch_callback_variable::UseUnknownInCatchCallbackVariable {
23942387
const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new();
23952388
const ANY_NODE_TYPE: bool = true;
2396-
23972389
}
23982390

23992391
impl RuleRunner for crate::rules::unicorn::catch_error_name::CatchErrorName {
@@ -2787,7 +2779,6 @@ impl RuleRunner for crate::rules::unicorn::prefer_includes::PreferIncludes {
27872779
impl RuleRunner for crate::rules::unicorn::prefer_logical_operator_over_ternary::PreferLogicalOperatorOverTernary {
27882780
const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new();
27892781
const ANY_NODE_TYPE: bool = true;
2790-
27912782
}
27922783

27932784
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
29242915
impl RuleRunner for crate::rules::unicorn::require_number_to_fixed_digits_argument::RequireNumberToFixedDigitsArgument {
29252916
const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new();
29262917
const ANY_NODE_TYPE: bool = true;
2927-
29282918
}
29292919

29302920
impl RuleRunner
@@ -2979,7 +2969,6 @@ impl RuleRunner for crate::rules::vitest::prefer_to_be_truthy::PreferToBeTruthy
29792969
impl RuleRunner for crate::rules::vitest::require_local_test_context_for_concurrent_snapshots::RequireLocalTestContextForConcurrentSnapshots {
29802970
const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new();
29812971
const ANY_NODE_TYPE: bool = true;
2982-
29832972
}
29842973

29852974
impl RuleRunner for crate::rules::vue::valid_define_emits::ValidDefineEmits {

tasks/linter_codegen/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ doctest = false
1616
[dependencies]
1717
convert_case = { workspace = true }
1818
project-root = { workspace = true }
19+
rustc-hash = { workspace = true }
1920
syn = { workspace = true, features = ["full", "visit", "parsing"] }

tasks/linter_codegen/src/main.rs

Lines changed: 118 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#![allow(clippy::print_stdout)]
22

33
use std::{
4-
collections::BTreeSet,
54
fmt::Write as _,
65
fs,
76
io::{self, Write as _},
@@ -10,6 +9,7 @@ use std::{
109
};
1110

1211
use convert_case::{Case, Casing};
12+
use rustc_hash::FxHashSet;
1313
use syn::{Expr, ExprIf, File, Pat, Path as SynPath, Stmt}; // keep syn in scope for parse_file used elsewhere
1414

1515
fn main() -> io::Result<()> {
@@ -35,32 +35,35 @@ pub fn generate_rule_runner_impls() -> io::Result<()> {
3535

3636
for rule in &rule_entries {
3737
// Try to open the rule source file and use syn to detect node types
38-
let mut detected_types: BTreeSet<String> = BTreeSet::new();
38+
let mut detected_types: NodeTypeSet = NodeTypeSet::new();
3939
if let Some(src_path) = find_rule_source_file(&root, rule)
4040
&& let Ok(src_contents) = fs::read_to_string(&src_path)
4141
&& let Ok(file) = syn::parse_file(&src_contents)
42-
&& let Some(bitset) = detect_top_level_node_types(&file, rule)
42+
&& let Some(node_types) = detect_top_level_node_types(&file, rule)
4343
{
44-
detected_types.extend(bitset);
44+
detected_types.extend(node_types);
4545
}
4646

4747
let has_detected = !detected_types.is_empty();
4848
let (node_types_init, any_node_type) = if has_detected {
49-
// Map variant name to AstType constant path (AstType::Variant)
50-
let type_idents: Vec<String> =
51-
detected_types.into_iter().map(|v| format!("AstType::{v}")).collect();
52-
(format!("AstTypesBitset::from_types(&[{}])", type_idents.join(", ")), false)
49+
(detected_types.to_ast_type_bitset_string(), false)
5350
} else {
5451
("AstTypesBitset::new()".to_string(), true)
5552
};
5653

5754
write!(
5855
out,
59-
"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",
56+
r#"
57+
impl RuleRunner for crate::rules::{plugin_module}::{rule_module}::{rule_struct} {{
58+
const NODE_TYPES: &AstTypesBitset = &{node_types_init};
59+
const ANY_NODE_TYPE: bool = {any_node_type};
60+
}}
61+
"#,
6062
plugin_module = rule.plugin_module_name,
6163
rule_module = rule.rule_module_name,
6264
rule_struct = rule.rule_struct_name(),
63-
).unwrap();
65+
)
66+
.unwrap();
6467
}
6568

6669
let formatted_out = rust_fmt(&out);
@@ -147,23 +150,60 @@ fn get_all_rules(contents: &str) -> io::Result<Vec<RuleEntry<'_>>> {
147150
Ok(rule_entries)
148151
}
149152

153+
/// A set of AstKind variants, used for storing the unique node types detected in a rule,
154+
/// or a portion of the rule file.
155+
struct NodeTypeSet {
156+
node_types: FxHashSet<String>,
157+
}
158+
159+
impl NodeTypeSet {
160+
/// Create a new set of node variants
161+
fn new() -> Self {
162+
Self { node_types: FxHashSet::default() }
163+
}
164+
165+
/// Insert a variant into the set
166+
fn insert(&mut self, node_type_variant: String) {
167+
self.node_types.insert(node_type_variant);
168+
}
169+
170+
/// Returns `true` if there are no node types in the set.
171+
fn is_empty(&self) -> bool {
172+
self.node_types.is_empty()
173+
}
174+
175+
/// Extend the set with another set of node types.
176+
fn extend(&mut self, other: NodeTypeSet) {
177+
self.node_types.extend(other.node_types);
178+
}
179+
180+
/// Returns the generated code string to initialize an `AstTypesBitset` with the variants
181+
/// in this set.
182+
fn to_ast_type_bitset_string(&self) -> String {
183+
let mut variants: Vec<String> = self.node_types.iter().cloned().collect();
184+
variants.sort_unstable();
185+
let type_idents: Vec<String> =
186+
variants.into_iter().map(|v| format!("AstType::{v}")).collect();
187+
format!("AstTypesBitset::from_types(&[{}])", type_idents.join(", "))
188+
}
189+
}
190+
150191
/// Detect the top-level node types used in a lint rule file by analyzing the Rust AST with `syn`.
151192
/// Returns `Some(bitset)` if at least one node type can be determined, otherwise `None`.
152-
fn detect_top_level_node_types(file: &File, rule: &RuleEntry) -> Option<BTreeSet<String>> {
193+
fn detect_top_level_node_types(file: &File, rule: &RuleEntry) -> Option<NodeTypeSet> {
153194
let rule_impl = find_rule_impl_block(file, &rule.rule_struct_name())?;
154195
let run_func = find_impl_function(rule_impl, "run")?;
155196

156-
let variants: BTreeSet<String> = if let Some(det) = IfElseKindDetector::from_run_func(run_func)
157-
{
158-
det.variants
197+
let node_types = if let Some(node_types) = IfElseKindDetector::from_run_func(run_func) {
198+
node_types
159199
} else {
160200
return None;
161201
};
162-
if variants.is_empty() {
202+
if node_types.is_empty() {
163203
return None;
164204
}
165205

166-
Some(variants)
206+
Some(node_types)
167207
}
168208

169209
fn find_rule_impl_block<'a>(file: &'a File, rule_struct_name: &str) -> Option<&'a syn::ItemImpl> {
@@ -194,27 +234,83 @@ fn find_impl_function<'a>(imp: &'a syn::ItemImpl, func_name: &str) -> Option<&'a
194234

195235
/// Detects top-level `if let AstKind::... = node.kind()` patterns in the `run` method.
196236
struct IfElseKindDetector {
197-
variants: BTreeSet<String>,
237+
node_types: NodeTypeSet,
198238
}
199239

200240
impl IfElseKindDetector {
201-
fn from_run_func(run_func: &syn::ImplItemFn) -> Option<Self> {
241+
fn from_run_func(run_func: &syn::ImplItemFn) -> Option<NodeTypeSet> {
202242
// Only consider when the body has exactly one top-level statement and it's an `if`.
203243
let block = &run_func.block;
204244
if block.stmts.len() != 1 {
205245
return None;
206246
}
207247
let stmt = &block.stmts[0];
208248
let Stmt::Expr(Expr::If(ifexpr), _) = stmt else { return None };
209-
let mut variants = BTreeSet::new();
210-
let result = collect_if_chain_variants(ifexpr, &mut variants);
211-
if result == CollectionResult::Incomplete || variants.is_empty() {
249+
let mut detector = Self { node_types: NodeTypeSet::new() };
250+
let result = detector.collect_if_chain_variants(ifexpr);
251+
if result == CollectionResult::Incomplete || detector.node_types.is_empty() {
212252
return None;
213253
}
214-
Some(Self { variants })
254+
Some(detector.node_types)
255+
}
256+
257+
/// Collects AstKind variants from an if-else chain of `if let AstKind::Xxx(..) = node.kind()`.
258+
/// Returns `true` if all syntax was recognized as supported, otherwise `false`, indicating that
259+
/// the variants collected may be incomplete and should not be treated as valid.
260+
fn collect_if_chain_variants(&mut self, ifexpr: &ExprIf) -> CollectionResult {
261+
// Extract variants from condition like `if let AstKind::Xxx(..) = node.kind()`.
262+
if self.extract_variants_from_if_let_condition(&ifexpr.cond) == CollectionResult::Incomplete
263+
{
264+
// If syntax is not recognized, return Incomplete.
265+
return CollectionResult::Incomplete;
266+
}
267+
// Walk else-if chain.
268+
if let Some((_, else_branch)) = &ifexpr.else_branch {
269+
match &**else_branch {
270+
Expr::If(nested) => self.collect_if_chain_variants(nested),
271+
// plain `else { ... }` should default to any node type
272+
_ => CollectionResult::Incomplete,
273+
}
274+
} else {
275+
CollectionResult::Complete
276+
}
277+
}
278+
279+
/// Extracts AstKind variants from an `if let` condition like `if let AstKind::Xxx(..) = node.kind()`.
280+
fn extract_variants_from_if_let_condition(&mut self, cond: &Expr) -> CollectionResult {
281+
let Expr::Let(let_expr) = cond else { return CollectionResult::Incomplete };
282+
// RHS must be `node.kind()`
283+
if is_node_kind_call(&let_expr.expr) {
284+
self.extract_variants_from_pat(&let_expr.pat)
285+
} else {
286+
CollectionResult::Incomplete
287+
}
288+
}
289+
290+
fn extract_variants_from_pat(&mut self, pat: &Pat) -> CollectionResult {
291+
match pat {
292+
Pat::Or(orpat) => {
293+
for p in &orpat.cases {
294+
if self.extract_variants_from_pat(p) == CollectionResult::Incomplete {
295+
return CollectionResult::Incomplete;
296+
}
297+
}
298+
CollectionResult::Complete
299+
}
300+
Pat::TupleStruct(ts) => {
301+
if let Some(variant) = astkind_variant_from_path(&ts.path) {
302+
self.node_types.insert(variant);
303+
CollectionResult::Complete
304+
} else {
305+
CollectionResult::Incomplete
306+
}
307+
}
308+
_ => CollectionResult::Incomplete,
309+
}
215310
}
216311
}
217312

313+
/// Result of attempting to collect node type variants.
218314
#[derive(Debug, PartialEq, Eq)]
219315
enum CollectionResult {
220316
/// All syntax recognized as supported, variants collected should be complete.
@@ -224,37 +320,6 @@ enum CollectionResult {
224320
Incomplete,
225321
}
226322

227-
/// Collects AstKind variants from an if-else chain of `if let AstKind::Xxx(..) = node.kind()`.
228-
/// Returns `true` if all syntax was recognized as supported, otherwise `false`, indicating that
229-
/// the variants collected may be incomplete and should not be treated as valid.
230-
fn collect_if_chain_variants(ifexpr: &ExprIf, out: &mut BTreeSet<String>) -> CollectionResult {
231-
// Extract variants from condition like `if let AstKind::Xxx(..) = node.kind()`.
232-
if extract_variants_from_if_condition(&ifexpr.cond, out) == CollectionResult::Incomplete {
233-
// If syntax is not recognized, return Incomplete.
234-
return CollectionResult::Incomplete;
235-
}
236-
// Walk else-if chain.
237-
if let Some((_, else_branch)) = &ifexpr.else_branch {
238-
match &**else_branch {
239-
Expr::If(nested) => collect_if_chain_variants(nested, out),
240-
// plain `else { ... }` should default to any node type
241-
_ => CollectionResult::Incomplete,
242-
}
243-
} else {
244-
CollectionResult::Complete
245-
}
246-
}
247-
248-
fn extract_variants_from_if_condition(cond: &Expr, out: &mut BTreeSet<String>) -> CollectionResult {
249-
let Expr::Let(let_expr) = cond else { return CollectionResult::Incomplete };
250-
// RHS must be `node.kind()`
251-
if is_node_kind_call(&let_expr.expr) {
252-
extract_variants_from_pat(&let_expr.pat, out)
253-
} else {
254-
CollectionResult::Incomplete
255-
}
256-
}
257-
258323
fn is_node_kind_call(expr: &Expr) -> bool {
259324
if let Expr::MethodCall(mc) = expr
260325
&& mc.method == "kind"
@@ -266,28 +331,6 @@ fn is_node_kind_call(expr: &Expr) -> bool {
266331
false
267332
}
268333

269-
fn extract_variants_from_pat(pat: &Pat, out: &mut BTreeSet<String>) -> CollectionResult {
270-
match pat {
271-
Pat::Or(orpat) => {
272-
for p in &orpat.cases {
273-
if extract_variants_from_pat(p, out) == CollectionResult::Incomplete {
274-
return CollectionResult::Incomplete;
275-
}
276-
}
277-
CollectionResult::Complete
278-
}
279-
Pat::TupleStruct(ts) => {
280-
if let Some(variant) = astkind_variant_from_path(&ts.path) {
281-
out.insert(variant);
282-
CollectionResult::Complete
283-
} else {
284-
CollectionResult::Incomplete
285-
}
286-
}
287-
_ => CollectionResult::Incomplete,
288-
}
289-
}
290-
291334
/// Extract AstKind variant from something like `AstKind::Variant`
292335
fn astkind_variant_from_path(path: &SynPath) -> Option<String> {
293336
// Expect `AstKind::Variant`

0 commit comments

Comments
 (0)