Skip to main content

gobject_linter/rules/
use_g_str_has_prefix_suffix.rs

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    /// Check for strncmp(str, "prefix", strlen("prefix")) == 0 pattern
99    #[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        // strncmp_side must be a call to strncmp
111        let Expression::Call(call) = strncmp_side else {
112            return;
113        };
114
115        if !call.is_function("strncmp") {
116            return;
117        }
118
119        // value_side must be 0
120        if !value_side.is_zero() {
121            return;
122        }
123
124        // Must have 3 arguments
125        if call.arguments.len() != 3 {
126            return;
127        }
128
129        // Second argument must be a string literal
130        let Some(prefix_text) = call.arguments[1].extract_string_value() else {
131            return;
132        };
133
134        // Third argument must be strlen(prefix_text)
135        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    /// Check for strcmp(str + strlen(str) - strlen("suffix"), "suffix") == 0
161    /// pattern
162    #[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        // strcmp_side must be a call to strcmp
174        let Expression::Call(call) = strcmp_side else {
175            return;
176        };
177
178        if !call.is_function("strcmp") {
179            return;
180        }
181
182        // value_side must be 0
183        if !value_side.is_zero() {
184            return;
185        }
186
187        // Must have 2 arguments
188        if call.arguments.len() != 2 {
189            return;
190        }
191
192        // Second argument must be a string literal
193        let Some(suffix_text) = call.arguments[1].extract_string_value() else {
194            return;
195        };
196
197        // First argument must be: str + strlen(str) - strlen("suffix")
198        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    /// Validates that arg is `<str_expr> + strlen(<str_expr>) -
219    /// strlen("suffix")` and returns `str_expr` if so.
220    fn extract_suffix_base<'a>(&self, arg: &'a Expression, suffix_text: &str) -> Option<&'a str> {
221        // Top level: X - strlen("suffix")
222        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        // Right side must be strlen("suffix") - note suffix_text comes from
231        // extract_string_value so no quotes We need to wrap it in quotes for
232        // comparison since expr_to_text adds quotes
233        if !self.is_strlen_of_arg_by_value(&top_bin.right, suffix_text) {
234            return None;
235        }
236
237        // Left side: <str_expr> + strlen(<str_expr>)
238        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        // Right side must be strlen(str_expr)
249        if !self.is_strlen_of_arg(&inner_bin.right, str_expr) {
250            return None;
251        }
252
253        Some(str_expr)
254    }
255
256    /// Returns true if arg is strlen(expected_text)
257    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        // Extract string value and compare
271        if let Some(str_val) = call.arguments[0].extract_string_value() {
272            return str_val == expected_text;
273        }
274
275        false
276    }
277
278    /// Returns true if expr is strlen(expected_text_with_quotes)
279    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    /// Returns true if expr is strlen("expected_string_value")
300    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        // Extract string value and compare
314        if let Some(str_val) = call.arguments[0].extract_string_value() {
315            return str_val == expected_string_value;
316        }
317
318        false
319    }
320}