Skip to main content

gobject_linter/rules/
use_g_file_load_bytes.rs

1use std::collections::HashSet;
2
3use gobject_ast::model::{Expression, FileModel, FunctionDefItem, Statement, UnaryOp};
4
5use crate::{
6    ast_context::AstContext,
7    config::Config,
8    rules::{Rule, Violation},
9};
10
11pub struct UseGFileLoadBytes;
12
13impl Rule for UseGFileLoadBytes {
14    fn name(&self) -> &'static str {
15        "use_g_file_load_bytes"
16    }
17
18    fn description(&self) -> &'static str {
19        "Suggest g_file_load_bytes/g_file_load_bytes_async instead of g_file_load_contents + g_bytes_new_take"
20    }
21
22    fn category(&self) -> crate::rules::Category {
23        crate::rules::Category::Complexity
24    }
25
26    fn fixable(&self) -> bool {
27        false // Complex pattern, needs manual review
28    }
29
30    fn min_glib_version(&self) -> Option<(u32, u32)> {
31        Some((2, 56))
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        self.check_function(func, file, violations);
43    }
44}
45
46impl UseGFileLoadBytes {
47    fn check_function(
48        &self,
49        func: &FunctionDefItem,
50        file: &FileModel,
51        violations: &mut Vec<Violation>,
52    ) {
53        // Find all g_file_load_contents calls and track their output variables
54        let load_contents_vars = self.find_load_contents_vars(func);
55
56        // Find all g_bytes_new_take calls that use those variables
57        self.find_bytes_new_take_violations(
58            &func.body_statements,
59            file,
60            &load_contents_vars,
61            violations,
62        );
63    }
64
65    /// Find all g_file_load_contents calls and return the set of variables they
66    /// populate
67    fn find_load_contents_vars<'a>(&self, func: &'a FunctionDefItem) -> HashSet<&'a str> {
68        let mut result = HashSet::new();
69
70        // Find all g_file_load_contents or g_file_load_contents_finish calls
71        for call in func.find_calls(&["g_file_load_contents", "g_file_load_contents_finish"]) {
72            // g_file_load_contents(file, cancellable, &contents, &length, &etag, &error)
73            //                      0     1            2          3         4       5
74            if call.arguments.len() >= 6 {
75                // Extract the contents variable from argument 2 (&contents)
76                if let Some(contents_var) = self.extract_pointer_var(&call.arguments[2]) {
77                    result.insert(contents_var);
78                }
79            }
80        }
81
82        result
83    }
84
85    fn find_bytes_new_take_violations(
86        &self,
87        statements: &[Statement],
88        file: &FileModel,
89        load_contents_vars: &HashSet<&str>,
90        violations: &mut Vec<Violation>,
91    ) {
92        for stmt in statements {
93            stmt.walk(&mut |s| match s {
94                Statement::Expression(expr_stmt) => {
95                    self.check_expr_for_bytes_new_take(
96                        expr_stmt,
97                        file,
98                        load_contents_vars,
99                        violations,
100                    );
101                }
102                Statement::Declaration(decl) => {
103                    if let Some(init) = &decl.initializer {
104                        self.check_expr_for_bytes_new_take(
105                            init,
106                            file,
107                            load_contents_vars,
108                            violations,
109                        );
110                    }
111                }
112                Statement::Return(ret) => {
113                    if let Some(expr) = &ret.value {
114                        self.check_expr_for_bytes_new_take(
115                            expr,
116                            file,
117                            load_contents_vars,
118                            violations,
119                        );
120                    }
121                }
122                _ => {}
123            });
124        }
125    }
126
127    fn check_expr_for_bytes_new_take(
128        &self,
129        expr: &Expression,
130        file: &FileModel,
131        load_contents_vars: &HashSet<&str>,
132        violations: &mut Vec<Violation>,
133    ) {
134        if let Expression::Call(call) = expr
135            && call.is_function("g_bytes_new_take")
136            && call.arguments.len() >= 2
137        {
138            // Extract the first argument (contents variable)
139            if let Some(contents_var) = self.extract_contents_var(&call.arguments[0]) {
140                // Check if this contents variable came from g_file_load_contents
141                if load_contents_vars.contains(&contents_var) {
142                    violations.push(self.violation_at(
143                            &file.path,
144                            &call.location,
145                            "Consider using g_file_load_bytes/g_file_load_bytes_async instead of g_file_load_contents + g_bytes_new_take for simplicity".to_string(),
146                        ));
147                }
148            }
149        }
150    }
151
152    /// Extract variable name from &var argument
153    fn extract_pointer_var<'a>(&self, arg: &'a Expression) -> Option<&'a str> {
154        // Handle &var
155        if let Expression::Unary(unary) = arg
156            && unary.operator == UnaryOp::AddressOf
157        {
158            return unary.operand.extract_variable_name();
159        }
160
161        None
162    }
163
164    /// Extract variable name from first argument of g_bytes_new_take
165    /// Handles: contents, g_steal_pointer(&contents)
166    fn extract_contents_var<'a>(&self, arg: &'a Expression) -> Option<&'a str> {
167        match arg {
168            // Direct variable: contents
169            Expression::Identifier(id) => Some(id.name.as_str()),
170            Expression::FieldAccess(f) => f.location.as_str(),
171            // g_steal_pointer(&contents)
172            Expression::Call(call) => {
173                if call.is_function("g_steal_pointer") && !call.arguments.is_empty() {
174                    self.extract_pointer_var(&call.arguments[0])
175                } else {
176                    None
177                }
178            }
179            _ => None,
180        }
181    }
182}