1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
use gobject_ast::model::{BinaryOp, Expression, FileModel, FunctionDefItem, UnaryOp};
use crate::{
ast_context::AstContext,
config::Config,
rules::{Fix, Rule, Violation},
};
pub struct StrcmpExplicitComparison;
impl Rule for StrcmpExplicitComparison {
fn name(&self) -> &'static str {
"strcmp_explicit_comparison"
}
fn description(&self) -> &'static str {
"Require explicit comparison with 0 for strcmp/g_strcmp0 (returns 0 for equality, not TRUE)"
}
fn category(&self) -> crate::rules::Category {
crate::rules::Category::Correctness
}
fn fixable(&self) -> bool {
true
}
fn check_func_impl(
&self,
_ast_context: &AstContext,
_config: &Config,
func: &FunctionDefItem,
file: &FileModel,
violations: &mut Vec<Violation>,
) {
for stmt in &func.body_statements {
for if_stmt in stmt.iter_if_statements() {
self.check_condition(&if_stmt.condition, file, violations);
}
}
}
}
impl StrcmpExplicitComparison {
fn check_condition(
&self,
condition: &Expression,
file: &FileModel,
violations: &mut Vec<Violation>,
) {
match condition {
// Binary expression: check if it's a comparison with strcmp, or recurse for logical ops
Expression::Binary(binary) => {
// If it's a comparison operator, don't flag strcmp calls on either side
// (they already have explicit comparison)
match binary.operator {
BinaryOp::Equal
| BinaryOp::NotEqual
| BinaryOp::Less
| BinaryOp::LessEqual
| BinaryOp::Greater
| BinaryOp::GreaterEqual => {
// Don't recurse - strcmp calls here are OK
}
// For logical operators, recurse into both sides
BinaryOp::LogicalAnd | BinaryOp::LogicalOr => {
self.check_condition(&binary.left, file, violations);
self.check_condition(&binary.right, file, violations);
}
_ => {
// For other binary operators, recurse
self.check_condition(&binary.left, file, violations);
self.check_condition(&binary.right, file, violations);
}
}
}
// Bare call: if (strcmp(a, b)) or if (g_strcmp0(a, b))
Expression::Call(call)
if call
.function_name_str()
.is_some_and(|name| self.is_str_compare(name)) =>
{
let func_name = call.function_name();
// Fix: add "!= 0" after the call
let fix = Fix::new(
call.location.end_byte,
call.location.end_byte,
" != 0".to_string(),
);
violations.push(self.violation_with_fix_at(
&file.path,
&call.location,
format!(
"{}() returns 0 for equality — add explicit comparison: '{}(...) != 0'",
func_name, func_name
),
fix,
));
}
// Negated call: if (!strcmp(a, b)) or if (!g_strcmp0(a, b))
Expression::Unary(unary) if unary.operator == UnaryOp::Not => {
if let Expression::Call(call) = &*unary.operand
&& call
.function_name_str()
.is_some_and(|name| self.is_str_compare(name))
{
let func_name = call.function_name();
// Fix: remove the '!' and add ' == 0' after the call
let fixes = vec![
// Remove the '!' operator
Fix::delete(unary.location.start_byte, call.location.start_byte),
// Add ' == 0' after the call
Fix::new(
call.location.end_byte,
call.location.end_byte,
" == 0".to_string(),
),
];
violations.push(self.violation_with_fixes_at(
&file.path,
&call.location,
format!(
"{}() returns 0 for equality — use '{}(...) == 0' instead of '!{}(...)'",
func_name, func_name, func_name
),
fixes,
));
}
}
_ => {}
}
}
fn is_str_compare(&self, func_name: &str) -> bool {
matches!(func_name, "strcmp" | "g_strcmp0")
}
}