Skip to main content

gobject_linter/rules/
gi_not_bindings_friendly.rs

1use 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            // Skip first param (self)
126            if i == 0 {
127                continue;
128            }
129            // Skip GError ** as last param
130            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}