Skip to main content

gobject_linter/rules/
use_g_bytes_unref_to_data.rs

1use gobject_ast::model::{
2    Assignment, AssignmentOp, CallExpression, Expression, FileModel, FunctionDefItem, Statement,
3};
4
5use crate::{
6    ast_context::AstContext,
7    config::Config,
8    rules::{Fix, Rule, Violation},
9};
10
11pub struct UseGBytesUnrefToData;
12
13impl Rule for UseGBytesUnrefToData {
14    fn name(&self) -> &'static str {
15        "use_g_bytes_unref_to_data"
16    }
17
18    fn description(&self) -> &'static str {
19        "Use g_bytes_unref_to_data() instead of g_bytes_get_data() + g_bytes_unref()"
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, 32))
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 |stmt1, stmt2| {
43            self.try_bytes_pattern(stmt1, stmt2, file, config, violations);
44        });
45    }
46}
47
48impl UseGBytesUnrefToData {
49    /// Try to match: dest = g_bytes_get_data(bytes, ...); g_bytes_unref(bytes);
50    fn try_bytes_pattern(
51        &self,
52        stmt1: &Statement,
53        stmt2: &Statement,
54        file: &FileModel,
55        config: &Config,
56        violations: &mut Vec<Violation>,
57    ) {
58        // First statement: dest = g_bytes_get_data(bytes, &size)
59        let Some((dest, bytes_var, size_arg, assignment, _call1)) =
60            self.extract_bytes_get_data(stmt1)
61        else {
62            return;
63        };
64
65        // Second statement: g_bytes_unref(bytes)
66        if self.extract_bytes_unref(stmt2, bytes_var).is_none() {
67            return;
68        };
69
70        // Build the replacement
71        let call = config
72            .style
73            .format_call("g_bytes_unref_to_data", &[bytes_var, size_arg]);
74        let replacement = format!("{} = {};", dest, call);
75        let message = format!(
76            "Use g_bytes_unref_to_data({}, {}) instead of g_bytes_get_data() followed by g_bytes_unref()",
77            bytes_var, size_arg
78        );
79        // Use two separate fixes to preserve comments between statements
80        let stmt1_end = stmt1.location().find_semicolon_end();
81        let fixes = vec![
82            // Replace the first statement with the new call
83            Fix::new(stmt1.location().start_byte, stmt1_end, replacement),
84            // Delete the entire second line
85            Fix::delete_line(stmt2.location()),
86        ];
87
88        violations.push(self.violation_with_fixes_at(
89            &file.path,
90            &assignment.location,
91            message,
92            fixes,
93        ));
94    }
95
96    /// Extract components from: dest = g_bytes_get_data(bytes, size_arg)
97    /// Returns (dest_text, bytes_var, size_arg, assignment, call)
98    fn extract_bytes_get_data<'a>(
99        &self,
100        stmt: &'a Statement,
101    ) -> Option<(
102        &'a str,
103        &'a str,
104        &'a str,
105        &'a Assignment,
106        &'a CallExpression,
107    )> {
108        let Statement::Expression(expr_stmt) = stmt else {
109            return None;
110        };
111        let Expression::Assignment(assignment) = expr_stmt.as_ref() else {
112            return None;
113        };
114
115        // Check operator is "="
116        if assignment.operator != AssignmentOp::Assign {
117            return None;
118        }
119
120        // Right side should be a call to g_bytes_get_data
121        let Expression::Call(call) = assignment.rhs.as_ref() else {
122            return None;
123        };
124
125        if !call.is_function("g_bytes_get_data") {
126            return None;
127        }
128
129        // Need exactly 2 arguments
130        if call.arguments.len() != 2 {
131            return None;
132        }
133
134        // Extract argument text from source
135        let bytes_var = call.get_arg_text(0)?;
136        let size_arg = call.get_arg_text(1)?;
137
138        let dest_var = assignment.lhs_as_text();
139        if dest_var.is_empty() {
140            return None;
141        }
142
143        Some((dest_var, bytes_var, size_arg, assignment, call))
144    }
145
146    /// Extract call from: g_bytes_unref(expected_var)
147    /// Returns the end_byte of the statement (including semicolon)
148    fn extract_bytes_unref(&self, stmt: &Statement, expected_var: &str) -> Option<usize> {
149        let Statement::Expression(expr_stmt) = stmt else {
150            return None;
151        };
152
153        let Expression::Call(call) = expr_stmt.as_ref() else {
154            return None;
155        };
156
157        if !call.is_function("g_bytes_unref") {
158            return None;
159        }
160
161        // Need exactly 1 argument
162        if call.arguments.len() != 1 {
163            return None;
164        }
165
166        // Check argument matches expected variable
167        let arg_text = call.get_arg_text(0)?;
168        if arg_text != expected_var {
169            return None;
170        }
171
172        Some(expr_stmt.location().find_semicolon_end())
173    }
174}