Skip to content
Closed
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/oxc_ast/src/generated/ast_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ pub enum AstType {
JSDocUnknownType = 186,
}

/// The largest `usize` value that can be mapped to an `AstType`.
pub const AST_TYPE_MAX: usize = 186usize;

/// Untyped AST Node Kind
#[derive(Debug, Clone, Copy)]
#[repr(C, u8)]
Expand Down
4 changes: 4 additions & 0 deletions crates/oxc_linter/src/rules/eslint/for_direction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ impl Rule for ForDirection {
);
}
}

fn should_run(&self, ctx: &crate::context::ContextHost) -> bool {
ctx.semantic().nodes().contains(oxc_ast::AstType::ForStatement)
}
}

type UpdateDirection = i32;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ impl Rule for NoAsyncPromiseExecutor {

ctx.diagnostic(no_async_promise_executor_diagnostic(span));
}

fn should_run(&self, ctx: &crate::context::ContextHost) -> bool {
ctx.semantic().nodes().contains(oxc_ast::AstType::NewExpression)
}
}

#[test]
Expand Down
4 changes: 4 additions & 0 deletions crates/oxc_linter/src/rules/eslint/no_caller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ impl Rule for NoCaller {
}
}
}

fn should_run(&self, ctx: &crate::context::ContextHost) -> bool {
ctx.semantic().nodes().contains(oxc_ast::AstType::StaticMemberExpression)
}
}

#[test]
Expand Down
4 changes: 4 additions & 0 deletions crates/oxc_linter/src/rules/eslint/no_class_assign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ impl Rule for NoClassAssign {
}
}
}

fn should_run(&self, ctx: &crate::context::ContextHost) -> bool {
ctx.semantic().nodes().contains(oxc_ast::AstType::Class)
}
}

#[test]
Expand Down
8 changes: 8 additions & 0 deletions crates/oxc_linter/src/rules/eslint/no_compare_neg_zero.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ impl Rule for NoCompareNegZero {
}
}
}

fn should_run(&self, ctx: &crate::context::ContextHost) -> bool {
ctx.semantic().nodes().contains(oxc_ast::AstType::BinaryExpression)
&& ctx
.semantic()
.nodes()
.contains_any(&[oxc_ast::AstType::NumericLiteral, oxc_ast::AstType::BigIntLiteral])
}
Comment on lines +114 to +120
Copy link
Member

@overlookmotel overlookmotel Jul 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From benchmarks, obviously some of these changes are having a positive effect.

But some of these methods may be of limited value. How many ASTs don't contain a single BinaryExpression and not a single NumericLiteral?

I imagine most of the gain is from rarer types e.g. AstType::Class and AstType::DebuggerStatement.

You may want to "pick your fights".

}

impl NoCompareNegZero {
Expand Down
11 changes: 11 additions & 0 deletions crates/oxc_linter/src/rules/eslint/no_cond_assign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,17 @@ impl Rule for NoCondAssign {
_ => {}
}
}

fn should_run(&self, ctx: &crate::context::ContextHost) -> bool {
ctx.semantic().nodes().contains(oxc_ast::AstType::AssignmentExpression)
&& ctx.semantic().nodes().contains_any(&[
oxc_ast::AstType::IfStatement,
oxc_ast::AstType::WhileStatement,
oxc_ast::AstType::DoWhileStatement,
oxc_ast::AstType::ForStatement,
oxc_ast::AstType::ConditionalExpression,
])
}
}

impl NoCondAssign {
Expand Down
4 changes: 4 additions & 0 deletions crates/oxc_linter/src/rules/eslint/no_const_assign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ impl Rule for NoConstAssign {
}
}
}

fn should_run(&self, ctx: &crate::context::ContextHost) -> bool {
ctx.semantic().nodes().contains(oxc_ast::AstType::VariableDeclaration)
}
}

#[test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ impl Rule for NoConstantBinaryExpression {
_ => {}
}
}

fn should_run(&self, ctx: &crate::context::ContextHost) -> bool {
ctx.semantic().nodes().contains_any(&[
oxc_ast::AstType::BinaryExpression,
oxc_ast::AstType::LogicalExpression,
])
}
}

impl NoConstantBinaryExpression {
Expand Down
10 changes: 10 additions & 0 deletions crates/oxc_linter/src/rules/eslint/no_constant_condition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,16 @@ impl Rule for NoConstantCondition {
_ => {}
}
}

fn should_run(&self, ctx: &crate::context::ContextHost) -> bool {
ctx.semantic().nodes().contains_any(&[
oxc_ast::AstType::IfStatement,
oxc_ast::AstType::ConditionalExpression,
oxc_ast::AstType::WhileStatement,
oxc_ast::AstType::DoWhileStatement,
oxc_ast::AstType::ForStatement,
])
}
}

impl NoConstantCondition {
Expand Down
8 changes: 8 additions & 0 deletions crates/oxc_linter/src/rules/eslint/no_control_regex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,14 @@ impl Rule for NoControlRegex {
check_pattern(ctx, pattern, span);
});
}

fn should_run(&self, ctx: &crate::context::ContextHost) -> bool {
ctx.semantic().nodes().contains_any(&[
oxc_ast::AstType::RegExpLiteral,
oxc_ast::AstType::NewExpression,
oxc_ast::AstType::CallExpression,
])
}
}

fn check_pattern(context: &LintContext, pattern: &Pattern, span: Span) {
Expand Down
4 changes: 4 additions & 0 deletions crates/oxc_linter/src/rules/eslint/no_debugger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ impl Rule for NoDebugger {
});
}
}

fn should_run(&self, ctx: &crate::context::ContextHost) -> bool {
ctx.semantic().nodes().contains(oxc_ast::AstType::DebuggerStatement)
}
}

#[test]
Expand Down
4 changes: 4 additions & 0 deletions crates/oxc_linter/src/rules/eslint/no_delete_var.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ impl Rule for NoDeleteVar {
ctx.diagnostic(no_delete_var_diagnostic(expr.span));
}
}

fn should_run(&self, ctx: &crate::context::ContextHost) -> bool {
ctx.semantic().nodes().contains(oxc_ast::AstType::UnaryExpression)
}
}

#[test]
Expand Down
4 changes: 4 additions & 0 deletions crates/oxc_linter/src/rules/eslint/no_dupe_class_members.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ impl Rule for NoDupeClassMembers {
}
});
}

fn should_run(&self, ctx: &crate::context::ContextHost) -> bool {
ctx.semantic().nodes().contains(oxc_ast::AstType::Class)
}
}

#[test]
Expand Down
4 changes: 4 additions & 0 deletions crates/oxc_linter/src/rules/eslint/no_dupe_else_if.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ impl Rule for NoDupeElseIf {
}
}
}

fn should_run(&self, ctx: &crate::context::ContextHost) -> bool {
ctx.semantic().nodes().contains(oxc_ast::AstType::IfStatement)
}
}

fn split_by_or<'a, 'b>(expr: &'a Expression<'b>) -> Vec<&'a Expression<'b>> {
Expand Down
4 changes: 4 additions & 0 deletions crates/oxc_linter/src/rules/eslint/no_dupe_keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ impl Rule for NoDupeKeys {
}
}
}

fn should_run(&self, ctx: &crate::context::ContextHost) -> bool {
ctx.semantic().nodes().contains(oxc_ast::AstType::ObjectExpression)
}
}

fn prop_key_name<'a>(key: &PropertyKey<'a>, ctx: &LintContext<'a>) -> &'a str {
Expand Down
1 change: 1 addition & 0 deletions crates/oxc_semantic/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ oxc_index = { workspace = true }
oxc_span = { workspace = true }
oxc_syntax = { workspace = true }

fixedbitset = { workspace = true }
itertools = { workspace = true }
phf = { workspace = true, features = ["macros"] }
rustc-hash = { workspace = true }
Expand Down
65 changes: 63 additions & 2 deletions crates/oxc_semantic/src/node.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use std::iter::FusedIterator;

use fixedbitset::FixedBitSet;
use oxc_allocator::{Address, GetAddress};
use oxc_ast::{AstKind, ast::Program};
use oxc_ast::{AstKind, AstType, ast::Program, ast_kind::AST_TYPE_MAX};
use oxc_cfg::BlockNodeId;
use oxc_index::{IndexSlice, IndexVec};
use oxc_span::{GetSpan, Span};
Expand Down Expand Up @@ -96,14 +97,29 @@ impl GetAddress for AstNode<'_> {
}

/// Untyped AST nodes flattened into an vec
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct AstNodes<'a> {
program: Option<&'a Program<'a>>,
nodes: IndexVec<NodeId, AstNode<'a>>,
/// Stores a set of bits of a fixed size, where each bit represents a single [`AstKind`]. If the bit is set (1),
/// then the AST contains at least one node of that kind. If the bit is not set (0), then the AST does not contain
/// any nodes of that kind.
node_kinds_set: FixedBitSet,
/// `node` -> `parent`
parent_ids: IndexVec<NodeId, NodeId>,
}

impl Default for AstNodes<'_> {
fn default() -> Self {
Self {
program: None,
nodes: IndexVec::new(),
node_kinds_set: FixedBitSet::with_capacity(AST_TYPE_MAX + 1),
parent_ids: IndexVec::new(),
}
}
}

impl<'a> AstNodes<'a> {
/// Iterate over all [`AstNode`]s in this AST.
pub fn iter(&self) -> impl Iterator<Item = &AstNode<'a>> + '_ {
Expand Down Expand Up @@ -210,6 +226,12 @@ impl<'a> AstNodes<'a> {
let node_id = self.parent_ids.push(parent_node_id);
let node = AstNode::new(kind, scope_id, cfg_id, flags, node_id);
self.nodes.push(node);
debug_assert!((kind.ty() as usize) < self.node_kinds_set.len());
// SAFETY: `AstKind` maps exactly to `AstType`, and there should be exactly
// enough bits to insert it into `node_kinds`, so we can skip a bounds check here.
unsafe {
self.node_kinds_set.insert_unchecked(kind.ty() as usize);
}
node_id
}

Expand All @@ -233,6 +255,12 @@ impl<'a> AstNodes<'a> {
let node_id = self.parent_ids.push(NodeId::ROOT);
let node = AstNode::new(kind, scope_id, cfg_id, flags, node_id);
self.nodes.push(node);
debug_assert!((kind.ty() as usize) < self.node_kinds_set.len());
// SAFETY: `AstKind` maps exactly to `AstType`, and there should be exactly
// enough bits to insert it into `node_kinds`, so we can skip a bounds check here.
unsafe {
self.node_kinds_set.insert_unchecked(kind.ty() as usize);
}
node_id
}

Expand All @@ -241,6 +269,39 @@ impl<'a> AstNodes<'a> {
self.nodes.reserve(additional);
self.parent_ids.reserve(additional);
}

/// Checks if the AST contains any nodes of the given types.
///
/// Example:
/// - `.contains_any(&[AstType::ForStatement])` returns `true` if there is a `for` loop anywhere in the AST.
/// - `.contains_any(&[AstType::ImportDeclaration, AstType::ExportDeclaration])` returns `true` if there are any imports OR exports in the AST.
pub fn contains_any(&self, types: &[AstType]) -> bool {
// SAFETY: We already check that every `AstType` is within the bounds when inserting, so it should
// be safe to access the bitset without bounds checking.
types.iter().any(|ty| unsafe { self.node_kinds_set.contains_unchecked(*ty as usize) })
}

/// Checks if the AST contains all of the given types.
///
/// Example:
/// - `.contains_all(&[AstType::ForStatement])` returns `true` if there is a `for` loop anywhere in the AST.
/// - `.contains_all(&[AstType::ImportDeclaration, AstType::ExportDeclaration])` returns `true` only if there is at least one import AND one export in the AST.
pub fn contains_all(&self, types: &[AstType]) -> bool {
// SAFETY: We already check that every `AstType` is within the bounds when inserting, so it should
// be safe to access the bitset without bounds checking.
types.iter().all(|ty| unsafe { self.node_kinds_set.contains_unchecked(*ty as usize) })
}

/// Checks if the AST contains a node of the given type.
///
/// Example:
/// - `.contains(AstType::ForStatement)` returns `true` if there is a `for` loop anywhere in the AST.
/// - `.contains(AstType::ImportDeclaration)` returns `true` if there is an import in the AST.
pub fn contains(&self, ty: AstType) -> bool {
// SAFETY: We already check that every `AstType` is within the bounds when inserting, so it should
// be safe to access the bitset without bounds checking.
unsafe { self.node_kinds_set.contains_unchecked(ty as usize) }
}
}

impl<'a, 'n> IntoIterator for &'n AstNodes<'a> {
Expand Down
6 changes: 6 additions & 0 deletions tasks/ast_tools/src/generators/ast_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ impl Generator for AstKindGenerator {
let mut span_match_arms = quote!();
let mut address_match_arms = quote!();
let mut as_methods = quote!();
let mut ast_type_max_usize: usize = 0;

let mut next_index = 0u16;
for type_def in &schema.types {
Expand All @@ -104,6 +105,7 @@ impl Generator for AstKindGenerator {

assert!(u8::try_from(next_index).is_ok());
let index = number_lit(next_index);
ast_type_max_usize = next_index as usize;
type_variants.extend(quote!( #type_ident = #index, ));
kind_variants.extend(quote!( #type_ident(&'a #type_ty) = AstType::#type_ident as u8, ));

Expand Down Expand Up @@ -152,6 +154,10 @@ impl Generator for AstKindGenerator {
#type_variants
}

///@@line_break
/// The largest `usize` value that can be mapped to an `AstType`.
pub const AST_TYPE_MAX: usize = #ast_type_max_usize;

///@@line_break
/// Untyped AST Node Kind
#[derive(Debug, Clone, Copy)]
Expand Down
Loading