use super::super::{CertRule, RuleViolation};
use crate::manifest::{RuleCategory, Severity};
use crate::utility::cert_c::ast_utils;
use lang_parsing_substrate::query;
use tree_sitter::Node;
const MAX_VALUE_CONSTANTS: &[&str] = &[
"SIZE_MAX",
"UINT_MAX",
"UINT8_MAX",
"UINT16_MAX",
"UINT32_MAX",
"UINT64_MAX",
"ULONG_MAX",
"ULLONG_MAX",
"INT_MAX",
"LONG_MAX",
"LLONG_MAX",
];
#[derive(Debug, PartialEq, Eq)]
enum Step {
Unit,
NonUnit,
}
#[derive(Debug)]
pub struct Msc21C;
impl Msc21C {
#[allow(dead_code)]
pub fn new() -> Self {
Msc21C
}
fn literal_binary_step(&self, expr: &Node, var_name: &str, source: &str) -> Option<Step> {
if expr.kind() != "binary_expression" {
return None;
}
let op = ast_utils::get_node_text(&expr.child(1)?, source);
if op != "+" && op != "-" {
return None;
}
let left = expr.child_by_field_name("left")?;
let right = expr.child_by_field_name("right")?;
let literal_side =
if left.kind() == "identifier" && ast_utils::get_node_text(&left, source) == var_name {
right
} else if right.kind() == "identifier"
&& ast_utils::get_node_text(&right, source) == var_name
{
left
} else {
return None;
};
if literal_side.kind() != "number_literal" {
return None;
}
if ast_utils::get_node_text(&literal_side, source) == "1" {
Some(Step::Unit)
} else {
Some(Step::NonUnit)
}
}
fn classify_step(&self, update: &Node, var_name: &str, source: &str) -> Option<Step> {
match update.kind() {
"update_expression" => query::find_descendants_of_kind(*update, "identifier")
.iter()
.any(|n| ast_utils::get_node_text(n, source) == var_name)
.then_some(Step::Unit),
"assignment_expression" => {
let left = update.child_by_field_name("left")?;
if left.kind() != "identifier"
|| ast_utils::get_node_text(&left, source) != var_name
{
return None;
}
let op = ast_utils::get_node_text(&update.child(1)?, source);
let right = update.child_by_field_name("right")?;
match op {
"+=" | "-=" => {
if right.kind() != "number_literal" {
return None;
}
if ast_utils::get_node_text(&right, source) == "1" {
Some(Step::Unit)
} else {
Some(Step::NonUnit)
}
}
"=" => self.literal_binary_step(&right, var_name, source),
_ => None,
}
}
_ => None,
}
}
fn check_for_loop(&self, for_stmt: &Node, source: &str, violations: &mut Vec<RuleViolation>) {
let Some(cond) = for_stmt.child_by_field_name("condition") else {
return;
};
if cond.kind() != "binary_expression" {
return;
}
let Some(op) = cond.child(1) else { return };
let op_text = ast_utils::get_node_text(&op, source);
let Some(left) = cond.child_by_field_name("left") else {
return;
};
let Some(right) = cond.child_by_field_name("right") else {
return;
};
if left.kind() != "identifier" {
return;
}
let var_name = ast_utils::get_node_text(&left, source);
let step = for_stmt
.child_by_field_name("update")
.and_then(|u| self.classify_step(&u, var_name, source));
if (op_text == "!=" || op_text == "==") && step == Some(Step::NonUnit) {
let pos = cond.start_position();
violations.push(RuleViolation {
rule_id: "MSC21-C".to_string(),
severity: Severity::Low,
line: pos.row + 1,
column: pos.column + 1,
message: format!(
"loop termination uses '{}' with a step other than +-1 -- the counter can skip over the target value and never terminate",
op_text
),
file_path: String::new(),
suggestion: Some(
"Use a relational operator (<, <=, >, >=) to terminate the loop instead of an equality operator, unless the step is exactly 1"
.to_string(),
),
requires_manual_review: Some(false),
});
return;
}
if op_text == "<=" || op_text == ">=" {
if right.kind() == "identifier"
&& MAX_VALUE_CONSTANTS.contains(&ast_utils::get_node_text(&right, source))
{
let pos = cond.start_position();
violations.push(RuleViolation {
rule_id: "MSC21-C".to_string(),
severity: Severity::Low,
line: pos.row + 1,
column: pos.column + 1,
message: format!(
"loop condition compares against {} with '{}' -- once the counter reaches the type's boundary value, advancing it wraps around instead of exceeding the bound",
ast_utils::get_node_text(&right, source), op_text
),
file_path: String::new(),
suggestion: Some(
"Reduce the boundary by the loop's step (e.g. `i <= SIZE_MAX - step`) so the comparison can't be skipped by wraparound"
.to_string(),
),
requires_manual_review: Some(false),
});
}
}
}
fn traverse(&self, root: &Node, source: &str, violations: &mut Vec<RuleViolation>) {
for for_stmt in query::find_descendants_of_kind(*root, "for_statement") {
self.check_for_loop(&for_stmt, source, violations);
}
}
}
impl CertRule for Msc21C {
fn rule_id(&self) -> &'static str {
"MSC21-C"
}
fn description(&self) -> &'static str {
"Use robust loop termination conditions"
}
fn category(&self) -> RuleCategory {
RuleCategory::Rule
}
fn severity(&self) -> Severity {
Severity::Low
}
fn cert_id(&self) -> &'static str {
"MSC21-C"
}
fn scan(&self, root: &Node, source: &str, violations: &mut Vec<RuleViolation>) {
self.traverse(root, source, violations);
}
}