syncable_cli/analyzer/hadolint/
pragma.rs

1//! Pragma parsing for inline rule ignores.
2//!
3//! Hadolint supports inline pragmas to ignore rules:
4//! - `# hadolint ignore=DL3008,DL3009` - Ignore for next instruction
5//! - `# hadolint global ignore=DL3008` - Ignore for entire file
6//! - `# hadolint shell=/bin/bash` - Set shell for ShellCheck
7
8use crate::analyzer::hadolint::types::RuleCode;
9use std::collections::{HashMap, HashSet};
10
11/// Parsed pragma state for a Dockerfile.
12#[derive(Debug, Clone, Default)]
13pub struct PragmaState {
14    /// Per-line ignored rules: line -> set of ignored codes.
15    pub ignored: HashMap<u32, HashSet<RuleCode>>,
16    /// Globally ignored rules.
17    pub global_ignored: HashSet<RuleCode>,
18    /// Shell override (if specified).
19    pub shell: Option<String>,
20}
21
22impl PragmaState {
23    /// Create a new empty pragma state.
24    pub fn new() -> Self {
25        Self::default()
26    }
27
28    /// Check if a rule should be ignored on a specific line.
29    pub fn is_ignored(&self, code: &RuleCode, line: u32) -> bool {
30        // Check global ignores
31        if self.global_ignored.contains(code) {
32            return true;
33        }
34
35        // Check line-specific ignores (check previous line, as pragma applies to next line)
36        if let Some(ignored) = self.ignored.get(&line)
37            && ignored.contains(code)
38        {
39            return true;
40        }
41
42        // Also check if the pragma was on the line before
43        if line > 0
44            && let Some(ignored) = self.ignored.get(&(line - 1))
45            && ignored.contains(code)
46        {
47            return true;
48        }
49
50        false
51    }
52}
53
54/// Parse pragma from a comment string.
55/// Returns the pragma type and any associated data.
56pub fn parse_pragma(comment: &str) -> Option<Pragma> {
57    let comment = comment.trim();
58
59    // Look for hadolint pragma
60    let pragma_start = comment.find("hadolint")?;
61    let pragma_content = &comment[pragma_start + "hadolint".len()..].trim();
62
63    // Parse global ignore
64    if let Some(rest) = pragma_content.strip_prefix("global") {
65        let rest = rest.trim();
66        if let Some(codes) = parse_ignore_list(rest) {
67            return Some(Pragma::GlobalIgnore(codes));
68        }
69    }
70
71    // Parse ignore
72    if let Some(codes) = parse_ignore_list(pragma_content) {
73        return Some(Pragma::Ignore(codes));
74    }
75
76    // Parse shell
77    if let Some(shell) = pragma_content.strip_prefix("shell=") {
78        let shell = shell.trim();
79        return Some(Pragma::Shell(shell.to_string()));
80    }
81
82    None
83}
84
85/// Parse an ignore list from a pragma string.
86fn parse_ignore_list(s: &str) -> Option<Vec<RuleCode>> {
87    let s = s.trim();
88
89    // Look for ignore= pattern
90    if !s.starts_with("ignore=") && !s.starts_with("ignore =") {
91        return None;
92    }
93
94    // Find the = sign and get the codes
95    let eq_pos = s.find('=')?;
96    let codes_str = &s[eq_pos + 1..].trim();
97
98    // Split by comma and parse codes
99    let codes: Vec<RuleCode> = codes_str
100        .split(',')
101        .map(|s| s.trim())
102        .filter(|s| !s.is_empty())
103        .map(RuleCode::new)
104        .collect();
105
106    if codes.is_empty() { None } else { Some(codes) }
107}
108
109/// Parsed pragma types.
110#[derive(Debug, Clone)]
111pub enum Pragma {
112    /// Ignore rules for the next instruction.
113    Ignore(Vec<RuleCode>),
114    /// Ignore rules globally for the entire file.
115    GlobalIgnore(Vec<RuleCode>),
116    /// Set shell for ShellCheck analysis.
117    Shell(String),
118}
119
120/// Extract pragma state from Dockerfile instructions.
121pub fn extract_pragmas(
122    instructions: &[crate::analyzer::hadolint::parser::InstructionPos],
123) -> PragmaState {
124    let mut state = PragmaState::new();
125
126    for instr in instructions {
127        if let crate::analyzer::hadolint::parser::instruction::Instruction::Comment(comment) =
128            &instr.instruction
129            && let Some(pragma) = parse_pragma(comment)
130        {
131            match pragma {
132                Pragma::Ignore(codes) => {
133                    // Ignore applies to the next line
134                    let entry = state.ignored.entry(instr.line_number).or_default();
135                    for code in codes {
136                        entry.insert(code);
137                    }
138                }
139                Pragma::GlobalIgnore(codes) => {
140                    for code in codes {
141                        state.global_ignored.insert(code);
142                    }
143                }
144                Pragma::Shell(shell) => {
145                    state.shell = Some(shell);
146                }
147            }
148        }
149    }
150
151    state
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_parse_ignore() {
160        let pragma = parse_pragma("# hadolint ignore=DL3008,DL3009").unwrap();
161        match pragma {
162            Pragma::Ignore(codes) => {
163                assert_eq!(codes.len(), 2);
164                assert_eq!(codes[0].as_str(), "DL3008");
165                assert_eq!(codes[1].as_str(), "DL3009");
166            }
167            _ => panic!("Expected Ignore pragma"),
168        }
169    }
170
171    #[test]
172    fn test_parse_global_ignore() {
173        let pragma = parse_pragma("# hadolint global ignore=DL3008").unwrap();
174        match pragma {
175            Pragma::GlobalIgnore(codes) => {
176                assert_eq!(codes.len(), 1);
177                assert_eq!(codes[0].as_str(), "DL3008");
178            }
179            _ => panic!("Expected GlobalIgnore pragma"),
180        }
181    }
182
183    #[test]
184    fn test_parse_shell() {
185        let pragma = parse_pragma("# hadolint shell=/bin/bash").unwrap();
186        match pragma {
187            Pragma::Shell(shell) => {
188                assert_eq!(shell, "/bin/bash");
189            }
190            _ => panic!("Expected Shell pragma"),
191        }
192    }
193
194    #[test]
195    fn test_no_pragma() {
196        assert!(parse_pragma("# This is a regular comment").is_none());
197    }
198
199    #[test]
200    fn test_pragma_state_is_ignored() {
201        let mut state = PragmaState::new();
202
203        // Add line-specific ignore
204        let mut codes = HashSet::new();
205        codes.insert(RuleCode::new("DL3008"));
206        state.ignored.insert(5, codes);
207
208        // Add global ignore
209        state.global_ignored.insert(RuleCode::new("DL3009"));
210
211        // Test line-specific (pragma on line 5 affects line 6)
212        assert!(state.is_ignored(&RuleCode::new("DL3008"), 6));
213        assert!(!state.is_ignored(&RuleCode::new("DL3008"), 10));
214
215        // Test global
216        assert!(state.is_ignored(&RuleCode::new("DL3009"), 1));
217        assert!(state.is_ignored(&RuleCode::new("DL3009"), 100));
218
219        // Test non-ignored
220        assert!(!state.is_ignored(&RuleCode::new("DL3010"), 1));
221    }
222}