Skip to main content

gobject_linter/rules/
use_pragma_once.rs

1use gobject_ast::model::{
2    ConditionalKind, PragmaKind, PreprocessorDirective, SourceLocation, TopLevelItem,
3};
4
5use crate::{
6    ast_context::AstContext,
7    config::Config,
8    rules::{Fix, Rule, Violation},
9};
10
11pub struct UsePragmaOnce;
12
13impl Rule for UsePragmaOnce {
14    fn name(&self) -> &'static str {
15        "use_pragma_once"
16    }
17
18    fn description(&self) -> &'static str {
19        "Suggest #pragma once instead of traditional include guards"
20    }
21
22    fn category(&self) -> crate::rules::Category {
23        crate::rules::Category::Style
24    }
25
26    fn fixable(&self) -> bool {
27        true
28    }
29
30    fn check_all(
31        &self,
32        ast_context: &AstContext,
33        _config: &Config,
34        violations: &mut Vec<Violation>,
35    ) {
36        for (path, file) in ast_context.iter_header_files() {
37            // Check if the file already uses #pragma once
38            let has_pragma_once = file.top_level_items.iter().any(|item| {
39                matches!(
40                    item,
41                    TopLevelItem::Preprocessor(PreprocessorDirective::Pragma {
42                        kind: PragmaKind::Once,
43                        ..
44                    })
45                )
46            });
47
48            if has_pragma_once {
49                continue; // Already using #pragma once
50            }
51
52            // Look for traditional include guard pattern
53            if let Some((ifndef_loc, define_loc, endif_loc, guard_name)) =
54                self.find_include_guard(&file.top_level_items)
55            {
56                // Build fixes:
57                // 1. Replace #ifndef and #define lines with #pragma once
58                // 2. Remove the #endif line (including any comment) at the end
59                let mut fixes = Vec::new();
60
61                // Fix 1: Delete #ifndef line (with following blank line if any)
62                let ifndef_fix_loc =
63                    ifndef_loc.with_byte_range(ifndef_loc.start_byte, ifndef_loc.start_byte);
64                fixes.push(Fix::delete_line_and_trailing_blank(&ifndef_fix_loc));
65
66                // Fix 2: Replace #define line with #pragma once
67                let (define_start, define_end) = define_loc.find_line_bounds();
68                fixes.push(Fix::new(define_start, define_end, "#pragma once\n"));
69
70                // Fix 3: Remove the entire #endif line (with preceding blank line if any)
71                fixes.push(Fix::delete_line_and_leading_blank(&endif_loc));
72
73                violations.push(self.violation_with_fixes_at(
74                    path,
75                    &ifndef_loc,
76                    format!("Use #pragma once instead of include guard '{}'", guard_name),
77                    fixes,
78                ));
79            }
80        }
81    }
82}
83
84impl UsePragmaOnce {
85    /// Find traditional include guard pattern
86    /// Returns (ifndef_location, define_location, endif_location, guard_name)
87    fn find_include_guard<'a>(
88        &self,
89        items: &'a [TopLevelItem],
90    ) -> Option<(SourceLocation, SourceLocation, SourceLocation, &'a str)> {
91        // The first non-comment item should be #ifndef (traditional include guard)
92        items
93            .iter()
94            .find(|item| !matches!(item, TopLevelItem::Comment(_)))
95            .and_then(|item| match item {
96                TopLevelItem::Preprocessor(PreprocessorDirective::Conditional {
97                    kind: ConditionalKind::Ifndef,
98                    condition: Some(name),
99                    body,
100                    location,
101                }) => {
102                    // Found #ifndef - check it contains matching #define as first item
103                    let define_loc = self.find_matching_define(body, name)?;
104
105                    let endif_loc = location.with_byte_range(location.end_byte, location.end_byte);
106                    let ifndef_loc = location.clone();
107
108                    Some((ifndef_loc, define_loc, endif_loc, name.as_str()))
109                }
110                _ => None, // First item is not #ifndef
111            })
112    }
113
114    /// Find matching #define inside the #ifndef body
115    /// Returns the define location only if it's a guard (no value) and there's
116    /// content after it
117    fn find_matching_define(
118        &self,
119        body: &[TopLevelItem],
120        guard_name: &str,
121    ) -> Option<SourceLocation> {
122        let non_comment_items: Vec<_> = body
123            .iter()
124            .filter(|item| !matches!(item, TopLevelItem::Comment(_)))
125            .collect();
126
127        if non_comment_items.len() < 2 {
128            return None;
129        }
130
131        non_comment_items
132            .first()
133            .and_then(|item| match item {
134                TopLevelItem::Preprocessor(PreprocessorDirective::Define {
135                    name,
136                    value,
137                    location,
138                }) => {
139                    if name == guard_name && value.is_none() {
140                        Some(location)
141                    } else {
142                        None
143                    }
144                }
145                _ => None,
146            })
147            .cloned()
148    }
149}