gobject_linter/rules/
use_g_source_once.rs1use 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 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 if !self.is_callback_used_elsewhere(callback_name, file) {
60 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 let args_str = call
73 .arguments
74 .iter()
75 .enumerate()
76 .filter_map(|(i, arg)| {
77 if i == idx {
78 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 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 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 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 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 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 let replacement = format!(
218 "{:width$}",
219 "void",
220 width = func.return_type.display_name().len()
221 );
222
223 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}