1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
use gobject_ast::model::{
CallExpression, Expression, FileModel, FunctionDefItem, SourceLocation, Statement,
};
use crate::{
ast_context::AstContext,
config::Config,
rules::{Rule, Violation},
};
pub struct UseGObjectNewWithProperties;
impl Rule for UseGObjectNewWithProperties {
fn name(&self) -> &'static str {
"use_g_object_new_with_properties"
}
fn description(&self) -> &'static str {
"Suggest setting properties in g_object_new instead of separate g_object_set calls"
}
fn category(&self) -> crate::rules::Category {
crate::rules::Category::Complexity
}
fn check_func_impl(
&self,
_ast_context: &AstContext,
_config: &Config,
func: &FunctionDefItem,
file: &FileModel,
violations: &mut Vec<Violation>,
) {
// Find all g_object_new calls with no properties
let empty_new_calls: Vec<_> = func
.find_calls(&["g_object_new"])
.into_iter()
.filter(|call| self.is_g_object_new_empty(call))
.collect();
if empty_new_calls.is_empty() {
return;
}
// Check statements for the pattern
self.check_statements(&func.body_statements, &empty_new_calls, file, violations);
}
}
impl UseGObjectNewWithProperties {
fn check_statements(
&self,
statements: &[Statement],
empty_new_calls: &[&CallExpression],
file: &FileModel,
violations: &mut Vec<Violation>,
) {
for i in 0..statements.len() {
// Check if this statement contains one of our empty g_object_new calls
if let Some((var_name, location)) =
self.find_empty_new_in_statement(&statements[i], empty_new_calls)
{
// Count consecutive g_object_set calls on the same variable
let mut set_count = 0;
for next_stmt in statements.iter().skip(i + 1) {
if let Some(set_var) = self.extract_g_object_set(next_stmt)
&& set_var == var_name
{
set_count += 1;
continue;
}
// Stop if we hit something that's not a g_object_set on our variable
break;
}
// Only report if there's at least one g_object_set call
if set_count > 0 {
violations.push(self.violation_at(
&file.path,
location,
format!(
"Set properties in g_object_new() instead of {} separate g_object_set() call{}",
set_count,
if set_count > 1 { "s" } else { "" }
),
));
}
}
statements[i].for_each_child_block(|body| {
self.check_statements(body, empty_new_calls, file, violations);
});
}
}
/// Check if a statement contains one of the empty g_object_new calls
/// Returns (variable_name, statement_location) if found
fn find_empty_new_in_statement<'a>(
&self,
stmt: &'a Statement,
empty_new_calls: &[&CallExpression],
) -> Option<(&'a str, &'a SourceLocation)> {
match stmt {
// Declaration: FooObject *obj = g_object_new(TYPE, NULL);
Statement::Declaration(decl) => {
if let Some(Expression::Call(call)) = &decl.initializer {
// Check if this call is one of our empty g_object_new calls
for empty_call in empty_new_calls {
if call.location.start_byte == empty_call.location.start_byte {
return Some((decl.name.as_str(), &decl.location));
}
}
}
}
// Assignment: obj = g_object_new(TYPE, NULL);
Statement::Expression(expr_stmt) => {
if let Expression::Assignment(assign) = expr_stmt.as_ref()
&& let Expression::Call(call) = &*assign.rhs
{
for empty_call in empty_new_calls {
if call.location.start_byte == empty_call.location.start_byte {
let var_name = assign.lhs_as_text();
if !var_name.is_empty() {
return Some((var_name, expr_stmt.location()));
}
}
}
}
}
_ => {}
}
None
}
/// Check if a call is g_object_new with no properties (just NULL or type
/// only)
fn is_g_object_new_empty(&self, call: &CallExpression) -> bool {
if !call.is_function("g_object_new") {
return false;
}
// g_object_new with just type and NULL, or just type
// g_object_new(TYPE, NULL) - 2 args
// g_object_new(TYPE) - 1 arg (rare but valid)
match call.arguments.len() {
1 => true,
2 => call.arguments[1].is_null(),
_ => false,
}
}
/// Extract g_object_set call, return the object variable
fn extract_g_object_set(&self, stmt: &Statement) -> Option<String> {
let Statement::Expression(expr_stmt) = stmt else {
return None;
};
let Expression::Call(call) = expr_stmt.as_ref() else {
return None;
};
if !call.is_function("g_object_set") {
return None;
}
// Get the first argument (the object)
let expr = call.get_arg(0)?;
expr.extract_variable_name()
.map(std::string::ToString::to_string)
}
}