Skip to main content

gobject_linter/rules/
use_g_set_str.rs

1use gobject_ast::model::{
2    AssignmentOp, Expression, FileModel, FunctionDefItem, Statement, UnaryOp,
3};
4
5use crate::{
6    ast_context::AstContext,
7    config::Config,
8    rules::{Fix, Rule, Violation},
9};
10
11pub struct UseGSetStr;
12
13impl Rule for UseGSetStr {
14    fn name(&self) -> &'static str {
15        "use_g_set_str"
16    }
17
18    fn description(&self) -> &'static str {
19        "Suggest g_set_str() instead of manual g_free and g_strdup"
20    }
21
22    fn category(&self) -> crate::rules::Category {
23        crate::rules::Category::Complexity
24    }
25
26    fn fixable(&self) -> bool {
27        true
28    }
29
30    fn min_glib_version(&self) -> Option<(u32, u32)> {
31        Some((2, 76))
32    }
33
34    fn check_func_impl(
35        &self,
36        _ast_context: &AstContext,
37        config: &Config,
38        func: &FunctionDefItem,
39        file: &FileModel,
40        violations: &mut Vec<Violation>,
41    ) {
42        Statement::walk_pairs(&func.body_statements, &mut |s1, s2| {
43            self.try_free_then_strdup(s1, s2, file, config, violations);
44        });
45    }
46}
47
48impl UseGSetStr {
49    fn try_free_then_strdup(
50        &self,
51        s1: &Statement,
52        s2: &Statement,
53        file: &FileModel,
54        config: &Config,
55        violations: &mut Vec<Violation>,
56    ) -> bool {
57        // First statement: g_free(var) or g_clear_pointer(&var, g_free)
58        let Some(var_name) = self.extract_gfree_var(s1) else {
59            return false;
60        };
61
62        // Second statement: var = g_strdup(...)
63        let Some((assign_var, new_val)) = self.extract_strdup_assignment(s2) else {
64            return false;
65        };
66
67        if assign_var != var_name {
68            return false;
69        }
70
71        let replacement = config
72            .style
73            .format_addr_call_stmt("g_set_str", var_name, &[new_val]);
74        let message = format!("Use {replacement} instead of g_free and g_strdup");
75        // Use two separate fixes to preserve comments between statements
76        let s2_end = s2.location().find_semicolon_end();
77        let fixes = vec![
78            // Delete the entire first line (g_free/g_clear_pointer)
79            Fix::delete_line(s1.location()),
80            // Replace the second statement with g_set_str
81            Fix::new(s2.location().start_byte, s2_end, replacement),
82        ];
83
84        violations.push(self.violation_with_fixes_at(&file.path, s1.location(), message, fixes));
85        true
86    }
87
88    /// Extract variable from g_free(var) or g_clear_pointer(&var, g_free)
89    fn extract_gfree_var<'a>(&'a self, stmt: &'a Statement) -> Option<&'a str> {
90        let Statement::Expression(expr_stmt) = stmt else {
91            return None;
92        };
93
94        let Expression::Call(call) = expr_stmt.as_ref() else {
95            return None;
96        };
97
98        if call.is_function("g_free") {
99            return call.get_arg(0)?.location().as_str();
100        } else if call.is_function("g_clear_pointer") {
101            // g_clear_pointer(&var, g_free)
102            if call.arguments.len() != 2 {
103                return None;
104            }
105
106            // Check if second argument is g_free
107            let second_arg = call.get_arg(1)?;
108
109            if let Expression::Identifier(id) = second_arg {
110                if id.name != "g_free" {
111                    return None;
112                }
113            } else {
114                return None;
115            }
116
117            // First argument is &var - extract var
118            let first_arg = call.get_arg(0)?;
119            if let Expression::Unary(unary) = first_arg
120                && unary.operator == UnaryOp::AddressOf
121            {
122                return unary.operand.location().as_str();
123            }
124        }
125
126        None
127    }
128
129    /// Extract (var, new_val) from var = g_strdup(new_val) or var = cond ?
130    /// g_strdup(...) : NULL
131    fn extract_strdup_assignment<'a>(&self, stmt: &'a Statement) -> Option<(&'a str, &'a str)> {
132        let Statement::Expression(expr_stmt) = stmt else {
133            return None;
134        };
135
136        let Expression::Assignment(assign) = expr_stmt.as_ref() else {
137            return None;
138        };
139
140        if assign.operator != AssignmentOp::Assign {
141            return None;
142        }
143
144        // Direct g_strdup call: var = g_strdup(new_val)
145        if let Expression::Call(call) = &*assign.rhs
146            && call.is_function("g_strdup")
147            && !call.arguments.is_empty()
148        {
149            let new_val = call.get_arg(0)?.location().as_str()?;
150            let var_name = assign.lhs_as_text();
151            if !var_name.is_empty() {
152                return Some((var_name, new_val));
153            }
154        }
155
156        // Ternary: var = cond ? g_strdup(...) : NULL
157        if let Expression::Conditional(cond) = &*assign.rhs
158            && cond.then_expr.is_call_to_any(&["g_strdup", "g_strndup"])
159        {
160            // Use the condition variable as the value
161            let cond_text = cond.condition.location().as_str()?;
162            let var_name = assign.lhs_as_text();
163            if !var_name.is_empty() {
164                return Some((var_name, cond_text));
165            }
166        }
167
168        None
169    }
170}