use super::{AnalysisContext, CfgAnalysis, CfgFinding, Confidence, is_sink};
use crate::cfg::{EdgeKind, StmtKind};
use crate::patterns::Severity;
use petgraph::graph::NodeIndex;
use petgraph::visit::EdgeRef;
pub struct IncompleteErrorHandling;
fn branch_terminates(cfg: &crate::cfg::Cfg, if_node: NodeIndex) -> bool {
let true_successors: Vec<NodeIndex> = cfg
.edges(if_node)
.filter(|e| matches!(e.weight(), EdgeKind::True))
.map(|e| e.target())
.collect();
if true_successors.is_empty() {
return false;
}
for &start in &true_successors {
if terminates_on_all_paths(cfg, start, if_node) {
return true;
}
}
false
}
fn terminates_on_all_paths(
cfg: &crate::cfg::Cfg,
node: NodeIndex,
_scope_entry: NodeIndex,
) -> bool {
use std::collections::HashSet;
let mut visited = HashSet::new();
let mut stack = vec![node];
while let Some(current) = stack.pop() {
if !visited.insert(current) {
continue;
}
let info = &cfg[current];
match info.kind {
StmtKind::Return | StmtKind::Break | StmtKind::Continue => {
continue;
}
_ => {}
}
let successors: Vec<_> = cfg.neighbors(current).collect();
if successors.is_empty() {
return false;
}
for succ in successors {
let is_back_edge = cfg
.edges(current)
.any(|e| e.target() == succ && matches!(e.weight(), EdgeKind::Back));
if !is_back_edge {
stack.push(succ);
}
}
}
true
}
fn find_post_if_sinks(cfg: &crate::cfg::Cfg, if_node: NodeIndex) -> Vec<NodeIndex> {
let mut sinks_after = Vec::new();
let mut visited = std::collections::HashSet::new();
let mut stack: Vec<NodeIndex> = cfg.neighbors(if_node).collect();
while let Some(current) = stack.pop() {
if !visited.insert(current) {
continue;
}
let info = &cfg[current];
if is_sink(info) || (info.kind == StmtKind::Call && info.callee.is_some()) {
sinks_after.push(current);
}
for succ in cfg.neighbors(current) {
let is_back_edge = cfg
.edges(current)
.any(|e| e.target() == succ && matches!(e.weight(), EdgeKind::Back));
if !is_back_edge {
stack.push(succ);
}
}
}
sinks_after
}
impl CfgAnalysis for IncompleteErrorHandling {
fn name(&self) -> &'static str {
"incomplete-error-handling"
}
fn run(&self, ctx: &AnalysisContext) -> Vec<CfgFinding> {
let mut findings = Vec::new();
for idx in ctx.cfg.node_indices() {
let info = &ctx.cfg[idx];
if info.kind != StmtKind::If {
continue;
}
let mentions_err = info.uses.iter().any(|u| {
let lower = u.to_ascii_lowercase();
lower == "err" || lower == "error" || lower.contains("err")
});
if !mentions_err {
continue;
}
if branch_terminates(ctx.cfg, idx) {
continue;
}
let post_sinks = find_post_if_sinks(ctx.cfg, idx);
let has_dangerous_successor = post_sinks.iter().any(|&s| is_sink(&ctx.cfg[s]));
if has_dangerous_successor {
findings.push(CfgFinding {
rule_id: "cfg-error-fallthrough".to_string(),
title: "Error check without return".to_string(),
severity: Severity::Medium,
confidence: Confidence::Medium,
span: info.span,
message: "Error check does not terminate on error; \
execution falls through to dangerous operations"
.to_string(),
evidence: vec![idx],
score: None,
});
}
}
findings
}
}