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;
pub struct Dcl00C;
impl CertRule for Dcl00C {
fn rule_id(&self) -> &'static str {
"DCL00-C"
}
fn description(&self) -> &'static str {
"Const-qualify immutable objects"
}
fn severity(&self) -> Severity {
Severity::Medium
}
fn category(&self) -> RuleCategory {
RuleCategory::Recommendation
}
fn cert_id(&self) -> &'static str {
"DCL00-C"
}
fn check(&self, node: &Node, source: &str) -> Vec<RuleViolation> {
query::find_descendants_of_kind(*node, "init_declarator")
.into_iter()
.filter_map(|init_declarator| self.check_init_declarator(init_declarator, source))
.collect()
}
}
impl Dcl00C {
fn check_init_declarator(&self, node: Node, source: &str) -> Option<RuleViolation> {
let declarator = node.child_by_field_name("declarator")?;
let value = node.child_by_field_name("value")?;
let parent_decl = find_parent_declaration(&node)?;
if is_in_for_loop_init(&parent_decl) {
return None;
}
if has_const_qualifier(&parent_decl, source) {
return None;
}
let var_name = ast_utils::get_identifier_from_declarator(&declarator, source);
if should_be_const(&parent_decl, &declarator, &value, &var_name, source)
&& !is_modified_in_scope(&parent_decl, &var_name, source)
{
let start_point = parent_decl.start_position();
return Some(RuleViolation {
rule_id: self.rule_id().to_string(),
severity: Severity::Medium,
message: format!(
"Variable '{}' is initialized but never modified, consider const-qualifying it",
var_name
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some(format!("Add 'const' qualifier: const {} = ...", var_name)),
..Default::default()
});
}
None
}
}
fn find_parent_declaration<'a>(node: &'a Node<'a>) -> Option<Node<'a>> {
let mut current = node.parent();
while let Some(parent) = current {
if parent.kind() == "declaration" {
return Some(parent);
}
current = parent.parent();
}
None
}
fn is_in_for_loop_init(decl_node: &Node) -> bool {
if let Some(parent) = decl_node.parent() {
if parent.kind() == "for_statement" {
if let Some(init) = parent.child_by_field_name("initializer") {
return init.id() == decl_node.id();
}
}
}
false
}
fn has_const_qualifier(node: &Node, source: &str) -> bool {
let text = &source[node.start_byte()..node.end_byte()];
text.contains("const")
}
fn should_be_const(
decl_node: &Node,
declarator_node: &Node,
value_node: &Node,
var_name: &str,
source: &str,
) -> bool {
let value_text = &source[value_node.start_byte()..value_node.end_byte()];
let decl_text = &source[decl_node.start_byte()..decl_node.end_byte()];
let var_lower = var_name.to_lowercase();
let is_char_type = decl_text.contains("char");
let is_array = declarator_node.kind() == "array_declarator";
let is_pointer = is_pointer_declarator(declarator_node);
let has_string_literal = value_text.starts_with('"') && value_text.ends_with('"');
let has_brace_init = value_text.contains('{') && value_text.contains('}');
let excluded_prefixes = ["current_", "temp_", "tmp_", "buffer_", "buf_", "work_"];
if excluded_prefixes
.iter()
.any(|prefix| var_lower.starts_with(prefix))
{
return false;
}
if is_char_type && is_array && has_string_literal {
return true;
}
if is_char_type && is_pointer && has_string_literal {
return true;
}
if is_array && has_brace_init && decl_text.contains("(*") {
return true;
}
if is_array && has_brace_init {
let excluded_patterns = [
"fibonacci",
"test",
"temp",
"tmp",
"buffer",
"buf",
"_array",
"_data",
"sort",
"work",
"example",
"demo",
];
let is_excluded = excluded_patterns
.iter()
.any(|pattern| var_lower.contains(pattern));
if !is_excluded {
let semantic_patterns = [
"days",
"month",
"prime",
"color",
"command",
"lookup",
"table",
"digit",
"palette",
"rgb",
"hex",
"state",
"operation",
"function",
"menu",
];
if semantic_patterns
.iter()
.any(|pattern| var_lower.contains(pattern))
{
return true;
}
}
}
let is_int_type = decl_text.contains("int");
let is_scalar = !is_array && !is_pointer;
let is_numeric_literal = value_text
.chars()
.all(|c| c.is_numeric() || c == '-' || c == '+');
if is_int_type && is_scalar && is_numeric_literal {
let counter_patterns = ["count", "counter", "index", "step", "iter"];
if counter_patterns
.iter()
.any(|pattern| var_lower.contains(pattern))
{
return false;
}
let scalar_constant_patterns = [
"rows", "cols", "columns", "_size", "limit", "bound", "width", "height", "depth",
"num_", "start_", "end_", "batch_",
];
if scalar_constant_patterns
.iter()
.any(|pattern| var_lower.contains(pattern))
{
return true;
}
}
let math_physics_constants = [
"pi",
"tau",
"euler",
"e", "gravity",
"g",
"speed_of_light",
"c",
"planck",
"h", "avogadro",
"boltzmann",
"gas_constant",
"r", "epsilon",
"mu",
"sigma", ];
for constant in &math_physics_constants {
if var_lower == *constant || var_lower.ends_with(&format!("_{}", constant)) {
return true;
}
}
let conversion_patterns = [
"_per_", "_to_", "_rate", "_factor", "_ratio", "meters_", "kg_", "pounds_", "miles_",
"feet_",
];
for pattern in &conversion_patterns {
if var_lower.contains(pattern) {
return true;
}
}
if var_name.len() > 1 {
let all_caps = var_name
.chars()
.all(|c| c.is_uppercase() || c == '_' || c.is_numeric());
let has_alpha = var_name.chars().any(|c| c.is_alphabetic());
if all_caps && has_alpha {
return true;
}
}
if var_name.starts_with("k") && var_name.len() > 1 {
if let Some(second_char) = var_name.chars().nth(1) {
if second_char.is_uppercase() {
return true;
}
}
}
let constant_suffixes = [
"_MAX",
"_MIN",
"_SIZE",
"_COUNT",
"_LIMIT",
"_CAPACITY",
"_LENGTH",
"_WIDTH",
"_HEIGHT",
"_TIMEOUT",
"_INTERVAL",
"_THRESHOLD",
];
for suffix in &constant_suffixes {
if var_name.contains(suffix) {
return true;
}
}
if is_char_type && has_string_literal {
let path_file_patterns = [
"_dir",
"_path",
"_folder",
"_directory",
"_extension",
"_ext",
"_prefix",
"_suffix",
"_url",
"_uri",
"_pattern",
"_format",
"_template",
];
for pattern in &path_file_patterns {
if var_lower.contains(pattern) {
return true;
}
}
if var_lower.contains("file") && has_string_literal {
return true;
}
}
if has_brace_init && decl_text.contains("struct") {
let struct_constant_names = [
"origin",
"config",
"configuration",
"default",
"initial",
"settings",
"options",
"params",
"parameters",
];
for name in &struct_constant_names {
if var_lower.contains(name) {
return true;
}
}
}
false
}
fn is_modified_in_scope(decl_node: &Node, var_name: &str, source: &str) -> bool {
let scope = match enclosing_scope(decl_node) {
Some(s) => s,
None => return false,
};
scope_has_mutation(&scope, var_name, source)
}
fn enclosing_scope<'a>(node: &'a Node<'a>) -> Option<Node<'a>> {
let mut current = node.parent();
let mut topmost = None;
while let Some(n) = current {
if n.kind() == "compound_statement" || n.kind() == "translation_unit" {
return Some(n);
}
topmost = Some(n);
current = n.parent();
}
topmost
}
fn scope_has_mutation(node: &Node, var_name: &str, source: &str) -> bool {
query::find_first_descendant(*node, |n| match n.kind() {
"assignment_expression" => n
.child_by_field_name("left")
.map(|left| lvalue_base_is(&left, var_name, source))
.unwrap_or(false),
"update_expression" => n
.child_by_field_name("argument")
.map(|arg| lvalue_base_is(&arg, var_name, source))
.unwrap_or(false),
"pointer_expression" => {
if let Some(op) = n.child_by_field_name("operator") {
if &source[op.start_byte()..op.end_byte()] == "&" {
if let Some(arg) = n.child_by_field_name("argument") {
return lvalue_base_is(&arg, var_name, source);
}
}
}
false
}
_ => false,
})
.is_some()
}
fn lvalue_base_is(node: &Node, var_name: &str, source: &str) -> bool {
match node.kind() {
"identifier" => &source[node.start_byte()..node.end_byte()] == var_name,
"field_expression" | "subscript_expression" | "pointer_expression" => node
.child_by_field_name("argument")
.map(|a| lvalue_base_is(&a, var_name, source))
.unwrap_or(false),
"parenthesized_expression" => (0..node.child_count())
.filter_map(|i| node.child(i))
.any(|c| lvalue_base_is(&c, var_name, source)),
_ => false,
}
}
fn is_pointer_declarator(node: &Node) -> bool {
node.kind() == "pointer_declarator" || node.to_sexp().contains("pointer_declarator")
}