use super::super::{CertRule, RuleViolation};
use crate::manifest::{RuleCategory, Severity};
use tree_sitter::Node;
pub struct Pre05C;
impl CertRule for Pre05C {
fn rule_id(&self) -> &'static str {
"PRE05-C"
}
fn description(&self) -> &'static str {
"Understand macro replacement when concatenating tokens or performing stringification"
}
fn severity(&self) -> Severity {
Severity::Low
}
fn category(&self) -> RuleCategory {
RuleCategory::Recommendation
}
fn cert_id(&self) -> &'static str {
"PRE05-C"
}
fn check(&self, node: &Node, source: &str) -> Vec<RuleViolation> {
let mut violations = Vec::new();
let called_macros = self.find_called_macros(node, source);
self.check_node(node, source, &mut violations, &called_macros);
violations
}
}
impl Pre05C {
fn find_called_macros(&self, node: &Node, source: &str) -> Vec<String> {
let mut called = Vec::new();
self.collect_called_macros(node, source, &mut called);
called
}
fn collect_called_macros(&self, node: &Node, source: &str, called: &mut Vec<String>) {
if node.kind() == "preproc_function_def" {
if let Some(value_node) = node.child_by_field_name("value") {
let value_text = &source[value_node.start_byte()..value_node.end_byte()];
for word in value_text.split(|c: char| !c.is_alphanumeric() && c != '_') {
if !word.is_empty() && !called.contains(&word.to_string()) {
called.push(word.to_string());
}
}
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
self.collect_called_macros(&child, source, called);
}
}
}
fn check_node(
&self,
node: &Node,
source: &str,
violations: &mut Vec<RuleViolation>,
called_macros: &[String],
) {
if node.kind() == "preproc_function_def" {
self.check_macro_definition(node, source, violations, called_macros);
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
self.check_node(&child, source, violations, called_macros);
}
}
}
fn check_macro_definition(
&self,
node: &Node,
source: &str,
violations: &mut Vec<RuleViolation>,
called_macros: &[String],
) {
let macro_text = &source[node.start_byte()..node.end_byte()];
if macro_text.contains("##") || self.contains_stringification(macro_text) {
let macro_name = self.extract_macro_name(node, source);
if self.is_likely_helper_macro(¯o_name) {
return;
}
if self.has_proper_wrapper(node, source, ¯o_name, called_macros) {
return;
}
let operator = if macro_text.contains("##") { "##" } else { "#" };
let operation = if operator == "##" {
"token concatenation"
} else {
"stringification"
};
violations.push(RuleViolation {
rule_id: self.rule_id().to_string(),
severity: Severity::Low,
message: format!(
"Macro '{}' uses {} operator ({}) which prevents parameter expansion - consider using two-level macro indirection for proper expansion",
macro_name, operation, operator
),
file_path: String::new(),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
suggestion: Some(format!(
"Use two-level indirection: define a wrapper macro that calls another macro with {}, allowing parameters to expand first",
operator
)),
..Default::default()
});
}
}
fn contains_stringification(&self, macro_text: &str) -> bool {
let chars: Vec<char> = macro_text.chars().collect();
for i in 0..chars.len() {
if chars[i] == '#' {
if i == 0 {
continue;
}
if i + 1 < chars.len() && chars[i + 1] == '#' {
continue;
}
if i > 0 && chars[i - 1] == '#' {
continue;
}
return true;
}
}
false
}
fn extract_macro_name(&self, node: &Node, source: &str) -> String {
if let Some(name_node) = node.child_by_field_name("name") {
source[name_node.start_byte()..name_node.end_byte()].to_string()
} else {
"unknown".to_string()
}
}
fn is_likely_helper_macro(&self, name: &str) -> bool {
let upper = name.to_uppercase();
upper.ends_with("_AGAIN")
|| upper.ends_with("_IMPL")
|| upper.ends_with("_INTERNAL")
|| upper.ends_with("_HELPER")
|| upper.ends_with("_INNER")
|| upper.ends_with("_")
}
fn has_proper_wrapper(
&self,
_node: &Node,
source: &str,
macro_name: &str,
_called_macros: &[String],
) -> bool {
let lines: Vec<&str> = source.lines().collect();
for line in &lines {
let trimmed = line.trim();
if !trimmed.starts_with("#define") {
continue;
}
if trimmed.contains(&format!("#define {}", macro_name))
|| trimmed.contains(&format!("#define {}(", macro_name))
{
continue;
}
if trimmed.contains(macro_name) && trimmed.contains('(') {
if !trimmed.contains("##") && !self.line_contains_stringification(trimmed) {
return true;
}
}
}
false
}
fn line_contains_stringification(&self, line: &str) -> bool {
let chars: Vec<char> = line.chars().collect();
for i in 0..chars.len() {
if chars[i] == '#' {
if i == 0 {
continue;
}
if i + 1 < chars.len() && chars[i + 1] == '#' {
continue;
}
if i > 0 && chars[i - 1] == '#' {
continue;
}
return true;
}
}
false
}
}