gobject_linter/rules/
gi_not_bindings_friendly.rs1use gobject_ast::model::{FunctionAnnotation, FunctionDeclItem, Parameter};
2
3use crate::{
4 ast_context::AstContext,
5 config::Config,
6 rules::{Category, Rule, Violation},
7};
8
9const GLIB_CONTAINER_TYPES: &[&str] = &[
10 "GList",
11 "GSList",
12 "GHashTable",
13 "GPtrArray",
14 "GArray",
15 "GByteArray",
16];
17
18pub struct GiNotBindingsFriendly;
19
20impl Rule for GiNotBindingsFriendly {
21 fn name(&self) -> &'static str {
22 "gi_not_bindings_friendly"
23 }
24
25 fn description(&self) -> &'static str {
26 "Detect public API patterns that are problematic for GObject Introspection bindings"
27 }
28
29 fn category(&self) -> Category {
30 Category::Introspection
31 }
32
33 fn requires_meson(&self) -> bool {
34 true
35 }
36
37 fn opt_in(&self) -> bool {
38 true
39 }
40
41 fn opt_in_reason(&self) -> Option<&'static str> {
42 Some("Only relevant to libraries maintaining GObject Introspection bindings")
43 }
44
45 fn check_all(
46 &self,
47 ast_context: &AstContext,
48 _config: &Config,
49 violations: &mut Vec<Violation>,
50 ) {
51 if !ast_context.has_public_private_info() {
52 return;
53 }
54
55 for (path, file) in ast_context.iter_header_files() {
56 if !ast_context.is_gir_header(path).unwrap_or(false) {
57 continue;
58 }
59
60 for func in file.iter_function_declarations() {
61 if func.export_macros.is_empty() {
62 continue;
63 }
64 if is_skipped(func) {
65 continue;
66 }
67 if func.name.ends_with("_get_type") || func.name.ends_with("_error_quark") {
68 continue;
69 }
70
71 self.check_variadic(func, path, violations);
72 self.check_out_params(func, path, violations);
73 self.check_container_types(func, path, violations);
74 }
75 }
76 }
77}
78
79fn is_skipped(func: &FunctionDeclItem) -> bool {
80 func.doc
81 .as_ref()
82 .is_some_and(|d| d.annotations.contains(&FunctionAnnotation::Skip))
83}
84
85impl GiNotBindingsFriendly {
86 fn check_variadic(
87 &self,
88 func: &FunctionDeclItem,
89 path: &std::path::Path,
90 violations: &mut Vec<Violation>,
91 ) {
92 if func
93 .parameters
94 .iter()
95 .any(|p| matches!(p, Parameter::Variadic))
96 {
97 violations.push(self.violation_at(
98 path,
99 &func.location,
100 format!(
101 "Function '{}' uses variadic arguments which cannot be introspected",
102 func.name,
103 ),
104 ));
105 }
106 }
107
108 fn check_out_params(
109 &self,
110 func: &FunctionDeclItem,
111 path: &std::path::Path,
112 violations: &mut Vec<Violation>,
113 ) {
114 let params = &func.parameters;
115 if params.is_empty() {
116 return;
117 }
118
119 let mut out_count = 0usize;
120
121 for (i, param) in params.iter().enumerate() {
122 let Parameter::Regular { type_info, .. } = param else {
123 continue;
124 };
125 if i == 0 {
127 continue;
128 }
129 if i == params.len() - 1
131 && type_info.base_type == "GError"
132 && type_info.pointer_depth >= 2
133 {
134 continue;
135 }
136 let min_depth = if type_info.is_basic() { 1 } else { 2 };
137 if type_info.pointer_depth >= min_depth {
138 out_count += 1;
139 }
140 }
141
142 if out_count > 2 {
143 violations.push(self.violation_at(
144 path,
145 &func.location,
146 format!(
147 "Function '{}' has {} out parameters (pointer-to-pointer); \
148 consider reducing to at most 2 for better bindings",
149 func.name, out_count,
150 ),
151 ));
152 }
153 }
154
155 fn check_container_types(
156 &self,
157 func: &FunctionDeclItem,
158 path: &std::path::Path,
159 violations: &mut Vec<Violation>,
160 ) {
161 if is_container_type(&func.return_type.base_type) && func.return_type.pointer_depth >= 1 {
162 violations.push(self.violation_at(
163 path,
164 &func.location,
165 format!(
166 "Function '{}' returns {}* in public API; {}",
167 func.name,
168 func.return_type.base_type,
169 container_suggestion(&func.return_type.base_type),
170 ),
171 ));
172 }
173
174 for param in &func.parameters {
175 let Parameter::Regular {
176 type_info, name, ..
177 } = param
178 else {
179 continue;
180 };
181 if is_container_type(&type_info.base_type) && type_info.pointer_depth >= 1 {
182 let param_label = name.as_deref().unwrap_or("(unnamed)");
183 violations.push(self.violation_at(
184 path,
185 &func.location,
186 format!(
187 "Function '{}' parameter '{}' uses {}* in public API; {}",
188 func.name,
189 param_label,
190 type_info.base_type,
191 container_suggestion(&type_info.base_type),
192 ),
193 ));
194 }
195 }
196 }
197}
198
199fn is_container_type(base_type: &str) -> bool {
200 GLIB_CONTAINER_TYPES.contains(&base_type)
201}
202
203fn container_suggestion(base_type: &str) -> &'static str {
204 match base_type {
205 "GList" | "GSList" => "Consider GListModel for better introspection support",
206 "GHashTable" => "Introspection cannot determine key/value types",
207 _ => "Consider a typed alternative for better introspection support",
208 }
209}