use super::super::{CertRule, RuleViolation};
use crate::manifest::{RuleCategory, Severity};
use crate::utility::cert_c::ast_utils;
use tree_sitter::Node;
pub struct Dcl13C;
impl CertRule for Dcl13C {
fn rule_id(&self) -> &'static str {
"DCL13-C"
}
fn description(&self) -> &'static str {
"Declare function parameters that are pointers to values not changed by the function as const"
}
fn severity(&self) -> Severity {
Severity::Low
}
fn category(&self) -> RuleCategory {
RuleCategory::Recommendation
}
fn cert_id(&self) -> &'static str {
"DCL13-C"
}
fn check(&self, node: &Node, source: &str) -> Vec<RuleViolation> {
let mut violations = Vec::new();
check_functions_recursively(node, source, &mut violations, self.rule_id());
violations
}
}
fn check_functions_recursively(
node: &Node,
source: &str,
violations: &mut Vec<RuleViolation>,
rule_id: &str,
) {
if node.kind() == "function_definition" {
check_function_definition(node, source, violations, rule_id);
} else if node.kind() == "declaration" {
check_function_declaration(node, source, violations, rule_id);
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
check_functions_recursively(&child, source, violations, rule_id);
}
}
}
fn get_function_name(func_node: &Node, source: &str) -> Option<String> {
for i in 0..func_node.child_count() {
if let Some(child) = func_node.child(i) {
if let Some(name) = find_name_in_declarator(&child, source) {
return Some(name);
}
}
}
None
}
fn find_name_in_declarator(node: &Node, source: &str) -> Option<String> {
if node.kind() == "function_declarator" {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "identifier" {
return Some(ast_utils::get_node_text(&child, source).to_string());
}
if child.kind() == "pointer_declarator" {
if let Some(name) = find_name_in_declarator(&child, source) {
return Some(name);
}
}
}
}
} else if node.kind() == "pointer_declarator" {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if let Some(name) = find_name_in_declarator(&child, source) {
return Some(name);
}
}
}
}
None
}
fn check_function_definition(
func_node: &Node,
source: &str,
violations: &mut Vec<RuleViolation>,
rule_id: &str,
) {
if get_function_name(func_node, source).as_deref() == Some("main") {
return;
}
let params = extract_function_parameters(func_node, source);
let body = find_compound_statement(func_node);
for (param_name, is_const, is_pointer, line, col) in params {
if !is_pointer {
continue; }
if is_const {
continue;
}
let is_modified = if let Some(body_node) = body {
if is_pointer_param_modified(&body_node, ¶m_name, source) {
true
} else {
let aliases = collect_pointer_aliases(&body_node, ¶m_name, source);
aliases
.iter()
.any(|alias| is_pointer_param_modified(&body_node, alias, source))
}
} else {
false };
if !is_modified {
violations.push(RuleViolation {
rule_id: rule_id.to_string(),
severity: Severity::Low,
message: format!(
"Pointer parameter '{}' is not modified and should be declared const",
param_name
),
file_path: String::new(),
line,
column: col,
suggestion: Some(format!(
"Declare parameter as 'const <type> *{}'",
param_name
)),
..Default::default()
});
}
}
}
fn check_function_declaration(
decl_node: &Node,
source: &str,
violations: &mut Vec<RuleViolation>,
rule_id: &str,
) {
for i in 0..decl_node.child_count() {
if let Some(child) = decl_node.child(i) {
if child.kind() == "function_declarator" || is_function_declarator(&child) {
let params = extract_params_from_declarator(&child, source);
for (param_name, is_const, is_pointer, line, col) in params {
if is_pointer && !is_const && !param_name.is_empty() {
if is_likely_readonly_param(¶m_name) {
violations.push(RuleViolation {
rule_id: rule_id.to_string(),
severity: Severity::Low,
message: format!(
"Pointer parameter '{}' should likely be declared const",
param_name
),
file_path: String::new(),
line,
column: col,
suggestion: Some(format!(
"Consider declaring parameter as 'const <type> *{}'",
param_name
)),
..Default::default()
});
}
}
}
}
}
}
}
fn is_likely_readonly_param(name: &str) -> bool {
let lowercase = name.to_lowercase();
lowercase.starts_with("src")
|| lowercase.starts_with("source")
|| lowercase.starts_with("input")
|| lowercase.starts_with("in_")
|| lowercase.contains("read")
|| name.ends_with("2") }
fn is_function_declarator(node: &Node) -> bool {
if node.kind() == "function_declarator" {
return true;
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if is_function_declarator(&child) {
return true;
}
}
}
false
}
fn extract_params_from_declarator(
declarator: &Node,
source: &str,
) -> Vec<(String, bool, bool, usize, usize)> {
let mut params = Vec::new();
if let Some(param_list) = find_parameter_list(declarator) {
for i in 0..param_list.child_count() {
if let Some(param) = param_list.child(i) {
if param.kind() == "parameter_declaration" {
if let Some((name, is_const, is_pointer, line, col)) =
analyze_parameter(¶m, source)
{
params.push((name, is_const, is_pointer, line, col));
}
}
}
}
}
params
}
fn find_parameter_list<'a>(declarator: &Node<'a>) -> Option<Node<'a>> {
for i in 0..declarator.child_count() {
if let Some(child) = declarator.child(i) {
if child.kind() == "parameter_list" {
return Some(child);
}
if let Some(found) = find_parameter_list(&child) {
return Some(found);
}
}
}
None
}
const READ_ONLY_FUNCTIONS: &[&str] = &[
"printf", "fprintf", "vprintf", "vfprintf", "wprintf", "fwprintf", "puts", "fputs", "putchar",
"fputc", "putc", "ftell", "feof", "ferror", "fopen", "strlen", "strcmp", "strncmp", "strchr", "strrchr", "strstr", "strpbrk", "strspn", "strcspn",
"strerror", "wcslen", "wcscmp", "wcsncmp", "wcschr", "wcsrchr", "wcsstr",
"memcmp", "memchr",
"bsearch", "strdup", "strndup", "wcsdup", "atoi", "atol", "atof", "atoll", "isalpha", "isdigit", "isalnum", "isspace", "isupper", "islower", "ispunct", "isprint",
"iscntrl", "isxdigit", "isgraph", "toupper", "tolower", "abs", "labs", "llabs", "fabs", "sqrt", "pow", "ceil", "floor", "log", "log10", "exp", "sin",
"cos", "tan", "assert", "exit", "abort", "_exit", "_Exit", "write", "open", "close", "perror", "access", "stat", "lstat",
];
fn is_pointer_param_modified(body: &Node, param_name: &str, source: &str) -> bool {
check_node_for_pointer_modification(body, param_name, source)
}
fn collect_pointer_aliases(body: &Node, param_name: &str, source: &str) -> Vec<String> {
let mut aliases = Vec::new();
collect_aliases_recursive(body, param_name, source, &mut aliases);
aliases
}
fn collect_aliases_recursive(
node: &Node,
param_name: &str,
source: &str,
aliases: &mut Vec<String>,
) {
if node.kind() == "declaration" {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "init_declarator" {
if let Some(value) = child.child_by_field_name("value") {
if arg_is_param(&value, param_name, source) {
if let Some(declarator) = child.child_by_field_name("declarator") {
if let Some(name) =
find_identifier_in_declarator(&declarator, source)
{
aliases.push(name);
}
}
}
}
}
}
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
collect_aliases_recursive(&child, param_name, source, aliases);
}
}
}
fn find_identifier_in_declarator(node: &Node, source: &str) -> Option<String> {
if node.kind() == "identifier" {
return Some(ast_utils::get_node_text(node, source).to_string());
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if let Some(name) = find_identifier_in_declarator(&child, source) {
return Some(name);
}
}
}
None
}
fn check_node_for_pointer_modification(node: &Node, param_name: &str, source: &str) -> bool {
if node.kind() == "assignment_expression" {
if let Some(left) = node.child_by_field_name("left") {
if is_write_through_param(&left, param_name, source) {
return true;
}
}
}
if node.kind() == "update_expression" {
if let Some(argument) = node.child_by_field_name("argument") {
if is_write_through_param(&argument, param_name, source) {
return true;
}
}
}
if node.kind() == "call_expression" {
if is_param_passed_to_modifying_call(node, param_name, source) {
return true;
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if check_node_for_pointer_modification(&child, param_name, source) {
return true;
}
}
}
false
}
fn is_write_through_param(node: &Node, param_name: &str, source: &str) -> bool {
match node.kind() {
"pointer_expression" => {
if let Some(argument) = node.child_by_field_name("argument") {
let text = ast_utils::get_node_text(&argument, source);
if text == param_name {
return true;
}
}
}
"field_expression" => {
if let Some(argument) = node.child_by_field_name("argument") {
let text = ast_utils::get_node_text(&argument, source);
if text == param_name {
return true;
}
return is_write_through_param(&argument, param_name, source);
}
}
"subscript_expression" => {
if let Some(argument) = node.child_by_field_name("argument") {
let text = ast_utils::get_node_text(&argument, source);
if text == param_name {
return true;
}
return is_write_through_param(&argument, param_name, source);
}
}
"parenthesized_expression" => {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() != "(" && child.kind() != ")" {
return is_write_through_param(&child, param_name, source);
}
}
}
}
_ => {}
}
false
}
fn arg_addresses_param_member(node: &Node, param_name: &str, source: &str) -> bool {
if node.kind() != "pointer_expression" {
return false;
}
if let Some(op_node) = node.child_by_field_name("operator") {
if ast_utils::get_node_text(&op_node, source) != "&" {
return false;
}
} else {
return false;
}
if let Some(argument) = node.child_by_field_name("argument") {
return expr_derives_from_param(&argument, param_name, source);
}
false
}
fn expr_derives_from_param(node: &Node, param_name: &str, source: &str) -> bool {
match node.kind() {
"field_expression" => {
if let Some(argument) = node.child_by_field_name("argument") {
let text = ast_utils::get_node_text(&argument, source);
if text == param_name {
return true;
}
return expr_derives_from_param(&argument, param_name, source);
}
false
}
"subscript_expression" => {
if let Some(argument) = node.child(0) {
let text = ast_utils::get_node_text(&argument, source);
if text == param_name {
return true;
}
return expr_derives_from_param(&argument, param_name, source);
}
false
}
"pointer_expression" => {
if let Some(argument) = node.child_by_field_name("argument") {
let text = ast_utils::get_node_text(&argument, source);
if text == param_name {
return true;
}
return expr_derives_from_param(&argument, param_name, source);
}
false
}
"parenthesized_expression" => {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() != "(" && child.kind() != ")" {
return expr_derives_from_param(&child, param_name, source);
}
}
}
false
}
_ => false,
}
}
fn arg_is_param(node: &Node, param_name: &str, source: &str) -> bool {
match node.kind() {
"identifier" => ast_utils::get_node_text(node, source) == param_name,
"cast_expression" => {
if let Some(value) = node.child_by_field_name("value") {
return arg_is_param(&value, param_name, source);
}
false
}
"parenthesized_expression" => {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() != "(" && child.kind() != ")" {
return arg_is_param(&child, param_name, source);
}
}
}
false
}
_ => false,
}
}
fn is_param_passed_to_modifying_call(call_node: &Node, param_name: &str, source: &str) -> bool {
let args = match call_node.child_by_field_name("arguments") {
Some(a) => a,
None => return false,
};
let mut param_is_arg = false;
for i in 0..args.child_count() {
if let Some(arg) = args.child(i) {
if arg.kind() == "," || arg.kind() == "(" || arg.kind() == ")" {
continue;
}
if arg_is_param(&arg, param_name, source)
|| arg_addresses_param_member(&arg, param_name, source)
{
param_is_arg = true;
break;
}
}
}
if !param_is_arg {
return false;
}
if let Some(func) = call_node.child_by_field_name("function") {
let func_name = ast_utils::get_node_text(&func, source);
if READ_ONLY_FUNCTIONS.contains(&func_name) {
return false;
}
}
true
}
fn extract_function_parameters(
func_node: &Node,
source: &str,
) -> Vec<(String, bool, bool, usize, usize)> {
let mut parameters = Vec::new();
for i in 0..func_node.child_count() {
if let Some(child) = func_node.child(i) {
if child.kind() == "function_declarator"
|| is_pointer_or_array_with_func_declarator(&child)
{
if let Some(param_list) = find_parameter_list(&child) {
for j in 0..param_list.child_count() {
if let Some(param) = param_list.child(j) {
if param.kind() == "parameter_declaration" {
if let Some((name, is_const, is_pointer, line, col)) =
analyze_parameter(¶m, source)
{
parameters.push((name, is_const, is_pointer, line, col));
}
}
}
}
}
}
}
}
parameters
}
fn is_pointer_or_array_with_func_declarator(node: &Node) -> bool {
if node.kind() == "pointer_declarator" || node.kind() == "array_declarator" {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "function_declarator"
|| is_pointer_or_array_with_func_declarator(&child)
{
return true;
}
}
}
}
false
}
fn analyze_parameter(param: &Node, source: &str) -> Option<(String, bool, bool, usize, usize)> {
let mut is_const = false;
let mut is_pointer = false;
let mut param_name = String::new();
let mut line = 0;
let mut col = 0;
for i in 0..param.child_count() {
if let Some(child) = param.child(i) {
match child.kind() {
"type_qualifier" => {
let text = ast_utils::get_node_text(&child, source);
if text == "const" {
is_const = true;
}
}
"pointer_declarator" => {
is_pointer = true;
param_name = ast_utils::get_identifier_from_declarator(&child, source);
if !param_name.is_empty() {
let pos = child.start_position();
line = pos.row + 1;
col = pos.column + 1;
}
}
"array_declarator" => {
is_pointer = true; param_name = ast_utils::get_identifier_from_declarator(&child, source);
if !param_name.is_empty() {
let pos = child.start_position();
line = pos.row + 1;
col = pos.column + 1;
}
}
"identifier"
if param_name.is_empty() => {
param_name = ast_utils::get_node_text(&child, source).to_string();
let pos = child.start_position();
line = pos.row + 1;
col = pos.column + 1;
}
"primitive_type" | "type_identifier" | "struct_specifier" | "union_specifier"
| "enum_specifier"
if i + 1 < param.child_count() => {
if let Some(next) = param.child(i + 1) {
if next.kind() == "*" || next.kind() == "abstract_pointer_declarator" {
is_pointer = true;
}
}
}
_ => {}
}
}
}
if !param_name.is_empty() {
Some((param_name, is_const, is_pointer, line, col))
} else {
None
}
}
fn find_compound_statement<'a>(func_node: &Node<'a>) -> Option<Node<'a>> {
for i in 0..func_node.child_count() {
if let Some(child) = func_node.child(i) {
if child.kind() == "compound_statement" {
return Some(child);
}
}
}
None
}