diff --git a/crates/oxc_ast/src/ast_kind.rs b/crates/oxc_ast/src/ast_kind.rs index 551ee88b61a06..08ddc5a4a7580 100644 --- a/crates/oxc_ast/src/ast_kind.rs +++ b/crates/oxc_ast/src/ast_kind.rs @@ -588,7 +588,7 @@ impl<'a> AstKind<'a> { Self::PrivateIdentifier(x) => format!("PrivateIdentifier({})", x.name).into(), Self::NumericLiteral(n) => format!("NumericLiteral({})", n.value).into(), - Self::StringLiteral(s) => format!("NumericLiteral({})", s.value).into(), + Self::StringLiteral(s) => format!("StringLiteral({})", s.value).into(), Self::BooleanLiteral(b) => format!("BooleanLiteral({})", b.value).into(), Self::NullLiteral(_) => "NullLiteral".into(), Self::BigintLiteral(b) => format!("BigintLiteral({})", b.raw).into(), diff --git a/crates/oxc_linter/src/rules/eslint/getter_return.rs b/crates/oxc_linter/src/rules/eslint/getter_return.rs index 5e1a610941f33..c6abd401eae6d 100644 --- a/crates/oxc_linter/src/rules/eslint/getter_return.rs +++ b/crates/oxc_linter/src/rules/eslint/getter_return.rs @@ -193,6 +193,11 @@ impl GetterReturn { | EdgeType::NewFunction // Unreachable nodes aren't reachable so we don't follow them. | EdgeType::Unreachable + // TODO: For now we ignore the error path to simplify this rule, We can also + // analyze the error path as a nice to have addition. + | EdgeType::Error(_) + | EdgeType::Finalize + | EdgeType::Join // By returning Some(X), // we signal that we don't walk to this path any farther. // diff --git a/crates/oxc_linter/src/rules/eslint/no_this_before_super.rs b/crates/oxc_linter/src/rules/eslint/no_this_before_super.rs index 3c945b89074c0..ab3e0a319342a 100644 --- a/crates/oxc_linter/src/rules/eslint/no_this_before_super.rs +++ b/crates/oxc_linter/src/rules/eslint/no_this_before_super.rs @@ -7,7 +7,10 @@ use oxc_ast::{ AstKind, }; use oxc_macros::declare_oxc_lint; -use oxc_semantic::{pg::neighbors_filtered_by_edge_weight, AstNodeId, BasicBlockId, EdgeType}; +use oxc_semantic::{ + petgraph::visit::EdgeRef, pg::neighbors_filtered_by_edge_weight, AstNodeId, BasicBlockId, + ControlFlowGraph, EdgeType, +}; use oxc_span::{GetSpan, Span}; use crate::{context::LintContext, rule::Rule, AstNode}; @@ -48,6 +51,7 @@ enum DefinitelyCallsThisBeforeSuper { #[default] No, Yes, + Maybe(BasicBlockId), } impl Rule for NoThisBeforeSuper { @@ -99,35 +103,20 @@ impl Rule for NoThisBeforeSuper { // second pass, walk cfg for wanted nodes and propagate // cross-block super calls: for node in wanted_nodes { - let output = neighbors_filtered_by_edge_weight( - &cfg.graph, + let output = Self::analyze( + cfg, node.cfg_id(), - &|edge| match edge { - EdgeType::Jump | EdgeType::Normal => None, - EdgeType::Unreachable | EdgeType::Backedge | EdgeType::NewFunction => { - Some(DefinitelyCallsThisBeforeSuper::No) - } - }, - &mut |basic_block_id, _| { - let super_called = basic_blocks_with_super_called.contains(basic_block_id); - if basic_blocks_with_local_violations.contains_key(basic_block_id) { - // super was not called before this in the current code path: - return (DefinitelyCallsThisBeforeSuper::Yes, false); - } - - if super_called { - (DefinitelyCallsThisBeforeSuper::No, false) - } else { - (DefinitelyCallsThisBeforeSuper::No, true) - } - }, + &basic_blocks_with_super_called, + &basic_blocks_with_local_violations, + false, ); - // Deciding whether we definitely call this before super in all - // codepaths is as simple as seeing if any individual codepath - // definitely calls this before super. - let violation_in_any_codepath = - output.into_iter().any(|y| matches!(y, DefinitelyCallsThisBeforeSuper::Yes)); + let violation_in_any_codepath = Self::check_for_violation( + cfg, + output, + &basic_blocks_with_super_called, + &basic_blocks_with_local_violations, + ); // If not, flag it as a diagnostic. if violation_in_any_codepath { @@ -163,6 +152,108 @@ impl NoThisBeforeSuper { false } + + fn analyze( + cfg: &ControlFlowGraph, + id: BasicBlockId, + basic_blocks_with_super_called: &HashSet, + basic_blocks_with_local_violations: &HashMap< + oxc_semantic::petgraph::prelude::NodeIndex, + Vec, + >, + follow_join: bool, + ) -> Vec { + neighbors_filtered_by_edge_weight( + &cfg.graph, + id, + &|edge| match edge { + EdgeType::Jump | EdgeType::Normal => None, + EdgeType::Join if follow_join => None, + EdgeType::Unreachable + | EdgeType::Join + | EdgeType::Error(_) + | EdgeType::Finalize + | EdgeType::Backedge + | EdgeType::NewFunction => Some(DefinitelyCallsThisBeforeSuper::No), + }, + &mut |basic_block_id, _| { + let super_called = basic_blocks_with_super_called.contains(basic_block_id); + if basic_blocks_with_local_violations.contains_key(basic_block_id) { + // super was not called before this in the current code path: + return (DefinitelyCallsThisBeforeSuper::Yes, false); + } + + if super_called { + // If super is called but we are in a try-catch(-finally) block mark it as a + // maybe, since we might throw on super call and still call this in + // `catch`/`finally` block(s). + if cfg.graph.edges(*basic_block_id).any(|it| { + matches!( + it.weight(), + EdgeType::Error(oxc_semantic::ErrorEdgeKind::Explicit) + | EdgeType::Finalize + ) + }) { + (DefinitelyCallsThisBeforeSuper::Maybe(*basic_block_id), false) + // Otherwise we know for sure that super is called in this branch before + // reaching a this expression. + } else { + (DefinitelyCallsThisBeforeSuper::No, false) + } + // If we haven't visited a super call and we have a non-error/finalize path + // forward, continue visiting this branch. + } else if cfg + .graph + .edges(*basic_block_id) + .any(|it| !matches!(it.weight(), EdgeType::Error(_) | EdgeType::Finalize)) + { + (DefinitelyCallsThisBeforeSuper::No, true) + // Otherwise we mark it as a `Maybe` so we can analyze error/finalize paths separately. + } else { + (DefinitelyCallsThisBeforeSuper::Maybe(*basic_block_id), false) + } + }, + ) + } + + fn check_for_violation( + cfg: &ControlFlowGraph, + output: Vec, + basic_blocks_with_super_called: &HashSet, + basic_blocks_with_local_violations: &HashMap< + oxc_semantic::petgraph::prelude::NodeIndex, + Vec, + >, + ) -> bool { + // Deciding whether we definitely call this before super in all + // codepaths is as simple as seeing if any individual codepath + // definitely calls this before super. + output.into_iter().any(|y| match y { + DefinitelyCallsThisBeforeSuper::Yes => true, + DefinitelyCallsThisBeforeSuper::No => false, + DefinitelyCallsThisBeforeSuper::Maybe(id) => cfg.graph.edges(id).any(|edge| { + let weight = edge.weight(); + let is_explicit_error = + matches!(weight, EdgeType::Error(oxc_semantic::ErrorEdgeKind::Explicit)); + if is_explicit_error || matches!(weight, EdgeType::Finalize) { + Self::check_for_violation( + cfg, + Self::analyze( + cfg, + edge.target(), + basic_blocks_with_super_called, + basic_blocks_with_local_violations, + is_explicit_error, + ), + basic_blocks_with_super_called, + basic_blocks_with_local_violations, + ) + } else { + false + } + }), + }) + } } #[test] diff --git a/crates/oxc_linter/src/rules/react/require_render_return.rs b/crates/oxc_linter/src/rules/react/require_render_return.rs index 75dd39f568118..8ec9de1d656c6 100644 --- a/crates/oxc_linter/src/rules/react/require_render_return.rs +++ b/crates/oxc_linter/src/rules/react/require_render_return.rs @@ -100,9 +100,12 @@ fn contains_return_statement<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>) -> b // We only care about normal edges having a return statement. EdgeType::Jump | EdgeType::Normal => None, // For these two type, we flag it as not found. - EdgeType::Unreachable | EdgeType::NewFunction | EdgeType::Backedge => { - Some(FoundReturn::No) - } + EdgeType::Unreachable + | EdgeType::Error(_) + | EdgeType::Finalize + | EdgeType::Join + | EdgeType::NewFunction + | EdgeType::Backedge => Some(FoundReturn::No), }, &mut |basic_block_id, _state_going_into_this_rule| { // If its an arrow function with an expression, marked as founded and stop walking. diff --git a/crates/oxc_linter/src/rules/react/rules_of_hooks.rs b/crates/oxc_linter/src/rules/react/rules_of_hooks.rs index 4ae1d27ebcc83..34adc5f95062d 100644 --- a/crates/oxc_linter/src/rules/react/rules_of_hooks.rs +++ b/crates/oxc_linter/src/rules/react/rules_of_hooks.rs @@ -4,7 +4,7 @@ use oxc_ast::{ }; use oxc_macros::declare_oxc_lint; use oxc_semantic::{ - algo, petgraph::visit::Control, AstNodeId, AstNodes, BasicBlockId, EdgeType, InstructionKind, + algo, petgraph::visit::Control, AstNodeId, AstNodes, EdgeType, ErrorEdgeKind, InstructionKind, }; use oxc_span::{Atom, CompactStr}; use oxc_syntax::operator::AssignmentOperator; @@ -238,7 +238,7 @@ impl Rule for RulesOfHooks { return ctx.diagnostic(diagnostics::loop_hook(span, hook_name)); } - if has_conditional_path_accept_throw(ctx, func_cfg_id, node_cfg_id) { + if has_conditional_path_accept_throw(ctx, parent_func, node) { #[allow(clippy::needless_return)] return ctx.diagnostic(diagnostics::conditional_hook(span, hook_name)); } @@ -247,20 +247,62 @@ impl Rule for RulesOfHooks { fn has_conditional_path_accept_throw( ctx: &LintContext<'_>, - from: BasicBlockId, - to: BasicBlockId, + from: &AstNode<'_>, + to: &AstNode<'_>, ) -> bool { + let from_graph_id = from.cfg_id(); + let to_graph_id = to.cfg_id(); let cfg = ctx.semantic().cfg(); let graph = &cfg.graph; + if graph + .edges(to_graph_id) + .any(|it| matches!(it.weight(), EdgeType::Error(ErrorEdgeKind::Explicit))) + { + // TODO: We are simplifying here, There is a real need for a trait like `MayThrow` that + // would provide a method `may_throw`, since not everything may throw and break the control flow. + return true; + // let paths = algo::all_simple_paths::, _>(graph, from_graph_id, to_graph_id, 0, None); + // if paths + // .flatten() + // .flat_map(|id| cfg.basic_block(id).instructions()) + // .filter_map(|it| match it { + // Instruction { kind: InstructionKind::Statement, node_id: Some(node_id) } => { + // let r = Some(nodes.get_node(*node_id)); + // dbg!(&r); + // r + // } + // _ => None, + // }) + // .filter(|it| it.id() != to.id()) + // .any(|it| { + // // TODO: it.may_throw() + // matches!( + // it.kind(), + // AstKind::ExpressionStatement(ExpressionStatement { + // expression: Expression::CallExpression(_), + // .. + // }) + // ) + // }) + // { + // // return true; + // } + } // All nodes should be able to reach the hook node, Otherwise we have a conditional/branching flow. - algo::dijkstra(graph, from, Some(to), |e| match e.weight() { - EdgeType::NewFunction => 1, - EdgeType::Jump | EdgeType::Unreachable | EdgeType::Backedge | EdgeType::Normal => 0, + algo::dijkstra(graph, from_graph_id, Some(to_graph_id), |e| match e.weight() { + EdgeType::NewFunction | EdgeType::Error(ErrorEdgeKind::Implicit) => 1, + EdgeType::Error(ErrorEdgeKind::Explicit) + | EdgeType::Join + | EdgeType::Finalize + | EdgeType::Jump + | EdgeType::Unreachable + | EdgeType::Backedge + | EdgeType::Normal => 0, }) .into_iter() .filter(|(_, val)| *val == 0) .any(|(f, _)| { - !cfg.is_reachabale_filtered(f, to, |it| { + !cfg.is_reachabale_filtered(f, to_graph_id, |it| { if cfg .basic_block(it) .instructions() diff --git a/crates/oxc_linter/src/snapshots/empty_brace_spaces.snap b/crates/oxc_linter/src/snapshots/empty_brace_spaces.snap index 3751f3c9594e9..a79cab1eff18c 100644 --- a/crates/oxc_linter/src/snapshots/empty_brace_spaces.snap +++ b/crates/oxc_linter/src/snapshots/empty_brace_spaces.snap @@ -93,13 +93,6 @@ expression: empty_brace_spaces ╰──── help: There should be no spaces or new lines inside a pair of empty braces as it affects the overall readability of the code. - ⚠ eslint-plugin-unicorn(empty-brace-spaces): No spaces inside empty pair of braces allowed - ╭─[empty_brace_spaces.tsx:1:29] - 1 │ try {} catch(foo){} finally { } - · ─── - ╰──── - help: There should be no spaces or new lines inside a pair of empty braces as it affects the overall readability of the code. - ⚠ eslint-plugin-unicorn(empty-brace-spaces): No spaces inside empty pair of braces allowed ╭─[empty_brace_spaces.tsx:1:4] 1 │ do { } while (foo) @@ -268,13 +261,6 @@ expression: empty_brace_spaces ╰──── help: There should be no spaces or new lines inside a pair of empty braces as it affects the overall readability of the code. - ⚠ eslint-plugin-unicorn(empty-brace-spaces): No spaces inside empty pair of braces allowed - ╭─[empty_brace_spaces.tsx:1:29] - 1 │ try {} catch(foo){} finally { } - · ───── - ╰──── - help: There should be no spaces or new lines inside a pair of empty braces as it affects the overall readability of the code. - ⚠ eslint-plugin-unicorn(empty-brace-spaces): No spaces inside empty pair of braces allowed ╭─[empty_brace_spaces.tsx:1:4] 1 │ do { } while (foo) @@ -450,13 +436,6 @@ expression: empty_brace_spaces ╰──── help: There should be no spaces or new lines inside a pair of empty braces as it affects the overall readability of the code. - ⚠ eslint-plugin-unicorn(empty-brace-spaces): No spaces inside empty pair of braces allowed - ╭─[empty_brace_spaces.tsx:1:29] - 1 │ try {} catch(foo){} finally { } - · ────────── - ╰──── - help: There should be no spaces or new lines inside a pair of empty braces as it affects the overall readability of the code. - ⚠ eslint-plugin-unicorn(empty-brace-spaces): No spaces inside empty pair of braces allowed ╭─[empty_brace_spaces.tsx:1:4] 1 │ do { } while (foo) @@ -638,14 +617,6 @@ expression: empty_brace_spaces ╰──── help: There should be no spaces or new lines inside a pair of empty braces as it affects the overall readability of the code. - ⚠ eslint-plugin-unicorn(empty-brace-spaces): No spaces inside empty pair of braces allowed - ╭─[empty_brace_spaces.tsx:1:29] - 1 │ ╭─▶ try {} catch(foo){} finally { - 2 │ │ - 3 │ ╰─▶ } - ╰──── - help: There should be no spaces or new lines inside a pair of empty braces as it affects the overall readability of the code. - ⚠ eslint-plugin-unicorn(empty-brace-spaces): No spaces inside empty pair of braces allowed ╭─[empty_brace_spaces.tsx:1:4] 1 │ ╭─▶ do { @@ -825,13 +796,6 @@ expression: empty_brace_spaces ╰──── help: There should be no spaces or new lines inside a pair of empty braces as it affects the overall readability of the code. - ⚠ eslint-plugin-unicorn(empty-brace-spaces): No spaces inside empty pair of braces allowed - ╭─[empty_brace_spaces.tsx:1:29] - 1 │ ╭─▶ try {} catch(foo){} finally { - 2 │ ╰─▶ } - ╰──── - help: There should be no spaces or new lines inside a pair of empty braces as it affects the overall readability of the code. - ⚠ eslint-plugin-unicorn(empty-brace-spaces): No spaces inside empty pair of braces allowed ╭─[empty_brace_spaces.tsx:1:4] 1 │ ╭─▶ do { diff --git a/crates/oxc_linter/src/snapshots/no_empty.snap b/crates/oxc_linter/src/snapshots/no_empty.snap index 599c74e8c6c10..db55ba3fe39a1 100644 --- a/crates/oxc_linter/src/snapshots/no_empty.snap +++ b/crates/oxc_linter/src/snapshots/no_empty.snap @@ -18,14 +18,6 @@ expression: no_empty ╰──── help: Add comment inside empty block statement - ⚠ eslint(no-empty): Disallow empty block statements - ╭─[no_empty.tsx:1:45] - 1 │ try { foo() } catch (ex) {throw ex} finally {} - · ─┬ - · ╰── Empty block statement - ╰──── - help: Add comment inside empty block statement - ⚠ eslint(no-empty): Disallow empty block statements ╭─[no_empty.tsx:1:26] 1 │ try { foo() } catch (ex) {} @@ -90,14 +82,6 @@ expression: no_empty ╰──── help: Add comment inside empty block statement - ⚠ eslint(no-empty): Disallow empty block statements - ╭─[no_empty.tsx:1:38] - 1 │ try { foo(); } catch (ex) {} finally {} - · ─┬ - · ╰── Empty block statement - ╰──── - help: Add comment inside empty block statement - ⚠ eslint(no-empty): Disallow empty block statements ╭─[no_empty.tsx:1:5] 1 │ try {} catch (ex) {} finally {} @@ -114,14 +98,6 @@ expression: no_empty ╰──── help: Add comment inside empty block statement - ⚠ eslint(no-empty): Disallow empty block statements - ╭─[no_empty.tsx:1:30] - 1 │ try {} catch (ex) {} finally {} - · ─┬ - · ╰── Empty block statement - ╰──── - help: Add comment inside empty block statement - ⚠ eslint(no-empty): Disallow empty block statements ╭─[no_empty.tsx:1:27] 1 │ try { foo(); } catch (ex) {} finally {} @@ -137,11 +113,3 @@ expression: no_empty · ╰── Empty block statement ╰──── help: Add comment inside empty block statement - - ⚠ eslint(no-empty): Disallow empty block statements - ╭─[no_empty.tsx:1:38] - 1 │ try { foo(); } catch (ex) {} finally {} - · ─┬ - · ╰── Empty block statement - ╰──── - help: Add comment inside empty block statement diff --git a/crates/oxc_linter/src/snapshots/no_unsafe_finally.snap b/crates/oxc_linter/src/snapshots/no_unsafe_finally.snap index 10f83abc6ab3d..b6368aff2854f 100644 --- a/crates/oxc_linter/src/snapshots/no_unsafe_finally.snap +++ b/crates/oxc_linter/src/snapshots/no_unsafe_finally.snap @@ -11,15 +11,6 @@ expression: no_unsafe_finally ╰──── help: Control flow inside try or catch blocks will be overwritten by this statement - ⚠ eslint(no-unsafe-finally): Unsafe finally block - ╭─[no_unsafe_finally.tsx:7:2] - 6 │ } finally { - 7 │ return 3; - · ───────── - 8 │ } - ╰──── - help: Control flow inside try or catch blocks will be overwritten by this statement - ⚠ eslint(no-unsafe-finally): Unsafe finally block ╭─[no_unsafe_finally.tsx:1:86] 1 │ var foo = function() { try { return 1 } catch(err) { return 2 } finally { if(true) { return 3 } else { return 2 } } } @@ -34,27 +25,6 @@ expression: no_unsafe_finally ╰──── help: Control flow inside try or catch blocks will be overwritten by this statement - ⚠ eslint(no-unsafe-finally): Unsafe finally block - ╭─[no_unsafe_finally.tsx:1:86] - 1 │ var foo = function() { try { return 1 } catch(err) { return 2 } finally { if(true) { return 3 } else { return 2 } } } - · ──────── - ╰──── - help: Control flow inside try or catch blocks will be overwritten by this statement - - ⚠ eslint(no-unsafe-finally): Unsafe finally block - ╭─[no_unsafe_finally.tsx:1:104] - 1 │ var foo = function() { try { return 1 } catch(err) { return 2 } finally { if(true) { return 3 } else { return 2 } } } - · ──────── - ╰──── - help: Control flow inside try or catch blocks will be overwritten by this statement - - ⚠ eslint(no-unsafe-finally): Unsafe finally block - ╭─[no_unsafe_finally.tsx:1:75] - 1 │ var foo = function() { try { return 1 } catch(err) { return 2 } finally { return 3 } } - · ──────── - ╰──── - help: Control flow inside try or catch blocks will be overwritten by this statement - ⚠ eslint(no-unsafe-finally): Unsafe finally block ╭─[no_unsafe_finally.tsx:1:75] 1 │ var foo = function() { try { return 1 } catch(err) { return 2 } finally { return 3 } } @@ -69,20 +39,6 @@ expression: no_unsafe_finally ╰──── help: Control flow inside try or catch blocks will be overwritten by this statement - ⚠ eslint(no-unsafe-finally): Unsafe finally block - ╭─[no_unsafe_finally.tsx:1:75] - 1 │ var foo = function() { try { return 1 } catch(err) { return 2 } finally { return function(x) { return y } } } - · ─────────────────────────────── - ╰──── - help: Control flow inside try or catch blocks will be overwritten by this statement - - ⚠ eslint(no-unsafe-finally): Unsafe finally block - ╭─[no_unsafe_finally.tsx:1:75] - 1 │ var foo = function() { try { return 1 } catch(err) { return 2 } finally { return { x: function(c) { return c } } } } - · ────────────────────────────────────── - ╰──── - help: Control flow inside try or catch blocks will be overwritten by this statement - ⚠ eslint(no-unsafe-finally): Unsafe finally block ╭─[no_unsafe_finally.tsx:1:75] 1 │ var foo = function() { try { return 1 } catch(err) { return 2 } finally { return { x: function(c) { return c } } } } @@ -97,34 +53,6 @@ expression: no_unsafe_finally ╰──── help: Control flow inside try or catch blocks will be overwritten by this statement - ⚠ eslint(no-unsafe-finally): Unsafe finally block - ╭─[no_unsafe_finally.tsx:1:75] - 1 │ var foo = function() { try { return 1 } catch(err) { return 2 } finally { throw new Error() } } - · ───────────────── - ╰──── - help: Control flow inside try or catch blocks will be overwritten by this statement - - ⚠ eslint(no-unsafe-finally): Unsafe finally block - ╭─[no_unsafe_finally.tsx:1:74] - 1 │ var foo = function() { try { foo(); } finally { try { bar(); } finally { return; } } }; - · ─────── - ╰──── - help: Control flow inside try or catch blocks will be overwritten by this statement - - ⚠ eslint(no-unsafe-finally): Unsafe finally block - ╭─[no_unsafe_finally.tsx:1:74] - 1 │ var foo = function() { try { foo(); } finally { try { bar(); } finally { return; } } }; - · ─────── - ╰──── - help: Control flow inside try or catch blocks will be overwritten by this statement - - ⚠ eslint(no-unsafe-finally): Unsafe finally block - ╭─[no_unsafe_finally.tsx:1:74] - 1 │ var foo = function() { try { foo(); } finally { try { bar(); } finally { return; } } }; - · ─────── - ╰──── - help: Control flow inside try or catch blocks will be overwritten by this statement - ⚠ eslint(no-unsafe-finally): Unsafe finally block ╭─[no_unsafe_finally.tsx:1:74] 1 │ var foo = function() { try { foo(); } finally { try { bar(); } finally { return; } } }; @@ -139,13 +67,6 @@ expression: no_unsafe_finally ╰──── help: Control flow inside try or catch blocks will be overwritten by this statement - ⚠ eslint(no-unsafe-finally): Unsafe finally block - ╭─[no_unsafe_finally.tsx:1:59] - 1 │ var foo = function() { label: try { return 0; } finally { break label; } return 1; } - · ──────────── - ╰──── - help: Control flow inside try or catch blocks will be overwritten by this statement - ⚠ eslint(no-unsafe-finally): Unsafe finally block ╭─[no_unsafe_finally.tsx:7:2] 6 │ } finally { @@ -155,22 +76,6 @@ expression: no_unsafe_finally ╰──── help: Control flow inside try or catch blocks will be overwritten by this statement - ⚠ eslint(no-unsafe-finally): Unsafe finally block - ╭─[no_unsafe_finally.tsx:7:2] - 6 │ } finally { - 7 │ break a; - · ──────── - 8 │ } - ╰──── - help: Control flow inside try or catch blocks will be overwritten by this statement - - ⚠ eslint(no-unsafe-finally): Unsafe finally block - ╭─[no_unsafe_finally.tsx:1:54] - 1 │ var foo = function() { while (true) try {} finally { break; } } - · ────── - ╰──── - help: Control flow inside try or catch blocks will be overwritten by this statement - ⚠ eslint(no-unsafe-finally): Unsafe finally block ╭─[no_unsafe_finally.tsx:1:54] 1 │ var foo = function() { while (true) try {} finally { break; } } @@ -185,20 +90,6 @@ expression: no_unsafe_finally ╰──── help: Control flow inside try or catch blocks will be overwritten by this statement - ⚠ eslint(no-unsafe-finally): Unsafe finally block - ╭─[no_unsafe_finally.tsx:1:54] - 1 │ var foo = function() { while (true) try {} finally { continue; } } - · ───────── - ╰──── - help: Control flow inside try or catch blocks will be overwritten by this statement - - ⚠ eslint(no-unsafe-finally): Unsafe finally block - ╭─[no_unsafe_finally.tsx:1:68] - 1 │ var foo = function() { switch (true) { case true: try {} finally { break; } } } - · ────── - ╰──── - help: Control flow inside try or catch blocks will be overwritten by this statement - ⚠ eslint(no-unsafe-finally): Unsafe finally block ╭─[no_unsafe_finally.tsx:1:68] 1 │ var foo = function() { switch (true) { case true: try {} finally { break; } } } @@ -213,20 +104,6 @@ expression: no_unsafe_finally ╰──── help: Control flow inside try or catch blocks will be overwritten by this statement - ⚠ eslint(no-unsafe-finally): Unsafe finally block - ╭─[no_unsafe_finally.tsx:1:84] - 1 │ var foo = function() { a: while (true) try {} finally { switch (true) { case true: break a; } } } - · ──────── - ╰──── - help: Control flow inside try or catch blocks will be overwritten by this statement - - ⚠ eslint(no-unsafe-finally): Unsafe finally block - ╭─[no_unsafe_finally.tsx:1:84] - 1 │ var foo = function() { a: while (true) try {} finally { switch (true) { case true: continue; } } } - · ───────── - ╰──── - help: Control flow inside try or catch blocks will be overwritten by this statement - ⚠ eslint(no-unsafe-finally): Unsafe finally block ╭─[no_unsafe_finally.tsx:1:84] 1 │ var foo = function() { a: while (true) try {} finally { switch (true) { case true: continue; } } } @@ -240,10 +117,3 @@ expression: no_unsafe_finally · ──────── ╰──── help: Control flow inside try or catch blocks will be overwritten by this statement - - ⚠ eslint(no-unsafe-finally): Unsafe finally block - ╭─[no_unsafe_finally.tsx:1:98] - 1 │ var foo = function() { a: switch (true) { case true: try {} finally { switch (true) { case true: break a; } } } } - · ──────── - ╰──── - help: Control flow inside try or catch blocks will be overwritten by this statement diff --git a/crates/oxc_linter/src/snapshots/require_yields.snap b/crates/oxc_linter/src/snapshots/require_yields.snap index 2eb1ba4074cd0..f540f5ff1727c 100644 --- a/crates/oxc_linter/src/snapshots/require_yields.snap +++ b/crates/oxc_linter/src/snapshots/require_yields.snap @@ -225,20 +225,6 @@ expression: require_yields ╰──── help: Add `@yields` tag to the JSDoc comment. - ⚠ eslint-plugin-jsdoc(require-yields): Missing JSDoc `@yields` declaration for generator function. - ╭─[require_yields.tsx:5:20] - 4 │ */ - 5 │ ╭─▶ function * quux () { - 6 │ │ try { - 7 │ │ } finally { - 8 │ │ yield true; - 9 │ │ } - 10 │ │ yield; - 11 │ ╰─▶ } - 12 │ - ╰──── - help: Add `@yields` tag to the JSDoc comment. - ⚠ eslint-plugin-jsdoc(require-yields): Missing JSDoc `@yields` declaration for generator function. ╭─[require_yields.tsx:5:20] 4 │ */ diff --git a/crates/oxc_semantic/examples/cfg.rs b/crates/oxc_semantic/examples/cfg.rs index 1817d925b84a7..f0aabb89cf422 100644 --- a/crates/oxc_semantic/examples/cfg.rs +++ b/crates/oxc_semantic/examples/cfg.rs @@ -90,7 +90,7 @@ fn main() -> std::io::Result<()> { &[Config::EdgeNoLabel, Config::NodeNoLabel], &|_graph, edge| { let weight = edge.weight(); - let label = format!("label = {weight:?}"); + let label = format!("label = \"{weight:?}\""); if matches!(weight, EdgeType::Unreachable) { format!("{label}, style = \"dotted\"") } else { diff --git a/crates/oxc_semantic/src/builder.rs b/crates/oxc_semantic/src/builder.rs index 3b5946d3a9c4e..c801ca90462c9 100644 --- a/crates/oxc_semantic/src/builder.rs +++ b/crates/oxc_semantic/src/builder.rs @@ -17,7 +17,8 @@ use crate::{ checker::{EarlyErrorJavaScript, EarlyErrorTypeScript}, class::ClassTableBuilder, control_flow::{ - ControlFlowGraphBuilder, CtxCursor, CtxFlags, EdgeType, Register, ReturnInstructionKind, + ControlFlowGraphBuilder, CtxCursor, CtxFlags, EdgeType, ErrorEdgeKind, Register, + ReturnInstructionKind, }, diagnostics::redeclaration, jsdoc::JSDocBuilder, @@ -447,17 +448,24 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { flags }); program.scope_id.set(Some(self.current_scope_id)); - self.enter_node(kind); /* cfg */ - let _program_basic_block = self.cfg.new_basic_block(); + let error_harness = self.cfg.attach_error_harness(ErrorEdgeKind::Implicit); + let _program_basic_block = self.cfg.new_basic_block_normal(); /* cfg - must be above directives as directives are in cfg */ + self.enter_node(kind); + for directive in &program.directives { self.visit_directive(directive); } self.visit_statements(&program.body); + + /* cfg */ + self.cfg.release_error_harness(error_harness); + /* cfg */ + self.leave_node(kind); self.leave_scope(); } @@ -531,7 +539,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let before_do_while_stmt_graph_ix = self.cfg.current_node_ix; - let start_body_graph_ix = self.cfg.new_basic_block(); + let start_body_graph_ix = self.cfg.new_basic_block_normal(); self.cfg.ctx(None).default().allow_break().allow_continue(); /* cfg */ @@ -540,7 +548,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg - condition basic block */ let after_body_graph_ix = self.cfg.current_node_ix; - let start_of_condition_graph_ix = self.cfg.new_basic_block(); + let start_of_condition_graph_ix = self.cfg.new_basic_block_normal(); /* cfg */ self.visit_expression(&stmt.test); @@ -548,7 +556,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let end_of_condition_graph_ix = self.cfg.current_node_ix; - let end_do_while_graph_ix = self.cfg.new_basic_block(); + let end_do_while_graph_ix = self.cfg.new_basic_block_normal(); // before do while to start of body basic block self.cfg.add_edge(before_do_while_stmt_graph_ix, start_body_graph_ix, EdgeType::Normal); @@ -591,14 +599,14 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let left_expr_end_ix = self.cfg.current_node_ix; - let right_expr_start_ix = self.cfg.new_basic_block(); + let right_expr_start_ix = self.cfg.new_basic_block_normal(); /* cfg */ self.visit_expression(&expr.right); /* cfg */ let right_expr_end_ix = self.cfg.current_node_ix; - let after_logical_expr_ix = self.cfg.new_basic_block(); + let after_logical_expr_ix = self.cfg.new_basic_block_normal(); self.cfg.add_edge(left_expr_end_ix, right_expr_start_ix, EdgeType::Normal); self.cfg.add_edge(left_expr_end_ix, after_logical_expr_ix, EdgeType::Normal); @@ -622,7 +630,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let cfg_ixs = if expr.operator.is_logical() { let target_end_ix = self.cfg.current_node_ix; - let expr_start_ix = self.cfg.new_basic_block(); + let expr_start_ix = self.cfg.new_basic_block_normal(); Some((target_end_ix, expr_start_ix)) } else { None @@ -634,7 +642,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ if let Some((target_end_ix, expr_start_ix)) = cfg_ixs { let expr_end_ix = self.cfg.current_node_ix; - let after_assignment_ix = self.cfg.new_basic_block(); + let after_assignment_ix = self.cfg.new_basic_block_normal(); self.cfg.add_edge(target_end_ix, expr_start_ix, EdgeType::Normal); self.cfg.add_edge(target_end_ix, after_assignment_ix, EdgeType::Normal); @@ -654,14 +662,14 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let before_conditional_expr_graph_ix = self.cfg.current_node_ix; // conditional expression basic block - let before_consequent_expr_graph_ix = self.cfg.new_basic_block(); + let before_consequent_expr_graph_ix = self.cfg.new_basic_block_normal(); /* cfg */ self.visit_expression(&expr.consequent); /* cfg */ let after_consequent_expr_graph_ix = self.cfg.current_node_ix; - let start_alternate_graph_ix = self.cfg.new_basic_block(); + let start_alternate_graph_ix = self.cfg.new_basic_block_normal(); /* cfg */ self.visit_expression(&expr.alternate); @@ -669,7 +677,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let after_alternate_graph_ix = self.cfg.current_node_ix; /* bb after conditional expression joins consequent and alternate */ - let after_conditional_graph_ix = self.cfg.new_basic_block(); + let after_conditional_graph_ix = self.cfg.new_basic_block_normal(); /* cfg */ self.cfg.add_edge( @@ -706,7 +714,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { } /* cfg */ let before_for_graph_ix = self.cfg.current_node_ix; - let test_graph_ix = self.cfg.new_basic_block(); + let test_graph_ix = self.cfg.new_basic_block_normal(); /* cfg */ if let Some(test) = &stmt.test { self.visit_expression(test); @@ -714,7 +722,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let after_test_graph_ix = self.cfg.current_node_ix; - let update_graph_ix = self.cfg.new_basic_block(); + let update_graph_ix = self.cfg.new_basic_block_normal(); /* cfg */ if let Some(update) = &stmt.update { @@ -722,7 +730,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { } /* cfg */ - let before_body_graph_ix = self.cfg.new_basic_block(); + let before_body_graph_ix = self.cfg.new_basic_block_normal(); self.cfg.ctx(None).default().allow_break().allow_continue(); /* cfg */ @@ -731,7 +739,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let after_body_graph_ix = self.cfg.current_node_ix; - let after_for_stmt = self.cfg.new_basic_block(); + let after_for_stmt = self.cfg.new_basic_block_normal(); self.cfg.add_edge(before_for_graph_ix, test_graph_ix, EdgeType::Normal); self.cfg.add_edge(after_test_graph_ix, before_body_graph_ix, EdgeType::Normal); self.cfg.add_edge(after_body_graph_ix, update_graph_ix, EdgeType::Backedge); @@ -778,7 +786,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let before_for_stmt_graph_ix = self.cfg.current_node_ix; - let start_prepare_cond_graph_ix = self.cfg.new_basic_block(); + let start_prepare_cond_graph_ix = self.cfg.new_basic_block_normal(); /* cfg */ self.visit_expression(&stmt.right); @@ -786,8 +794,8 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let end_of_prepare_cond_graph_ix = self.cfg.current_node_ix; // this basic block is always empty since there's no update condition in a for-in loop. - let basic_block_with_backedge_graph_ix = self.cfg.new_basic_block(); - let body_graph_ix = self.cfg.new_basic_block(); + let basic_block_with_backedge_graph_ix = self.cfg.new_basic_block_normal(); + let body_graph_ix = self.cfg.new_basic_block_normal(); self.cfg.ctx(None).default().allow_break().allow_continue(); /* cfg */ @@ -796,7 +804,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let end_of_body_graph_ix = self.cfg.current_node_ix; - let after_for_graph_ix = self.cfg.new_basic_block(); + let after_for_graph_ix = self.cfg.new_basic_block_normal(); // connect before for statement to the iterable expression self.cfg.add_edge(before_for_stmt_graph_ix, start_prepare_cond_graph_ix, EdgeType::Normal); // connect the end of the iterable expression to the basic block with back edge @@ -843,7 +851,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let before_for_stmt_graph_ix = self.cfg.current_node_ix; - let start_prepare_cond_graph_ix = self.cfg.new_basic_block(); + let start_prepare_cond_graph_ix = self.cfg.new_basic_block_normal(); /* cfg */ self.visit_expression(&stmt.right); @@ -851,8 +859,8 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let end_of_prepare_cond_graph_ix = self.cfg.current_node_ix; // this basic block is always empty since there's no update condition in a for-of loop. - let basic_block_with_backedge_graph_ix = self.cfg.new_basic_block(); - let body_graph_ix = self.cfg.new_basic_block(); + let basic_block_with_backedge_graph_ix = self.cfg.new_basic_block_normal(); + let body_graph_ix = self.cfg.new_basic_block_normal(); self.cfg.ctx(None).default().allow_break().allow_continue(); /* cfg */ @@ -861,7 +869,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let end_of_body_graph_ix = self.cfg.current_node_ix; - let after_for_graph_ix = self.cfg.new_basic_block(); + let after_for_graph_ix = self.cfg.new_basic_block_normal(); // connect before for statement to the iterable expression self.cfg.add_edge(before_for_stmt_graph_ix, start_prepare_cond_graph_ix, EdgeType::Normal); // connect the end of the iterable expression to the basic block with back edge @@ -904,7 +912,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let before_if_stmt_graph_ix = self.cfg.current_node_ix; - let before_consequent_stmt_graph_ix = self.cfg.new_basic_block(); + let before_consequent_stmt_graph_ix = self.cfg.new_basic_block_normal(); /* cfg */ self.visit_statement(&stmt.consequent); @@ -915,7 +923,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { let else_graph_ix = if let Some(alternate) = &stmt.alternate { /* cfg */ - let else_graph_ix = self.cfg.new_basic_block(); + let else_graph_ix = self.cfg.new_basic_block_normal(); /* cfg */ self.visit_statement(alternate); @@ -926,7 +934,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { }; /* cfg - bb after if statement joins consequent and alternate */ - let after_if_graph_ix = self.cfg.new_basic_block(); + let after_if_graph_ix = self.cfg.new_basic_block_normal(); self.cfg.add_edge(after_consequent_stmt_graph_ix, after_if_graph_ix, EdgeType::Normal); @@ -975,7 +983,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let after_body_graph_ix = self.cfg.current_node_ix; - let after_labeled_stmt_graph_ix = self.cfg.new_basic_block(); + let after_labeled_stmt_graph_ix = self.cfg.new_basic_block_normal(); self.cfg.add_edge(after_body_graph_ix, after_labeled_stmt_graph_ix, EdgeType::Normal); self.cfg.ctx(Some(label.as_str())).mark_break(after_labeled_stmt_graph_ix).resolve(); @@ -1084,7 +1092,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ // make a new basic block so that we can jump to it later from the switch // discriminant and the switch cases above it (if they don't test ss true) - let switch_cond_graph_ix = self.cfg.new_basic_block(); + let switch_cond_graph_ix = self.cfg.new_basic_block_normal(); self.cfg .switch_case_conditions .last_mut() @@ -1097,7 +1105,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { } /* cfg */ - let statements_in_switch_graph_ix = self.cfg.new_basic_block(); + let statements_in_switch_graph_ix = self.cfg.new_basic_block_normal(); self.cfg.add_edge(switch_cond_graph_ix, statements_in_switch_graph_ix, EdgeType::Normal); /* cfg */ @@ -1128,186 +1136,102 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { let kind = AstKind::TryStatement(self.alloc(stmt)); self.enter_node(kind); - // There are 3 possible kinds of Try Statements (See - // ): - // 1. try-catch - // 2. try-finally - // 3. try-catch-finally - // - // We will consider each kind of try statement separately. - // - // For a try-catch, there are only 2 ways to reach - // the outgoing node (after the entire statement): - // - // 1. after the try block completing successfully - // 2. after the catch block completing successfully, - // in which case some statement in the try block - // must have thrown. - // - // For a try-finally, there is only 1 way to reach - // the outgoing node, whereby: - // - the try block completed successfully, and - // - the finally block completed successfully - // - // But the finally block can also be reached when the try - // fails. We thus need to fork the control flow graph into - // 2 different finally statements: - // 1. one where the try block completes successfully, (finally_succ) - // 2. one where some statement in the try block throws (finally_err) - // Only the end of the try block will have an incoming edge to the - // finally_succ, and only finally_succ will have an outgoing node to - // the next statement. - // - // For a try-catch-finally, we have seemlingly more cases: - // 1. after the try block completing successfully - // 2. after the catch block completing successfully - // 3. after the try block if the catch block throws - // Despite having 3 distings scenarios, we can simplify the control flow - // graph by still only using a finally_succ and a finally_err node. - // The key is that the outgoing edge going past the entire - // try-catch-finally statement is guaranteed that all code paths have - // either completed the try block or the catch block in full. - - // Implementation notes: - // We will use the following terminology: - // - // the "parent after_throw block" is the block that would be the target - // of a throw if there were no try-catch-finally. - // - // Within the try block, a throw will not go to the parent after_throw - // block. Instead, it will go to the catch block in a try-catch or to - // the finally_err block in a try-catch-finally. - // - // In a catch block, a throw will go to the finally_err block in a - // try-catch-finally, or to the parent after_throw block in a basic - // try-catch. - // - // In a finally block, a throw will always go to the parent after_throw - // block, both for finally_succ and finally_err. - - /* cfg */ - // TODO: support unwinding finally/catch blocks that aren't in this function - // even if something throws. - let parent_after_throw_block_ix = self.cfg.after_throw_block; - - let try_stmt_pre_start_ix = self.cfg.current_node_ix; - - let try_stmt_start_ix = self.cfg.new_basic_block(); - self.cfg.add_edge(try_stmt_pre_start_ix, try_stmt_start_ix, EdgeType::Normal); - let try_after_throw_block_ix = self.cfg.new_basic_block(); - - self.cfg.current_node_ix = try_stmt_start_ix; - - // every statement created with this active adds an edge from that node to this node - // - // NOTE: we oversimplify here, realistically even in between basic blocks we - // do throwsy things which could cause problems, but for the most part simply - // pointing the end of every basic block to the catch block is enough - self.cfg.after_throw_block = Some(try_after_throw_block_ix); - // The one case that needs to be handled specially is if the first statement in the - // try block throws. In that case, it is not sufficient to rely on an edge after that - // statement, because the catch will run before that edge is taken. - self.cfg.add_edge(try_stmt_pre_start_ix, try_after_throw_block_ix, EdgeType::Normal); + /* cfg */ + let before_try_statement_graph_ix = self.cfg.current_node_ix; + let error_harness = + stmt.handler.as_ref().map(|_| self.cfg.attach_error_harness(ErrorEdgeKind::Explicit)); + let before_finalizer_graph_ix = + stmt.finalizer.as_ref().map(|_| self.cfg.attach_finalizer()); + let before_try_block_graph_ix = self.cfg.new_basic_block_normal(); /* cfg */ self.visit_block_statement(&stmt.block); /* cfg */ - let end_of_try_block_ix = self.cfg.current_node_ix; - self.cfg.add_edge(end_of_try_block_ix, try_after_throw_block_ix, EdgeType::Normal); - self.cfg.after_throw_block = parent_after_throw_block_ix; - - let start_of_finally_err_block_ix = if stmt.finalizer.is_some() { - if stmt.handler.is_some() { - // try-catch-finally - Some(self.cfg.new_basic_block()) - } else { - // try-finally - Some(try_after_throw_block_ix) - } - } else { - // try-catch - None - }; + let after_try_block_graph_ix = self.cfg.current_node_ix; /* cfg */ let catch_block_end_ix = if let Some(handler) = &stmt.handler { /* cfg */ - let catch_after_throw_block_ix = if stmt.finalizer.is_some() { - start_of_finally_err_block_ix - } else { - parent_after_throw_block_ix + let Some(error_harness) = error_harness else { + unreachable!("we always create an error harness if we have a catch block."); }; - self.cfg.after_throw_block = catch_after_throw_block_ix; - - let catch_block_start_ix = try_after_throw_block_ix; - self.cfg.current_node_ix = catch_block_start_ix; - - if let Some(catch_after_throw_block_ix) = catch_after_throw_block_ix { - self.cfg.add_edge( - catch_block_start_ix, - catch_after_throw_block_ix, - EdgeType::Normal, - ); - } + self.cfg.release_error_harness(error_harness); + let catch_block_start_ix = self.cfg.new_basic_block_normal(); + self.cfg.add_edge(error_harness, catch_block_start_ix, EdgeType::Normal); /* cfg */ self.visit_catch_clause(handler); /* cfg */ - Some(self.cfg.current_node_ix) + let catch_block_end_ix = self.cfg.current_node_ix; + // TODO: we shouldn't directly change the current node index. + self.cfg.current_node_ix = after_try_block_graph_ix; + Some(catch_block_end_ix) /* cfg */ } else { None }; - // Restore the after_throw_block - self.cfg.after_throw_block = parent_after_throw_block_ix; - - if let Some(finalizer) = &stmt.finalizer { + let finally_block_end_ix = if let Some(finalizer) = &stmt.finalizer { /* cfg */ - let finally_err_block_start_ix = - start_of_finally_err_block_ix.expect("this try statement has a finally_err block"); - - self.cfg.current_node_ix = finally_err_block_start_ix; + let Some(before_finalizer_graph_ix) = before_finalizer_graph_ix else { + unreachable!("we always create a finalizer when there is a finally block."); + }; + self.cfg.release_finalizer(before_finalizer_graph_ix); + let start_finally_graph_ix = self.cfg.new_basic_block_normal(); + self.cfg.add_edge(before_finalizer_graph_ix, start_finally_graph_ix, EdgeType::Normal); /* cfg */ self.visit_finally_clause(finalizer); /* cfg */ - // append an unreachable after the finally_err block - self.cfg.append_unreachable(); - - let finally_succ_block_start_ix = self.cfg.new_basic_block(); + let finally_block_end_ix = self.cfg.current_node_ix; + // TODO: we shouldn't directly change the current node index. + self.cfg.current_node_ix = after_try_block_graph_ix; + Some(finally_block_end_ix) + /* cfg */ + } else { + None + }; - // The end_of_try_block has an outgoing edge to finally_succ also - // for when the try block completes successfully. - self.cfg.add_edge(end_of_try_block_ix, finally_succ_block_start_ix, EdgeType::Normal); + /* cfg */ + let after_try_statement_block_ix = self.cfg.new_basic_block_normal(); + self.cfg.add_edge( + before_try_statement_graph_ix, + before_try_block_graph_ix, + EdgeType::Normal, + ); + if let Some(catch_block_end_ix) = catch_block_end_ix { + if finally_block_end_ix.is_none() { + self.cfg.add_edge( + after_try_block_graph_ix, + after_try_statement_block_ix, + EdgeType::Normal, + ); - // The end_of_catch_block has an outgoing edge to finally_succ for - // when the catch block in a try-catch-finally completes successfully. - if let Some(end_of_catch_block_ix) = catch_block_end_ix { - // try-catch-finally self.cfg.add_edge( - end_of_catch_block_ix, - finally_succ_block_start_ix, + catch_block_end_ix, + after_try_statement_block_ix, EdgeType::Normal, ); } - /* cfg */ - - // TODO: Is it intentional that we visit this twice? - self.visit_finally_clause(finalizer); } - - /* cfg */ - let try_statement_block_end_ix = self.cfg.current_node_ix; - let after_try_statement_block_ix = self.cfg.new_basic_block(); - self.cfg.add_edge( - try_statement_block_end_ix, - after_try_statement_block_ix, - EdgeType::Normal, - ); + if let Some(finally_block_end_ix) = finally_block_end_ix { + if catch_block_end_ix.is_some() { + self.cfg.add_edge( + finally_block_end_ix, + after_try_statement_block_ix, + EdgeType::Normal, + ); + } else { + self.cfg.add_edge( + finally_block_end_ix, + after_try_statement_block_ix, + EdgeType::Join, + ); + } + } /* cfg */ self.leave_node(kind); @@ -1342,13 +1266,13 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg - condition basic block */ let before_while_stmt_graph_ix = self.cfg.current_node_ix; - let condition_graph_ix = self.cfg.new_basic_block(); + let condition_graph_ix = self.cfg.new_basic_block_normal(); /* cfg */ self.visit_expression(&stmt.test); /* cfg - body basic block */ - let body_graph_ix = self.cfg.new_basic_block(); + let body_graph_ix = self.cfg.new_basic_block_normal(); self.cfg.ctx(None).default().allow_break().allow_continue(); /* cfg */ @@ -1357,7 +1281,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg - after body basic block */ let after_body_graph_ix = self.cfg.current_node_ix; - let after_while_graph_ix = self.cfg.new_basic_block(); + let after_while_graph_ix = self.cfg.new_basic_block_normal(); self.cfg.add_edge(before_while_stmt_graph_ix, condition_graph_ix, EdgeType::Normal); self.cfg.add_edge(condition_graph_ix, body_graph_ix, EdgeType::Normal); @@ -1380,19 +1304,19 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg - condition basic block */ let before_with_stmt_graph_ix = self.cfg.current_node_ix; - let condition_graph_ix = self.cfg.new_basic_block(); + let condition_graph_ix = self.cfg.new_basic_block_normal(); /* cfg */ self.visit_expression(&stmt.object); /* cfg - body basic block */ - let body_graph_ix = self.cfg.new_basic_block(); + let body_graph_ix = self.cfg.new_basic_block_normal(); /* cfg */ self.visit_statement(&stmt.body); /* cfg - after body basic block */ - let after_body_graph_ix = self.cfg.new_basic_block(); + let after_body_graph_ix = self.cfg.new_basic_block_normal(); self.cfg.add_edge(before_with_stmt_graph_ix, condition_graph_ix, EdgeType::Normal); self.cfg.add_edge(condition_graph_ix, body_graph_ix, EdgeType::Normal); @@ -1418,7 +1342,8 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { let preserved = self.cfg.preserve_expression_state(); let before_function_graph_ix = self.cfg.current_node_ix; - let function_graph_ix = self.cfg.new_basic_block_for_function(); + let error_harness = self.cfg.attach_error_harness(ErrorEdgeKind::Implicit); + let function_graph_ix = self.cfg.new_basic_block_function(); self.cfg.ctx(None).new_function(); /* cfg */ @@ -1441,7 +1366,8 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ self.cfg.restore_expression_state(preserved); self.cfg.ctx(None).resolve_expect(CtxFlags::FUNCTION); - let after_function_graph_ix = self.cfg.new_basic_block(); + self.cfg.release_error_harness(error_harness); + let after_function_graph_ix = self.cfg.new_basic_block_normal(); self.cfg.add_edge(before_function_graph_ix, after_function_graph_ix, EdgeType::Normal); /* cfg */ @@ -1527,7 +1453,8 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ let preserved = self.cfg.preserve_expression_state(); let current_node_ix = self.cfg.current_node_ix; - let function_graph_ix = self.cfg.new_basic_block_for_function(); + let error_harness = self.cfg.attach_error_harness(ErrorEdgeKind::Implicit); + let function_graph_ix = self.cfg.new_basic_block_function(); self.cfg.ctx(None).new_function(); /* cfg */ @@ -1549,6 +1476,7 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { /* cfg */ self.cfg.restore_expression_state(preserved); self.cfg.ctx(None).resolve_expect(CtxFlags::FUNCTION); + self.cfg.release_error_harness(error_harness); self.cfg.current_node_ix = current_node_ix; /* cfg */ if let Some(parameters) = &expr.type_parameters { diff --git a/crates/oxc_semantic/src/control_flow/builder/mod.rs b/crates/oxc_semantic/src/control_flow/builder/mod.rs index 12e0694db98ce..a5e088a6df75f 100644 --- a/crates/oxc_semantic/src/control_flow/builder/mod.rs +++ b/crates/oxc_semantic/src/control_flow/builder/mod.rs @@ -6,16 +6,23 @@ use context::Ctx; pub use context::{CtxCursor, CtxFlags}; use super::{ - AstNodeId, BasicBlock, BasicBlockId, CompactStr, ControlFlowGraph, EdgeType, Graph, - Instruction, InstructionKind, LabeledInstruction, PreservedExpressionState, Register, + AstNodeId, BasicBlock, BasicBlockId, CompactStr, ControlFlowGraph, EdgeType, ErrorEdgeKind, + Graph, Instruction, InstructionKind, LabeledInstruction, PreservedExpressionState, Register, }; +#[derive(Debug, Default)] +struct ErrorHarness(ErrorEdgeKind, BasicBlockId); + #[derive(Debug, Default)] pub struct ControlFlowGraphBuilder<'a> { pub graph: Graph, pub basic_blocks: Vec, pub current_node_ix: BasicBlockId, ctx_stack: Vec>, + /// Contains the error unwinding path represented as a stack of `ErrorHarness`es + error_path: Vec, + /// Stack of finalizers, the top most element is always the appropriate one for current node. + finalizers: Vec, // note: this should only land in the big box for all things that take arguments // ie: callexpression, arrayexpression, etc // todo: add assert that it is used every time? @@ -55,33 +62,35 @@ impl<'a> ControlFlowGraphBuilder<'a> { .expect("expected `self.current_node_ix` to be a valid node index in self.graph") } - #[must_use] - pub fn new_basic_block_for_function(&mut self) -> BasicBlockId { + pub(self) fn new_basic_block(&mut self) -> BasicBlockId { + // current length would be the index of block we are adding on the next line. + let basic_block_ix = self.basic_blocks.len(); self.basic_blocks.push(BasicBlock::new()); - let basic_block_id = self.basic_blocks.len() - 1; - let graph_index = self.graph.add_node(basic_block_id); - self.current_node_ix = graph_index; - - // todo: get smarter about what can throw, ie: return can't throw but it's expression can - if let Some(after_throw_block) = self.after_throw_block { - self.add_edge(graph_index, after_throw_block, EdgeType::NewFunction); - } + self.graph.add_node(basic_block_ix) + } - graph_index + #[must_use] + pub fn new_basic_block_function(&mut self) -> BasicBlockId { + // we might want to differentiate between function blocks and normal blocks down the road. + self.new_basic_block_normal() } + /// # Panics if there is no error harness to attach to. #[must_use] - pub fn new_basic_block(&mut self) -> BasicBlockId { - self.basic_blocks.push(BasicBlock::new()); - let graph_index = self.graph.add_node(self.basic_blocks.len() - 1); - self.current_node_ix = graph_index; + pub fn new_basic_block_normal(&mut self) -> BasicBlockId { + let graph_ix = self.new_basic_block(); + self.current_node_ix = graph_ix; - // todo: get smarter about what can throw, ie: return can't throw but it's expression can - if let Some(after_throw_block) = self.after_throw_block { - self.add_edge(graph_index, after_throw_block, EdgeType::Normal); + // add an error edge to this block. + let ErrorHarness(error_edge_kind, error_graph_ix) = + self.error_path.last().expect("normal basic blocks need an error harness to attach to"); + self.add_edge(graph_ix, *error_graph_ix, EdgeType::Error(*error_edge_kind)); + + if let Some(finalizer) = self.finalizers.last() { + self.add_edge(graph_ix, *finalizer, EdgeType::Finalize); } - graph_index + graph_ix } pub fn add_edge(&mut self, a: BasicBlockId, b: BasicBlockId, weight: EdgeType) { @@ -96,6 +105,45 @@ impl<'a> ControlFlowGraphBuilder<'a> { self.push_instruction(InstructionKind::Return(kind), Some(node)); } + /// Creates and push a new `BasicBlockId` onto `self.error_path` stack. + /// Returns the `BasicBlockId` of the created error harness block. + pub fn attach_error_harness(&mut self, kind: ErrorEdgeKind) -> BasicBlockId { + let graph_ix = self.new_basic_block(); + self.error_path.push(ErrorHarness(kind, graph_ix)); + graph_ix + } + + /// # Panics if there is no error harness pushed onto the stack, + /// Or last harness doesn't match the expected `BasicBlockId`. + pub fn release_error_harness(&mut self, expect: BasicBlockId) { + let harness = self + .error_path + .pop() + .expect("there is no error harness in the `self.error_path` stack"); + assert_eq!( + harness.1, expect, + "expected harness doesn't match the last harness pushed onto the stack." + ); + } + + /// Creates and push a new `BasicBlockId` onto `self.finalizers` stack. + /// Returns the `BasicBlockId` of the created finalizer block. + pub fn attach_finalizer(&mut self) -> BasicBlockId { + let graph_ix = self.new_basic_block(); + self.finalizers.push(graph_ix); + graph_ix + } + + /// # Panics if last finalizer doesn't match the expected `BasicBlockId`. + pub fn release_finalizer(&mut self, expect: BasicBlockId) { + // return early if there is no finalizer. + let Some(finalizer) = self.finalizers.pop() else { return }; + assert_eq!( + finalizer, expect, + "expected finalizer doesn't match the last finalizer pushed onto the stack." + ); + } + pub fn append_throw(&mut self, node: AstNodeId) { self.push_instruction(InstructionKind::Throw, Some(node)); self.append_unreachable(); @@ -131,7 +179,7 @@ impl<'a> ControlFlowGraphBuilder<'a> { pub fn append_unreachable(&mut self) { let current_node_ix = self.current_node_ix; - let basic_block_with_unreachable_graph_ix = self.new_basic_block(); + let basic_block_with_unreachable_graph_ix = self.new_basic_block_normal(); self.add_edge( current_node_ix, basic_block_with_unreachable_graph_ix, diff --git a/crates/oxc_semantic/src/control_flow/dot.rs b/crates/oxc_semantic/src/control_flow/dot.rs index 1642ce83222bd..f835879bc4d37 100644 --- a/crates/oxc_semantic/src/control_flow/dot.rs +++ b/crates/oxc_semantic/src/control_flow/dot.rs @@ -42,7 +42,7 @@ impl DisplayDot for ControlFlowGraph { &[Config::EdgeNoLabel, Config::NodeNoLabel], &|_graph, edge| { let weight = edge.weight(); - let label = format!("label = {weight:?} "); + let label = format!("label = \"{weight:?}\" "); if matches!(weight, EdgeType::Unreachable) { format!("{label}, style = \"dotted\" ") } else { @@ -98,7 +98,7 @@ impl DebugDot for ControlFlowGraph { &[Config::EdgeNoLabel, Config::NodeNoLabel], &|_graph, edge| { let weight = edge.weight(); - let label = format!("label = {weight:?} "); + let label = format!("label = \"{weight:?}\" "); if matches!(weight, EdgeType::Unreachable) { format!("{label}, style = \"dotted\" ") } else { diff --git a/crates/oxc_semantic/src/control_flow/mod.rs b/crates/oxc_semantic/src/control_flow/mod.rs index 988f6823fcec2..63ff31235a62b 100644 --- a/crates/oxc_semantic/src/control_flow/mod.rs +++ b/crates/oxc_semantic/src/control_flow/mod.rs @@ -164,11 +164,33 @@ pub enum LabeledInstruction { #[derive(Debug, Clone)] pub enum EdgeType { + /// Conditional jumps Jump, + /// Normal control flow path Normal, + /// Cyclic aka loops Backedge, + /// Marks start of a function subgraph NewFunction, + /// Finally + Finalize, + /// Error Path + Error(ErrorEdgeKind), + + // misc edges Unreachable, + /// Used to mark the end of a finalizer. It is an experimental approach might + /// move to it's respective edge kind enum or get removed altogether. + Join, +} + +#[derive(Default, Debug, Clone, Copy)] +pub enum ErrorEdgeKind { + /// Error kind for edges between a block which can throw, to it's respective catch block. + Explicit, + /// Any block that can throw would have an implicit error block connected using this kind. + #[default] + Implicit, } #[derive(Debug)] @@ -240,7 +262,7 @@ impl ControlFlowGraph { let graph = &self.graph; // All nodes should be able to reach the `to` node, Otherwise we have a conditional/branching flow. petgraph::algo::dijkstra(graph, from, Some(to), |e| match e.weight() { - EdgeType::NewFunction => 1, + EdgeType::NewFunction | EdgeType::Error(_) | EdgeType::Finalize | EdgeType::Join => 1, EdgeType::Jump | EdgeType::Unreachable | EdgeType::Backedge | EdgeType::Normal => 0, }) .into_iter() diff --git a/crates/oxc_semantic/src/lib.rs b/crates/oxc_semantic/src/lib.rs index 97c13d99d4d2e..c50692274bb9f 100644 --- a/crates/oxc_semantic/src/lib.rs +++ b/crates/oxc_semantic/src/lib.rs @@ -34,8 +34,8 @@ pub use crate::{ control_flow::{ AssignmentValue, BasicBlock, BasicBlockId, BinaryAssignmentValue, BinaryOp, CallType, CalleeWithArgumentsAssignmentValue, CollectionAssignmentValue, ControlFlowGraph, DebugDot, - DebugDotContext, DisplayDot, EdgeType, Instruction, InstructionKind, LabeledInstruction, - ObjectPropertyAccessAssignmentValue, Register, ReturnInstructionKind, + DebugDotContext, DisplayDot, EdgeType, ErrorEdgeKind, Instruction, InstructionKind, + LabeledInstruction, ObjectPropertyAccessAssignmentValue, Register, ReturnInstructionKind, UnaryExpressioneAssignmentValue, UpdateAssignmentValue, }, node::{AstNode, AstNodeId, AstNodes}, diff --git a/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@argument_map.js-2.snap b/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@argument_map.js-2.snap index 9a769749fc8c1..012952d5aa232 100644 --- a/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@argument_map.js-2.snap +++ b/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@argument_map.js-2.snap @@ -5,8 +5,13 @@ input_file: crates/oxc_semantic/tests/integration/cfg_fixtures/argument_map.js --- digraph { 0 [ label = "" ] - 1 [ label = "ExpressionStatement" ] + 1 [ label = "" ] 2 [ label = "" ] - 0 -> 1 [ label = NewFunction ] - 0 -> 2 [ label = Normal ] + 3 [ label = "ExpressionStatement" ] + 4 [ label = "" ] + 1 -> 0 [ label = "Error(Implicit)" ] + 3 -> 2 [ label = "Error(Implicit)" ] + 1 -> 3 [ label = "NewFunction" ] + 4 -> 0 [ label = "Error(Implicit)" ] + 1 -> 4 [ label = "Normal" ] } diff --git a/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@argument_map.js.snap b/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@argument_map.js.snap index e656ed4aeb184..80a5bbb355db1 100644 --- a/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@argument_map.js.snap +++ b/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@argument_map.js.snap @@ -8,9 +8,17 @@ bb0: { } bb1: { - statement + } bb2: { } + +bb3: { + statement +} + +bb4: { + +} diff --git a/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@arrow_expressions.js-2.snap b/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@arrow_expressions.js-2.snap index 2fe6d9a0a4605..73efdcd4740b2 100644 --- a/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@arrow_expressions.js-2.snap +++ b/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@arrow_expressions.js-2.snap @@ -4,9 +4,15 @@ expression: output.cfg_dot_diagram() input_file: crates/oxc_semantic/tests/integration/cfg_fixtures/arrow_expressions.js --- digraph { - 0 [ label = "ExpressionStatement\nVariableDeclaration" ] - 1 [ label = "ExpressionStatement" ] - 2 [ label = "ExpressionStatement" ] - 0 -> 1 [ label = NewFunction ] - 0 -> 2 [ label = NewFunction ] + 0 [ label = "" ] + 1 [ label = "ExpressionStatement\nVariableDeclaration" ] + 2 [ label = "" ] + 3 [ label = "ExpressionStatement" ] + 4 [ label = "" ] + 5 [ label = "ExpressionStatement" ] + 1 -> 0 [ label = "Error(Implicit)" ] + 3 -> 2 [ label = "Error(Implicit)" ] + 1 -> 3 [ label = "NewFunction" ] + 5 -> 4 [ label = "Error(Implicit)" ] + 1 -> 5 [ label = "NewFunction" ] } diff --git a/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@arrow_expressions.js.snap b/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@arrow_expressions.js.snap index 25311403d2a8a..332fd2a485457 100644 --- a/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@arrow_expressions.js.snap +++ b/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@arrow_expressions.js.snap @@ -4,14 +4,26 @@ expression: output.basic_blocks_printed() input_file: crates/oxc_semantic/tests/integration/cfg_fixtures/arrow_expressions.js --- bb0: { - statement - statement + } bb1: { statement + statement } bb2: { + +} + +bb3: { + statement +} + +bb4: { + +} + +bb5: { statement } diff --git a/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@assignment_operators.js-2.snap b/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@assignment_operators.js-2.snap index 15f277be09927..51541ea57bf93 100644 --- a/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@assignment_operators.js-2.snap +++ b/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@assignment_operators.js-2.snap @@ -4,20 +4,28 @@ expression: output.cfg_dot_diagram() input_file: crates/oxc_semantic/tests/integration/cfg_fixtures/assignment_operators.js --- digraph { - 0 [ label = "ExpressionStatement" ] - 1 [ label = "" ] - 2 [ label = "ExpressionStatement" ] - 3 [ label = "" ] - 4 [ label = "ExpressionStatement" ] - 5 [ label = "" ] - 6 [ label = "ExpressionStatement\nExpressionStatement" ] - 0 -> 1 [ label = Normal ] - 0 -> 2 [ label = Normal ] - 1 -> 2 [ label = Normal ] - 2 -> 3 [ label = Normal ] - 2 -> 4 [ label = Normal ] - 3 -> 4 [ label = Normal ] - 4 -> 5 [ label = Normal ] - 4 -> 6 [ label = Normal ] - 5 -> 6 [ label = Normal ] + 0 [ label = "" ] + 1 [ label = "ExpressionStatement" ] + 2 [ label = "" ] + 3 [ label = "ExpressionStatement" ] + 4 [ label = "" ] + 5 [ label = "ExpressionStatement" ] + 6 [ label = "" ] + 7 [ label = "ExpressionStatement\nExpressionStatement" ] + 1 -> 0 [ label = "Error(Implicit)" ] + 2 -> 0 [ label = "Error(Implicit)" ] + 3 -> 0 [ label = "Error(Implicit)" ] + 1 -> 2 [ label = "Normal" ] + 1 -> 3 [ label = "Normal" ] + 2 -> 3 [ label = "Normal" ] + 4 -> 0 [ label = "Error(Implicit)" ] + 5 -> 0 [ label = "Error(Implicit)" ] + 3 -> 4 [ label = "Normal" ] + 3 -> 5 [ label = "Normal" ] + 4 -> 5 [ label = "Normal" ] + 6 -> 0 [ label = "Error(Implicit)" ] + 7 -> 0 [ label = "Error(Implicit)" ] + 5 -> 6 [ label = "Normal" ] + 5 -> 7 [ label = "Normal" ] + 6 -> 7 [ label = "Normal" ] } diff --git a/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@assignment_operators.js.snap b/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@assignment_operators.js.snap index 717614e61375f..39f9573c8e2f2 100644 --- a/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@assignment_operators.js.snap +++ b/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@assignment_operators.js.snap @@ -4,30 +4,34 @@ expression: output.basic_blocks_printed() input_file: crates/oxc_semantic/tests/integration/cfg_fixtures/assignment_operators.js --- bb0: { - statement + } bb1: { - + statement } bb2: { - statement + } bb3: { - + statement } bb4: { - statement + } bb5: { - + statement } bb6: { + +} + +bb7: { statement statement } diff --git a/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@break_from_a_label_in_global_scope.js-2.snap b/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@break_from_a_label_in_global_scope.js-2.snap index 58c801e1df3b2..61d72eee3242e 100644 --- a/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@break_from_a_label_in_global_scope.js-2.snap +++ b/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@break_from_a_label_in_global_scope.js-2.snap @@ -4,10 +4,14 @@ expression: output.cfg_dot_diagram() input_file: crates/oxc_semantic/tests/integration/cfg_fixtures/break_from_a_label_in_global_scope.js --- digraph { - 0 [ label = "LabeledStatement\nbreak " ] - 1 [ label = "unreachable" ] - 2 [ label = "" ] - 0 -> 1 [ label = Unreachable , style = "dotted" ] - 1 -> 2 [ label = Normal ] - 0 -> 2 [ label = Jump ] + 0 [ label = "" ] + 1 [ label = "LabeledStatement\nbreak " ] + 2 [ label = "unreachable" ] + 3 [ label = "" ] + 1 -> 0 [ label = "Error(Implicit)" ] + 2 -> 0 [ label = "Error(Implicit)" ] + 1 -> 2 [ label = "Unreachable" , style = "dotted" ] + 3 -> 0 [ label = "Error(Implicit)" ] + 2 -> 3 [ label = "Normal" ] + 1 -> 3 [ label = "Jump" ] } diff --git a/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@break_from_a_label_in_global_scope.js.snap b/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@break_from_a_label_in_global_scope.js.snap index e4076e8d4010e..8a1b08dddda89 100644 --- a/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@break_from_a_label_in_global_scope.js.snap +++ b/crates/oxc_semantic/tests/integration/snapshots/integration__cfg__cfg_files@break_from_a_label_in_global_scope.js.snap @@ -4,14 +4,18 @@ expression: output.basic_blocks_printed() input_file: crates/oxc_semantic/tests/integration/cfg_fixtures/break_from_a_label_in_global_scope.js --- bb0: { + +} + +bb1: { statement break