use crate::ast::FunctionNode;
use crate::cfg::{Cfg, NodeId, NodeKind};
use swc_ecma_ast::*;
pub fn build_cfg(function: &FunctionNode) -> Cfg {
let mut builder = CfgBuilder::new();
let block_stmt = function.body.as_ecmascript();
builder.build_from_body(block_stmt);
builder.cfg
}
struct BreakableContext {
label: Option<String>,
break_target: Option<NodeId>,
continue_target: Option<NodeId>,
}
struct CfgBuilder {
cfg: Cfg,
current_node: Option<NodeId>,
breakable_stack: Vec<BreakableContext>,
pending_label: Option<String>,
}
impl CfgBuilder {
fn new() -> Self {
let cfg = Cfg::new();
let entry = cfg.entry;
CfgBuilder {
cfg,
current_node: Some(entry),
breakable_stack: Vec::new(),
pending_label: None,
}
}
fn take_label(&mut self) -> Option<String> {
self.pending_label.take()
}
fn get_or_create_break_target(&mut self, idx: usize) -> NodeId {
if let Some(target) = self.breakable_stack[idx].break_target {
return target;
}
let join_node = self.cfg.add_node(NodeKind::Join);
self.breakable_stack[idx].break_target = Some(join_node);
join_node
}
fn build_from_body(&mut self, body: &BlockStmt) {
for stmt in &body.stmts {
self.visit_stmt(stmt);
}
if let Some(last_node) = self.current_node {
if last_node != self.cfg.exit {
let has_exit_edge = self
.cfg
.edges
.iter()
.any(|e| e.from == last_node && e.to == self.cfg.exit);
if !has_exit_edge {
self.cfg.add_edge(last_node, self.cfg.exit);
}
}
}
}
fn visit_stmt(&mut self, stmt: &Stmt) {
match stmt {
Stmt::Labeled(labeled) => {
self.pending_label = Some(labeled.label.sym.to_string());
self.visit_stmt(&labeled.body);
self.pending_label = None;
}
Stmt::If(if_stmt) => self.visit_if(if_stmt),
Stmt::While(while_stmt) => self.visit_while(while_stmt),
Stmt::DoWhile(do_while_stmt) => self.visit_do_while(do_while_stmt),
Stmt::For(for_stmt) => self.visit_for(for_stmt),
Stmt::ForIn(for_in_stmt) => self.visit_for_in(for_in_stmt),
Stmt::ForOf(for_of_stmt) => self.visit_for_of(for_of_stmt),
Stmt::Switch(switch_stmt) => self.visit_switch(switch_stmt),
Stmt::Return(return_stmt) => self.visit_return(return_stmt),
Stmt::Throw(throw_stmt) => self.visit_throw(throw_stmt),
Stmt::Try(try_stmt) => self.visit_try(try_stmt),
Stmt::Break(break_stmt) => self.visit_break(break_stmt),
Stmt::Continue(continue_stmt) => self.visit_continue(continue_stmt),
Stmt::Block(block_stmt) => {
for stmt in &block_stmt.stmts {
self.visit_stmt(stmt);
}
}
_ => {
if let Some(from_node) = self.current_node {
let stmt_node = self.cfg.add_node(NodeKind::Statement);
self.cfg.add_edge(from_node, stmt_node);
self.current_node = Some(stmt_node);
}
}
}
}
fn visit_if(&mut self, if_stmt: &IfStmt) {
let Some(from_node) = self.current_node else {
return;
};
let condition_node = self.cfg.add_node(NodeKind::Condition);
self.cfg.add_edge(from_node, condition_node);
let then_start = self.cfg.add_node(NodeKind::Statement);
self.cfg.add_edge(condition_node, then_start);
self.current_node = Some(then_start);
self.visit_stmt(&if_stmt.cons);
let then_end = self.current_node;
if let Some(alt) = &if_stmt.alt {
let else_start = self.cfg.add_node(NodeKind::Statement);
self.cfg.add_edge(condition_node, else_start);
self.current_node = Some(else_start);
self.visit_stmt(alt);
let else_end = self.current_node;
if then_end.is_some() || else_end.is_some() {
let join_node = self.cfg.add_node(NodeKind::Join);
if let Some(t) = then_end {
self.cfg.add_edge(t, join_node);
}
if let Some(e) = else_end {
self.cfg.add_edge(e, join_node);
}
self.current_node = Some(join_node);
} else {
self.current_node = None; }
} else {
let join_node = self.cfg.add_node(NodeKind::Join);
self.cfg.add_edge(condition_node, join_node);
if let Some(t) = then_end {
self.cfg.add_edge(t, join_node);
}
self.current_node = Some(join_node);
}
}
fn visit_while(&mut self, while_stmt: &WhileStmt) {
let Some(from_node) = self.current_node else {
return;
};
let label = self.take_label();
let header_node = self.cfg.add_node(NodeKind::LoopHeader);
self.cfg.add_edge(from_node, header_node);
let condition_node = self.cfg.add_node(NodeKind::Condition);
self.cfg.add_edge(header_node, condition_node);
let join_node = self.cfg.add_node(NodeKind::Join);
self.cfg.add_edge(condition_node, join_node);
self.breakable_stack.push(BreakableContext {
label,
break_target: Some(join_node),
continue_target: Some(header_node),
});
let body_start = self.cfg.add_node(NodeKind::Statement);
self.cfg.add_edge(condition_node, body_start);
self.current_node = Some(body_start);
self.visit_stmt(&while_stmt.body);
let body_end = self.current_node.unwrap_or(body_start);
self.breakable_stack.pop();
if self.current_node.is_some() {
self.cfg.add_edge(body_end, header_node);
}
self.current_node = Some(join_node);
}
fn visit_do_while(&mut self, do_while_stmt: &DoWhileStmt) {
let Some(from_node) = self.current_node else {
return;
};
let label = self.take_label();
let header_node = self.cfg.add_node(NodeKind::LoopHeader);
self.cfg.add_edge(from_node, header_node);
self.breakable_stack.push(BreakableContext {
label,
break_target: None,
continue_target: Some(header_node),
});
let body_start = self.cfg.add_node(NodeKind::Statement);
self.cfg.add_edge(header_node, body_start);
self.current_node = Some(body_start);
self.visit_stmt(&do_while_stmt.body);
let body_end = self.current_node.unwrap_or(body_start);
let body_completed = self.current_node.is_some();
let ctx = self.breakable_stack.pop().unwrap();
let lazy_join = ctx.break_target;
if body_completed {
let join_node = lazy_join.unwrap_or_else(|| self.cfg.add_node(NodeKind::Join));
let condition_node = self.cfg.add_node(NodeKind::Condition);
self.cfg.add_edge(body_end, condition_node);
self.cfg.add_edge(condition_node, header_node);
self.cfg.add_edge(condition_node, join_node);
self.current_node = Some(join_node);
} else if let Some(join_node) = lazy_join {
self.current_node = Some(join_node);
} else {
self.current_node = None;
}
}
fn visit_for(&mut self, for_stmt: &ForStmt) {
let Some(from_node) = self.current_node else {
return;
};
let label = self.take_label();
let init_end = if for_stmt.init.is_some() {
let node = self.cfg.add_node(NodeKind::Statement);
self.cfg.add_edge(from_node, node);
node
} else {
from_node
};
let header_node = self.cfg.add_node(NodeKind::LoopHeader);
self.cfg.add_edge(init_end, header_node);
let (condition_node, initial_break_target) = if for_stmt.test.is_some() {
let cnode = self.cfg.add_node(NodeKind::Condition);
self.cfg.add_edge(header_node, cnode);
let join = self.cfg.add_node(NodeKind::Join);
self.cfg.add_edge(cnode, join);
(cnode, Some(join))
} else {
(header_node, None)
};
self.breakable_stack.push(BreakableContext {
label,
break_target: initial_break_target,
continue_target: Some(header_node),
});
let body_start = self.cfg.add_node(NodeKind::Statement);
self.cfg.add_edge(condition_node, body_start);
self.current_node = Some(body_start);
self.visit_stmt(&for_stmt.body);
let mut body_end = self.current_node.unwrap_or(body_start);
let ctx = self.breakable_stack.pop().unwrap();
if self.current_node.is_some() {
if for_stmt.update.is_some() {
let update_node = self.cfg.add_node(NodeKind::Statement);
self.cfg.add_edge(body_end, update_node);
body_end = update_node;
}
self.cfg.add_edge(body_end, header_node);
}
if let Some(join_node) = ctx.break_target {
self.current_node = Some(join_node);
} else {
self.current_node = None;
}
}
fn visit_for_in(&mut self, for_in_stmt: &ForInStmt) {
let Some(from_node) = self.current_node else {
return;
};
let label = self.take_label();
let header_node = self.cfg.add_node(NodeKind::LoopHeader);
self.cfg.add_edge(from_node, header_node);
let condition_node = self.cfg.add_node(NodeKind::Condition);
self.cfg.add_edge(header_node, condition_node);
let join_node = self.cfg.add_node(NodeKind::Join);
self.cfg.add_edge(condition_node, join_node);
self.breakable_stack.push(BreakableContext {
label,
break_target: Some(join_node),
continue_target: Some(header_node),
});
let body_start = self.cfg.add_node(NodeKind::Statement);
self.cfg.add_edge(condition_node, body_start);
self.current_node = Some(body_start);
self.visit_stmt(&for_in_stmt.body);
let body_end = self.current_node.unwrap_or(body_start);
self.breakable_stack.pop();
if self.current_node.is_some() {
self.cfg.add_edge(body_end, header_node);
}
self.current_node = Some(join_node);
}
fn visit_for_of(&mut self, for_of_stmt: &ForOfStmt) {
let Some(from_node) = self.current_node else {
return;
};
let label = self.take_label();
let header_node = self.cfg.add_node(NodeKind::LoopHeader);
self.cfg.add_edge(from_node, header_node);
let condition_node = self.cfg.add_node(NodeKind::Condition);
self.cfg.add_edge(header_node, condition_node);
let join_node = self.cfg.add_node(NodeKind::Join);
self.cfg.add_edge(condition_node, join_node);
self.breakable_stack.push(BreakableContext {
label,
break_target: Some(join_node),
continue_target: Some(header_node),
});
let body_start = self.cfg.add_node(NodeKind::Statement);
self.cfg.add_edge(condition_node, body_start);
self.current_node = Some(body_start);
self.visit_stmt(&for_of_stmt.body);
let body_end = self.current_node.unwrap_or(body_start);
self.breakable_stack.pop();
if self.current_node.is_some() {
self.cfg.add_edge(body_end, header_node);
}
self.current_node = Some(join_node);
}
fn visit_switch(&mut self, switch_stmt: &SwitchStmt) {
let Some(from_node) = self.current_node else {
return;
};
let label = self.take_label();
let switch_node = self.cfg.add_node(NodeKind::Condition);
self.cfg.add_edge(from_node, switch_node);
self.breakable_stack.push(BreakableContext {
label,
break_target: None,
continue_target: None, });
let mut prev_case_end: Option<NodeId> = None;
for case in &switch_stmt.cases {
let case_node = self.cfg.add_node(NodeKind::Statement);
self.cfg.add_edge(switch_node, case_node);
if let Some(prev_end) = prev_case_end {
self.cfg.add_edge(prev_end, case_node);
}
self.current_node = Some(case_node);
for stmt in &case.cons {
self.visit_stmt(stmt);
if self.current_node.is_none() {
break;
}
}
prev_case_end = self.current_node;
}
let ctx = self.breakable_stack.pop().unwrap();
let lazy_join = ctx.break_target;
if let Some(last_end) = prev_case_end {
let join_node = lazy_join.unwrap_or_else(|| self.cfg.add_node(NodeKind::Join));
self.cfg.add_edge(last_end, join_node);
self.current_node = Some(join_node);
} else if let Some(join_node) = lazy_join {
self.current_node = Some(join_node);
} else {
self.current_node = None;
}
}
fn visit_return(&mut self, _return_stmt: &ReturnStmt) {
let Some(from_node) = self.current_node else {
return;
};
self.cfg.add_edge(from_node, self.cfg.exit);
self.current_node = None;
}
fn visit_throw(&mut self, _throw_stmt: &ThrowStmt) {
let Some(from_node) = self.current_node else {
return;
};
self.cfg.add_edge(from_node, self.cfg.exit);
self.current_node = None;
}
fn build_catch_block(&mut self, try_stmt: &TryStmt, try_start: NodeId) -> Option<NodeId> {
let handler = try_stmt.handler.as_ref()?;
let catch_start = self.cfg.add_node(NodeKind::Statement);
self.cfg.add_edge(try_start, catch_start);
self.current_node = Some(catch_start);
self.build_from_body(&handler.body);
self.current_node
}
fn connect_finally(
&mut self,
finally_block: &BlockStmt,
try_start: NodeId,
try_end: NodeId,
try_completed: bool,
catch_end: Option<NodeId>,
) {
let finally_start = self.cfg.add_node(NodeKind::Statement);
if try_completed {
self.cfg.add_edge(try_end, finally_start);
}
if let Some(catch) = catch_end {
if catch != self.cfg.exit {
self.cfg.add_edge(catch, finally_start);
}
}
if !try_completed && catch_end.is_none() {
self.cfg.add_edge(try_start, finally_start);
}
self.current_node = Some(finally_start);
self.build_from_body(finally_block);
if self.current_node.is_some() {
let finally_end = self.current_node.unwrap();
let join_node = self.cfg.add_node(NodeKind::Join);
self.cfg.add_edge(finally_end, join_node);
self.current_node = Some(join_node);
}
}
fn connect_no_finally(
&mut self,
try_start: NodeId,
try_end: NodeId,
try_completed: bool,
catch_end: Option<NodeId>,
has_handler: bool,
) {
let catch_completed = catch_end.map(|c| c != self.cfg.exit).unwrap_or(false);
if try_completed || catch_completed {
let join_node = self.cfg.add_node(NodeKind::Join);
if try_completed {
self.cfg.add_edge(try_end, join_node);
}
if let Some(catch) = catch_end {
if catch != self.cfg.exit {
self.cfg.add_edge(catch, join_node);
}
}
if !has_handler {
self.cfg.add_edge(try_start, self.cfg.exit);
}
self.current_node = Some(join_node);
} else {
if !has_handler {
self.cfg.add_edge(try_start, self.cfg.exit);
}
self.current_node = None;
}
}
fn visit_try(&mut self, try_stmt: &TryStmt) {
let Some(from_node) = self.current_node else {
return;
};
let try_start = self.cfg.add_node(NodeKind::Statement);
self.cfg.add_edge(from_node, try_start);
self.current_node = Some(try_start);
self.build_from_body(&try_stmt.block);
let try_end = self.current_node.unwrap_or(try_start);
let try_completed = self.current_node.is_some();
let catch_end = self.build_catch_block(try_stmt, try_start);
let has_handler = try_stmt.handler.is_some();
if let Some(finally_block) = &try_stmt.finalizer {
self.connect_finally(finally_block, try_start, try_end, try_completed, catch_end);
} else {
self.connect_no_finally(try_start, try_end, try_completed, catch_end, has_handler);
}
}
fn visit_break(&mut self, break_stmt: &BreakStmt) {
let Some(from_node) = self.current_node else {
return;
};
let target = if let Some(label) = &break_stmt.label {
let idx = self
.breakable_stack
.iter()
.rposition(|ctx| ctx.label.as_deref() == Some(&*label.sym));
idx.map(|i| self.get_or_create_break_target(i))
} else {
let idx = self.breakable_stack.len().checked_sub(1);
idx.map(|i| self.get_or_create_break_target(i))
};
if let Some(target) = target {
self.cfg.add_edge(from_node, target);
} else {
self.cfg.add_edge(from_node, self.cfg.exit);
}
self.current_node = None;
}
fn visit_continue(&mut self, continue_stmt: &ContinueStmt) {
let Some(from_node) = self.current_node else {
return;
};
let target = if let Some(label) = &continue_stmt.label {
self.breakable_stack
.iter()
.rev()
.find(|ctx| {
ctx.label.as_deref() == Some(&*label.sym) && ctx.continue_target.is_some()
})
.and_then(|ctx| ctx.continue_target)
} else {
self.breakable_stack
.iter()
.rev()
.find(|ctx| ctx.continue_target.is_some())
.and_then(|ctx| ctx.continue_target)
};
if let Some(target) = target {
self.cfg.add_edge(from_node, target);
} else {
self.cfg.add_edge(from_node, self.cfg.exit);
}
self.current_node = None;
}
}
impl Default for CfgBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::discover::discover_functions;
use crate::parser::parse_source;
use swc_common::{sync::Lrc, SourceMap};
fn build_cfg_for(source: &str) -> Cfg {
let sm = Lrc::new(SourceMap::default());
let module = parse_source(source, &sm, "test.ts").unwrap();
let functions = discover_functions(&module, 0, source, &sm);
assert!(!functions.is_empty(), "Expected at least one function");
build_cfg(&functions[0])
}
#[test]
fn test_break_routes_to_loop_join() {
let cfg = build_cfg_for("function f() { while (true) { break; } }");
cfg.validate().expect("CFG should be valid");
let break_to_exit = cfg.edges.iter().any(|e| {
e.to == cfg.exit
&& e.from != cfg.entry
&& !matches!(
cfg.nodes[e.from.0].kind,
NodeKind::Statement | NodeKind::Join
)
});
assert!(
!break_to_exit || cfg.edges.iter().filter(|e| e.to == cfg.exit).count() <= 2,
"break should not create extra edges to exit"
);
}
#[test]
fn test_continue_routes_to_loop_header() {
let cfg = build_cfg_for("function f() { while (true) { continue; } }");
cfg.validate().expect("CFG should be valid");
let has_edge_to_header = cfg.edges.iter().any(|e| {
matches!(cfg.nodes[e.to.0].kind, NodeKind::LoopHeader)
&& e.from != cfg.entry
&& !matches!(
cfg.nodes[e.from.0].kind,
NodeKind::Entry | NodeKind::LoopHeader | NodeKind::Condition
)
});
assert!(has_edge_to_header, "continue should route to loop header");
}
#[test]
fn test_labeled_break_routes_to_outer_loop() {
let cfg =
build_cfg_for("function f() { outer: while (true) { while (true) { break outer; } } }");
cfg.validate().expect("CFG should be valid");
}
#[test]
fn test_labeled_continue_routes_to_outer_header() {
let cfg = build_cfg_for(
"function f() { outer: while (true) { while (true) { continue outer; } } }",
);
cfg.validate().expect("CFG should be valid");
}
#[test]
fn test_switch_break_routes_to_switch_join() {
let cfg =
build_cfg_for("function f(x: number) { switch(x) { case 1: break; case 2: break; } }");
cfg.validate().expect("CFG should be valid");
let exit_edges: Vec<_> = cfg.edges.iter().filter(|e| e.to == cfg.exit).collect();
assert!(
exit_edges.len() <= 1,
"switch breaks should route to join, not exit. Exit edges: {:?}",
exit_edges
);
}
#[test]
fn test_nested_loop_break_targets_inner() {
let cfg = build_cfg_for(
"function f() { for (let i = 0; i < 10; i++) { for (let j = 0; j < 10; j++) { break; } } }",
);
cfg.validate().expect("CFG should be valid");
}
#[test]
fn test_for_of_with_break_and_continue() {
let cfg = build_cfg_for(
r#"
function f(arr: number[]) {
let sum = 0;
for (const item of arr) {
if (item < 0) { break; }
if (item > 100) { continue; }
sum += item;
}
return sum;
}
"#,
);
cfg.validate().expect("CFG should be valid");
}
#[test]
fn test_do_while_body_always_returns() {
let cfg = build_cfg_for(
r#"
function f(x: number): number {
do {
return x * 2;
} while (x > 0);
}
"#,
);
cfg.validate().expect("CFG should be valid");
}
#[test]
fn test_do_while_with_break() {
let cfg = build_cfg_for(
r#"
function f(x: number): number {
do {
if (x < 0) { break; }
x--;
} while (x > 0);
return x;
}
"#,
);
cfg.validate().expect("CFG should be valid");
}
#[test]
fn test_do_while_normal_completion() {
let cfg = build_cfg_for(
r#"
function f(x: number): number {
do {
x--;
} while (x > 0);
return x;
}
"#,
);
cfg.validate().expect("CFG should be valid");
}
#[test]
fn test_infinite_for_body_always_returns() {
let cfg = build_cfg_for(
r#"
function f(x: number): number {
for (;;) {
return x;
}
}
"#,
);
cfg.validate().expect("CFG should be valid");
}
#[test]
fn test_infinite_for_with_break() {
let cfg = build_cfg_for(
r#"
function f(x: number): number {
for (;;) {
if (x <= 0) { break; }
x--;
}
return x;
}
"#,
);
cfg.validate().expect("CFG should be valid");
}
#[test]
fn test_switch_all_cases_return() {
let cfg = build_cfg_for(
r#"
function f(x: number): string {
switch (x) {
case 1: return "one";
case 2: return "two";
default: return "other";
}
}
"#,
);
cfg.validate().expect("CFG should be valid");
}
#[test]
fn test_try_return_catch_throw_finally_reachable() {
let cfg = build_cfg_for(
r#"
async function execute(x: number): Promise<number> {
try {
return x * 2;
} catch (error) {
throw error;
} finally {
cleanup();
}
}
"#,
);
cfg.validate().expect("CFG should be valid");
}
#[test]
fn test_dead_code_after_return_no_panic() {
let cfg = build_cfg_for(
r#"
function f(x: number): number {
if (x > 0) {
return x;
}
throw new Error("negative");
if (x < 0) { return -x; }
}
"#,
);
cfg.validate().expect("CFG should be valid");
}
}