Skip to main content

gobject_linter/rules/
g_source_id_not_stored.rs

1use std::sync::LazyLock;
2
3use gobject_ast::model::{Expression, FileModel, FunctionDefItem, Statement};
4
5use crate::{
6    ast_context::AstContext,
7    config::Config,
8    rules::{ConfigOption, Rule, Violation},
9};
10
11pub struct GSourceIdNotStored;
12
13const SOURCE_FUNCTIONS: &[&str] = &[
14    "g_timeout_add",
15    "g_timeout_add_full",
16    "g_timeout_add_seconds",
17    "g_timeout_add_seconds_full",
18    "g_idle_add",
19    "g_idle_add_full",
20    "gtk_widget_add_tick_callback",
21];
22
23const ONCE_SOURCE_FUNCTIONS: &[&str] = &[
24    "g_timeout_add_once",
25    "g_timeout_add_seconds_once",
26    "g_idle_add_once",
27];
28
29impl Rule for GSourceIdNotStored {
30    fn name(&self) -> &'static str {
31        "g_source_id_not_stored"
32    }
33
34    fn description(&self) -> &'static str {
35        "Warn when GSource timeout/idle functions are called without storing the returned ID"
36    }
37
38    fn category(&self) -> crate::rules::Category {
39        crate::rules::Category::Suspicious
40    }
41
42    fn config_options(&self) -> &'static [ConfigOption] {
43        static OPTIONS: LazyLock<Vec<ConfigOption>> = LazyLock::new(|| {
44            vec![ConfigOption {
45                name: "check_once_functions",
46                option_type: "bool",
47                default_value: "true",
48                example_value: "false",
49                description: "Whether to check _once variants (g_idle_add_once, g_timeout_add_once, etc.)",
50            }]
51        });
52
53        &OPTIONS
54    }
55
56    fn check_func_impl(
57        &self,
58        _ast_context: &AstContext,
59        config: &Config,
60        func: &FunctionDefItem,
61        file: &FileModel,
62        violations: &mut Vec<Violation>,
63    ) {
64        let with_once = config
65            .get_rule_config(self.name())
66            .and_then(|rc| rc.options.get("check_once_functions"))
67            .and_then(toml::Value::as_bool)
68            .unwrap_or(true);
69
70        for stmt in &func.body_statements {
71            stmt.walk(&mut |s| {
72                if let Statement::Expression(expr_stmt) = s
73                    && (expr_stmt.is_call_to_any(SOURCE_FUNCTIONS)
74                        || (with_once && expr_stmt.is_call_to_any(ONCE_SOURCE_FUNCTIONS)))
75                    && let Expression::Call(call) = expr_stmt.as_ref()
76                    && !call.arguments.is_empty()
77                        && call.has_arg_matching(call.arguments.len() - 1, |expr| !expr.is_null())
78                    {
79                        violations.push(self.violation_at(
80                            &file.path,
81                            &call.location,
82                            format!(
83                                "{}() called without storing the returned source ID. If the object is destroyed before the callback fires, this will cause a use-after-free. Store the ID and use g_clear_handle_id() in dispose.",
84                                call.function_name()
85                            ),
86                        ));
87                    }
88            });
89        }
90    }
91}