diff --git a/crates/oxc/Cargo.toml b/crates/oxc/Cargo.toml index b6840defd9ffb..3c240a11dfe01 100644 --- a/crates/oxc/Cargo.toml +++ b/crates/oxc/Cargo.toml @@ -54,7 +54,6 @@ full = [ "transformer", "isolated_declarations", "ast_visit", - "cfg", "regular_expression", ] @@ -63,7 +62,7 @@ transformer = ["oxc_transformer", "oxc_transformer_plugins"] minifier = ["oxc_mangler", "oxc_minifier"] codegen = ["oxc_codegen"] mangler = ["oxc_mangler"] -cfg = ["oxc_cfg"] +cfg = ["oxc_cfg", "oxc_semantic/cfg"] isolated_declarations = ["oxc_isolated_declarations"] ast_visit = ["oxc_ast_visit"] regular_expression = ["oxc_regular_expression", "oxc_parser/regular_expression"] diff --git a/crates/oxc_linter/Cargo.toml b/crates/oxc_linter/Cargo.toml index 584a9f22e953a..0d9056ace61ac 100644 --- a/crates/oxc_linter/Cargo.toml +++ b/crates/oxc_linter/Cargo.toml @@ -40,7 +40,7 @@ oxc_macros = { workspace = true, features = ["ruledocs"] } oxc_parser = { workspace = true } oxc_regular_expression = { workspace = true } oxc_resolver = { workspace = true } -oxc_semantic = { workspace = true } +oxc_semantic = { workspace = true, features = ["cfg"] } oxc_span = { workspace = true, features = ["schemars", "serialize"] } oxc_syntax = { workspace = true, features = ["serialize"] } diff --git a/crates/oxc_semantic/Cargo.toml b/crates/oxc_semantic/Cargo.toml index 93fd5206bbfc9..37ab3be412260 100644 --- a/crates/oxc_semantic/Cargo.toml +++ b/crates/oxc_semantic/Cargo.toml @@ -22,7 +22,7 @@ doctest = true oxc_allocator = { workspace = true } oxc_ast = { workspace = true } oxc_ast_visit = { workspace = true } -oxc_cfg = { workspace = true } +oxc_cfg = { workspace = true, optional = true } oxc_data_structures = { workspace = true, features = ["assert_unchecked"] } oxc_diagnostics = { workspace = true } oxc_ecmascript = { workspace = true } @@ -44,4 +44,5 @@ serde_json = { workspace = true } [features] default = [] +cfg = ["dep:oxc_cfg"] serialize = ["oxc_span/serialize", "oxc_syntax/serialize"] diff --git a/crates/oxc_semantic/examples/cfg.rs b/crates/oxc_semantic/examples/cfg.rs index e8d8b14935263..6d0842789c399 100644 --- a/crates/oxc_semantic/examples/cfg.rs +++ b/crates/oxc_semantic/examples/cfg.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "cfg")] #![expect(clippy::print_stdout)] //! # Control Flow Graph (CFG) Example //! diff --git a/crates/oxc_semantic/src/builder.rs b/crates/oxc_semantic/src/builder.rs index 72efa3b9ca517..c1e91a5f0f0e8 100644 --- a/crates/oxc_semantic/src/builder.rs +++ b/crates/oxc_semantic/src/builder.rs @@ -10,6 +10,7 @@ use rustc_hash::FxHashMap; use oxc_allocator::Address; use oxc_ast::{AstKind, ast::*}; use oxc_ast_visit::Visit; +#[cfg(feature = "cfg")] use oxc_cfg::{ ControlFlowGraphBuilder, CtxCursor, CtxFlags, EdgeType, ErrorEdgeKind, InstructionKind, IterationInstructionKind, ReturnInstructionKind, @@ -37,12 +38,21 @@ use crate::{ unresolved_stack::UnresolvedReferencesStack, }; +#[cfg(feature = "cfg")] macro_rules! control_flow { ($self:ident, |$cfg:tt| $body:expr) => { if let Some($cfg) = &mut $self.cfg { $body } else { Default::default() } }; } +#[cfg(not(feature = "cfg"))] +macro_rules! control_flow { + ($self:ident, |$cfg:tt| $body:expr) => {{ + let _ = $self; // Suppress unused variable warning + () + }}; +} + /// Semantic Builder /// /// Traverses a parsed AST and builds a [`Semantic`] representation of the @@ -89,10 +99,15 @@ pub struct SemanticBuilder<'a> { /// See: [`crate::checker::check`] check_syntax_error: bool, + #[cfg(feature = "cfg")] pub(crate) cfg: Option>, + #[cfg(not(feature = "cfg"))] + #[allow(unused)] + pub(crate) cfg: (), pub(crate) class_table_builder: ClassTableBuilder<'a>, + #[cfg(feature = "cfg")] ast_node_records: Vec, } @@ -133,8 +148,12 @@ impl<'a> SemanticBuilder<'a> { stats: None, excess_capacity: 0.0, check_syntax_error: false, + #[cfg(feature = "cfg")] cfg: None, + #[cfg(not(feature = "cfg"))] + cfg: (), class_table_builder: ClassTableBuilder::new(), + #[cfg(feature = "cfg")] ast_node_records: Vec::new(), } } @@ -163,11 +182,17 @@ impl<'a> SemanticBuilder<'a> { /// /// [`ControlFlowGraph`]: oxc_cfg::ControlFlowGraph #[must_use] + #[cfg(feature = "cfg")] pub fn with_cfg(mut self, cfg: bool) -> Self { self.cfg = if cfg { Some(ControlFlowGraphBuilder::default()) } else { None }; self } + #[cfg(not(feature = "cfg"))] + pub fn with_cfg(self, _cfg: bool) -> Self { + self + } + #[must_use] pub fn with_scope_tree_child_ids(mut self, yes: bool) -> Self { self.scoping.scope_build_child_ids = yes; @@ -280,7 +305,10 @@ impl<'a> SemanticBuilder<'a> { classes: self.class_table_builder.build(), jsdoc, unused_labels: self.unused_labels.labels, + #[cfg(feature = "cfg")] cfg: self.cfg.map(ControlFlowGraphBuilder::build), + #[cfg(not(feature = "cfg"))] + cfg: (), }; SemanticBuilderReturn { semantic, errors: self.errors.into_inner() } } @@ -320,6 +348,7 @@ impl<'a> SemanticBuilder<'a> { } #[inline] + #[cfg(feature = "cfg")] fn record_ast_nodes(&mut self) { if self.cfg.is_some() { self.ast_node_records.push(NodeId::DUMMY); @@ -327,6 +356,7 @@ impl<'a> SemanticBuilder<'a> { } #[inline] + #[cfg(feature = "cfg")] fn retrieve_recorded_ast_node(&mut self) -> Option { if self.cfg.is_some() { Some(self.ast_node_records.pop().expect("there is no ast node record to stop.")) @@ -336,6 +366,7 @@ impl<'a> SemanticBuilder<'a> { } #[inline] + #[cfg(feature = "cfg")] fn record_ast_node(&mut self) { // The `self.cfg.is_some()` check here could be removed, since `ast_node_records` is empty // if CFG is disabled. But benchmarks showed removing the extra check is a perf regression. @@ -349,6 +380,10 @@ impl<'a> SemanticBuilder<'a> { } } + #[inline] + #[cfg(not(feature = "cfg"))] + fn record_ast_node(&mut self) {} + #[inline] pub(crate) fn current_scope_flags(&self) -> ScopeFlags { self.scoping.scope_flags(self.current_scope_id) @@ -599,6 +634,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { fn visit_program(&mut self, program: &Program<'a>) { let kind = AstKind::Program(self.alloc(program)); /* cfg */ + #[cfg(feature = "cfg")] let error_harness = control_flow!(self, |cfg| { let error_harness = cfg.attach_error_harness(ErrorEdgeKind::Implicit); let _program_basic_block = cfg.new_basic_block_normal(); @@ -661,6 +697,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.enter_node(kind); /* cfg */ + #[cfg(feature = "cfg")] let node_id = self.current_node_id; /* cfg */ @@ -752,6 +789,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.enter_node(kind); /* cfg */ + #[cfg(feature = "cfg")] let node_id = self.current_node_id; /* cfg */ @@ -772,6 +810,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.enter_node(kind); /* cfg */ + #[cfg(feature = "cfg")] let (before_do_while_stmt_graph_ix, start_body_graph_ix) = control_flow!(self, |cfg| { let before_do_while_stmt_graph_ix = cfg.current_node_ix; let start_body_graph_ix = cfg.new_basic_block_normal(); @@ -783,6 +822,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.visit_statement(&stmt.body); /* cfg - condition basic block */ + #[cfg(feature = "cfg")] let (after_body_graph_ix, start_of_condition_graph_ix) = control_flow!(self, |cfg| { let after_body_graph_ix = cfg.current_node_ix; let start_of_condition_graph_ix = cfg.new_basic_block_normal(); @@ -790,8 +830,10 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { }); /* cfg */ + #[cfg(feature = "cfg")] self.record_ast_nodes(); self.visit_expression(&stmt.test); + #[cfg(feature = "cfg")] let test_node_id = self.retrieve_recorded_ast_node(); /* cfg */ @@ -832,6 +874,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.visit_expression(&expr.left); /* cfg */ + #[cfg(feature = "cfg")] let (left_expr_end_ix, right_expr_start_ix) = control_flow!(self, |cfg| { let left_expr_end_ix = cfg.current_node_ix; let right_expr_start_ix = cfg.new_basic_block_normal(); @@ -874,6 +917,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.visit_assignment_target(&expr.left); /* cfg */ + #[cfg(feature = "cfg")] let cfg_ixs = control_flow!(self, |cfg| { if expr.operator.is_logical() { let target_end_ix = cfg.current_node_ix; @@ -908,6 +952,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.enter_node(kind); /* cfg - condition basic block */ + #[cfg(feature = "cfg")] let (before_conditional_graph_ix, start_of_condition_graph_ix) = control_flow!(self, |cfg| { let before_conditional_graph_ix = cfg.current_node_ix; @@ -916,11 +961,14 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { }); /* cfg */ + #[cfg(feature = "cfg")] self.record_ast_nodes(); self.visit_expression(&expr.test); + #[cfg(feature = "cfg")] let test_node_id = self.retrieve_recorded_ast_node(); /* cfg */ + #[cfg(feature = "cfg")] let (after_condition_graph_ix, before_consequent_expr_graph_ix) = control_flow!(self, |cfg| { cfg.append_condition_to(start_of_condition_graph_ix, test_node_id); @@ -934,6 +982,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.visit_expression(&expr.consequent); /* cfg */ + #[cfg(feature = "cfg")] let (after_consequent_expr_graph_ix, start_alternate_graph_ix) = control_flow!(self, |cfg| { let after_consequent_expr_graph_ix = cfg.current_node_ix; @@ -979,6 +1028,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.visit_for_statement_init(init); } /* cfg */ + #[cfg(feature = "cfg")] let (before_for_graph_ix, test_graph_ix) = control_flow!(self, |cfg| { let before_for_graph_ix = cfg.current_node_ix; let test_graph_ix = cfg.new_basic_block_normal(); @@ -987,8 +1037,10 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ if let Some(test) = &stmt.test { + #[cfg(feature = "cfg")] self.record_ast_nodes(); self.visit_expression(test); + #[cfg(feature = "cfg")] let test_node_id = self.retrieve_recorded_ast_node(); /* cfg */ @@ -997,6 +1049,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { } /* cfg */ + #[cfg(feature = "cfg")] let (after_test_graph_ix, update_graph_ix) = control_flow!(self, |cfg| (cfg.current_node_ix, cfg.new_basic_block_normal())); /* cfg */ @@ -1006,6 +1059,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { } /* cfg */ + #[cfg(feature = "cfg")] let before_body_graph_ix = control_flow!(self, |cfg| { let before_body_graph_ix = cfg.new_basic_block_normal(); cfg.ctx(None).default().allow_break().allow_continue(); @@ -1044,15 +1098,19 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.visit_for_statement_left(&stmt.left); /* cfg */ + #[cfg(feature = "cfg")] let (before_for_stmt_graph_ix, start_prepare_cond_graph_ix) = control_flow!(self, |cfg| (cfg.current_node_ix, cfg.new_basic_block_normal(),)); /* cfg */ + #[cfg(feature = "cfg")] self.record_ast_nodes(); self.visit_expression(&stmt.right); + #[cfg(feature = "cfg")] let right_node_id = self.retrieve_recorded_ast_node(); /* cfg */ + #[cfg(feature = "cfg")] let (end_of_prepare_cond_graph_ix, iteration_graph_ix, body_graph_ix) = control_flow!(self, |cfg| { let end_of_prepare_cond_graph_ix = cfg.current_node_ix; @@ -1103,15 +1161,19 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.visit_for_statement_left(&stmt.left); /* cfg */ + #[cfg(feature = "cfg")] let (before_for_stmt_graph_ix, start_prepare_cond_graph_ix) = control_flow!(self, |cfg| (cfg.current_node_ix, cfg.new_basic_block_normal())); /* cfg */ + #[cfg(feature = "cfg")] self.record_ast_nodes(); self.visit_expression(&stmt.right); + #[cfg(feature = "cfg")] let right_node_id = self.retrieve_recorded_ast_node(); /* cfg */ + #[cfg(feature = "cfg")] let (end_of_prepare_cond_graph_ix, iteration_graph_ix, body_graph_ix) = control_flow!(self, |cfg| { let end_of_prepare_cond_graph_ix = cfg.current_node_ix; @@ -1158,15 +1220,19 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.enter_node(kind); /* cfg - condition basic block */ + #[cfg(feature = "cfg")] let (before_if_stmt_graph_ix, start_of_condition_graph_ix) = control_flow!(self, |cfg| (cfg.current_node_ix, cfg.new_basic_block_normal(),)); /* cfg */ + #[cfg(feature = "cfg")] self.record_ast_nodes(); self.visit_expression(&stmt.test); + #[cfg(feature = "cfg")] let test_node_id = self.retrieve_recorded_ast_node(); /* cfg */ + #[cfg(feature = "cfg")] let (after_test_graph_ix, before_consequent_stmt_graph_ix) = control_flow!(self, |cfg| { cfg.append_condition_to(start_of_condition_graph_ix, test_node_id); (cfg.current_node_ix, cfg.new_basic_block_normal()) @@ -1176,9 +1242,11 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.visit_statement(&stmt.consequent); /* cfg */ + #[cfg(feature = "cfg")] let after_consequent_stmt_graph_ix = control_flow!(self, |cfg| cfg.current_node_ix); /* cfg */ + #[cfg(feature = "cfg")] let else_graph_ix = if let Some(alternate) = &stmt.alternate { /* cfg */ let else_graph_ix = control_flow!(self, |cfg| cfg.new_basic_block_normal()); @@ -1191,6 +1259,11 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { None }; + #[cfg(not(feature = "cfg"))] + if let Some(alternate) = &stmt.alternate { + self.visit_statement(alternate); + } + /* cfg - bb after if statement joins consequent and alternate */ control_flow!(self, |cfg| { let after_if_graph_ix = cfg.new_basic_block_normal(); @@ -1221,6 +1294,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.unused_labels.add(stmt.label.name.as_str(), self.current_node_id); /* cfg */ + #[cfg(feature = "cfg")] let label = &stmt.label.name; control_flow!(self, |cfg| { let ctx = cfg.ctx(Some(label.as_str())).default().allow_break(); @@ -1253,9 +1327,11 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.enter_node(kind); /* cfg */ + #[cfg(feature = "cfg")] let node_id = self.current_node_id; /* cfg */ + #[cfg(feature = "cfg")] let ret_kind = if let Some(arg) = &stmt.argument { self.visit_expression(arg); ReturnInstructionKind::NotImplicitUndefined @@ -1263,6 +1339,11 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { ReturnInstructionKind::ImplicitUndefined }; + #[cfg(not(feature = "cfg"))] + if let Some(arg) = &stmt.argument { + self.visit_expression(arg); + } + /* cfg */ control_flow!(self, |cfg| { cfg.push_return(ret_kind, Some(node_id)); @@ -1280,18 +1361,23 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.enter_scope(ScopeFlags::empty(), &stmt.scope_id); /* cfg */ + #[cfg(feature = "cfg")] let discriminant_graph_ix = control_flow!(self, |cfg| { let discriminant_graph_ix = cfg.current_node_ix; cfg.ctx(None).default().allow_break(); discriminant_graph_ix }); + #[cfg(feature = "cfg")] let mut switch_case_graph_spans = vec![]; + #[cfg(feature = "cfg")] let mut have_default_case = false; /* cfg */ for case in &stmt.cases { + #[cfg(feature = "cfg")] let before_case_graph_ix = control_flow!(self, |cfg| cfg.new_basic_block_normal()); self.visit_switch_case(case); + #[cfg(feature = "cfg")] if case.is_default_case() { have_default_case = true; } @@ -1352,8 +1438,10 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.enter_node(kind); if let Some(expr) = &case.test { + #[cfg(feature = "cfg")] self.record_ast_nodes(); self.visit_expression(expr); + #[cfg(feature = "cfg")] let test_node_id = self.retrieve_recorded_ast_node(); control_flow!(self, |cfg| cfg.append_condition_to(cfg.current_node_ix, test_node_id)); } @@ -1376,6 +1464,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.enter_node(kind); /* cfg */ + #[cfg(feature = "cfg")] let node_id = self.current_node_id; /* cfg */ @@ -1394,6 +1483,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ + #[cfg(feature = "cfg")] let ( before_try_statement_graph_ix, error_harness, @@ -1418,9 +1508,11 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.visit_block_statement(&stmt.block); /* cfg */ + #[cfg(feature = "cfg")] let after_try_block_graph_ix = control_flow!(self, |cfg| cfg.current_node_ix); /* cfg */ + #[cfg(feature = "cfg")] let catch_block_end_ix = if let Some(handler) = &stmt.handler { /* cfg */ control_flow!(self, |cfg| { @@ -1447,6 +1539,12 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { None }; + #[cfg(not(feature = "cfg"))] + if let Some(handler) = &stmt.handler { + self.visit_catch_clause(handler); + } + + #[cfg(feature = "cfg")] let finally_block_end_ix = if let Some(finalizer) = &stmt.finalizer { /* cfg */ control_flow!(self, |cfg| { @@ -1473,6 +1571,11 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { None }; + #[cfg(not(feature = "cfg"))] + if let Some(finalizer) = &stmt.finalizer { + self.visit_block_statement(finalizer); + } + /* cfg */ control_flow!(self, |cfg| { let after_try_statement_block_ix = cfg.new_basic_block_normal(); @@ -1526,15 +1629,19 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.enter_node(kind); /* cfg - condition basic block */ + #[cfg(feature = "cfg")] let (before_while_stmt_graph_ix, condition_graph_ix) = control_flow!(self, |cfg| (cfg.current_node_ix, cfg.new_basic_block_normal())); /* cfg */ + #[cfg(feature = "cfg")] self.record_ast_nodes(); self.visit_expression(&stmt.test); + #[cfg(feature = "cfg")] let test_node_id = self.retrieve_recorded_ast_node(); /* cfg - body basic block */ + #[cfg(feature = "cfg")] let body_graph_ix = control_flow!(self, |cfg| { cfg.append_condition_to(condition_graph_ix, test_node_id); let body_graph_ix = cfg.new_basic_block_normal(); @@ -1570,6 +1677,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.enter_node(kind); /* cfg - condition basic block */ + #[cfg(feature = "cfg")] let (before_with_stmt_graph_ix, condition_graph_ix) = control_flow!(self, |cfg| (cfg.current_node_ix, cfg.new_basic_block_normal())); /* cfg */ @@ -1577,6 +1685,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.visit_expression(&stmt.object); /* cfg - body basic block */ + #[cfg(feature = "cfg")] let body_graph_ix = control_flow!(self, |cfg| cfg.new_basic_block_normal()); /* cfg */ @@ -1600,6 +1709,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ // We add a new basic block to the cfg before entering the node // so that the correct cfg_ix is associated with the ast node. + #[cfg(feature = "cfg")] let (before_function_graph_ix, error_harness, function_graph_ix) = control_flow!(self, |cfg| { let before_function_graph_ix = cfg.current_node_ix; @@ -1705,6 +1815,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ // We add a new basic block to the cfg before entering the node // so that the correct cfg_ix is associated with the ast node. + #[cfg(feature = "cfg")] let (current_node_ix, error_harness, function_graph_ix) = control_flow!(self, |cfg| { let current_node_ix = cfg.current_node_ix; cfg.push_finalization_stack(); diff --git a/crates/oxc_semantic/src/lib.rs b/crates/oxc_semantic/src/lib.rs index 6e2f7d4dec749..ccb23611df374 100644 --- a/crates/oxc_semantic/src/lib.rs +++ b/crates/oxc_semantic/src/lib.rs @@ -10,6 +10,7 @@ use std::ops::RangeBounds; use oxc_ast::{ AstKind, Comment, CommentsRange, ast::IdentifierReference, comments_range, has_comments_between, }; +#[cfg(feature = "cfg")] use oxc_cfg::ControlFlowGraph; use oxc_span::{GetSpan, SourceType, Span}; // Re-export flags and ID types @@ -20,6 +21,7 @@ pub use oxc_syntax::{ symbol::{SymbolFlags, SymbolId}, }; +#[cfg(feature = "cfg")] pub mod dot; mod ast_types_bitset; @@ -82,7 +84,11 @@ pub struct Semantic<'a> { /// Control flow graph. Only present if [`Semantic`] is built with cfg /// creation enabled using [`SemanticBuilder::with_cfg`]. + #[cfg(feature = "cfg")] cfg: Option, + #[cfg(not(feature = "cfg"))] + #[allow(unused)] + cfg: (), } impl<'a> Semantic<'a> { @@ -166,10 +172,16 @@ impl<'a> Semantic<'a> { /// /// Only present if [`Semantic`] is built with cfg creation enabled using /// [`SemanticBuilder::with_cfg`]. + #[cfg(feature = "cfg")] pub fn cfg(&self) -> Option<&ControlFlowGraph> { self.cfg.as_ref() } + #[cfg(not(feature = "cfg"))] + pub fn cfg(&self) -> Option<&()> { + None + } + /// Get statistics about data held in `Semantic`. pub fn stats(&self) -> Stats { #[expect(clippy::cast_possible_truncation)] diff --git a/crates/oxc_semantic/src/node.rs b/crates/oxc_semantic/src/node.rs index 35610cf5b3e16..65f97d9ec9b5e 100644 --- a/crates/oxc_semantic/src/node.rs +++ b/crates/oxc_semantic/src/node.rs @@ -2,6 +2,7 @@ use std::iter::FusedIterator; use oxc_allocator::{Address, GetAddress}; use oxc_ast::{AstKind, AstType, ast::Program}; +#[cfg(feature = "cfg")] use oxc_cfg::BlockNodeId; use oxc_index::{IndexSlice, IndexVec}; use oxc_span::{GetSpan, Span}; @@ -23,6 +24,7 @@ pub struct AstNode<'a> { scope_id: ScopeId, /// Associated `BasicBlockId` in CFG (initialized by control_flow) + #[cfg(feature = "cfg")] cfg_id: BlockNodeId, flags: NodeFlags, @@ -30,6 +32,7 @@ pub struct AstNode<'a> { impl<'a> AstNode<'a> { #[inline] + #[cfg(feature = "cfg")] pub(crate) fn new( kind: AstKind<'a>, scope_id: ScopeId, @@ -40,6 +43,17 @@ impl<'a> AstNode<'a> { Self { id, kind, scope_id, cfg_id, flags } } + #[cfg(not(feature = "cfg"))] + pub(crate) fn new( + kind: AstKind<'a>, + scope_id: ScopeId, + _cfg_id: (), + flags: NodeFlags, + id: NodeId, + ) -> Self { + Self { id, kind, scope_id, flags } + } + /// This node's unique identifier. #[inline] pub fn id(&self) -> NodeId { @@ -50,6 +64,7 @@ impl<'a> AstNode<'a> { /// /// See [oxc_cfg::ControlFlowGraph] for more information. #[inline] + #[cfg(feature = "cfg")] pub fn cfg_id(&self) -> BlockNodeId { self.cfg_id } @@ -207,6 +222,7 @@ impl<'a> AstNodes<'a> { /// [`Program`]: oxc_ast::ast::Program /// [`add_program_node`]: AstNodes::add_program_node #[inline] + #[cfg(feature = "cfg")] pub fn add_node( &mut self, kind: AstKind<'a>, @@ -222,11 +238,29 @@ impl<'a> AstNodes<'a> { node_id } + #[inline] + #[cfg(not(feature = "cfg"))] + pub fn add_node( + &mut self, + kind: AstKind<'a>, + scope_id: ScopeId, + parent_node_id: NodeId, + _cfg_id: (), + flags: NodeFlags, + ) -> NodeId { + let node_id = self.parent_ids.push(parent_node_id); + let node = AstNode::new(kind, scope_id, (), flags, node_id); + self.nodes.push(node); + self.node_kinds_set.set(kind.ty()); + node_id + } + /// Create and add an [`AstNode`] to the [`AstNodes`] tree and get its [`NodeId`]. /// /// # Panics /// /// Panics if this is not the first node being added to the AST. + #[cfg(feature = "cfg")] pub fn add_program_node( &mut self, kind: AstKind<'a>, @@ -245,6 +279,25 @@ impl<'a> AstNodes<'a> { NodeId::ROOT } + #[cfg(not(feature = "cfg"))] + pub fn add_program_node( + &mut self, + kind: AstKind<'a>, + scope_id: ScopeId, + _cfg_id: (), + flags: NodeFlags, + ) -> NodeId { + assert!(self.parent_ids.is_empty(), "Program node must be the first node in the AST."); + debug_assert!( + matches!(kind, AstKind::Program(_)), + "Program node must be of kind `AstKind::Program`" + ); + self.parent_ids.push(NodeId::ROOT); + self.nodes.push(AstNode::new(kind, scope_id, (), flags, NodeId::ROOT)); + self.node_kinds_set.set(AstType::Program); + NodeId::ROOT + } + /// Reserve space for at least `additional` more nodes. pub fn reserve(&mut self, additional: usize) { self.nodes.reserve(additional); diff --git a/crates/oxc_semantic/tests/integration/cfg.rs b/crates/oxc_semantic/tests/integration/cfg.rs index eb3be46ce7737..5c23080cddac8 100644 --- a/crates/oxc_semantic/tests/integration/cfg.rs +++ b/crates/oxc_semantic/tests/integration/cfg.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "cfg")] + use std::fs; use oxc_span::SourceType; diff --git a/crates/oxc_semantic/tests/integration/util/mod.rs b/crates/oxc_semantic/tests/integration/util/mod.rs index 60471770fa1fa..569b32348270e 100644 --- a/crates/oxc_semantic/tests/integration/util/mod.rs +++ b/crates/oxc_semantic/tests/integration/util/mod.rs @@ -3,9 +3,12 @@ use std::sync::Arc; use itertools::Itertools; use oxc_allocator::Allocator; +#[cfg(feature = "cfg")] use oxc_cfg::DisplayDot; use oxc_diagnostics::{Error, NamedSource, OxcDiagnostic}; -use oxc_semantic::{Semantic, SemanticBuilder, SemanticBuilderReturn, dot::DebugDot}; +#[cfg(feature = "cfg")] +use oxc_semantic::dot::DebugDot; +use oxc_semantic::{Semantic, SemanticBuilder, SemanticBuilderReturn}; use oxc_span::SourceType; mod class_tester; @@ -176,11 +179,18 @@ impl<'a> SemanticTester<'a> { .build(self.allocator.alloc(parse.program)) } + #[cfg(feature = "cfg")] pub fn basic_blocks_count(&self) -> usize { let built = self.build(); built.cfg().map_or(0, |cfg| cfg.basic_blocks.len()) } + #[cfg(not(feature = "cfg"))] + pub fn basic_blocks_count(&self) -> usize { + 0 + } + + #[cfg(feature = "cfg")] pub fn basic_blocks_printed(&self) -> String { let built = self.build(); built.cfg().map_or_else(String::default, |cfg| { @@ -198,11 +208,22 @@ impl<'a> SemanticTester<'a> { }) } + #[cfg(not(feature = "cfg"))] + pub fn basic_blocks_printed(&self) -> String { + String::default() + } + + #[cfg(feature = "cfg")] pub fn cfg_dot_diagram(&self) -> String { let semantic = self.build(); semantic.cfg().map_or_else(String::default, |cfg| cfg.debug_dot(semantic.nodes().into())) } + #[cfg(not(feature = "cfg"))] + pub fn cfg_dot_diagram(&self) -> String { + String::default() + } + /// Tests that a symbol with the given name exists at the top-level scope and provides a /// wrapper for writing assertions about the found symbol. /// diff --git a/napi/playground/Cargo.toml b/napi/playground/Cargo.toml index a8cb4cfb61173..4da081d160bae 100644 --- a/napi/playground/Cargo.toml +++ b/napi/playground/Cargo.toml @@ -22,7 +22,7 @@ test = false doctest = false [dependencies] -oxc = { workspace = true, features = ["ast_visit", "codegen", "minifier", "mangler", "semantic", "serialize", "transformer", "isolated_declarations", "regular_expression"] } +oxc = { workspace = true, features = ["ast_visit", "codegen", "minifier", "mangler", "semantic", "serialize", "transformer", "isolated_declarations", "regular_expression", "cfg"] } oxc_formatter = { workspace = true } oxc_index = { workspace = true } oxc_linter = { workspace = true } diff --git a/tasks/benchmark/Cargo.toml b/tasks/benchmark/Cargo.toml index ce84226483dd3..dd65e8a3a30ff 100644 --- a/tasks/benchmark/Cargo.toml +++ b/tasks/benchmark/Cargo.toml @@ -123,7 +123,13 @@ transformer = [ "dep:oxc_tasks_common", "dep:oxc_transformer", ] -semantic = ["dep:oxc_allocator", "dep:oxc_parser", "dep:oxc_semantic", "dep:oxc_span", "dep:oxc_tasks_common"] +semantic = [ + "dep:oxc_allocator", + "dep:oxc_parser", + "dep:oxc_semantic", + "dep:oxc_span", + "dep:oxc_tasks_common", +] minifier = [ "dep:oxc_allocator", "dep:oxc_minifier", @@ -150,5 +156,6 @@ linter = [ "dep:oxc_semantic", "dep:oxc_span", "dep:oxc_tasks_common", + "oxc_semantic/cfg", ] formatter = ["dep:oxc_allocator", "dep:oxc_parser", "dep:oxc_formatter", "dep:oxc_span", "dep:oxc_tasks_common"]