1use gobject_ast::model::{BinaryOp, Expression, FileModel, FunctionDefItem, SourceLocation};
2
3use crate::{
4 ast_context::AstContext,
5 config::Config,
6 rules::{Fix, Rule, Violation},
7};
8
9pub struct UseGStrHasPrefixSuffix;
10
11impl Rule for UseGStrHasPrefixSuffix {
12 fn name(&self) -> &'static str {
13 "use_g_str_has_prefix_suffix"
14 }
15
16 fn description(&self) -> &'static str {
17 "Use g_str_has_prefix/g_str_has_suffix() instead of manual strncmp/strcmp comparisons"
18 }
19
20 fn category(&self) -> crate::rules::Category {
21 crate::rules::Category::Style
22 }
23
24 fn fixable(&self) -> bool {
25 true
26 }
27
28 fn check_func_impl(
29 &self,
30 _ast_context: &AstContext,
31 config: &Config,
32 func: &FunctionDefItem,
33 file: &FileModel,
34 violations: &mut Vec<Violation>,
35 ) {
36 for stmt in &func.body_statements {
37 stmt.walk_expressions(&mut |expr| {
38 expr.walk(&mut |e| {
39 self.check_expression(e, file, &config.style, violations);
40 });
41 });
42 }
43 }
44}
45
46impl UseGStrHasPrefixSuffix {
47 fn check_expression(
48 &self,
49 expr: &Expression,
50 file: &FileModel,
51 style: &crate::config::Style,
52 violations: &mut Vec<Violation>,
53 ) {
54 let Expression::Binary(bin) = expr else {
55 return;
56 };
57 if !matches!(bin.operator, BinaryOp::Equal | BinaryOp::NotEqual) {
58 return;
59 }
60 self.check_for_prefix_pattern(
61 &bin.left,
62 &bin.right,
63 &bin.operator,
64 file,
65 style,
66 &bin.location,
67 violations,
68 );
69 self.check_for_prefix_pattern(
70 &bin.right,
71 &bin.left,
72 &bin.operator,
73 file,
74 style,
75 &bin.location,
76 violations,
77 );
78 self.check_for_suffix_pattern(
79 &bin.left,
80 &bin.right,
81 &bin.operator,
82 file,
83 style,
84 &bin.location,
85 violations,
86 );
87 self.check_for_suffix_pattern(
88 &bin.right,
89 &bin.left,
90 &bin.operator,
91 file,
92 style,
93 &bin.location,
94 violations,
95 );
96 }
97
98 #[allow(clippy::too_many_arguments)]
100 fn check_for_prefix_pattern(
101 &self,
102 strncmp_side: &Expression,
103 value_side: &Expression,
104 operator: &BinaryOp,
105 file: &FileModel,
106 style: &crate::config::Style,
107 location: &SourceLocation,
108 violations: &mut Vec<Violation>,
109 ) {
110 let Expression::Call(call) = strncmp_side else {
112 return;
113 };
114
115 if !call.is_function("strncmp") {
116 return;
117 }
118
119 if !value_side.is_zero() {
121 return;
122 }
123
124 if call.arguments.len() != 3 {
126 return;
127 }
128
129 let Some(prefix_text) = call.arguments[1].extract_string_value() else {
131 return;
132 };
133
134 if !self.is_strlen_of(&call.arguments[2], &prefix_text) {
136 return;
137 }
138
139 let str_arg_text = call
140 .get_arg(0)
141 .and_then(|e| e.location().as_str())
142 .unwrap_or_default();
143
144 let prefix_arg = format!("\"{}\"", prefix_text);
145 let call = style.format_call("g_str_has_prefix", &[str_arg_text, &prefix_arg]);
146 let replacement = if *operator == BinaryOp::Equal {
147 call
148 } else {
149 format!("!{call}")
150 };
151 let message = format!(
152 "Use {replacement} instead of strncmp() {} 0",
153 operator.as_str()
154 );
155 let fix = Fix::new(location.start_byte, location.end_byte, replacement);
156
157 violations.push(self.violation_with_fix_at(&file.path, location, message, fix));
158 }
159
160 #[allow(clippy::too_many_arguments)]
163 fn check_for_suffix_pattern(
164 &self,
165 strcmp_side: &Expression,
166 value_side: &Expression,
167 operator: &BinaryOp,
168 file: &FileModel,
169 style: &crate::config::Style,
170 location: &SourceLocation,
171 violations: &mut Vec<Violation>,
172 ) {
173 let Expression::Call(call) = strcmp_side else {
175 return;
176 };
177
178 if !call.is_function("strcmp") {
179 return;
180 }
181
182 if !value_side.is_zero() {
184 return;
185 }
186
187 if call.arguments.len() != 2 {
189 return;
190 }
191
192 let Some(suffix_text) = call.arguments[1].extract_string_value() else {
194 return;
195 };
196
197 let Some(str_expr) = self.extract_suffix_base(&call.arguments[0], &suffix_text) else {
199 return;
200 };
201
202 let suffix_arg = format!("\"{}\"", suffix_text);
203 let call = style.format_call("g_str_has_suffix", &[str_expr, &suffix_arg]);
204 let replacement = if *operator == BinaryOp::Equal {
205 call
206 } else {
207 format!("!{call}")
208 };
209 let message = format!(
210 "Use {replacement} instead of strcmp() {} 0",
211 operator.as_str()
212 );
213 let fix = Fix::new(location.start_byte, location.end_byte, replacement);
214
215 violations.push(self.violation_with_fix_at(&file.path, location, message, fix));
216 }
217
218 fn extract_suffix_base<'a>(&self, arg: &'a Expression, suffix_text: &str) -> Option<&'a str> {
221 let Expression::Binary(top_bin) = arg else {
223 return None;
224 };
225
226 if top_bin.operator != BinaryOp::Subtract {
227 return None;
228 }
229
230 if !self.is_strlen_of_arg_by_value(&top_bin.right, suffix_text) {
234 return None;
235 }
236
237 let Expression::Binary(inner_bin) = &*top_bin.left else {
239 return None;
240 };
241
242 if inner_bin.operator != BinaryOp::Add {
243 return None;
244 }
245
246 let str_expr = inner_bin.left.location().as_str()?;
247
248 if !self.is_strlen_of_arg(&inner_bin.right, str_expr) {
250 return None;
251 }
252
253 Some(str_expr)
254 }
255
256 fn is_strlen_of(&self, arg: &Expression, expected_text: &str) -> bool {
258 let Expression::Call(call) = arg else {
259 return false;
260 };
261
262 if !call.is_function("strlen") {
263 return false;
264 }
265
266 if call.arguments.len() != 1 {
267 return false;
268 }
269
270 if let Some(str_val) = call.arguments[0].extract_string_value() {
272 return str_val == expected_text;
273 }
274
275 false
276 }
277
278 fn is_strlen_of_arg(&self, expr: &Expression, expected_text_with_quotes: &str) -> bool {
280 let Expression::Call(call) = expr else {
281 return false;
282 };
283
284 if !call.is_function("strlen") {
285 return false;
286 }
287
288 if call.arguments.len() != 1 {
289 return false;
290 }
291
292 call.get_arg(0).is_some_and(|e| {
293 e.location()
294 .as_str()
295 .is_some_and(|s| s == expected_text_with_quotes)
296 })
297 }
298
299 fn is_strlen_of_arg_by_value(&self, expr: &Expression, expected_string_value: &str) -> bool {
301 let Expression::Call(call) = expr else {
302 return false;
303 };
304
305 if !call.is_function("strlen") {
306 return false;
307 }
308
309 if call.arguments.len() != 1 {
310 return false;
311 }
312
313 if let Some(str_val) = call.arguments[0].extract_string_value() {
315 return str_val == expected_string_value;
316 }
317
318 false
319 }
320}