use super::super::{CertRule, RuleViolation};
use crate::analyze::const_eval::{collect_macro_constants, try_evaluate_expr, MacroConstantMap};
use crate::manifest::{RuleCategory, Severity};
use crate::utility::cert_c::ast_utils::{
find_containing_function, get_function_parameters, is_array_parameter_type,
is_function_parameter, is_inside_loop, is_write_context,
};
use crate::utility::cert_c::size_analysis::{
find_allocation_size, find_element_size, find_string_literal_length,
};
use crate::utility::cert_c::variable_analysis::{
has_bounds_validation, has_validation_before_loop, is_uninitialized_variable,
is_user_input_variable,
};
use lang_parsing_substrate::query;
use tree_sitter::Node;
pub struct Arr00C;
impl CertRule for Arr00C {
fn rule_id(&self) -> &'static str {
"ARR00-C"
}
fn description(&self) -> &'static str {
"Understand how arrays are used"
}
fn severity(&self) -> Severity {
Severity::Medium
}
fn category(&self) -> RuleCategory {
RuleCategory::Recommendation
}
fn cert_id(&self) -> &'static str {
"ARR00-C"
}
fn check(&self, node: &Node, source: &str) -> Vec<RuleViolation> {
let mut violations = Vec::new();
let macros = collect_macro_constants(node, source);
let candidates = query::find_descendants_of_kinds(
*node,
&[
"assignment_expression",
"sizeof_expression",
"binary_expression",
"declaration",
"call_expression",
"for_statement",
"return_statement",
"subscript_expression",
],
);
for current in candidates {
let node = ¤t;
match node.kind() {
"assignment_expression" => {
if let Some(violation) = check_array_assignment(node, source) {
violations.push(violation);
}
}
"sizeof_expression" => {
if let Some(violation) = check_sizeof_misuse(node, source) {
violations.push(violation);
}
}
"binary_expression" => {
if let Some(violation) = check_array_comparison(node, source) {
violations.push(violation);
}
if let Some(violation) = check_pointer_arithmetic(node, source) {
violations.push(violation);
}
if let Some(violation) = check_pointer_subtraction(node, source) {
violations.push(violation);
}
}
"declaration" => {
if let Some(violation) = check_vla_declaration(node, source) {
violations.push(violation);
}
}
"call_expression" => {
if let Some(violation) = check_dangerous_functions(node, source) {
violations.push(violation);
}
if let Some(violation) = check_obvious_string_overflow(node, source) {
violations.push(violation);
}
if let Some(violation) = check_memcpy_size_mismatch(node, source) {
violations.push(violation);
}
if let Some(violation) = check_memory_operation_overflow(node, source) {
violations.push(violation);
}
}
"for_statement" => {
if let Some(violation) = check_loop_exceeds_allocation(node, source) {
violations.push(violation);
}
if let Some(violation) = check_loop_bound_exceeds_array(node, source) {
violations.push(violation);
}
if let Some(violation) = check_loop_array_access(node, source) {
violations.push(violation);
}
}
"return_statement" => {
if let Some(violation) = check_return_local_array(node, source) {
violations.push(violation);
}
}
"subscript_expression" => {
if let Some(violation) = check_comma_in_subscript(node, source) {
violations.push(violation);
}
if let Some(violation) = check_uninitialized_array_read(node, source) {
violations.push(violation);
}
if let Some(violation) = check_use_after_free(node, source) {
violations.push(violation);
}
if let Some(violation) = check_constant_out_of_bounds(node, source, ¯os) {
violations.push(violation);
}
if let Some(violation) = check_subscript_bounds(node, source) {
violations.push(violation);
}
if let Some(violation) = check_boundary_value_index(node, source) {
violations.push(violation);
}
}
_ => {}
}
}
violations
}
}
fn check_array_assignment(node: &Node, source: &str) -> Option<RuleViolation> {
if let Some(op) = node.child_by_field_name("operator") {
let op_text = &source[op.start_byte()..op.end_byte()];
if op_text != "=" {
return None;
}
}
let left = node.child_by_field_name("left")?;
let right = node.child_by_field_name("right")?;
if is_array_identifier(&left, source) && !is_subscript(&left) {
if is_array_identifier(&right, source) {
let start_point = node.start_position();
let left_text = &source[left.start_byte()..left.end_byte()];
let right_text = &source[right.start_byte()..right.end_byte()];
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::High,
message: format!(
"Cannot directly assign arrays: '{}' = '{}'. Arrays are not assignable in C.",
left_text, right_text
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some("Use memcpy() or a loop to copy array elements".to_string()),
..Default::default()
});
}
}
None
}
fn check_sizeof_misuse(node: &Node, source: &str) -> Option<RuleViolation> {
if node.child_count() >= 2 {
if let Some(arg_expr) = node.child(1) {
if arg_expr.kind() == "parenthesized_expression" {
for i in 0..arg_expr.child_count() {
if let Some(child) = arg_expr.child(i) {
if child.kind() == "identifier" {
return check_if_array_parameter(&child, node, source);
}
}
}
} else if arg_expr.kind() == "identifier" {
return check_if_array_parameter(&arg_expr, node, source);
}
}
}
None
}
fn check_if_array_parameter(
identifier_node: &Node,
sizeof_node: &Node,
source: &str,
) -> Option<RuleViolation> {
let identifier_name = &source[identifier_node.start_byte()..identifier_node.end_byte()];
let function_def = find_containing_function(identifier_node)?;
let parameters = get_function_parameters(&function_def, source)?;
for (param_name, param_type) in parameters {
if param_name == identifier_name && is_array_parameter_type(¶m_type) {
let start_point = sizeof_node.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::Medium,
message: format!(
"Misuse of sizeof() on array parameter '{}'. Array parameters decay to pointers, sizeof will return pointer size not array size.",
identifier_name
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some("Pass array size as a separate parameter or use a different method to track array size".to_string()),
..Default::default()
});
}
}
None
}
fn check_vla_declaration(node: &Node, source: &str) -> Option<RuleViolation> {
let mut declarator = None;
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "array_declarator" {
declarator = Some(child);
break;
} else if child.kind() == "init_declarator" {
for j in 0..child.child_count() {
if let Some(inner) = child.child(j) {
if inner.kind() == "array_declarator" {
declarator = Some(inner);
break;
}
}
}
}
}
}
let declarator = declarator?;
let mut size_node = None;
let mut found_open_bracket = false;
for i in 0..declarator.child_count() {
if let Some(child) = declarator.child(i) {
if child.kind() == "[" {
found_open_bracket = true;
continue;
}
if found_open_bracket && child.kind() != "]" {
size_node = Some(child);
break;
}
}
}
let size_node = size_node?;
let size_text = &source[size_node.start_byte()..size_node.end_byte()];
let is_vla = size_node.kind() == "identifier"
|| size_node.kind() == "call_expression"
|| size_node.kind() == "binary_expression"
|| (size_node.kind() != "number_literal" && !size_text.chars().all(|c| c.is_numeric()));
if !is_vla {
if size_text == "0" {
let start_point = declarator.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::High,
message: "Array declared with size 0. Zero-length arrays have undefined behavior."
.to_string(),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some(
"Use a positive constant size or validate variable size before declaration"
.to_string(),
),
..Default::default()
});
}
return None; }
if size_node.kind() == "identifier" {
let size_var_name = size_text;
if let Some(violation) = check_vla_size_validation(node, size_var_name, source, &declarator)
{
return Some(violation);
}
}
None
}
fn check_vla_size_validation(
decl_node: &Node,
size_var: &str,
source: &str,
declarator: &Node,
) -> Option<RuleViolation> {
let function_node = find_containing_function(decl_node)?;
let vla_position = decl_node.start_byte();
let function_start = function_node.start_byte();
let preceding_text = &source[function_start..vla_position];
if preceding_text.contains(&format!("{} = 0", size_var))
|| preceding_text.contains(&format!("{}=0", size_var))
{
let start_point = declarator.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::High,
message: format!(
"Variable Length Array declared with size '{}' which is assigned 0. VLAs must have positive size.",
size_var
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some("Validate that the size is positive before declaring the VLA".to_string()),
..Default::default()
});
}
if is_function_parameter(&function_node, size_var, source) {
if !has_size_validation_before(preceding_text, size_var) {
let start_point = declarator.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::Medium,
message: format!(
"Variable Length Array declared with unvalidated parameter '{}'. Size could be zero or negative.",
size_var
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some("Add validation: if (size <= 0 || size > MAX_SIZE) return;".to_string()),
..Default::default()
});
}
}
None
}
fn is_loop_variable(var_name: &str, preceding_text: &str) -> bool {
let mut in_for_init = false;
let mut paren_depth = 0;
let mut chars = preceding_text.chars().peekable();
let mut current_word = String::new();
let mut for_init_content = String::new();
while let Some(ch) = chars.next() {
current_word.push(ch);
if current_word.len() > 3 {
current_word.remove(0);
}
if current_word == "for" {
let remaining: String = chars.clone().collect();
if remaining.trim_start().starts_with('(') {
in_for_init = true;
for_init_content.clear();
paren_depth = 0;
continue;
}
}
if in_for_init {
if ch == '(' {
paren_depth += 1;
if paren_depth > 1 {
for_init_content.push(ch);
}
} else if ch == ')' {
paren_depth -= 1;
if paren_depth > 0 {
for_init_content.push(ch);
}
} else if ch == ';' && paren_depth == 1 {
let words: Vec<&str> = for_init_content
.split(|c: char| !c.is_alphanumeric() && c != '_')
.filter(|s| !s.is_empty())
.collect();
if words.contains(&var_name) {
return true;
}
in_for_init = false;
for_init_content.clear();
} else if paren_depth >= 1 {
for_init_content.push(ch);
}
}
}
false
}
fn has_size_validation_before(text: &str, size_var: &str) -> bool {
let simple_patterns = [
format!("{} == 0", size_var),
format!("{} <= 0", size_var),
format!("{} < 1", size_var),
format!("0 == {}", size_var),
format!("{}==0", size_var),
format!("{}<=0", size_var),
format!("{}<1", size_var),
];
simple_patterns.iter().any(|pattern| text.contains(pattern))
}
fn check_dangerous_functions(node: &Node, source: &str) -> Option<RuleViolation> {
let function = node.child_by_field_name("function")?;
let func_text = &source[function.start_byte()..function.end_byte()];
if func_text == "gets" {
let start_point = node.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::Critical,
message: "Use of gets() demonstrates misunderstanding of array bounds. It is deprecated and has no safe usage.".to_string(),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some("Use fgets(buffer, sizeof(buffer), stdin) which respects buffer size".to_string()),
..Default::default()
});
}
if func_text == "scanf" || func_text == "fscanf" || func_text == "sscanf" {
let arguments = node.child_by_field_name("arguments")?;
let mut args = Vec::new();
for i in 0..arguments.child_count() {
if let Some(child) = arguments.child(i) {
let kind = child.kind();
if kind != "," && kind != "(" && kind != ")" {
args.push(child);
}
}
}
let format_arg_index = if func_text == "scanf" { 0 } else { 1 };
if args.len() <= format_arg_index {
return None;
}
let format_arg = args[format_arg_index];
if format_arg.kind() == "string_literal" {
let format_text = &source[format_arg.start_byte()..format_arg.end_byte()];
if format_text.contains("%s") {
let has_unbounded_s = format_text.match_indices("%s").any(|(pos, _)| {
if pos > 0 {
let before_percent = &format_text[..pos];
if let Some(percent_pos) = before_percent.rfind('%') {
let between = &before_percent[percent_pos + 1..];
!between.chars().all(|c| c.is_ascii_digit())
} else {
true }
} else {
true }
});
if has_unbounded_s {
let start_point = node.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::Critical,
message: format!(
"Use of {}() with unbounded '%s' format specifier can overflow buffer. This demonstrates misunderstanding of array bounds.",
func_text
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some(format!(
"Use a width specifier like '%Ns' where N is buffer size minus 1, e.g., {}(\"%9s\", buffer) for char buffer[10]",
func_text
)),
..Default::default()
});
}
}
}
}
None
}
fn check_obvious_string_overflow(node: &Node, source: &str) -> Option<RuleViolation> {
let function = node.child_by_field_name("function")?;
let func_text = &source[function.start_byte()..function.end_byte()];
let is_str_concat = func_text == "strcat" || func_text == "strcpy";
let is_sprintf = func_text == "sprintf" || func_text == "snprintf";
if !is_str_concat && !is_sprintf {
return None;
}
let arguments = node.child_by_field_name("arguments")?;
let mut args = Vec::new();
for i in 0..arguments.child_count() {
if let Some(child) = arguments.child(i) {
let kind = child.kind();
if kind != "," && kind != "(" && kind != ")" {
args.push(child);
}
}
}
let (dest, src) = if is_sprintf {
if args.len() < 3 {
return None; }
let format_arg = args[1];
let format_text = &source[format_arg.start_byte()..format_arg.end_byte()];
if !format_text.contains("%s") {
return None; }
(args[0], args[2]) } else {
if args.len() < 2 {
return None;
}
(args[0], args[1])
};
let src_len = if src.kind() == "string_literal" {
let src_text = &source[src.start_byte()..src.end_byte()];
Some(src_text.trim_matches('"').len())
} else if src.kind() == "identifier" {
let src_var_name = &source[src.start_byte()..src.end_byte()];
find_string_literal_length(src_var_name, node, source)
} else {
None
};
let src_len = src_len?;
let dest_name = &source[dest.start_byte()..dest.end_byte()];
let function_node = find_containing_function(node)?;
let function_start = function_node.start_byte();
let call_position = node.start_byte();
let preceding_text = &source[function_start..call_position];
if let Some(dest_size) = find_array_size(dest_name, preceding_text) {
let required_space = if func_text == "strcat" {
src_len + 1 } else {
src_len + 1 };
if required_space > dest_size {
let start_point = node.start_position();
let src_display = &source[src.start_byte()..src.end_byte()];
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::Critical,
message: format!(
"{}({}, {}) will overflow buffer. Source string is {} bytes but destination has only {} bytes.",
func_text,
dest_name,
src_display,
src_len,
dest_size
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some(format!(
"Use safer alternatives like strncat/strncpy with proper size limits, or increase buffer size to at least {} bytes",
required_space
)),
..Default::default()
});
}
}
None
}
fn check_memcpy_size_mismatch(node: &Node, source: &str) -> Option<RuleViolation> {
let function = node.child_by_field_name("function")?;
let func_text = &source[function.start_byte()..function.end_byte()];
if func_text != "memcpy" && func_text != "memmove" {
return None;
}
let arguments = node.child_by_field_name("arguments")?;
let mut args = Vec::new();
for i in 0..arguments.child_count() {
if let Some(child) = arguments.child(i) {
let kind = child.kind();
if kind != "," && kind != "(" && kind != ")" {
args.push(child);
}
}
}
if args.len() < 3 {
return None;
}
let dest = args[0];
let src = args[1];
let size_arg = args[2];
if size_arg.kind() != "sizeof_expression" {
return None; }
let sizeof_arg = size_arg.child_by_field_name("value")?;
let sizeof_arg_text = &source[sizeof_arg.start_byte()..sizeof_arg.end_byte()];
let dest_name = &source[dest.start_byte()..dest.end_byte()];
let src_name = &source[src.start_byte()..src.end_byte()];
let function_node = find_containing_function(node)?;
let function_start = function_node.start_byte();
let call_position = node.start_byte();
let preceding_text = &source[function_start..call_position];
let sizeof_stripped = sizeof_arg_text.trim_matches('(').trim_matches(')').trim();
if sizeof_stripped == src_name {
let dest_size = find_array_size(dest_name, preceding_text)?;
let src_size = find_array_size(src_name, preceding_text)?;
if dest_size < src_size {
let start_point = node.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::Critical,
message: format!(
"{}({}, {}, sizeof({})) will overflow destination. Destination array '{}' has {} elements but source '{}' has {} elements.",
func_text,
dest_name,
src_name,
src_name,
dest_name,
dest_size,
src_name,
src_size
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some(format!(
"Use sizeof({}) instead of sizeof({}) to avoid buffer overflow",
dest_name,
src_name
)),
..Default::default()
});
}
}
None
}
fn check_memory_operation_overflow(node: &Node, source: &str) -> Option<RuleViolation> {
let function = node.child_by_field_name("function")?;
let func_text = &source[function.start_byte()..function.end_byte()];
let is_memset = func_text == "memset";
let is_memcpy = func_text == "memcpy";
let is_memmove = func_text == "memmove";
if !is_memset && !is_memcpy && !is_memmove {
return None;
}
let arguments = node.child_by_field_name("arguments")?;
let mut args = Vec::new();
for i in 0..arguments.child_count() {
if let Some(child) = arguments.child(i) {
let kind = child.kind();
if kind != "," && kind != "(" && kind != ")" {
args.push(child);
}
}
}
if args.len() < 3 {
return None;
}
let buffer = args[0];
let size_arg = args[2];
if size_arg.kind() != "number_literal" {
return None;
}
let size_text = &source[size_arg.start_byte()..size_arg.end_byte()];
let size_bytes: usize = size_text.parse().ok()?;
let buffer_name = &source[buffer.start_byte()..buffer.end_byte()];
let function_node = find_containing_function(node)?;
let function_start = function_node.start_byte();
let call_position = node.start_byte();
let preceding_text = &source[function_start..call_position];
let array_size = find_array_size(buffer_name, preceding_text)?;
let element_size = find_element_size(buffer_name, preceding_text);
let buffer_size_bytes = array_size * element_size;
if size_bytes > buffer_size_bytes {
let start_point = node.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::Critical,
message: format!(
"{}() call writes {} bytes to buffer '{}' which is only {} bytes ({} elements × {} bytes). This causes buffer overflow.",
func_text,
size_bytes,
buffer_name,
buffer_size_bytes,
array_size,
element_size
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some(format!(
"Use the actual buffer size: {}({}, ..., {}) or {}({}, ..., sizeof({}))",
func_text,
buffer_name,
buffer_size_bytes,
func_text,
buffer_name,
buffer_name
)),
..Default::default()
});
}
None
}
fn check_loop_exceeds_allocation(node: &Node, source: &str) -> Option<RuleViolation> {
let condition = node.child_by_field_name("condition")?;
if condition.kind() != "binary_expression" {
return None;
}
let operator_node = condition.child_by_field_name("operator")?;
let operator = &source[operator_node.start_byte()..operator_node.end_byte()];
let left = condition.child_by_field_name("left")?;
let right = condition.child_by_field_name("right")?;
let (loop_var, bound_node) = if left.kind() == "identifier" && right.kind() == "number_literal"
{
(left, right)
} else if right.kind() == "identifier" && left.kind() == "number_literal" {
(right, left)
} else {
return None;
};
let loop_var_name = &source[loop_var.start_byte()..loop_var.end_byte()];
let bound_text = &source[bound_node.start_byte()..bound_node.end_byte()];
let loop_bound: usize = match bound_text.parse() {
Ok(n) => n,
Err(_) => return None,
};
let body = node.child_by_field_name("body")?;
let body_text = &source[body.start_byte()..body.end_byte()];
let subscript_pattern = format!("[{}]", loop_var_name);
if !body_text.contains(&subscript_pattern) {
return None;
}
let array_name = extract_array_name_from_subscript(body_text, loop_var_name)?;
let function_node = find_containing_function(node)?;
let function_start = function_node.start_byte();
let loop_position = node.start_byte();
let preceding_text = &source[function_start..loop_position];
if let Some(alloc_size) = find_allocation_size(&array_name, preceding_text) {
let max_index = if operator == "<" {
loop_bound.saturating_sub(1)
} else if operator == "<=" {
loop_bound
} else {
return None; };
if max_index >= alloc_size {
let start_point = node.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::Critical,
message: format!(
"Loop accesses up to index {} but dynamically allocated array '{}' has size {}. This causes out-of-bounds access.",
max_index, array_name, alloc_size
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some(format!(
"Ensure loop bound does not exceed allocated size. Change to '{} < {}' to match allocation size.",
loop_var_name, alloc_size
)),
..Default::default()
});
}
}
None
}
fn check_loop_bound_exceeds_array(node: &Node, source: &str) -> Option<RuleViolation> {
let condition = node.child_by_field_name("condition")?;
if condition.kind() != "binary_expression" {
return None;
}
let operator_node = condition.child_by_field_name("operator")?;
let operator = &source[operator_node.start_byte()..operator_node.end_byte()];
let left = condition.child_by_field_name("left")?;
let right = condition.child_by_field_name("right")?;
let (loop_var, bound_node, is_inclusive) =
if left.kind() == "identifier" && right.kind() == "number_literal" {
let inclusive = operator == "<=" || operator == ">=";
(left, right, inclusive)
} else if right.kind() == "identifier" && left.kind() == "number_literal" {
let inclusive = operator == ">=" || operator == "<=";
(right, left, inclusive)
} else {
return None;
};
if !is_inclusive {
return None;
}
let loop_var_name = &source[loop_var.start_byte()..loop_var.end_byte()];
let bound_text = &source[bound_node.start_byte()..bound_node.end_byte()];
let bound_value: usize = match bound_text.parse() {
Ok(n) => n,
Err(_) => return None,
};
let body = node.child_by_field_name("body")?;
let body_text = &source[body.start_byte()..body.end_byte()];
let subscript_pattern = format!("[{}]", loop_var_name);
if !body_text.contains(&subscript_pattern) {
return None; }
let function_node = find_containing_function(node)?;
let function_start = function_node.start_byte();
let loop_position = node.start_byte();
let preceding_text = &source[function_start..loop_position];
if let Some(array_name) = extract_array_name_from_subscript(body_text, loop_var_name) {
if let Some(array_size) = find_array_size(&array_name, preceding_text) {
if bound_value >= array_size {
let start_point = node.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::Critical,
message: format!(
"Loop bound allows index {} but array '{}' has size {}. Valid indices are 0-{}. Using '<=' instead of '<' causes off-by-one error.",
bound_value, array_name, array_size, array_size - 1
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some(format!(
"Change loop condition to '{} < {}' instead of '{} <= {}'",
loop_var_name, bound_value, loop_var_name, bound_value
)),
..Default::default()
});
}
}
}
None
}
fn extract_array_name_from_subscript(body_text: &str, loop_var: &str) -> Option<String> {
let pattern = format!("[{}]", loop_var);
if let Some(pos) = body_text.find(&pattern) {
let before_bracket = &body_text[..pos];
let mut end = before_bracket.len();
while end > 0
&& before_bracket
.chars()
.nth(end - 1)
.is_some_and(|c| c.is_whitespace())
{
end -= 1;
}
let mut start = end;
while start > 0 {
let ch = before_bracket.chars().nth(start - 1)?;
if ch.is_alphanumeric() || ch == '_' {
start -= 1;
} else {
break;
}
}
if start < end {
return Some(before_bracket[start..end].to_string());
}
}
None
}
fn check_loop_array_access(node: &Node, source: &str) -> Option<RuleViolation> {
let condition = node.child_by_field_name("condition")?;
let bound_var = extract_loop_bound_variable(&condition, source)?;
let body = node.child_by_field_name("body")?;
let has_array_access = contains_array_access(&body);
if !has_array_access {
return None;
}
let function_node = find_containing_function(node)?;
let loop_position = node.start_byte();
let function_start = function_node.start_byte();
let preceding_text = &source[function_start..loop_position];
if is_function_parameter(&function_node, &bound_var, source) {
return None; }
if is_user_input_variable(&bound_var, preceding_text) {
if !has_validation_before_loop(&bound_var, preceding_text, loop_position, source) {
let start_point = node.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::High,
message: format!(
"Loop uses unvalidated user input '{}' as bound for array access. This can cause out-of-bounds access.",
bound_var
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some(format!(
"Validate '{}' against array size before using in loop: if ({} < 0 || {} > ARRAY_SIZE) {{ /* error */ }}",
bound_var, bound_var, bound_var
)),
..Default::default()
});
}
}
else if is_uninitialized_variable(&bound_var, preceding_text) {
let start_point = node.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::High,
message: format!(
"Loop uses uninitialized variable '{}' as bound for array access. This has indeterminate value and can cause out-of-bounds access.",
bound_var
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some(format!(
"Initialize '{}' to a valid value before using it in the loop",
bound_var
)),
..Default::default()
});
}
None
}
fn extract_loop_bound_variable(condition: &Node, source: &str) -> Option<String> {
if condition.kind() == "binary_expression" {
let left = condition.child_by_field_name("left")?;
let right = condition.child_by_field_name("right")?;
if right.kind() == "identifier" {
return Some(source[right.start_byte()..right.end_byte()].to_string());
}
if left.kind() == "identifier" {
let text = &source[left.start_byte()..left.end_byte()];
if text != "i" && text != "j" && text != "k" {
return Some(text.to_string());
}
}
}
None
}
fn contains_array_access(node: &Node) -> bool {
if node.kind() == "subscript_expression" {
return true;
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if contains_array_access(&child) {
return true;
}
}
}
false
}
fn check_subscript_bounds(node: &Node, source: &str) -> Option<RuleViolation> {
let array_node = node.child_by_field_name("argument")?;
if array_node.kind() == "identifier" {
let arr_name = &source[array_node.start_byte()..array_node.end_byte()];
if let Some(fn_node) = find_containing_function(node) {
if !has_array_declaration(&fn_node, arr_name, source) {
return None;
}
}
}
let index_node = node.child_by_field_name("index")?;
if index_node.kind() != "identifier" {
return None; }
let index_var = &source[index_node.start_byte()..index_node.end_byte()];
let function_node = find_containing_function(node)?;
let function_start = function_node.start_byte();
let subscript_position = node.start_byte();
let preceding_text = &source[function_start..subscript_position];
if is_loop_variable(index_var, preceding_text) {
return None; }
if is_function_parameter(&function_node, index_var, source) {
if is_static_function(&function_node, source)
&& is_user_defined_param_type(&function_node, index_var, source)
{
return None;
}
if !has_bounds_validation(index_var, preceding_text) {
let start_point = node.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::High,
message: format!(
"Array subscript uses function parameter '{}' without bounds checking. Caller could pass invalid index.",
index_var
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some(format!(
"Add bounds checking for '{}' before using it as an array index",
index_var
)),
..Default::default()
});
}
}
if is_user_input_variable(index_var, preceding_text)
&& !has_bounds_validation(index_var, preceding_text)
{
let start_point = node.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::High,
message: format!(
"Array subscript uses unvalidated user input '{}' from scanf(). This can cause out-of-bounds access.",
index_var
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some(format!(
"Validate '{}' against array bounds before using it as an index",
index_var
)),
..Default::default()
});
}
None
}
fn is_user_defined_param_type(func_node: &Node, param_name: &str, source: &str) -> bool {
let primitive_types = [
"int",
"long",
"short",
"char",
"float",
"double",
"void",
"signed",
"unsigned",
"size_t",
"ssize_t",
"ptrdiff_t",
"intptr_t",
"uintptr_t",
"bool",
"_Bool",
"int8_t",
"int16_t",
"int32_t",
"int64_t",
"uint8_t",
"uint16_t",
"uint32_t",
"uint64_t",
];
if let Some(declarator) = func_node.child_by_field_name("declarator") {
if let Some(param_list) = find_parameter_list_node(&declarator) {
for i in 0..param_list.child_count() {
if let Some(param) = param_list.child(i) {
if param.kind() != "parameter_declaration" {
continue;
}
let param_text = &source[param.start_byte()..param.end_byte()];
if !param_text.contains(param_name) {
continue;
}
if let Some(type_node) = param.child_by_field_name("type") {
let type_text = &source[type_node.start_byte()..type_node.end_byte()];
let stripped = type_text
.replace("const", "")
.replace("volatile", "")
.replace("restrict", "")
.replace("struct", "")
.replace("union", "")
.replace("enum", "");
let stripped = stripped.trim();
return !primitive_types.contains(&stripped);
}
}
}
}
}
false
}
fn find_parameter_list_node<'a>(node: &Node<'a>) -> Option<Node<'a>> {
if node.kind() == "parameter_list" {
return Some(*node);
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if let Some(found) = find_parameter_list_node(&child) {
return Some(found);
}
}
}
None
}
fn is_static_function(function_node: &Node, source: &str) -> bool {
for i in 0..function_node.child_count() {
if let Some(child) = function_node.child(i) {
if child.kind() == "storage_class_specifier" {
if &source[child.start_byte()..child.end_byte()] == "static" {
return true;
}
}
}
}
let func_text = &source[function_node.start_byte()..function_node.end_byte()];
let before_paren = func_text.split('(').next().unwrap_or("");
before_paren
.split_whitespace()
.any(|tok| tok.contains("STATIC"))
}
fn check_uninitialized_array_read(node: &Node, source: &str) -> Option<RuleViolation> {
if !is_inside_loop(node) {
return None;
}
let array_node = node.child_by_field_name("argument")?;
if array_node.kind() != "identifier" {
return None;
}
let array_name = &source[array_node.start_byte()..array_node.end_byte()];
if is_write_context(node) {
return None; }
let function_node = find_containing_function(node)?;
let function_start = function_node.start_byte();
let subscript_position = node.start_byte();
let preceding_text = &source[function_start..subscript_position];
if !has_array_declaration(&function_node, array_name, source) {
return None; }
if is_function_parameter(&function_node, array_name, source) {
return None; }
let init_with_braces = format!("{}[", array_name);
if let Some(decl_pos) = preceding_text.rfind(&init_with_braces) {
let after_decl = &preceding_text[decl_pos..];
if after_decl.contains("=")
&& after_decl.find('=').unwrap() < after_decl.find(';').unwrap_or(usize::MAX)
{
return None; }
}
let write_pattern = format!("{}[", array_name);
let mut found_write = false;
for line in preceding_text.lines() {
if line.contains(&write_pattern) && line.contains('=') {
if let Some(bracket_pos) = line.find(&write_pattern) {
if let Some(eq_pos) = line.find('=') {
let is_assignment = !line[..eq_pos].ends_with('!')
&& !line[..eq_pos].ends_with('<')
&& !line[..eq_pos].ends_with('>')
&& !line[eq_pos..].starts_with("==");
if bracket_pos < eq_pos && is_assignment {
found_write = true;
break;
}
}
}
}
}
if !found_write {
let start_point = node.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::Critical,
message: format!(
"Reading from uninitialized array '{}' inside a loop. Array was declared but never initialized before being read.",
array_name
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some(format!(
"Initialize array '{}' before reading: int {}[N] = {{0}}; or assign values before use",
array_name, array_name
)),
..Default::default()
});
}
None
}
fn check_use_after_free(node: &Node, source: &str) -> Option<RuleViolation> {
let array_node = node.child_by_field_name("argument")?;
if array_node.kind() != "identifier" {
return None; }
let array_name = &source[array_node.start_byte()..array_node.end_byte()];
let function_node = find_containing_function(node)?;
let function_start = function_node.start_byte();
let subscript_position = node.start_byte();
let preceding_text = &source[function_start..subscript_position];
let free_pattern = format!("free({})", array_name);
let free_pattern_space = format!("free ({})", array_name);
if preceding_text.contains(&free_pattern) || preceding_text.contains(&free_pattern_space) {
let start_point = node.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::Critical,
message: format!(
"Array/pointer '{}' is accessed after being freed. This is use-after-free, causing undefined behavior.",
array_name
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some(format!(
"Do not access '{}' after calling free(). Set pointer to NULL after freeing: free({}); {} = NULL;",
array_name, array_name, array_name
)),
..Default::default()
});
}
None
}
fn check_comma_in_subscript(node: &Node, source: &str) -> Option<RuleViolation> {
let subscript_text = &source[node.start_byte()..node.end_byte()];
if let Some(open_bracket) = subscript_text.find('[') {
if let Some(close_bracket) = subscript_text.rfind(']') {
let inside_brackets = &subscript_text[open_bracket + 1..close_bracket];
if inside_brackets.contains(',') {
let mut paren_depth = 0;
let mut found_comma_at_depth_zero = false;
for ch in inside_brackets.chars() {
match ch {
'(' => paren_depth += 1,
')' => paren_depth -= 1,
',' if paren_depth == 0 => {
found_comma_at_depth_zero = true;
break;
}
_ => {}
}
}
if found_comma_at_depth_zero {
let array_node = node.child_by_field_name("argument");
let array_name = if let Some(arr) = array_node {
&source[arr.start_byte()..arr.end_byte()]
} else {
"array"
};
let start_point = node.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::Critical,
message: format!(
"Incorrect multidimensional array access '{}'. The comma operator causes this to evaluate to the last expression only, not accessing element [i][j].",
subscript_text
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some(format!(
"For 2D array access, use multiple subscripts like '{}[i][j]' instead of '{}[i,j]'",
array_name,
array_name
)),
..Default::default()
});
}
}
}
}
None
}
fn check_constant_out_of_bounds(
node: &Node,
source: &str,
macros: &MacroConstantMap,
) -> Option<RuleViolation> {
let index_node = node.child_by_field_name("index")?;
if index_node.kind() != "number_literal" {
return None; }
let index_text = &source[index_node.start_byte()..index_node.end_byte()];
let array_node = node.child_by_field_name("argument")?;
if array_node.kind() != "identifier" {
return None; }
let array_name = &source[array_node.start_byte()..array_node.end_byte()];
if index_text.starts_with('-') {
let start_point = node.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::Critical,
message: format!(
"Array subscript {} is negative. Array indices must be non-negative (0 or greater).",
index_text
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some(format!(
"Array '{}' requires a non-negative index. Negative indices access memory before the array.",
array_name
)),
..Default::default()
});
}
let index_value: usize = match index_text.parse() {
Ok(n) => n,
Err(_) => return None,
};
let function_node = find_containing_function(node)?;
if !has_array_declaration(&function_node, array_name, source) {
return None;
}
if let Some(array_size) = resolve_declared_array_size(node, array_name, source, macros) {
if index_value >= array_size {
let start_point = node.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::Critical,
message: format!(
"Array subscript {} is out of bounds for array '{}[{}]'. Valid indices are 0 to {}.",
index_value, array_name, array_size, array_size.saturating_sub(1)
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some(format!(
"Use a valid index in the range [0, {}] for array '{}'",
array_size.saturating_sub(1), array_name
)),
..Default::default()
});
}
}
None
}
fn check_boundary_value_index(node: &Node, source: &str) -> Option<RuleViolation> {
let index_node = node.child_by_field_name("index")?;
if index_node.kind() != "identifier" {
return None; }
let index_var = &source[index_node.start_byte()..index_node.end_byte()];
let function_node = find_containing_function(node)?;
let function_start = function_node.start_byte();
let subscript_position = node.start_byte();
let preceding_text = &source[function_start..subscript_position];
if let Some(boundary_value) = is_initialized_to_boundary_value(index_var, preceding_text) {
if !has_bounds_validation(index_var, preceding_text) {
let start_point = node.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::High,
message: format!(
"Array subscript uses variable '{}' initialized to boundary value '{}' without bounds checking. This can cause overflow or out-of-bounds access.",
index_var,
boundary_value
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some(format!(
"Validate '{}' against array bounds before using it as an index, or avoid initializing indices to boundary values",
index_var
)),
..Default::default()
});
}
}
None
}
fn is_initialized_to_boundary_value(var_name: &str, preceding_text: &str) -> Option<&'static str> {
let boundary_values = [
"UINT_MAX",
"INT_MAX",
"SIZE_MAX",
"ULONG_MAX",
"LONG_MAX",
"ULLONG_MAX",
"LLONG_MAX",
];
let var_pattern = format!("{} =", var_name);
if let Some(init_pos) = preceding_text.rfind(&var_pattern) {
let after_eq = &preceding_text[init_pos..];
for &boundary in &boundary_values {
if after_eq.contains(boundary) {
if let Some(semicolon_pos) = after_eq.find(';') {
let init_statement = &after_eq[..semicolon_pos];
if init_statement.contains(boundary) {
return Some(boundary);
}
}
}
}
}
None
}
fn check_return_local_array(node: &Node, source: &str) -> Option<RuleViolation> {
let meaningful_children: Vec<Node> = (0..node.child_count())
.filter_map(|i| node.child(i))
.filter(|child| {
let kind = child.kind();
kind != "return" && kind != ";" })
.collect();
if meaningful_children.len() != 1 || meaningful_children[0].kind() != "identifier" {
return None;
}
let return_node = meaningful_children[0];
let return_var = &source[return_node.start_byte()..return_node.end_byte()];
if return_var == "NULL" || return_var.parse::<i32>().is_ok() {
return None;
}
let function_node = find_containing_function(node)?;
let function_text = &source[function_node.start_byte()..function_node.end_byte()];
let array_pattern = format!("{}[", return_var);
if !function_text.contains(&array_pattern) {
return None;
}
let body_start = function_text.find('{')?;
let params_section = &function_text[..body_start];
if params_section.contains(&array_pattern) {
return None;
}
let body_section = &function_text[body_start..];
let type_keywords = [
"int", "char", "float", "double", "long", "short", "unsigned", "signed", "size_t",
"uint8_t", "uint16_t", "uint32_t", "uint64_t",
];
let has_array_declaration = type_keywords.iter().any(|&type_kw| {
let decl_pattern = format!("{} {}", type_kw, array_pattern);
body_section.contains(&decl_pattern)
|| body_section.contains(&format!(
"{} *{}",
type_kw,
array_pattern.trim_end_matches('[')
))
});
if !has_array_declaration {
return None;
}
let line = source[..return_node.start_byte()]
.chars()
.filter(|&c| c == '\n')
.count()
+ 1;
let column = source[..return_node.start_byte()]
.lines()
.last()
.map(|l| l.len())
.unwrap_or(0)
+ 1;
Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::Critical,
message: format!(
"Returning pointer to local array '{}' which will be destroyed when function returns, creating a dangling pointer.",
return_var
),
file_path: String::new(),
line,
column,
suggestion: Some("Consider allocating the array dynamically (malloc/calloc), declaring it as static, or passing a buffer as a parameter.".to_string()),
..Default::default()
})
}
fn check_pointer_arithmetic(node: &Node, source: &str) -> Option<RuleViolation> {
let operator_node = node.child_by_field_name("operator")?;
let operator = &source[operator_node.start_byte()..operator_node.end_byte()];
if operator != "+" {
return None;
}
let left = node.child_by_field_name("left")?;
let right = node.child_by_field_name("right")?;
if right.kind() != "number_literal" {
return None;
}
let array_name = &source[left.start_byte()..left.end_byte()];
let offset_text = &source[right.start_byte()..right.end_byte()];
let offset: usize = match offset_text.parse() {
Ok(n) => n,
Err(_) => return None, };
let function_node = find_containing_function(node)?;
let mut body_start = function_node.start_byte();
for i in 0..function_node.child_count() {
if let Some(child) = function_node.child(i) {
if child.kind() == "compound_statement" {
body_start = child.start_byte() + 1;
break;
}
}
}
let ptr_position = node.start_byte();
if body_start > ptr_position {
return None;
}
let preceding_text = &source[body_start..ptr_position];
if let Some(array_size) = find_array_size(array_name, preceding_text) {
if offset > array_size {
let start_point = node.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::High,
message: format!(
"Pointer arithmetic '{}' goes {} elements past the end of array '{}[{}]'. This exceeds array bounds.",
&source[node.start_byte()..node.end_byte()],
offset - array_size,
array_name,
array_size
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some(format!(
"Ensure pointer arithmetic stays within array bounds (0 to {})",
array_size
)),
..Default::default()
});
}
}
None
}
fn check_pointer_subtraction(node: &Node, source: &str) -> Option<RuleViolation> {
let operator_node = node.child_by_field_name("operator")?;
let operator = &source[operator_node.start_byte()..operator_node.end_byte()];
if operator != "-" {
return None;
}
let left = node.child_by_field_name("left")?;
let right = node.child_by_field_name("right")?;
if left.kind() != "identifier" || right.kind() != "identifier" {
return None;
}
let left_name = &source[left.start_byte()..left.end_byte()];
let right_name = &source[right.start_byte()..right.end_byte()];
if left_name == right_name {
return None;
}
let function_node = find_containing_function(node)?;
let function_start = function_node.start_byte();
let subtraction_position = node.start_byte();
let preceding_text = &source[function_start..subtraction_position];
let left_array = find_pointer_source_array(left_name, preceding_text);
let right_array = find_pointer_source_array(right_name, preceding_text);
if let (Some(left_arr), Some(right_arr)) = (left_array, right_array) {
if left_arr != right_arr {
let start_point = node.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::Critical,
message: format!(
"Subtracting pointers from different arrays ('{}' from '{}' and '{}' from '{}'). Pointer subtraction is only defined for pointers within the same array object.",
left_name,
left_arr,
right_name,
right_arr
),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some(
"Only subtract pointers that point to elements within the same array object.".to_string()
),
..Default::default()
});
}
}
None
}
fn find_pointer_source_array(ptr_name: &str, preceding_text: &str) -> Option<String> {
find_pointer_source_array_recursive(ptr_name, preceding_text, 0)
}
fn find_pointer_source_array_recursive(
ptr_name: &str,
preceding_text: &str,
depth: usize,
) -> Option<String> {
if depth > 5 {
return None;
}
let patterns = [
format!("{} = &", ptr_name), format!("{} = ", ptr_name), format!("*{} = &", ptr_name), format!("*{} = ", ptr_name), ];
for pattern in &patterns {
if let Some(pos) = preceding_text.rfind(pattern) {
let after_eq = &preceding_text[pos + pattern.len()..];
let after_eq = after_eq.trim_start();
let after_eq = if let Some(stripped) = after_eq.strip_prefix('&') {
stripped
} else {
after_eq
};
let mut end_pos = 0;
for (i, c) in after_eq.char_indices() {
if c == '[' || c == '+' || c == ',' || c == ';' || c.is_whitespace() || c == ')' {
end_pos = i;
break;
}
}
if end_pos > 0 {
let source_name = &after_eq[..end_pos];
if !source_name.is_empty()
&& source_name.chars().all(|c| c.is_alphanumeric() || c == '_')
{
if let Some(resolved) = find_pointer_source_array_recursive(
source_name,
&preceding_text[..pos],
depth + 1,
) {
return Some(resolved);
}
return Some(source_name.to_string());
}
}
}
}
None
}
fn resolve_declared_array_size(
use_node: &Node,
var_name: &str,
source: &str,
macros: &MacroConstantMap,
) -> Option<usize> {
let arr_decl = find_array_declarator_in_scope(use_node, var_name, source)?;
let declared = match arr_decl.child_by_field_name("size") {
Some(size_expr) => match try_evaluate_expr(&size_expr, source, macros) {
Some(v) if v > 0 => Some(v as usize),
_ => return None,
},
None => None,
};
let init_count = arr_decl
.parent()
.filter(|p| p.kind() == "init_declarator")
.and_then(|p| p.child_by_field_name("value"))
.filter(|v| v.kind() == "initializer_list")
.and_then(|v| count_initializer_elements(&v));
match (declared, init_count) {
(Some(d), Some(c)) => Some(d.max(c)),
(Some(d), None) => Some(d),
(None, Some(c)) => Some(c),
(None, None) => None,
}
}
fn find_array_declarator_in_scope<'a>(
use_node: &Node<'a>,
var_name: &str,
source: &str,
) -> Option<Node<'a>> {
let before = use_node.start_byte();
let mut scope = use_node.parent();
while let Some(s) = scope {
if let Some(found) = scan_block_declarators(&s, var_name, source, before) {
return Some(found);
}
scope = s.parent();
}
None
}
fn scan_block_declarators<'a>(
scope: &Node<'a>,
var_name: &str,
source: &str,
before: usize,
) -> Option<Node<'a>> {
for i in 0..scope.child_count() {
if let Some(child) = scope.child(i) {
if matches!(child.kind(), "compound_statement" | "function_definition") {
continue;
}
if child.kind() == "array_declarator"
&& child.start_byte() < before
&& declarator_names(&child, source) == var_name
{
return Some(child);
}
if let Some(found) = scan_block_declarators(&child, var_name, source, before) {
return Some(found);
}
}
}
None
}
fn count_initializer_elements(list: &Node) -> Option<usize> {
let mut count = 0;
for i in 0..list.child_count() {
if let Some(child) = list.child(i) {
match child.kind() {
"{" | "}" | "," | "comment" => {}
"initializer_pair" => return None,
_ => count += 1,
}
}
}
if count > 0 {
Some(count)
} else {
None
}
}
fn find_array_size(array_name: &str, preceding_text: &str) -> Option<usize> {
let pattern = format!("{}[", array_name);
let mut search_start = 0;
while let Some(pos) = preceding_text[search_start..].find(&pattern) {
let absolute_pos = search_start + pos;
let after_name = &preceding_text[absolute_pos..];
if let Some(bracket_start) = after_name.find('[') {
if let Some(bracket_end) = after_name.find(']') {
if bracket_end > bracket_start {
let size_text = after_name[bracket_start + 1..bracket_end].trim();
if let Ok(size) = size_text.parse::<usize>() {
let before_name = &preceding_text[..absolute_pos];
let type_keywords = [
"int", "char", "float", "double", "long", "short", "unsigned",
"signed", "size_t", "void", "struct",
];
let check_range = if before_name.len() > 50 {
&before_name[before_name.len() - 50..]
} else {
before_name
};
if type_keywords.iter().any(|&kw| check_range.contains(kw)) {
return Some(size);
}
}
}
}
}
search_start = absolute_pos + pattern.len();
}
None
}
fn check_array_comparison(node: &Node, source: &str) -> Option<RuleViolation> {
let operator = node.child_by_field_name("operator")?;
let op_text = &source[operator.start_byte()..operator.end_byte()];
if op_text != "==" && op_text != "!=" {
return None;
}
let left = node.child_by_field_name("left")?;
let right = node.child_by_field_name("right")?;
let left_text = &source[left.start_byte()..left.end_byte()];
let right_text = &source[right.start_byte()..right.end_byte()];
if left_text == "NULL" || right_text == "NULL" {
return None;
}
if is_array_identifier(&left, source) && is_array_identifier(&right, source) {
let start_point = node.start_position();
return Some(RuleViolation {
rule_id: "ARR00-C".to_string(),
severity: Severity::Medium,
message: "Comparing arrays with == or != compares addresses, not contents".to_string(),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some("Use memcmp() or strcmp() to compare array contents".to_string()),
..Default::default()
});
}
None
}
fn is_array_identifier(node: &Node, source: &str) -> bool {
if node.kind() != "identifier" || is_function_call_name(node) {
return false;
}
let identifier_name = &source[node.start_byte()..node.end_byte()];
if let Some(function_node) = find_containing_function(node) {
return has_array_declaration(&function_node, identifier_name, source);
}
false
}
fn has_array_declaration(scope: &Node, var_name: &str, source: &str) -> bool {
match scope.kind() {
"declaration"
if declaration_is_array_for(scope, var_name, source) => {
return true;
}
"parameter_declaration"
if param_is_array_for(scope, var_name, source) => {
return true;
}
_ => {}
}
for i in 0..scope.child_count() {
if let Some(child) = scope.child(i) {
if has_array_declaration(&child, var_name, source) {
return true;
}
}
}
false
}
fn declaration_is_array_for(decl: &Node, var_name: &str, source: &str) -> bool {
for i in 0..decl.child_count() {
if let Some(child) = decl.child(i) {
if child.kind() == "array_declarator" {
if declarator_names(&child, source) == var_name {
return true;
}
}
if child.kind() == "init_declarator" {
for j in 0..child.child_count() {
if let Some(gc) = child.child(j) {
if gc.kind() == "array_declarator" {
if declarator_names(&gc, source) == var_name {
return true;
}
}
}
}
}
}
}
false
}
fn param_is_array_for(param: &Node, var_name: &str, source: &str) -> bool {
for i in 0..param.child_count() {
if let Some(child) = param.child(i) {
if child.kind() == "array_declarator" {
if declarator_names(&child, source) == var_name {
return true;
}
}
}
}
false
}
fn declarator_names<'a>(node: &Node, source: &'a str) -> &'a str {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "identifier" {
return &source[child.start_byte()..child.end_byte()];
}
if child.kind() == "array_declarator" || child.kind() == "pointer_declarator" {
return declarator_names(&child, source);
}
}
}
""
}
fn is_subscript(node: &Node) -> bool {
node.kind() == "subscript_expression"
}
fn is_function_call_name(node: &Node) -> bool {
if let Some(parent) = node.parent() {
parent.kind() == "call_expression" && parent.child_by_field_name("function") == Some(*node)
} else {
false
}
}