use super::super::{CertRule, RuleViolation};
use crate::analyze::const_eval;
use crate::manifest::{RuleCategory, Severity};
use crate::utility::cert_c::ast_utils::get_node_text;
use std::collections::HashSet;
use tree_sitter::Node;
pub struct Flp03C;
struct FpAnalyzer {
float_vars: HashSet<String>,
}
impl FpAnalyzer {
fn new() -> Self {
Self {
float_vars: HashSet::new(),
}
}
fn collect_float_vars(&mut self, node: &Node, source: &str) {
if node.kind() == "declaration" {
let decl_text = get_node_text(node, source);
if decl_text.starts_with("float")
|| decl_text.starts_with("double")
|| decl_text.contains(" float ")
|| decl_text.contains(" double ")
{
self.extract_identifiers_from_declaration(node, source);
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
self.collect_float_vars(&child, source);
}
}
}
fn extract_identifiers_from_declaration(&mut self, node: &Node, source: &str) {
if node.kind() == "identifier" {
if let Some(parent) = node.parent() {
let parent_kind = parent.kind();
if parent_kind == "init_declarator"
|| parent_kind == "declarator"
|| parent_kind == "pointer_declarator"
|| parent_kind == "array_declarator"
{
let var_name = get_node_text(node, source).to_string();
self.float_vars.insert(var_name);
}
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
self.extract_identifiers_from_declaration(&child, source);
}
}
}
fn is_fp_expression(&self, node: &Node, source: &str) -> bool {
let text = get_node_text(node, source);
if text.contains('.') && !text.starts_with("/*") {
return true;
}
let bytes = text.as_bytes();
for i in 1..bytes.len().saturating_sub(1) {
if bytes[i] == b'e' || bytes[i] == b'E' {
let before = bytes[i - 1];
let after = bytes[i + 1];
if before.is_ascii_digit()
&& (after.is_ascii_digit() || after == b'+' || after == b'-')
{
return true;
}
}
}
if text.contains("float") || text.contains("double") {
return true;
}
self.contains_float_identifier(node, source)
}
fn contains_float_identifier(&self, node: &Node, source: &str) -> bool {
if node.kind() == "identifier" {
let name = get_node_text(node, source);
if self.float_vars.contains(name) {
return true;
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if self.contains_float_identifier(&child, source) {
return true;
}
}
}
false
}
}
impl Flp03C {
#[allow(dead_code)]
pub fn new() -> Self {
Self
}
const FENV_FUNCTIONS: &'static [&'static str] = &[
"feclearexcept",
"fetestexcept",
"fegetexceptflag",
"fesetexceptflag",
"feraiseexcept",
];
const WINDOWS_FP_FUNCTIONS: &'static [&'static str] =
&["_clearfp", "_statusfp", "_controlfp", "_fpieee_flt"];
fn is_fp_error_check_function(&self, name: &str) -> bool {
Self::FENV_FUNCTIONS.contains(&name) || Self::WINDOWS_FP_FUNCTIONS.contains(&name)
}
fn contains_fp_error_checking(&self, node: &Node, source: &str) -> bool {
if node.kind() == "call_expression" {
if let Some(function) = node.child_by_field_name("function") {
let func_name = get_node_text(&function, source);
if self.is_fp_error_check_function(func_name) {
return true;
}
}
}
let node_text = get_node_text(node, source);
if node_text.contains("_try")
|| node_text.contains("__try")
|| node_text.contains("_except")
|| node_text.contains("__except")
{
return true;
}
if node_text.contains("_fpieee_flt") || node_text.contains("unmask_fpsr") {
return true;
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if self.contains_fp_error_checking(&child, source) {
return true;
}
}
}
false
}
fn check_fp_division(
&self,
node: &Node,
source: &str,
violations: &mut Vec<RuleViolation>,
analyzer: &FpAnalyzer,
) {
if node.kind() == "binary_expression" {
let mut is_division = false;
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "/" {
is_division = true;
break;
}
}
}
let left_fp = node
.child_by_field_name("left")
.is_some_and(|l| analyzer.is_fp_expression(&l, source));
let right_fp = node
.child_by_field_name("right")
.is_some_and(|r| analyzer.is_fp_expression(&r, source));
if is_division && (left_fp || right_fp) {
if self.is_inside_division_guard(node, source) {
return;
}
if let Some(right) = node.child_by_field_name("right") {
if right.kind() == "identifier" {
let var_name = get_node_text(&right, source);
if Self::divisor_provably_nonzero_fp(node, var_name, source) {
return;
}
}
}
if let Some(containing_func) = self.find_containing_function(node) {
if !self.contains_fp_error_checking(&containing_func, source) {
violations.push(RuleViolation {
rule_id: self.rule_id().to_string(),
severity: self.severity(),
message: "Floating-point division without error checking (consider using feclearexcept/fetestexcept)".to_string(),
file_path: String::new(),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
suggestion: Some(
"Use feclearexcept(FE_ALL_EXCEPT) before and fetestexcept(FE_ALL_EXCEPT) after floating-point operations".to_string()
),
..Default::default()
});
}
}
}
}
}
fn find_containing_function<'a>(&self, node: &Node<'a>) -> Option<Node<'a>> {
let mut current = Some(*node);
while let Some(n) = current {
if n.kind() == "function_definition" {
return Some(n);
}
current = n.parent();
}
None
}
fn is_inside_division_guard(&self, node: &Node, source: &str) -> bool {
let mut current = node.parent();
let mut depth = 0;
while let Some(n) = current {
if depth > 15 {
break;
}
if n.kind() == "if_statement" {
if let Some(condition) = n.child_by_field_name("condition") {
let cond_text = get_node_text(&condition, source);
if Self::is_division_guard_condition(&cond_text) {
return true;
}
}
}
current = n.parent();
depth += 1;
}
false
}
fn is_division_guard_condition(cond_text: &str) -> bool {
if cond_text.contains("fabs") || cond_text.contains("fabsf") || cond_text.contains("fabsl")
{
if cond_text.contains('>') {
return true;
}
}
if (cond_text.contains("!= 0") || cond_text.contains("!=0")) && !cond_text.contains("== 0")
{
return true;
}
if (cond_text.contains("> 0") || cond_text.contains(">0"))
&& !cond_text.contains(">= 0")
&& !cond_text.contains(">=0")
{
return true;
}
if (cond_text.contains("< 0") || cond_text.contains("<0"))
&& !cond_text.contains("<= 0")
&& !cond_text.contains("<=0")
{
return true;
}
false
}
fn divisor_provably_nonzero_fp(div_node: &Node, var_name: &str, source: &str) -> bool {
let mut current = Some(*div_node);
let func = loop {
match current {
Some(n) if n.kind() == "function_definition" => break n,
Some(n) => current = n.parent(),
None => return false,
}
};
let body = match func.child_by_field_name("body") {
Some(b) => b,
None => return false,
};
let mut all_nonzero = true;
let mut found_any = false;
Self::check_all_assignments(&body, var_name, source, &mut all_nonzero, &mut found_any);
if found_any && all_nonzero {
return true;
}
let root = {
let mut n = func;
while let Some(p) = n.parent() {
n = p;
}
n
};
let constants = const_eval::collect_macro_constants(&root, source);
let div_line = div_node.start_position().row;
let last_val =
Self::walk_scope_for_last_assignment(&body, var_name, source, div_line, &constants);
if last_val == Some(true) {
return true;
}
false
}
fn walk_scope_for_last_assignment(
scope: &Node,
var_name: &str,
source: &str,
div_line: usize,
constants: &const_eval::MacroConstantMap,
) -> Option<bool> {
let mut last_val: Option<bool> = None;
for i in 0..scope.named_child_count() {
let child = match scope.named_child(i) {
Some(c) => c,
None => continue,
};
if child.start_position().row >= div_line {
break;
}
if let Some(is_nz) = Self::get_assignment_value(&child, var_name, source) {
last_val = Some(is_nz);
continue;
}
if child.kind() == "if_statement" {
if let Some(cond) = child.child_by_field_name("condition") {
let const_val = Self::eval_condition_const(&cond, source, constants);
match const_val {
Some(true) => {
if let Some(then_br) = child.child_by_field_name("consequence") {
if let Some(v) = Self::walk_scope_for_last_assignment(
&then_br, var_name, source, div_line, constants,
) {
last_val = Some(v);
}
}
}
Some(false) => {
if let Some(else_br) = child.child_by_field_name("alternative") {
if let Some(v) = Self::walk_scope_for_last_assignment(
&else_br, var_name, source, div_line, constants,
) {
last_val = Some(v);
}
}
}
None => {
}
}
}
continue;
}
if child.kind() == "compound_statement" {
if let Some(v) = Self::walk_scope_for_last_assignment(
&child, var_name, source, div_line, constants,
) {
last_val = Some(v);
}
}
}
last_val
}
fn eval_condition_const(
cond: &Node,
source: &str,
constants: &const_eval::MacroConstantMap,
) -> Option<bool> {
let inner = if cond.kind() == "parenthesized_expression" {
cond.named_child(0).unwrap_or(*cond)
} else {
*cond
};
match inner.kind() {
"number_literal" => {
let text = get_node_text(&inner, source);
text.parse::<i64>().ok().map(|n| n != 0)
}
"identifier" => {
let name = get_node_text(&inner, source);
constants.get(name).map(|&v| v != 0)
}
"true" => Some(true),
"false" => Some(false),
_ => None,
}
}
fn check_all_assignments(
scope: &Node,
var_name: &str,
source: &str,
all_nonzero: &mut bool,
found_any: &mut bool,
) {
for i in 0..scope.named_child_count() {
let child = match scope.named_child(i) {
Some(c) => c,
None => continue,
};
if let Some(is_nz) = Self::get_assignment_value(&child, var_name, source) {
*found_any = true;
if !is_nz {
*all_nonzero = false;
}
} else {
Self::check_all_assignments(&child, var_name, source, all_nonzero, found_any);
}
}
}
fn get_assignment_value(node: &Node, var_name: &str, source: &str) -> Option<bool> {
match node.kind() {
"declaration" => {
for j in 0..node.named_child_count() {
if let Some(gc) = node.named_child(j) {
if gc.kind() == "init_declarator" {
let has_var = (0..gc.named_child_count()).any(|k| {
gc.named_child(k).is_some_and(|n| {
n.kind() == "identifier"
&& get_node_text(&n, source) == var_name
})
});
if has_var {
if let Some(val) = gc.named_child(gc.named_child_count() - 1) {
if val.kind() != "identifier"
|| get_node_text(&val, source) != var_name
{
return Some(Self::is_nonzero_literal(&val, source));
}
}
}
}
}
}
None
}
"expression_statement" => {
let expr = node.named_child(0)?;
if expr.kind() != "assignment_expression" {
return None;
}
let lhs = expr.child_by_field_name("left")?;
if lhs.kind() == "identifier" && get_node_text(&lhs, source) == var_name {
let rhs = expr.child_by_field_name("right")?;
return Some(Self::is_nonzero_literal(&rhs, source));
}
None
}
_ => None,
}
}
fn is_nonzero_literal(node: &Node, source: &str) -> bool {
match node.kind() {
"number_literal" => {
let text = get_node_text(node, source)
.trim_end_matches('f')
.trim_end_matches('F')
.trim_end_matches('l')
.trim_end_matches('L')
.to_string();
if let Ok(v) = text.parse::<f64>() {
return v != 0.0;
}
if let Ok(v) = text.parse::<i64>() {
return v != 0;
}
false
}
"unary_expression" => {
if let Some(arg) = node.child_by_field_name("argument") {
Self::is_nonzero_literal(&arg, source)
} else {
false
}
}
"parenthesized_expression" => {
if let Some(inner) = node.named_child(0) {
Self::is_nonzero_literal(&inner, source)
} else {
false
}
}
_ => false,
}
}
}
impl CertRule for Flp03C {
fn rule_id(&self) -> &'static str {
"FLP03-C"
}
fn description(&self) -> &'static str {
"Detect and handle floating-point errors"
}
fn severity(&self) -> Severity {
Severity::Low
}
fn category(&self) -> RuleCategory {
RuleCategory::Recommendation
}
fn cert_id(&self) -> &'static str {
"FLP03-C"
}
fn check(&self, node: &Node, source: &str) -> Vec<RuleViolation> {
let mut violations = Vec::new();
let mut analyzer = FpAnalyzer::new();
analyzer.collect_float_vars(node, source);
self.check_node(node, source, &mut violations, &analyzer);
violations
}
}
impl Flp03C {
fn check_node(
&self,
node: &Node,
source: &str,
violations: &mut Vec<RuleViolation>,
analyzer: &FpAnalyzer,
) {
if node.kind() == "binary_expression" {
self.check_fp_division(node, source, violations, analyzer);
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
self.check_node(&child, source, violations, analyzer);
}
}
}
}