Skip to main content

gobject_linter/rules/
gi_missing_since.rs

1use gobject_ast::model::{ExportMacro, FunctionAnnotation, PropertyAnnotation};
2
3use crate::{
4    ast_context::AstContext,
5    config::Config,
6    rules::{Category, Rule, Violation},
7};
8
9pub struct GiMissingSince;
10
11impl Rule for GiMissingSince {
12    fn name(&self) -> &'static str {
13        "gi_missing_since"
14    }
15
16    fn description(&self) -> &'static str {
17        "Detect public API with AVAILABLE_IN macros but missing or mismatched Since: annotations"
18    }
19
20    fn category(&self) -> Category {
21        Category::Introspection
22    }
23
24    fn requires_meson(&self) -> bool {
25        true
26    }
27
28    fn opt_in(&self) -> bool {
29        true
30    }
31
32    fn opt_in_reason(&self) -> Option<&'static str> {
33        Some("Only relevant to libraries maintaining GObject Introspection annotations")
34    }
35
36    fn check_all(
37        &self,
38        ast_context: &AstContext,
39        _config: &Config,
40        violations: &mut Vec<Violation>,
41    ) {
42        if !ast_context.has_public_private_info() {
43            return;
44        }
45
46        self.check_type_since(ast_context, violations);
47        self.check_functions_since(ast_context, violations);
48        self.check_property_since_consistency(ast_context, violations);
49        self.check_enum_value_inline_since(ast_context, violations);
50    }
51}
52
53impl GiMissingSince {
54    fn check_type_since(&self, ast_context: &AstContext, violations: &mut Vec<Violation>) {
55        for (path, file) in ast_context.iter_header_files() {
56            if !ast_context.is_public_header(path).unwrap_or(false) {
57                continue;
58            }
59
60            for gt in file.iter_all_gobject_types() {
61                let type_doc = ast_context.find_type_doc(&gt.type_name);
62
63                match gt.export_macros.iter().find(|m| m.version().is_some()) {
64                    Some(
65                        ExportMacro::DeprecatedIn(macro_ver)
66                        | ExportMacro::DeprecatedInFor(macro_ver, _),
67                    ) => {
68                        let dep_ver = type_doc.and_then(|d| d.deprecated.as_ref().map(|(v, _)| v));
69                        match dep_ver {
70                            None => {
71                                violations.push(self.violation_at(
72                                    path,
73                                    &gt.location,
74                                    format!(
75                                        "Type '{}' has DEPRECATED_IN_{}_{} but is missing a Deprecated: annotation",
76                                        gt.type_name,
77                                        macro_ver.major, macro_ver.minor,
78                                    ),
79                                ));
80                            }
81                            Some(v) if v != macro_ver => {
82                                violations.push(self.violation_at(
83                                    path,
84                                    &gt.location,
85                                    format!(
86                                        "Type '{}' has DEPRECATED_IN_{}_{} but Deprecated: says {}",
87                                        gt.type_name, macro_ver.major, macro_ver.minor, v,
88                                    ),
89                                ));
90                            }
91                            _ => {}
92                        }
93                    }
94                    Some(ExportMacro::AvailableIn(macro_ver)) => {
95                        let since = type_doc.and_then(|d| d.since.as_ref());
96                        match since {
97                            None => {
98                                violations.push(self.violation_at(
99                                    path,
100                                    &gt.location,
101                                    format!(
102                                        "Type '{}' has AVAILABLE_IN_{}_{} but is missing a Since: annotation",
103                                        gt.type_name,
104                                        macro_ver.major, macro_ver.minor,
105                                    ),
106                                ));
107                            }
108                            Some(v) if v != macro_ver => {
109                                violations.push(self.violation_at(
110                                    path,
111                                    &gt.location,
112                                    format!(
113                                        "Type '{}' has AVAILABLE_IN_{}_{} but Since: says {}",
114                                        gt.type_name, macro_ver.major, macro_ver.minor, v,
115                                    ),
116                                ));
117                            }
118                            _ => {}
119                        }
120                    }
121                    None if !gt.export_macros.is_empty() => {
122                        if let Some(since) = type_doc.and_then(|d| d.since.as_ref()) {
123                            violations.push(self.violation_at(
124                                path,
125                                &gt.location,
126                                format!(
127                                    "Type '{}' has Since: {} but is missing a versioned export macro",
128                                    gt.type_name,
129                                    since,
130                                ),
131                            ));
132                        }
133                    }
134                    _ => {}
135                }
136            }
137        }
138    }
139
140    fn check_functions_since(&self, ast_context: &AstContext, violations: &mut Vec<Violation>) {
141        for (path, file) in ast_context.iter_header_files() {
142            if !ast_context.is_public_header(path).unwrap_or(false) {
143                continue;
144            }
145
146            for func_decl in file.iter_function_declarations() {
147                if func_decl.name.ends_with("_get_type") || func_decl.name.ends_with("_error_quark")
148                {
149                    continue;
150                }
151
152                let func_doc = ast_context.find_func_doc(&func_decl.name);
153
154                match func_decl
155                    .export_macros
156                    .iter()
157                    .find(|m| m.version().is_some())
158                {
159                    Some(
160                        ExportMacro::DeprecatedIn(macro_ver)
161                        | ExportMacro::DeprecatedInFor(macro_ver, _),
162                    ) => {
163                        let dep_ver = func_doc.and_then(|d| d.deprecated.as_ref().map(|(v, _)| v));
164                        match dep_ver {
165                            None => {
166                                violations.push(self.violation_at(
167                                    path,
168                                    &func_decl.location,
169                                    format!(
170                                        "Function '{}' has DEPRECATED_IN_{}_{} but is missing a Deprecated: annotation",
171                                        func_decl.name,
172                                        macro_ver.major, macro_ver.minor,
173                                    ),
174                                ));
175                            }
176                            Some(v) if v != macro_ver => {
177                                violations.push(self.violation_at(
178                                    path,
179                                    &func_decl.location,
180                                    format!(
181                                        "Function '{}' has DEPRECATED_IN_{}_{} but Deprecated: says {}",
182                                        func_decl.name,
183                                        macro_ver.major, macro_ver.minor, v,
184                                    ),
185                                ));
186                            }
187                            _ => {}
188                        }
189                    }
190                    Some(ExportMacro::AvailableIn(macro_ver)) => {
191                        let since = func_doc.and_then(|d| d.since.as_ref());
192
193                        let parent_type_ver = file
194                            .iter_all_gobject_types()
195                            .find(|gt| func_decl.name.starts_with(&gt.function_prefix))
196                            .and_then(|gt| gt.export_macros.iter().find_map(|m| m.version()))
197                            .or_else(|| {
198                                file.iter_function_declarations()
199                                    .filter(|d| d.name.ends_with("_get_type"))
200                                    .find(|d| {
201                                        let prefix = &d.name[..d.name.len() - "_get_type".len()];
202                                        func_decl.name.starts_with(prefix)
203                                    })
204                                    .and_then(|d| d.export_macros.iter().find_map(|m| m.version()))
205                            });
206
207                        if parent_type_ver.is_some_and(|p| p == macro_ver) {
208                            continue;
209                        }
210
211                        match since {
212                            None => {
213                                violations.push(self.violation_at(
214                                    path,
215                                    &func_decl.location,
216                                    format!(
217                                        "Function '{}' has AVAILABLE_IN_{}_{} but is missing a Since: annotation",
218                                        func_decl.name,
219                                        macro_ver.major, macro_ver.minor,
220                                    ),
221                                ));
222                            }
223                            Some(v) if v != macro_ver => {
224                                violations.push(self.violation_at(
225                                    path,
226                                    &func_decl.location,
227                                    format!(
228                                        "Function '{}' has AVAILABLE_IN_{}_{} but Since: says {}",
229                                        func_decl.name, macro_ver.major, macro_ver.minor, v,
230                                    ),
231                                ));
232                            }
233                            _ => {}
234                        }
235                    }
236                    None if !func_decl.export_macros.is_empty() => {
237                        if let Some(since) = func_doc.and_then(|d| d.since.as_ref()) {
238                            violations.push(self.violation_at(
239                                path,
240                                &func_decl.location,
241                                format!(
242                                    "Function '{}' has Since: {} but is missing a versioned export macro",
243                                    func_decl.name,
244                                    since,
245                                ),
246                            ));
247                        }
248                    }
249                    _ => {}
250                }
251            }
252        }
253    }
254
255    fn check_property_since_consistency(
256        &self,
257        ast_context: &AstContext,
258        violations: &mut Vec<Violation>,
259    ) {
260        for (path, file) in ast_context.iter_all_files() {
261            for gt in file.iter_all_gobject_types() {
262                for prop_assignment in &gt.properties {
263                    let property = prop_assignment.property();
264                    let prop_name = &property.name;
265                    let prop_since = property.doc.as_ref().and_then(|d| d.since.as_ref());
266
267                    let mut getter_setter_names: Vec<(String, &str)> = Vec::new();
268
269                    if let Some(doc) = &property.doc {
270                        let prefix = &gt.function_prefix;
271                        for ann in &doc.annotations {
272                            match ann {
273                                PropertyAnnotation::Getter(short) => {
274                                    getter_setter_names
275                                        .push((format!("{prefix}_{short}"), "getter"));
276                                }
277                                PropertyAnnotation::Setter(short) => {
278                                    getter_setter_names
279                                        .push((format!("{prefix}_{short}"), "setter"));
280                                }
281                                _ => {}
282                            }
283                        }
284                    }
285
286                    for (_, decl_file) in ast_context.iter_header_files() {
287                        for func_decl in decl_file.iter_function_declarations() {
288                            if let Some(doc) = &func_decl.doc {
289                                for ann in &doc.annotations {
290                                    match ann {
291                                        FunctionAnnotation::GetProperty(p)
292                                            if p == prop_name
293                                                && !getter_setter_names
294                                                    .iter()
295                                                    .any(|(n, _)| n == &func_decl.name) =>
296                                        {
297                                            getter_setter_names
298                                                .push((func_decl.name.clone(), "getter"));
299                                        }
300                                        FunctionAnnotation::SetProperty(p)
301                                            if p == prop_name
302                                                && !getter_setter_names
303                                                    .iter()
304                                                    .any(|(n, _)| n == &func_decl.name) =>
305                                        {
306                                            getter_setter_names
307                                                .push((func_decl.name.clone(), "setter"));
308                                        }
309                                        _ => {}
310                                    }
311                                }
312                            }
313                        }
314                    }
315
316                    for (func_name, role) in &getter_setter_names {
317                        let func_since = ast_context
318                            .find_func_doc(func_name)
319                            .and_then(|d| d.since.as_ref());
320
321                        match (prop_since, func_since) {
322                            (Some(pv), None) => {
323                                violations.push(self.violation_at(
324                                    path,
325                                    &gt.location,
326                                    format!(
327                                        "Property '{prop_name}' has Since: {pv} but \
328                                         {role} '{func_name}' has no Since: annotation"
329                                    ),
330                                ));
331                            }
332                            (None, Some(fv)) => {
333                                violations.push(self.violation_at(
334                                    path,
335                                    &gt.location,
336                                    format!(
337                                        "Property '{prop_name}' has no Since: annotation but \
338                                         {role} '{func_name}' has Since: {fv}"
339                                    ),
340                                ));
341                            }
342                            (Some(pv), Some(fv)) if pv != fv => {
343                                violations.push(self.violation_at(
344                                    path,
345                                    &gt.location,
346                                    format!(
347                                        "Property '{prop_name}' Since: {pv} does not match \
348                                         {role} '{func_name}' Since: {fv}"
349                                    ),
350                                ));
351                            }
352                            _ => {}
353                        }
354                    }
355                }
356            }
357        }
358    }
359
360    fn check_enum_value_inline_since(
361        &self,
362        ast_context: &AstContext,
363        violations: &mut Vec<Violation>,
364    ) {
365        for (path, file) in ast_context.iter_header_files() {
366            if !ast_context.is_public_header(path).unwrap_or(false) {
367                continue;
368            }
369
370            for enum_info in file.iter_all_enums() {
371                for value in &enum_info.values {
372                    let Some(doc) = &value.doc else {
373                        continue;
374                    };
375                    if doc.since.is_some() {
376                        continue;
377                    }
378                    let has_inline_since =
379                        doc.description.iter().any(|line| line.contains("Since:"));
380                    if has_inline_since {
381                        violations.push(self.violation_at(
382                            path,
383                            &value.name_location,
384                            format!(
385                                "Enum member '{}' has Since: in inline doc which g-ir-scanner cannot detect — use a standalone /** {}: doc block",
386                                value.name, value.name,
387                            ),
388                        ));
389                    }
390                }
391            }
392        }
393    }
394}