Skip to main content

gobject_linter/rules/
use_g_source_once.rs

1use gobject_ast::model::{
2    BasicType, Expression, FileModel, FunctionDeclItem, FunctionDefItem, Statement,
3};
4
5use crate::{
6    ast_context::AstContext,
7    config::Config,
8    rules::{Fix, Rule, Violation},
9};
10
11fn gsource_callback_arg_index(func_name: &str) -> Option<usize> {
12    match func_name {
13        "g_idle_add" => Some(0),
14        "g_timeout_add" | "g_timeout_add_seconds" => Some(1),
15        _ => None,
16    }
17}
18
19pub struct UseGSourceOnce;
20
21impl Rule for UseGSourceOnce {
22    fn name(&self) -> &'static str {
23        "use_g_source_once"
24    }
25
26    fn description(&self) -> &'static str {
27        "Suggest using g_idle_add_once/g_timeout_add_once/g_timeout_add_seconds_once when callback always returns G_SOURCE_REMOVE"
28    }
29
30    fn category(&self) -> crate::rules::Category {
31        crate::rules::Category::Complexity
32    }
33
34    fn fixable(&self) -> bool {
35        true
36    }
37
38    fn min_glib_version(&self) -> Option<(u32, u32)> {
39        Some((2, 74))
40    }
41
42    fn check_func_impl(
43        &self,
44        _ast_context: &AstContext,
45        config: &Config,
46        func: &FunctionDefItem,
47        file: &FileModel,
48        violations: &mut Vec<Violation>,
49    ) {
50        // Find g_idle_add, g_timeout_add, and g_timeout_add_seconds calls
51        for call in func.find_calls(&["g_idle_add", "g_timeout_add", "g_timeout_add_seconds"]) {
52            let Some(idx) = gsource_callback_arg_index(call.function_name_str().unwrap_or(""))
53            else {
54                continue;
55            };
56            if let Some(callback_name) = call.get_arg(idx).and_then(|a| a.extract_identifier_name())
57            {
58                // Only proceed if callback is NOT used elsewhere
59                if !self.is_callback_used_elsewhere(callback_name, file) {
60                    // Find the callback function definition and check if all returns are
61                    // FALSE/G_SOURCE_REMOVE
62                    if let Some(callback_fixes) = self.get_callback_fixes(callback_name, file) {
63                        let func_name = call.function_name();
64                        let replacement = match func_name {
65                            "g_idle_add" => "g_idle_add_once",
66                            "g_timeout_add_seconds" => "g_timeout_add_seconds_once",
67                            _ => "g_timeout_add_once",
68                        };
69
70                        // Build arguments, replacing GSourceFunc cast with GSourceOnceFunc if
71                        // present
72                        let args_str = call
73                            .arguments
74                            .iter()
75                            .enumerate()
76                            .filter_map(|(i, arg)| {
77                                if i == idx {
78                                    // Callback argument - replace cast type if present
79                                    if let Expression::Cast(cast) = &**arg
80                                        && let Some(callback_name) =
81                                            cast.operand.location().as_str()
82                                    {
83                                        return Some(format!(
84                                            "(GSourceOnceFunc) {}",
85                                            callback_name
86                                        ));
87                                    }
88                                }
89                                arg.location().as_str().map(ToOwned::to_owned)
90                            })
91                            .collect::<Vec<_>>()
92                            .join(", ");
93
94                        // Fix 1: Replace g_idle_add → g_idle_add_once
95                        let arg_refs: Vec<&str> = args_str.split(", ").collect();
96                        let mut fixes = vec![Fix::new(
97                            call.location.start_byte,
98                            call.location.end_byte,
99                            config.style.format_call(replacement, &arg_refs),
100                        )];
101
102                        // Add callback fixes (return type + return statements)
103                        fixes.extend(callback_fixes);
104
105                        violations.push(self.violation_with_fixes_at(
106                            &file.path,
107                            &call.location,
108                            format!(
109                                "Callback '{}' always returns G_SOURCE_REMOVE. Use {} instead of {}",
110                                callback_name, replacement, func_name
111                            ),
112                            fixes,
113                        ));
114                    }
115                }
116            }
117        }
118    }
119}
120
121impl UseGSourceOnce {
122    fn get_callback_fixes(&self, callback_name: &str, file: &FileModel) -> Option<Vec<Fix>> {
123        let mut fixes = Vec::new();
124        let mut found_definition = false;
125
126        for func in file.iter_function_definitions() {
127            if func.name != callback_name {
128                continue;
129            }
130
131            let return_stmts: Vec<_> = func
132                .body_statements
133                .iter()
134                .flat_map(Statement::iter_returns)
135                .collect();
136            if return_stmts.is_empty() {
137                return None;
138            }
139
140            if !return_stmts.iter().all(|ret| {
141                ret.value.as_ref().is_some_and(|expr| {
142                    expr.is_falsy()
143                        || matches!(expr, Expression::Identifier(id) if id.name == "G_SOURCE_REMOVE")
144                })
145            }) {
146                return None;
147            }
148
149            if let Some(fix) = self.fix_definition_return_type(func) {
150                fixes.push(fix);
151            }
152
153            let last_top_level = func.body_statements.last().and_then(|s| {
154                if let Statement::Return(ret) = s {
155                    Some(&ret.location)
156                } else {
157                    None
158                }
159            });
160
161            for ret in &return_stmts {
162                if last_top_level.is_some_and(|loc| loc.start_byte == ret.location.start_byte) {
163                    fixes.push(Fix::delete_line_and_leading_blank(&ret.location));
164                } else {
165                    fixes.push(Fix::new(
166                        ret.location.start_byte,
167                        ret.location.end_byte,
168                        "return;",
169                    ));
170                }
171            }
172
173            found_definition = true;
174        }
175
176        for func in file.iter_function_declarations() {
177            if func.name != callback_name {
178                continue;
179            }
180            if let Some(fix) = self.fix_declaration_return_type(func) {
181                fixes.push(fix);
182            }
183        }
184
185        if found_definition && !fixes.is_empty() {
186            Some(fixes)
187        } else {
188            None
189        }
190    }
191
192    fn fix_definition_return_type(&self, func: &FunctionDefItem) -> Option<Fix> {
193        // Check if return type is gboolean
194        if func.return_type.as_basic() != Some(BasicType::Boolean)
195            && func.return_type.as_basic() != Some(BasicType::Int)
196        {
197            return None;
198        }
199
200        // Use the location from the return type's TypeInfo
201        Some(Fix::new(
202            func.return_type.location.start_byte,
203            func.return_type.location.end_byte,
204            "void".to_string(),
205        ))
206    }
207
208    fn fix_declaration_return_type(&self, func: &FunctionDeclItem) -> Option<Fix> {
209        // Check if return type is gboolean
210        if func.return_type.as_basic() != Some(BasicType::Boolean)
211            && func.return_type.as_basic() != Some(BasicType::Int)
212        {
213            return None;
214        }
215
216        // Preserve alignment by padding "void" to match the original type length
217        let replacement = format!(
218            "{:width$}",
219            "void",
220            width = func.return_type.display_name().len()
221        );
222
223        // Use the location from the return type's TypeInfo
224        Some(Fix::new(
225            func.return_type.location.start_byte,
226            func.return_type.location.end_byte,
227            replacement,
228        ))
229    }
230
231    fn is_callback_used_elsewhere(&self, callback_name: &str, file: &FileModel) -> bool {
232        for func in file.iter_function_definitions() {
233            if self.has_non_source_add_usage(&func.body_statements, callback_name) {
234                return true;
235            }
236        }
237
238        false
239    }
240
241    fn has_non_source_add_usage(&self, statements: &[Statement], callback_name: &str) -> bool {
242        for stmt in statements {
243            let mut found = false;
244            stmt.walk(&mut |s| {
245                if !self.is_source_add_statement(s, callback_name) {
246                    s.visit_expressions(&mut |e| {
247                        if e.contains_identifier(callback_name) {
248                            found = true;
249                        }
250                    });
251                }
252            });
253            if found {
254                return true;
255            }
256        }
257        false
258    }
259
260    fn is_source_add_statement(&self, stmt: &Statement, callback_name: &str) -> bool {
261        if let Statement::Expression(expr_stmt) = stmt
262            && let Expression::Call(call) = expr_stmt.as_ref()
263            && let Some(idx) = gsource_callback_arg_index(call.function_name_str().unwrap_or(""))
264            && let Some(name) = call.get_arg(idx).and_then(|a| a.extract_identifier_name())
265        {
266            name == callback_name
267        } else {
268            false
269        }
270    }
271}