1use gobject_ast::model::{AssignmentOp, Expression, FileModel, FunctionDefItem, Statement};
2
3use crate::{
4 ast_context::AstContext,
5 config::{Config, Style},
6 rules::{Fix, Rule, Violation},
7};
8
9pub struct UseGStealPointer;
10
11impl Rule for UseGStealPointer {
12 fn name(&self) -> &'static str {
13 "use_g_steal_pointer"
14 }
15
16 fn description(&self) -> &'static str {
17 "Use g_steal_pointer() instead of manually copying a pointer and setting it to NULL"
18 }
19
20 fn category(&self) -> crate::rules::Category {
21 crate::rules::Category::Style
22 }
23
24 fn fixable(&self) -> bool {
25 true
26 }
27
28 fn check_func_impl(
29 &self,
30 _ast_context: &AstContext,
31 config: &Config,
32 func: &FunctionDefItem,
33 file: &FileModel,
34 violations: &mut Vec<Violation>,
35 ) {
36 self.check_statements(&func.body_statements, file, &config.style, violations);
37 }
38}
39
40impl UseGStealPointer {
41 fn check_statements(
42 &self,
43 statements: &[Statement],
44 file: &FileModel,
45 style: &Style,
46 violations: &mut Vec<Violation>,
47 ) {
48 let mut i = 0;
49 while i < statements.len() {
50 if self.try_if_else_steal(&statements[i], file, style, violations) {
51 i += 1;
52 continue;
53 }
54 if self.try_if_no_else_steal(&statements[i], file, style, violations) {
55 i += 1;
56 continue;
57 }
58 if i + 2 < statements.len()
59 && self.try_declare_null_return(
60 &statements[i],
61 &statements[i + 1],
62 &statements[i + 2],
63 file,
64 style,
65 violations,
66 )
67 {
68 i += 3;
69 continue;
70 }
71 if i + 1 < statements.len()
72 && self.try_assign_null(&statements[i], &statements[i + 1], file, style, violations)
73 {
74 i += 2;
75 continue;
76 }
77 statements[i].for_each_child_block(|body| {
78 self.check_statements(body, file, style, violations);
79 });
80 i += 1;
81 }
82 }
83
84 fn try_declare_null_return(
86 &self,
87 s1: &Statement,
88 s2: &Statement,
89 s3: &Statement,
90 file: &FileModel,
91 style: &Style,
92 violations: &mut Vec<Violation>,
93 ) -> bool {
94 let Statement::Declaration(decl) = s1 else {
96 return false;
97 };
98
99 let Some(init_expr) = &decl.initializer else {
100 return false;
101 };
102
103 if init_expr.is_null() {
105 return false;
106 }
107
108 let Some(ptr_expr) = init_expr.extract_variable_name() else {
110 return false;
111 };
112
113 if ptr_expr.starts_with('*') {
115 return false;
116 }
117
118 let tmp_name = &decl.name;
119
120 if !s2.is_null_assignment_to(ptr_expr) {
122 return false;
123 }
124
125 let Statement::Return(ret) = s3 else {
127 return false;
128 };
129
130 if let Some(Expression::Identifier(id)) = &ret.value {
131 if id.name != *tmp_name {
132 return false;
133 }
134 } else {
135 return false;
136 }
137
138 let steal = style.format_addr_call("g_steal_pointer", ptr_expr, &[]);
139 let replacement = format!("return {steal};");
140 let message =
141 format!("Use {replacement} instead of copying {ptr_expr} and setting it to NULL");
142
143 let fixes = vec![
144 Fix::delete_line(s1.location()),
145 Fix::delete_line(s2.location()),
146 Fix::new(
147 s3.location().start_byte,
148 s3.location().end_byte,
149 replacement,
150 ),
151 ];
152
153 violations.push(self.violation_with_fixes_at(&file.path, s1.location(), message, fixes));
154 true
155 }
156
157 fn try_assign_null(
159 &self,
160 s1: &Statement,
161 s2: &Statement,
162 file: &FileModel,
163 style: &Style,
164 violations: &mut Vec<Violation>,
165 ) -> bool {
166 let Some((other_expr, ptr_expr)) = self.extract_assignment(s1) else {
167 return false;
168 };
169
170 if ptr_expr.starts_with('*') {
171 return false;
172 }
173
174 if !s2.is_null_assignment_to(ptr_expr) {
175 return false;
176 }
177
178 let steal = style.format_addr_call("g_steal_pointer", ptr_expr, &[]);
179 let replacement = format!("{other_expr} = {steal};");
180 let message = format!("Use {steal} instead of copying and setting to NULL");
181
182 let s2_end = s2.location().find_semicolon_end();
184 let fixes = vec![
185 Fix::delete_line(s1.location()),
187 Fix::new(s2.location().start_byte, s2_end, replacement),
189 ];
190
191 violations.push(self.violation_with_fixes_at(&file.path, s1.location(), message, fixes));
192 true
193 }
194
195 fn try_if_else_steal(
197 &self,
198 stmt: &Statement,
199 file: &FileModel,
200 style: &Style,
201 violations: &mut Vec<Violation>,
202 ) -> bool {
203 let Statement::If(if_stmt) = stmt else {
204 return false;
205 };
206
207 let Some(else_body) = &if_stmt.else_body else {
209 return false;
210 };
211
212 let Some(expr_text) = if_stmt.extract_null_check_variable() else {
214 return false;
215 };
216
217 if expr_text.starts_with('*') {
219 return false;
220 }
221
222 if if_stmt.then_body.len() != 2 {
224 return false;
225 }
226
227 let Some((dest_expr, rhs)) = self.extract_assignment(&if_stmt.then_body[0]) else {
229 return false;
230 };
231 if rhs != expr_text {
232 return false;
233 }
234
235 if !if_stmt.then_body[1].is_null_assignment_to(expr_text) {
237 return false;
238 }
239
240 if else_body.len() != 1 {
242 return false;
243 }
244 if !else_body[0].is_null_assignment_to(dest_expr) {
245 return false;
246 }
247
248 let steal = style.format_addr_call("g_steal_pointer", expr_text, &[]);
249 let replacement = format!("{dest_expr} = {steal};");
250 let message = format!("Use {steal} instead of if/else copy-and-NULL pattern");
251 let fix = Fix::new(
252 if_stmt.location.start_byte,
253 if_stmt.location.end_byte,
254 replacement,
255 );
256 violations.push(self.violation_with_fix_at(&file.path, &if_stmt.location, message, fix));
257 true
258 }
259
260 fn try_if_no_else_steal(
264 &self,
265 stmt: &Statement,
266 file: &FileModel,
267 style: &Style,
268 violations: &mut Vec<Violation>,
269 ) -> bool {
270 let Statement::If(if_stmt) = stmt else {
271 return false;
272 };
273
274 if if_stmt.else_body.is_some() {
276 return false;
277 }
278
279 let condition_expr = if_stmt.extract_null_check_variable();
281
282 if if_stmt.then_body.len() == 2 {
284 let Some((dest_expr, ptr_expr)) = self.extract_assignment(&if_stmt.then_body[0]) else {
285 return false;
286 };
287
288 if ptr_expr.starts_with('*') {
290 return false;
291 }
292
293 if !if_stmt.then_body[1].is_null_assignment_to(ptr_expr) {
294 return false;
295 }
296
297 let steal = style.format_addr_call("g_steal_pointer", ptr_expr, &[]);
298 let replacement = format!("{dest_expr} = {steal};");
299 let message = format!("Use {steal} instead of copying and setting to NULL");
300
301 let fix = if condition_expr == Some(ptr_expr) {
304 Fix::new(
305 if_stmt.location.start_byte,
306 if_stmt.location.end_byte,
307 replacement,
308 )
309 } else if if_stmt.then_has_braces {
310 let (open_brace, close_brace) =
311 if_stmt.then_body[0].location().find_braces_around();
312 Fix::new(open_brace, close_brace, replacement)
313 } else {
314 let body_start = if_stmt.then_body[0].location().start_byte;
315 let body_end = if_stmt.then_body[1].location().end_byte;
316 Fix::new(body_start, body_end, replacement)
317 };
318
319 violations.push(self.violation_with_fix_at(
320 &file.path,
321 if_stmt.then_body[0].location(),
322 message,
323 fix,
324 ));
325 return true;
326 }
327
328 if if_stmt.then_body.len() == 3 {
330 let Statement::Declaration(decl) = &if_stmt.then_body[0] else {
331 return false;
332 };
333
334 let Some(init_expr) = &decl.initializer else {
335 return false;
336 };
337
338 if init_expr.is_null() {
340 return false;
341 }
342
343 let Some(ptr_expr) = init_expr.extract_variable_name() else {
344 return false;
345 };
346
347 if ptr_expr.starts_with('*') {
349 return false;
350 }
351
352 let tmp_name = &decl.name;
353
354 if !if_stmt.then_body[1].is_null_assignment_to(ptr_expr) {
355 return false;
356 }
357
358 let Statement::Return(ret) = &if_stmt.then_body[2] else {
360 return false;
361 };
362
363 if let Some(Expression::Identifier(id)) = &ret.value {
364 if id.name != *tmp_name {
365 return false;
366 }
367 } else {
368 return false;
369 }
370
371 let steal = style.format_addr_call("g_steal_pointer", ptr_expr, &[]);
372 let replacement = format!("return {steal};");
373 let message =
374 format!("Use {replacement} instead of copying {ptr_expr} and setting it to NULL");
375
376 let fix = if condition_expr == Some(ptr_expr) {
378 Fix::new(
379 if_stmt.location.start_byte,
380 if_stmt.location.end_byte,
381 replacement,
382 )
383 } else if if_stmt.then_has_braces {
384 let (open_brace, close_brace) =
385 if_stmt.then_body[0].location().find_braces_around();
386 Fix::new(open_brace, close_brace, replacement)
387 } else {
388 let body_start = if_stmt.then_body[0].location().start_byte;
389 let body_end = if_stmt.then_body[2].location().end_byte;
390 Fix::new(body_start, body_end, replacement)
391 };
392
393 violations.push(self.violation_with_fix_at(
394 &file.path,
395 if_stmt.then_body[0].location(),
396 message,
397 fix,
398 ));
399 return true;
400 }
401
402 false
403 }
404
405 fn extract_assignment<'a>(&self, stmt: &'a Statement) -> Option<(&'a str, &'a str)> {
407 let Statement::Expression(expr_stmt) = stmt else {
408 return None;
409 };
410
411 let Expression::Assignment(assign) = expr_stmt.as_ref() else {
412 return None;
413 };
414
415 if assign.operator != AssignmentOp::Assign {
416 return None;
417 }
418
419 let rhs = match &*assign.rhs {
421 Expression::Identifier(id) => id.name.as_str(),
422 Expression::FieldAccess(f) => f.location.as_str().unwrap_or(""),
423 Expression::Null(_) | Expression::Call(_) => {
424 return None;
427 }
428 _ => {
429 return None;
430 }
431 };
432
433 let lhs = assign.lhs_as_text();
434 if lhs.is_empty() {
435 return None;
436 }
437 Some((lhs, rhs))
438 }
439}