Skip to main content

gobject_linter/rules/
missing_g_begin_decls.rs

1use gobject_ast::model::{Expression, PreprocessorDirective, TopLevelItem};
2
3use crate::{
4    ast_context::AstContext,
5    config::Config,
6    rules::{Category, Rule, Violation},
7};
8
9pub struct MissingGBeginDecls;
10
11impl Rule for MissingGBeginDecls {
12    fn name(&self) -> &'static str {
13        "missing_g_begin_decls"
14    }
15
16    fn description(&self) -> &'static str {
17        "Detect headers with missing or mismatched G_BEGIN_DECLS/G_END_DECLS"
18    }
19
20    fn category(&self) -> Category {
21        Category::Correctness
22    }
23
24    fn check_all(
25        &self,
26        ast_context: &AstContext,
27        _config: &Config,
28        violations: &mut Vec<Violation>,
29    ) {
30        for (path, file) in ast_context.iter_header_files() {
31            let decls_block = file.iter_all_items().find_map(|item| match item {
32                TopLevelItem::Preprocessor(PreprocessorDirective::GObjectDeclsBlock {
33                    location,
34                    ..
35                }) => Some(location),
36                _ => None,
37            });
38
39            if let Some(loc) = &decls_block {
40                // GObjectDeclsBlock exists, verify G_END_DECLS is actually present
41                let source = loc.source();
42                let mut pos = loc.end_byte;
43                while pos > 0 && source[pos - 1] != b'\n' {
44                    pos -= 1;
45                }
46                let end_line = source[pos..loc.end_byte].trim_ascii_start();
47                if !end_line.starts_with(b"G_END_DECLS") {
48                    violations.push(self.violation_at(
49                        path,
50                        loc,
51                        "G_BEGIN_DECLS without matching G_END_DECLS".to_string(),
52                    ));
53                }
54                continue;
55            }
56
57            let orphan_begin = file.iter_all_items().find_map(|item| match item {
58                TopLevelItem::Preprocessor(PreprocessorDirective::Call {
59                    directive,
60                    location,
61                }) if directive == "G_BEGIN_DECLS" => Some(location),
62                TopLevelItem::Expression(expr)
63                    if matches!(expr.as_ref(), Expression::Identifier(id) if id.name == "G_BEGIN_DECLS") =>
64                {
65                    match expr.as_ref() {
66                        Expression::Identifier(id) => Some(&id.location),
67                        _ => None,
68                    }
69                }
70                _ => None,
71            });
72
73            let orphan_end = file.iter_all_items().find_map(|item| match item {
74                TopLevelItem::Preprocessor(PreprocessorDirective::Call {
75                    directive,
76                    location,
77                }) if directive == "G_END_DECLS" => Some(location),
78                TopLevelItem::Expression(expr)
79                    if matches!(expr.as_ref(), Expression::Identifier(id) if id.name == "G_END_DECLS") =>
80                {
81                    match expr.as_ref() {
82                        Expression::Identifier(id) => Some(&id.location),
83                        _ => None,
84                    }
85                }
86                _ => None,
87            });
88
89            if let Some(loc) = orphan_begin {
90                violations.push(self.violation_at(
91                    path,
92                    loc,
93                    "G_BEGIN_DECLS without matching G_END_DECLS".to_string(),
94                ));
95            }
96
97            if let Some(loc) = orphan_end {
98                violations.push(self.violation_at(
99                    path,
100                    loc,
101                    "G_END_DECLS without matching G_BEGIN_DECLS".to_string(),
102                ));
103            }
104
105            if orphan_begin.is_none()
106                && orphan_end.is_none()
107                && ast_context.is_public_header(path) == Some(true)
108                && file.has_declarations()
109            {
110                violations.push(self.violation(
111                    path,
112                    1,
113                    1,
114                    "Public header is missing G_BEGIN_DECLS/G_END_DECLS".to_string(),
115                ));
116            }
117        }
118    }
119}