use super::super::{CertRule, RuleViolation};
use crate::manifest::{RuleCategory, Severity};
use crate::utility::cert_c::ast_utils::get_node_text;
use std::collections::{HashMap, HashSet};
use tree_sitter::Node;
pub struct Dcl06C;
#[derive(Debug, Clone)]
struct LiteralInfo {
value: String,
line: usize,
column: usize,
context: String,
}
impl CertRule for Dcl06C {
fn rule_id(&self) -> &'static str {
"DCL06-C"
}
fn description(&self) -> &'static str {
"Use meaningful symbolic constants to represent literal values"
}
fn severity(&self) -> Severity {
Severity::Low
}
fn category(&self) -> RuleCategory {
RuleCategory::Recommendation
}
fn cert_id(&self) -> &'static str {
"DCL06-C"
}
fn check(&self, node: &Node, source: &str) -> Vec<RuleViolation> {
let mut violations = Vec::new();
let mut literal_occurrences: HashMap<String, Vec<LiteralInfo>> = HashMap::new();
let mut array_sizes: Vec<LiteralInfo> = Vec::new();
self.analyze_literals(node, source, &mut literal_occurrences, &mut array_sizes);
let sizeof_arrays = self.find_sizeof_usages(node, source);
for (literal_value, occurrences) in &literal_occurrences {
if self.is_acceptable_literal(literal_value) {
continue;
}
let first = &occurrences[0];
let message = if occurrences.len() >= 2 {
format!(
"Magic number '{}' appears {} times. Consider using a symbolic constant.",
literal_value,
occurrences.len()
)
} else if literal_value.starts_with('"') {
format!(
"String literal {} should use a symbolic constant.",
literal_value
)
} else {
format!(
"Magic number '{}' should use a symbolic constant.",
literal_value
)
};
violations.push(RuleViolation {
rule_id: self.rule_id().to_string(),
message,
severity: self.severity(),
line: first.line,
column: first.column,
file_path: String::new(),
suggestion: Some(format!(
"Define a constant: enum {{ CONSTANT_NAME = {} }};",
literal_value
)),
requires_manual_review: None,
});
}
for array_size in &array_sizes {
if !self.is_acceptable_literal(&array_size.value) {
let array_name = &array_size.context;
if !array_name.is_empty() && sizeof_arrays.contains(array_name) {
continue;
}
violations.push(RuleViolation {
rule_id: self.rule_id().to_string(),
message: format!(
"Array size '{}' should use a symbolic constant instead of magic number.",
array_size.value
),
severity: self.severity(),
line: array_size.line,
column: array_size.column,
file_path: String::new(),
suggestion: Some(format!(
"Define a constant: enum {{ ARRAY_SIZE = {} }};",
array_size.value
)),
requires_manual_review: None,
});
}
}
violations
}
}
impl Dcl06C {
fn analyze_literals(
&self,
node: &Node,
source: &str,
occurrences: &mut HashMap<String, Vec<LiteralInfo>>,
array_sizes: &mut Vec<LiteralInfo>,
) {
let kind = node.kind();
if kind == "array_declarator" {
if let Some(size_node) = self.find_array_size(node) {
if size_node.kind() == "number_literal" {
let value = get_node_text(&size_node, source).to_string();
let array_name = self.extract_array_name(node, source);
array_sizes.push(LiteralInfo {
value,
line: size_node.start_position().row + 1,
column: size_node.start_position().column + 1,
context: array_name.unwrap_or_default(),
});
}
}
}
if kind == "number_literal" {
let value = get_node_text(node, source).to_string();
let context = self.get_literal_context(node);
if self.is_suspicious_context(&context) {
let info = LiteralInfo {
value: value.clone(),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
context,
};
occurrences.entry(value).or_default().push(info);
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
self.analyze_literals(&child, source, occurrences, array_sizes);
}
}
}
fn extract_array_name(&self, array_decl: &Node, source: &str) -> Option<String> {
for i in 0..array_decl.child_count() {
if let Some(child) = array_decl.child(i) {
if child.kind() == "identifier" {
return Some(get_node_text(&child, source).to_string());
}
}
}
None
}
fn find_array_size<'a>(&self, array_decl: &'a Node<'a>) -> Option<Node<'a>> {
for i in 0..array_decl.child_count() {
if let Some(child) = array_decl.child(i) {
if child.kind() != "[" && child.kind() != "]" {
if child.kind() == "number_literal"
|| child.kind() == "identifier"
|| child.kind() == "binary_expression"
{
if child.kind() == "number_literal" {
return Some(child);
}
}
}
}
}
for i in 0..array_decl.child_count() {
if let Some(child) = array_decl.child(i) {
if child.kind() == "number_literal" {
return Some(child);
}
}
}
None
}
fn get_literal_context(&self, node: &Node) -> String {
if let Some(parent) = node.parent() {
match parent.kind() {
"binary_expression" => "comparison".to_string(),
"call_expression" => "function_argument".to_string(),
"argument_list" => "function_argument".to_string(),
"assignment_expression" => "assignment".to_string(),
"init_declarator" => "initialization".to_string(),
"array_declarator" => "array_size".to_string(),
"for_statement" => "loop".to_string(),
"while_statement" => "loop".to_string(),
_ => "other".to_string(),
}
} else {
"unknown".to_string()
}
}
fn is_suspicious_context(&self, context: &str) -> bool {
matches!(context, "comparison" | "function_argument")
}
fn is_acceptable_literal(&self, value: &str) -> bool {
let trimmed = value.trim();
if let Some(positive) = trimmed.strip_prefix('-') {
return self.is_acceptable_literal(positive);
}
let stripped = trimmed.trim_end_matches(['f', 'F', 'u', 'U', 'l', 'L']);
if stripped == "0.0" || stripped == "0." || stripped == ".0" {
return true;
}
if let Some(int_part) = stripped.strip_suffix(".0") {
if self.is_acceptable_integer(int_part) {
return true;
}
}
self.is_acceptable_integer(stripped)
}
fn is_acceptable_integer(&self, value: &str) -> bool {
matches!(
value,
"0" | "1"
| "2"
| "3"
| "4"
| "5"
| "6"
| "7"
| "8"
| "9"
| "10"
| "0x0"
| "0x1"
| "0x2"
)
}
fn find_sizeof_usages(&self, node: &Node, source: &str) -> HashSet<String> {
let mut sizeof_vars = HashSet::new();
self.collect_sizeof_usages(node, source, &mut sizeof_vars);
sizeof_vars
}
fn collect_sizeof_usages(&self, node: &Node, source: &str, sizeof_vars: &mut HashSet<String>) {
if node.kind() == "sizeof_expression" {
let text = get_node_text(node, source);
if let Some(start) = text.find('(') {
if let Some(end) = text.rfind(')') {
let inner = text[start + 1..end].trim();
if !inner.is_empty() && !inner.contains(' ') {
sizeof_vars.insert(inner.to_string());
}
}
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
self.collect_sizeof_usages(&child, source, sizeof_vars);
}
}
}
}