use super::ast_utils::get_node_text;
use tree_sitter::Node;
const ORDERING_OPERATORS: &[&str] = &["<", "<=", ">", ">="];
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ComparisonKind {
Any,
OrderingOrExtremeEquality,
}
pub fn has_dominating_comparison(
var: &str,
site: &Node,
source: &str,
kind: ComparisonKind,
) -> bool {
dominating_conditions(site)
.iter()
.any(|cond| condition_compares_var(cond, var, source, kind))
}
pub fn call_arg_guards(call_node: &Node, source: &str) -> Vec<bool> {
let Some(arg_list) = call_node.child_by_field_name("arguments") else {
return Vec::new();
};
let mut guards = Vec::new();
for i in 0..arg_list.child_count() {
let Some(arg) = arg_list.child(i) else {
continue;
};
if !arg.is_named() || arg.kind() == "comment" {
continue;
}
let inner = strip_arg_wrappers(&arg);
guards.push(
inner.kind() == "identifier"
&& has_dominating_comparison(
&get_node_text(&inner, source),
call_node,
source,
ComparisonKind::Any,
),
);
}
guards
}
pub fn collect_call_arg_guards(
node: &Node,
source: &str,
out: &mut std::collections::HashMap<String, Vec<Vec<bool>>>,
) {
if node.kind() == "call_expression" {
if let Some(callee) = node.child_by_field_name("function") {
if callee.kind() == "identifier" {
out.entry(get_node_text(&callee, source).to_string())
.or_default()
.push(call_arg_guards(node, source));
}
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
collect_call_arg_guards(&child, source, out);
}
}
}
pub fn strip_arg_wrappers<'a>(node: &Node<'a>) -> Node<'a> {
let mut current = strip_parens(node);
while current.kind() == "cast_expression" {
let Some(value) = current.child_by_field_name("value") else {
break;
};
current = strip_parens(&value);
}
current
}
fn strip_parens<'a>(node: &Node<'a>) -> Node<'a> {
let mut n = *node;
while n.kind() == "parenthesized_expression" {
let Some(inner) = (0..n.child_count())
.filter_map(|i| n.child(i))
.find(|c| !matches!(c.kind(), "(" | ")"))
else {
break;
};
n = inner;
}
n
}
pub fn dominating_conditions<'a>(site: &Node<'a>) -> Vec<Node<'a>> {
let mut conditions = enclosing_conditions(site);
conditions.extend(preceding_if_conditions(site));
conditions
}
fn enclosing_conditions<'a>(site: &Node<'a>) -> Vec<Node<'a>> {
let mut conditions = Vec::new();
let mut current = *site;
while let Some(parent) = current.parent() {
match parent.kind() {
"if_statement"
| "while_statement"
| "for_statement"
| "switch_statement"
| "conditional_expression" => {
if let Some(cond) = parent.child_by_field_name("condition") {
if !spans(&cond, site) {
conditions.push(cond);
}
}
}
"binary_expression" => {
let is_logical = parent
.child_by_field_name("operator")
.map(|op| matches!(op.kind(), "&&" | "||"))
.unwrap_or(false);
if is_logical {
if let (Some(left), Some(right)) = (
parent.child_by_field_name("left"),
parent.child_by_field_name("right"),
) {
if spans(&right, site) {
conditions.push(left);
}
}
}
}
"function_definition" => break,
_ => {}
}
current = parent;
}
conditions
}
const BLOCK_LIKE_KINDS: &[&str] = &[
"compound_statement",
"preproc_if",
"preproc_ifdef",
"preproc_else",
"preproc_elif",
];
fn preceding_if_conditions<'a>(site: &Node<'a>) -> Vec<Node<'a>> {
let mut conditions = Vec::new();
let mut current = *site;
while let Some(parent) = current.parent() {
if BLOCK_LIKE_KINDS.contains(&parent.kind()) {
let mut cursor = parent.walk();
for stmt in parent.named_children(&mut cursor) {
if stmt.start_byte() >= current.start_byte() {
break;
}
collect_block_level_if_conditions(&stmt, &mut conditions);
}
}
if parent.kind() == "function_definition" {
break;
}
current = parent;
}
conditions
}
fn collect_block_level_if_conditions<'a>(stmt: &Node<'a>, out: &mut Vec<Node<'a>>) {
if BLOCK_LIKE_KINDS.contains(&stmt.kind()) && stmt.kind() != "compound_statement" {
let mut cursor = stmt.walk();
for inner in stmt.named_children(&mut cursor) {
collect_block_level_if_conditions(&inner, out);
}
return;
}
collect_if_chain_conditions(stmt, out);
}
fn collect_if_chain_conditions<'a>(stmt: &Node<'a>, out: &mut Vec<Node<'a>>) {
let mut current = *stmt;
loop {
if current.kind() != "if_statement" {
return;
}
if let Some(cond) = current.child_by_field_name("condition") {
out.push(cond);
}
let alternative = match current.child_by_field_name("alternative") {
Some(a) => a,
None => return,
};
current = if alternative.kind() == "else_clause" {
let mut cursor = alternative.walk();
let inner = alternative
.named_children(&mut cursor)
.find(|c| c.kind() == "if_statement");
match inner {
Some(inner) => inner,
None => return,
}
} else {
alternative
};
}
}
pub fn condition_compares_var(
condition: &Node,
var: &str,
source: &str,
kind: ComparisonKind,
) -> bool {
let operator = condition
.child_by_field_name("operator")
.map(|op| op.kind())
.unwrap_or("");
let is_test = match condition.kind() {
"binary_expression" => {
ORDERING_OPERATORS.contains(&operator)
|| (matches!(operator, "==" | "!=")
&& (kind == ComparisonKind::Any || equality_bounds_var(condition, var, source)))
}
"unary_expression" => operator == "!",
_ => false,
};
if is_test && mentions_var(condition, var, source) {
return true;
}
let mut cursor = condition.walk();
let any_child_compares = condition
.named_children(&mut cursor)
.any(|child| condition_compares_var(&child, var, source, kind));
any_child_compares
}
fn equality_bounds_var(comparison: &Node, var: &str, source: &str) -> bool {
let (Some(left), Some(right)) = (
comparison.child_by_field_name("left"),
comparison.child_by_field_name("right"),
) else {
return false;
};
let other = match (
mentions_var(&left, var, source),
mentions_var(&right, var, source),
) {
(true, false) => right,
(false, true) => left,
_ => return false,
};
let text = get_node_text(&other, source).trim();
is_zero_literal(text) || is_integer_limit_name(text)
}
fn is_zero_literal(text: &str) -> bool {
matches!(
text,
"0" | "0x0" | "0X0" | "0u" | "0U" | "0L" | "0l" | "0UL" | "0ul"
)
}
fn is_integer_limit_name(text: &str) -> bool {
if text.contains(char::is_lowercase) {
return false;
}
text.ends_with("_MIN")
|| text.ends_with("_MAX")
|| text.starts_with("SMALLEST_")
|| text.starts_with("LARGEST_")
}
pub fn mentions_var(node: &Node, var: &str, source: &str) -> bool {
if node.kind() == "identifier" {
return get_node_text(node, source) == var;
}
let mut cursor = node.walk();
let any_child_mentions = node
.named_children(&mut cursor)
.any(|child| mentions_var(&child, var, source));
any_child_mentions
}
fn spans(outer: &Node, inner: &Node) -> bool {
outer.start_byte() <= inner.start_byte() && inner.end_byte() <= outer.end_byte()
}
#[cfg(test)]
mod tests {
use super::*;
use lang_parsing_substrate::query;
use tree_sitter::Parser;
fn parse_c_code(code: &str) -> tree_sitter::Tree {
let mut parser = Parser::new();
let language = crate::parser::c_language();
parser.set_language(&language).unwrap();
parser.parse(code, None).unwrap()
}
fn guarded_at_arithmetic(src: &str, var: &str, kind: ComparisonKind) -> bool {
let tree = parse_c_code(src);
let site = query::find_descendants_of_kind(tree.root_node(), "binary_expression")
.into_iter()
.find(|n| {
n.child_by_field_name("operator")
.is_some_and(|op| matches!(op.kind(), "+" | "-" | "*" | "<<"))
})
.expect("fixture has an arithmetic expression");
has_dominating_comparison(var, &site, src, kind)
}
fn guarded(src: &str, var: &str) -> bool {
guarded_at_arithmetic(src, var, ComparisonKind::OrderingOrExtremeEquality)
}
#[test]
fn early_return_guard_without_spaces_counts() {
assert!(guarded(
"int f(int N){ if(N<0) return 0; return N+1; }",
"N"
));
}
#[test]
fn enclosing_branch_condition_counts() {
assert!(guarded(
"int f(int c){ if(c<128){ return c+1; } return 0; }",
"c"
));
}
#[test]
fn else_if_branch_carries_its_own_bound() {
assert!(guarded(
"int f(int c){ if(c<128){ return 0; } else if(c<65536){ return c+1; } return 0; }",
"c"
));
}
#[test]
fn loop_condition_bound_counts() {
assert!(guarded(
"int f(int argc){ int i; for(i=1;i<argc;i++){ return argc-1; } return 0; }",
"argc"
));
}
#[test]
fn reversed_operands_and_compound_bound_count() {
assert!(guarded(
"void f(struct s *p, unsigned idx){ if(idx>=p->num) return; while(idx+1<p->num) idx++; }",
"idx"
));
assert!(guarded(
"int f(int n){ if(5>n) return 0; return n+1; }",
"n"
));
}
#[test]
fn guard_as_one_conjunct_counts() {
assert!(guarded("int f(int n){ return n>0 && n+1; }", "n"));
}
#[test]
fn negation_guard_counts() {
assert!(guarded("int f(int n){ if(!n) return 0; return n*2; }", "n"));
}
#[test]
fn guard_after_the_arithmetic_does_not_count() {
assert!(!guarded(
"int f(int n){ int d = n*2; if(n<0) return 0; return d; }",
"n"
));
}
#[test]
fn guard_in_a_sibling_branch_does_not_count() {
assert!(!guarded(
"int f(int flag, int n){ if(flag){ if(n<100) return n; return 0; } return n+1; }",
"n"
));
}
#[test]
fn a_comparison_containing_the_arithmetic_does_not_guard_it() {
assert!(!guarded(
"int f(unsigned off, unsigned n, unsigned limit){ if(off+n>limit) return 0; return 1; }",
"n"
));
}
#[test]
fn guard_on_a_different_variable_does_not_count() {
assert!(!guarded(
"int f(int m, int n){ if(m<0) return 0; return n+1; }",
"n"
));
}
#[test]
fn substring_of_another_identifier_is_not_the_variable() {
assert!(!guarded(
"int f(int len, int n){ if(len<0) return 0; return n+1; }",
"n"
));
}
#[test]
fn guard_and_arithmetic_inside_one_preproc_block_are_siblings() {
assert!(guarded(
"int f(int sock){\n#ifdef USE_SELECT\n if(sock<0) return 0;\n return sock+1;\n#endif\n}",
"sock"
));
}
#[test]
fn arbitrary_equality_bounds_nothing_for_overflow() {
let src = "int f(int idx){ if(idx==DATA_VERSION){ return 0; } return 36+idx*4; }";
assert!(!guarded(src, "idx"));
assert!(guarded_at_arithmetic(src, "idx", ComparisonKind::Any));
}
#[test]
fn equality_against_the_overflowing_extreme_is_the_guard() {
assert!(guarded(
"int f(int n){ if(n==INT_MIN){ return 0; } return n*-1; }",
"n"
));
assert!(guarded(
"int f(int n){ if(n==SMALLEST_INT32){ return 0; } return n*-1; }",
"n"
));
}
#[test]
fn integer_limit_names_are_token_anchored() {
assert!(is_integer_limit_name("INT_MIN"));
assert!(is_integer_limit_name("SIZE_MAX"));
assert!(is_integer_limit_name("SMALLEST_INT32"));
assert!(!is_integer_limit_name("MAX_RETRIES"));
assert!(!is_integer_limit_name("int_max"));
}
#[test]
fn do_while_condition_does_not_guard_its_first_iteration() {
assert!(!guarded(
"int f(int n){ int t=0; do { t = n+1; } while(n>0); return t; }",
"n"
));
}
}