use super::{
AstMeta, BodyCfg, BodyId, CallMeta, Cfg, EdgeKind, FuncSummaries, NodeInfo, StmtKind,
TaintMeta, build_sub, collect_idents, connect_all, push_node, text_of,
};
use crate::labels::{Kind, LangAnalysisRules, lookup};
use petgraph::graph::NodeIndex;
use tree_sitter::Node;
fn lang_has_exclusive_cases(lang: &str) -> bool {
matches!(lang, "rust" | "go")
}
fn extract_scrutinee_node<'a>(ast: Node<'a>, lang: &str) -> Option<Node<'a>> {
let field = match lang {
"rust" => "value",
"go" => "value",
"java" => "condition",
_ => return None,
};
ast.child_by_field_name(field)
}
fn extract_case_literal_text<'a>(case: Node<'a>, lang: &str, code: &'a [u8]) -> Option<String> {
let kind = case.kind();
match (lang, kind) {
("rust", "match_arm") => {
if case.child_by_field_name("guard").is_some() {
return None;
}
let pattern = case.child_by_field_name("pattern")?;
let inner = {
let mut cursor = pattern.walk();
pattern
.children(&mut cursor)
.find(|c| c.is_named())
.unwrap_or(pattern)
};
if matches!(
inner.kind(),
"_" | "wildcard"
| "range_pattern"
| "or_pattern"
| "tuple_struct_pattern"
| "struct_pattern"
| "ref_pattern"
| "tuple_pattern"
| "slice_pattern"
| "captured_pattern"
| "binding_pattern"
) {
return None;
}
text_of(inner, code)
}
("go", "expression_case") => {
let value = case.child_by_field_name("value")?;
let mut named_children: Vec<Node> = Vec::new();
let mut cursor = value.walk();
for child in value.children(&mut cursor) {
if child.is_named() {
named_children.push(child);
}
}
if named_children.len() == 1 {
text_of(named_children[0], code)
} else {
None
}
}
("java", "switch_rule") => {
let mut cursor = case.walk();
for child in case.children(&mut cursor) {
if child.kind() != "switch_label" {
continue;
}
let mut named_values: Vec<Node> = Vec::new();
let mut sl_cursor = child.walk();
let mut saw_default = false;
for sl_child in child.children(&mut sl_cursor) {
let k = sl_child.kind();
if k == "default" || k == "default_label" {
saw_default = true;
break;
}
if k == "case" || k == ":" || k == "->" || k == "," {
continue;
}
if sl_child.is_named() {
named_values.push(sl_child);
}
}
if saw_default || named_values.len() != 1 {
return None;
}
return text_of(named_values[0], code);
}
None
}
_ => None,
}
}
pub(super) fn is_exception_source(info: &NodeInfo) -> bool {
matches!(info.kind, StmtKind::Call)
}
pub(super) fn extract_catch_param_name<'a>(
catch_node: Node<'a>,
lang: &str,
code: &'a [u8],
) -> Option<String> {
match lang {
"javascript" | "js" | "typescript" | "ts" | "tsx" => {
let param = catch_node.child_by_field_name("parameter")?;
text_of(param, code)
}
"java" => {
let mut cursor = catch_node.walk();
for child in catch_node.children(&mut cursor) {
if child.kind() == "catch_formal_parameter" {
if let Some(name_node) = child.child_by_field_name("name") {
return text_of(name_node, code);
}
}
}
None
}
"php" => {
let name_node = catch_node.child_by_field_name("name")?;
text_of(name_node, code).map(|s| s.trim_start_matches('$').to_string())
}
"cpp" | "c++" => {
let params = catch_node.child_by_field_name("parameters")?;
let mut idents = Vec::new();
collect_idents(params, code, &mut idents);
idents.pop()
}
"python" | "py" => {
let alias = catch_node.child_by_field_name("alias")?;
text_of(alias, code)
}
"ruby" | "rb" => {
let var_node = catch_node.child_by_field_name("variable")?;
let mut cursor = var_node.walk();
for child in var_node.children(&mut cursor) {
if child.kind() == "identifier" {
return text_of(child, code);
}
}
None
}
_ => None,
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn build_begin_rescue<'a>(
ast: Node<'a>,
preds: &[NodeIndex],
g: &mut Cfg,
lang: &str,
code: &'a [u8],
summaries: &mut FuncSummaries,
file_path: &str,
enclosing_func: Option<&str>,
call_ordinal: &mut u32,
analysis_rules: Option<&LangAnalysisRules>,
break_targets: &mut Vec<NodeIndex>,
continue_targets: &mut Vec<NodeIndex>,
throw_targets: &mut Vec<NodeIndex>,
bodies: &mut Vec<BodyCfg>,
next_body_id: &mut u32,
current_body_id: BodyId,
) -> Vec<NodeIndex> {
let mut body_children: Vec<Node<'a>> = Vec::new();
let mut rescue_clauses: Vec<Node<'a>> = Vec::new();
let mut else_clause: Option<Node<'a>> = None;
let mut ensure_clause: Option<Node<'a>> = None;
let mut cursor = ast.walk();
for child in ast.children(&mut cursor) {
match child.kind() {
"rescue" => rescue_clauses.push(child),
"else" => else_clause = Some(child),
"ensure" => ensure_clause = Some(child),
_ if lookup(lang, child.kind()) == Kind::Trivia => {}
"begin" | "end" => {}
_ => body_children.push(child),
}
}
let try_body_first_idx = g.node_count();
let mut try_throw_targets = Vec::new();
let mut frontier = preds.to_vec();
for child in &body_children {
frontier = build_sub(
*child,
&frontier,
g,
lang,
code,
summaries,
file_path,
enclosing_func,
call_ordinal,
analysis_rules,
break_targets,
continue_targets,
&mut try_throw_targets,
bodies,
next_body_id,
current_body_id,
);
}
let try_exits = frontier;
let try_body_last_idx = g.node_count();
let mut exception_sources: Vec<NodeIndex> = Vec::new();
for raw in try_body_first_idx..try_body_last_idx {
let idx = NodeIndex::new(raw);
if is_exception_source(&g[idx]) {
exception_sources.push(idx);
}
}
exception_sources.extend(&try_throw_targets);
let mut all_catch_exits: Vec<NodeIndex> = Vec::new();
for rescue_node in &rescue_clauses {
let param_name = extract_catch_param_name(*rescue_node, lang, code);
let catch_preds = if let Some(ref name) = param_name {
let synth = g.add_node(NodeInfo {
kind: StmtKind::Seq,
ast: AstMeta {
span: (rescue_node.start_byte(), rescue_node.start_byte()),
enclosing_func: enclosing_func.map(|s| s.to_string()),
},
taint: TaintMeta {
defines: Some(name.clone()),
..Default::default()
},
call: CallMeta {
callee: Some(format!("catch({name})")),
..Default::default()
},
catch_param: true,
..Default::default()
});
for &src in &exception_sources {
g.add_edge(src, synth, EdgeKind::Exception);
}
vec![synth]
} else {
Vec::new()
};
let catch_first_idx = NodeIndex::new(g.node_count());
let rescue_body = rescue_node.child_by_field_name("body");
let catch_exits = if let Some(body_node) = rescue_body {
build_sub(
body_node,
&catch_preds,
g,
lang,
code,
summaries,
file_path,
enclosing_func,
call_ordinal,
analysis_rules,
break_targets,
continue_targets,
throw_targets,
bodies,
next_body_id,
current_body_id,
)
} else {
let mut rescue_cursor = rescue_node.walk();
let mut rf = catch_preds.clone();
for child in rescue_node.children(&mut rescue_cursor) {
match child.kind() {
"exceptions" | "exception_variable" => {}
_ if lookup(lang, child.kind()) == Kind::Trivia => {}
"=>" | "rescue" => {}
_ => {
rf = build_sub(
child,
&rf,
g,
lang,
code,
summaries,
file_path,
enclosing_func,
call_ordinal,
analysis_rules,
break_targets,
continue_targets,
throw_targets,
bodies,
next_body_id,
current_body_id,
);
}
}
}
rf
};
if param_name.is_none() {
let catch_entry = if catch_first_idx.index() < g.node_count() {
catch_first_idx
} else {
continue;
};
for &src in &exception_sources {
g.add_edge(src, catch_entry, EdgeKind::Exception);
}
}
all_catch_exits.extend(catch_exits);
}
let normal_exits = if let Some(else_node) = else_clause {
build_sub(
else_node,
&try_exits,
g,
lang,
code,
summaries,
file_path,
enclosing_func,
call_ordinal,
analysis_rules,
break_targets,
continue_targets,
throw_targets,
bodies,
next_body_id,
current_body_id,
)
} else {
try_exits
};
if let Some(ensure_node) = ensure_clause {
let mut ensure_preds: Vec<NodeIndex> = Vec::new();
ensure_preds.extend(&normal_exits);
ensure_preds.extend(&all_catch_exits);
if rescue_clauses.is_empty() {
ensure_preds.extend(&try_throw_targets);
}
build_sub(
ensure_node,
&ensure_preds,
g,
lang,
code,
summaries,
file_path,
enclosing_func,
call_ordinal,
analysis_rules,
break_targets,
continue_targets,
throw_targets,
bodies,
next_body_id,
current_body_id,
)
} else {
let mut exits = normal_exits;
exits.extend(all_catch_exits);
exits
}
}
pub(super) fn is_switch_case_kind(kind: &str) -> bool {
matches!(
kind,
"switch_case"
| "switch_default"
| "case_statement"
| "default_statement"
| "expression_case"
| "default_case"
| "type_case"
| "type_switch_case"
| "communication_case"
| "switch_block_statement_group"
)
}
pub(super) fn is_default_case_kind(kind: &str) -> bool {
matches!(
kind,
"switch_default" | "default_statement" | "default_case"
)
}
pub(super) fn case_has_default_label(case: Node<'_>) -> bool {
let mut cursor = case.walk();
for child in case.children(&mut cursor) {
let k = child.kind();
if k == "default" || k == "default_label" {
return true;
}
}
false
}
#[allow(clippy::too_many_arguments)]
pub(super) fn build_switch<'a>(
ast: Node<'a>,
preds: &[NodeIndex],
g: &mut Cfg,
lang: &str,
code: &'a [u8],
summaries: &mut FuncSummaries,
file_path: &str,
enclosing_func: Option<&str>,
call_ordinal: &mut u32,
analysis_rules: Option<&LangAnalysisRules>,
_break_targets: &mut Vec<NodeIndex>,
continue_targets: &mut Vec<NodeIndex>,
throw_targets: &mut Vec<NodeIndex>,
bodies: &mut Vec<BodyCfg>,
next_body_id: &mut u32,
current_body_id: BodyId,
) -> Vec<NodeIndex> {
let body = ast.child_by_field_name("body").or_else(|| {
let mut c = ast.walk();
ast.children(&mut c)
.find(|n| matches!(lookup(lang, n.kind()), Kind::Block))
});
let container = body.unwrap_or(ast);
let mut cases: Vec<(Node<'a>, bool)> = Vec::new();
{
let mut cursor = container.walk();
for case in container.children(&mut cursor) {
let k = case.kind();
if !is_switch_case_kind(k) {
continue;
}
let is_default = is_default_case_kind(k) || case_has_default_label(case);
cases.push((case, is_default));
}
}
if cases.is_empty() {
let header = push_node(
g,
StmtKind::If,
ast,
lang,
code,
enclosing_func,
0,
analysis_rules,
);
connect_all(g, preds, header, EdgeKind::Seq);
let mut switch_breaks: Vec<NodeIndex> = Vec::new();
let mut frontier = vec![header];
let mut cursor = container.walk();
for child in container.children(&mut cursor) {
frontier = build_sub(
child,
&frontier,
g,
lang,
code,
summaries,
file_path,
enclosing_func,
call_ordinal,
analysis_rules,
&mut switch_breaks,
continue_targets,
throw_targets,
bodies,
next_body_id,
current_body_id,
);
}
let mut exits = switch_breaks;
exits.extend(frontier);
return exits;
}
let default_pos = cases.iter().position(|(_, d)| *d);
if let Some(pos) = default_pos
&& pos != cases.len() - 1
{
let default_pair = cases.remove(pos);
cases.push(default_pair);
}
let has_default = default_pos.is_some();
let supports_exclusive_cases = lang_has_exclusive_cases(lang) || lang == "java";
let (scrutinee_text, scrutinee_idents) = if supports_exclusive_cases {
match extract_scrutinee_node(ast, lang) {
Some(scrut) => {
let mut idents = Vec::new();
collect_idents(scrut, code, &mut idents);
idents.sort();
idents.dedup();
let text = text_of(scrut, code).map(|s| {
let trimmed = s.trim();
if trimmed.starts_with('(') && trimmed.ends_with(')') {
trimmed[1..trimmed.len() - 1].trim().to_string()
} else {
trimmed.to_string()
}
});
let single_ident =
matches!((&text, idents.as_slice()), (Some(t), [name]) if t == name);
if single_ident {
(text, idents)
} else {
(None, Vec::new())
}
}
None => (None, Vec::new()),
}
} else {
(None, Vec::new())
};
let mut switch_breaks: Vec<NodeIndex> = Vec::new();
let mut fallthrough_exits: Vec<NodeIndex> = Vec::new();
let mut last_header_false: Option<NodeIndex> = None;
let mut chain_preds: Vec<NodeIndex> = preds.to_vec();
for (idx, (case, is_default)) in cases.iter().copied().enumerate() {
let is_last = idx + 1 == cases.len();
let case_first_preds: Vec<NodeIndex> = if is_default && is_last {
let mut p = chain_preds.clone();
p.append(&mut fallthrough_exits);
last_header_false = chain_preds.first().copied();
p
} else {
let header = push_node(
g,
StmtKind::If,
case,
lang,
code,
enclosing_func,
0,
analysis_rules,
);
g[header].taint.labels.clear();
g[header].call.callee = None;
g[header].call.sink_payload_args = None;
g[header].call.destination_uses = None;
g[header].call.gate_filters.clear();
if let Some(scrut_text) = scrutinee_text.as_ref() {
if let Some(case_lit) = extract_case_literal_text(case, lang, code) {
g[header].condition_text = Some(format!("{} == {}", scrut_text, case_lit));
g[header].condition_vars = scrutinee_idents.clone();
g[header].condition_negated = false;
}
}
connect_all(g, &chain_preds, header, EdgeKind::Seq);
if let Some(prev) = last_header_false {
g.add_edge(prev, header, EdgeKind::False);
}
let mut p = vec![header];
p.append(&mut fallthrough_exits);
last_header_false = Some(header);
chain_preds = vec![header];
p
};
let body_first_idx = NodeIndex::new(g.node_count());
let exits = build_sub(
case,
&case_first_preds,
g,
lang,
code,
summaries,
file_path,
enclosing_func,
call_ordinal,
analysis_rules,
&mut switch_breaks,
continue_targets,
throw_targets,
bodies,
next_body_id,
current_body_id,
);
if body_first_idx.index() < g.node_count() {
let header_for_true = if is_default && is_last {
if let Some(prev) = last_header_false {
g.add_edge(prev, body_first_idx, EdgeKind::False);
}
None
} else {
chain_preds.first().copied()
};
if let Some(h) = header_for_true {
g.add_edge(h, body_first_idx, EdgeKind::True);
}
}
fallthrough_exits = exits;
let _ = is_default;
}
let mut exits: Vec<NodeIndex> = switch_breaks;
exits.append(&mut fallthrough_exits);
if !has_default {
if let Some(prev) = last_header_false {
exits.push(prev);
}
}
exits
}
#[allow(clippy::too_many_arguments)]
pub(super) fn build_try<'a>(
ast: Node<'a>,
preds: &[NodeIndex],
g: &mut Cfg,
lang: &str,
code: &'a [u8],
summaries: &mut FuncSummaries,
file_path: &str,
enclosing_func: Option<&str>,
call_ordinal: &mut u32,
analysis_rules: Option<&LangAnalysisRules>,
break_targets: &mut Vec<NodeIndex>,
continue_targets: &mut Vec<NodeIndex>,
throw_targets: &mut Vec<NodeIndex>,
bodies: &mut Vec<BodyCfg>,
next_body_id: &mut u32,
current_body_id: BodyId,
) -> Vec<NodeIndex> {
if ast.child_by_field_name("body").is_none() {
let mut cursor = ast.walk();
let has_rescue_or_ensure = ast
.children(&mut cursor)
.any(|c| c.kind() == "rescue" || c.kind() == "ensure");
if has_rescue_or_ensure {
return build_begin_rescue(
ast,
preds,
g,
lang,
code,
summaries,
file_path,
enclosing_func,
call_ordinal,
analysis_rules,
break_targets,
continue_targets,
throw_targets,
bodies,
next_body_id,
current_body_id,
);
}
}
let try_body = ast.child_by_field_name("body");
let catch_clauses: Vec<Node<'a>> = {
let mut clauses = Vec::new();
if let Some(handler) = ast.child_by_field_name("handler") {
clauses.push(handler);
}
let mut cursor = ast.walk();
for child in ast.children(&mut cursor) {
if (child.kind() == "catch_clause" || child.kind() == "except_clause")
&& !clauses.iter().any(|c| c.id() == child.id())
{
clauses.push(child);
}
}
clauses
};
let finally_clause = ast.child_by_field_name("finalizer").or_else(|| {
let mut cursor = ast.walk();
ast.children(&mut cursor)
.find(|child| child.kind() == "finally_clause")
});
let try_preds = if let Some(resources) = ast.child_by_field_name("resources") {
let first_resource_idx = g.node_count();
let result = build_sub(
resources,
preds,
g,
lang,
code,
summaries,
file_path,
enclosing_func,
call_ordinal,
analysis_rules,
break_targets,
continue_targets,
throw_targets,
bodies,
next_body_id,
current_body_id,
);
for raw in first_resource_idx..g.node_count() {
let idx = NodeIndex::new(raw);
if g[idx].kind == StmtKind::Call && g[idx].taint.defines.is_some() {
g[idx].managed_resource = true;
}
}
result
} else {
preds.to_vec()
};
let try_body_first_idx = g.node_count();
let mut try_throw_targets = Vec::new();
let try_exits = if let Some(body) = try_body {
build_sub(
body,
&try_preds,
g,
lang,
code,
summaries,
file_path,
enclosing_func,
call_ordinal,
analysis_rules,
break_targets,
continue_targets,
&mut try_throw_targets,
bodies,
next_body_id,
current_body_id,
)
} else {
try_preds
};
let try_body_last_idx = g.node_count();
let mut exception_sources: Vec<NodeIndex> = Vec::new();
for raw in try_body_first_idx..try_body_last_idx {
let idx = NodeIndex::new(raw);
if is_exception_source(&g[idx]) {
exception_sources.push(idx);
}
}
exception_sources.extend(&try_throw_targets);
let mut all_catch_exits: Vec<NodeIndex> = Vec::new();
if catch_clauses.is_empty() {
} else {
for catch_node in &catch_clauses {
let param_name = extract_catch_param_name(*catch_node, lang, code);
let catch_preds = if let Some(ref name) = param_name {
let synth = g.add_node(NodeInfo {
kind: StmtKind::Seq,
ast: AstMeta {
span: (catch_node.start_byte(), catch_node.start_byte()),
enclosing_func: enclosing_func.map(|s| s.to_string()),
},
taint: TaintMeta {
defines: Some(name.clone()),
..Default::default()
},
call: CallMeta {
callee: Some(format!("catch({name})")),
..Default::default()
},
catch_param: true,
..Default::default()
});
for &src in &exception_sources {
g.add_edge(src, synth, EdgeKind::Exception);
}
vec![synth]
} else {
Vec::new()
};
let catch_first_idx = NodeIndex::new(g.node_count());
let catch_exits = build_sub(
*catch_node,
&catch_preds,
g,
lang,
code,
summaries,
file_path,
enclosing_func,
call_ordinal,
analysis_rules,
break_targets,
continue_targets,
throw_targets,
bodies,
next_body_id,
current_body_id,
);
if param_name.is_none() {
let catch_entry = if catch_first_idx.index() < g.node_count() {
catch_first_idx
} else {
continue;
};
for &src in &exception_sources {
g.add_edge(src, catch_entry, EdgeKind::Exception);
}
}
all_catch_exits.extend(catch_exits);
}
}
if let Some(finally_node) = finally_clause {
let mut finally_preds: Vec<NodeIndex> = Vec::new();
finally_preds.extend(&try_exits);
finally_preds.extend(&all_catch_exits);
if catch_clauses.is_empty() {
finally_preds.extend(&try_throw_targets);
}
let finally_exits = build_sub(
finally_node,
&finally_preds,
g,
lang,
code,
summaries,
file_path,
enclosing_func,
call_ordinal,
analysis_rules,
break_targets,
continue_targets,
throw_targets,
bodies,
next_body_id,
current_body_id,
);
finally_exits
} else {
let mut exits = try_exits;
exits.extend(all_catch_exits);
exits
}
}