Skip to content

Commit 463be73

Browse files
committed
perf(linter): support as_* for node type codegen (#14582)
Enables the linter codegen to recognize code like `let foo = node.kind().as_ast_kind() else { return };` and generate the relevant AST kinds. <img width="724" height="429" alt="Screenshot 2025-10-13 at 11 30 46 PM" src="https://github.com/user-attachments/assets/8b71b44a-c22a-48c4-80c7-6d96f4960ca0" />
1 parent 8df1e24 commit 463be73

File tree

2 files changed

+66
-6
lines changed

2 files changed

+66
-6
lines changed

crates/oxc_linter/src/generated/rule_runner_impls.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,8 @@ impl RuleRunner for crate::rules::eslint::no_dupe_keys::NoDupeKeys {
304304
}
305305

306306
impl RuleRunner for crate::rules::eslint::no_duplicate_case::NoDuplicateCase {
307-
const NODE_TYPES: Option<&AstTypesBitset> = None;
307+
const NODE_TYPES: Option<&AstTypesBitset> =
308+
Some(&AstTypesBitset::from_types(&[AstType::SwitchStatement]));
308309
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run;
309310
}
310311

@@ -1967,12 +1968,14 @@ impl RuleRunner for crate::rules::promise::catch_or_return::CatchOrReturn {
19671968
}
19681969

19691970
impl RuleRunner for crate::rules::promise::no_callback_in_promise::NoCallbackInPromise {
1970-
const NODE_TYPES: Option<&AstTypesBitset> = None;
1971+
const NODE_TYPES: Option<&AstTypesBitset> =
1972+
Some(&AstTypesBitset::from_types(&[AstType::CallExpression]));
19711973
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run;
19721974
}
19731975

19741976
impl RuleRunner for crate::rules::promise::no_multiple_resolved::NoMultipleResolved {
1975-
const NODE_TYPES: Option<&AstTypesBitset> = None;
1977+
const NODE_TYPES: Option<&AstTypesBitset> =
1978+
Some(&AstTypesBitset::from_types(&[AstType::NewExpression]));
19761979
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run;
19771980
}
19781981

tasks/linter_codegen/src/let_else_detector.rs

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ impl LetElseDetector {
2121
let Stmt::Local(local) = first_stmt else { return None };
2222
// Must have an initializer that is a `node.kind()` call
2323
let Some(init) = &local.init else { return None };
24-
if !is_node_kind_call(&init.expr) {
24+
if !(is_node_kind_call(&init.expr) || is_node_kind_as_call(&init.expr)) {
2525
return None;
2626
}
2727
// Must have a diverging `else` block
@@ -38,8 +38,23 @@ impl LetElseDetector {
3838
}
3939

4040
let mut detector = Self { node_types: NodeTypeSet::new() };
41-
let result = detector.extract_variants_from_pat(&local.pat);
42-
if detector.node_types.is_empty() || result == CollectionResult::Incomplete {
41+
42+
if is_node_kind_as_call(&init.expr) {
43+
// If the initializer is `node.kind().as_<variant>()`, extract that variant.
44+
if let Expr::MethodCall(mc) = &*init.expr
45+
&& let Some(variant) = extract_variant_from_as_call(mc)
46+
{
47+
detector.node_types.insert(variant);
48+
}
49+
} else {
50+
// Otherwise, the initializer is `node.kind()`, so extract from the pattern.
51+
// Expecting `AstKind::Variant` pattern
52+
let result = detector.extract_variants_from_pat(&local.pat);
53+
if result == CollectionResult::Incomplete {
54+
return None;
55+
}
56+
}
57+
if detector.node_types.is_empty() {
4358
return None;
4459
}
4560

@@ -60,3 +75,45 @@ impl LetElseDetector {
6075
}
6176
}
6277
}
78+
79+
/// Checks if is `node.kind().as_some_ast_kind()`
80+
pub fn is_node_kind_as_call(expr: &Expr) -> bool {
81+
if let Expr::MethodCall(mc) = expr
82+
&& mc.method.to_string().starts_with("as_")
83+
&& mc.args.is_empty()
84+
&& is_node_kind_call(&mc.receiver)
85+
{
86+
return true;
87+
}
88+
false
89+
}
90+
91+
fn extract_variant_from_as_call(mc: &syn::ExprMethodCall) -> Option<String> {
92+
// Looking for `node.kind().as_<snake_case_variant>()`
93+
let method_ident = mc.method.to_string();
94+
if !method_ident.starts_with("as_") || !mc.args.is_empty() {
95+
return None;
96+
}
97+
// Receiver must be `node.kind()`
98+
if !is_node_kind_call(&mc.receiver) {
99+
return None;
100+
}
101+
let snake_variant = &method_ident[3..]; // strip `as_`
102+
if snake_variant == "member_expression_kind" {
103+
return None;
104+
}
105+
Some(snake_to_pascal_case(snake_variant))
106+
}
107+
108+
fn snake_to_pascal_case(s: &str) -> String {
109+
s.split('_')
110+
.filter(|seg| !seg.is_empty())
111+
.map(|seg| {
112+
let mut chars = seg.chars();
113+
match chars.next() {
114+
Some(first) => first.to_ascii_uppercase().to_string() + chars.as_str(),
115+
None => String::new(),
116+
}
117+
})
118+
.collect()
119+
}

0 commit comments

Comments
 (0)