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;
fn is_error_var_ident(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
if lower == "err" || lower == "error" {
return true;
}
if lower.starts_with("err_") || lower.starts_with("error_") {
return true;
}
if lower.ends_with("_err") || lower.ends_with("_error") {
return true;
}
false
}
fn contains_negated_err_identifier(text: &str) -> bool {
let bytes = text.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] != b'!' {
i += 1;
continue;
}
if i + 1 < bytes.len() && bytes[i + 1] == b'=' {
i += 1;
continue;
}
let mut j = i + 1;
while j < bytes.len() && (bytes[j] == b' ' || bytes[j] == b'\t') {
j += 1;
}
if j < bytes.len() && bytes[j] == b'(' {
j += 1;
while j < bytes.len() && (bytes[j] == b' ' || bytes[j] == b'\t') {
j += 1;
}
}
let start = j;
while j < bytes.len() {
let b = bytes[j];
if b.is_ascii_alphanumeric() || b == b'_' || b == b'.' || b == b'$' {
j += 1;
} else {
break;
}
}
if j > start {
let mut k = start;
while k + 2 < j {
if (bytes[k] | 0x20) == b'e'
&& (bytes[k + 1] | 0x20) == b'r'
&& (bytes[k + 2] | 0x20) == b'r'
{
return true;
}
k += 1;
}
}
i = if j > i { j } else { i + 1 };
}
false
}
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::Throw | 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
.edges(if_node)
.filter(|e| matches!(e.weight(), EdgeKind::False | EdgeKind::Seq))
.map(|e| e.target())
.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.call.callee.is_some()) {
sinks_after.push(current);
}
for edge in cfg.edges(current) {
let succ = edge.target();
if matches!(edge.weight(), EdgeKind::Back | EdgeKind::Exception) {
continue;
}
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.condition_vars.iter().any(|u| is_error_var_ident(u));
if !mentions_err {
continue;
}
if let Some(text) = info.condition_text.as_deref()
&& contains_negated_err_identifier(text)
{
continue;
}
if info.condition_negated {
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.ast.span,
message: "Error check does not terminate on error; \
execution falls through to dangerous operations"
.to_string(),
evidence: vec![idx],
score: None,
});
}
}
findings
}
}
#[cfg(test)]
mod negation_tests {
use super::contains_negated_err_identifier;
#[test]
fn detects_simple_negated_err() {
assert!(contains_negated_err_identifier("!err"));
assert!(contains_negated_err_identifier("!error"));
assert!(contains_negated_err_identifier("! err"));
}
#[test]
fn detects_negated_member_err() {
assert!(contains_negated_err_identifier("!data.error"));
assert!(contains_negated_err_identifier(
"data && !data.error && Array.isArray(results)"
));
assert!(contains_negated_err_identifier(
"!response.errorMsg && response.ok"
));
}
#[test]
fn does_not_match_inequality() {
assert!(!contains_negated_err_identifier("err != nil"));
assert!(!contains_negated_err_identifier("error !== null"));
}
#[test]
fn does_not_match_positive_err_checks() {
assert!(!contains_negated_err_identifier("err"));
assert!(!contains_negated_err_identifier("err != null"));
assert!(!contains_negated_err_identifier("response.error"));
assert!(!contains_negated_err_identifier("hasError(x)"));
}
}
#[cfg(test)]
mod err_ident_tests {
use super::is_error_var_ident;
#[test]
fn matches_canonical_error_vars() {
assert!(is_error_var_ident("err"));
assert!(is_error_var_ident("error"));
assert!(is_error_var_ident("ERR"));
assert!(is_error_var_ident("Error"));
}
#[test]
fn matches_snake_case_error_vars() {
assert!(is_error_var_ident("err_resp"));
assert!(is_error_var_ident("error_msg"));
assert!(is_error_var_ident("response_err"));
assert!(is_error_var_ident("parse_error"));
}
#[test]
fn rejects_camelcase_method_names() {
assert!(!is_error_var_ident("isErrorEnabled"));
assert!(!is_error_var_ident("getError"));
assert!(!is_error_var_ident("hasError"));
assert!(!is_error_var_ident("errorMsg"));
assert!(!is_error_var_ident("errCode"));
}
#[test]
fn rejects_unrelated_idents() {
assert!(!is_error_var_ident("user"));
assert!(!is_error_var_ident("merry"));
assert!(!is_error_var_ident("perform"));
}
}