syncable_cli/analyzer/hadolint/rules/
mod.rs

1//! Rule system framework for hadolint-rs.
2//!
3//! Provides the infrastructure for defining and running Dockerfile linting rules.
4//! The design matches hadolint's fold-based architecture:
5//!
6//! - `simple_rule` - Stateless rules that check each instruction independently
7//! - `custom_rule` - Stateful rules that accumulate state across instructions
8//! - `very_custom_rule` - Rules with custom finalization logic
9//! - `onbuild` - Wrapper to also check ONBUILD-wrapped instructions
10
11use crate::analyzer::hadolint::parser::instruction::Instruction;
12use crate::analyzer::hadolint::shell::ParsedShell;
13use crate::analyzer::hadolint::types::{CheckFailure, RuleCode, Severity};
14
15pub mod dl1001;
16pub mod dl3000;
17pub mod dl3001;
18pub mod dl3002;
19pub mod dl3003;
20pub mod dl3004;
21pub mod dl3005;
22pub mod dl3006;
23pub mod dl3007;
24pub mod dl3008;
25pub mod dl3009;
26pub mod dl3010;
27pub mod dl3011;
28pub mod dl3012;
29pub mod dl3013;
30pub mod dl3014;
31pub mod dl3015;
32pub mod dl3016;
33pub mod dl3017;
34pub mod dl3018;
35pub mod dl3019;
36pub mod dl3020;
37pub mod dl3021;
38pub mod dl3022;
39pub mod dl3023;
40pub mod dl3024;
41pub mod dl3025;
42pub mod dl3026;
43pub mod dl3027;
44pub mod dl3028;
45pub mod dl3029;
46pub mod dl3030;
47pub mod dl3031;
48pub mod dl3032;
49pub mod dl3033;
50pub mod dl3034;
51pub mod dl3035;
52pub mod dl3036;
53pub mod dl3037;
54pub mod dl3038;
55pub mod dl3039;
56pub mod dl3040;
57pub mod dl3041;
58pub mod dl3042;
59pub mod dl3043;
60pub mod dl3044;
61pub mod dl3045;
62pub mod dl3046;
63pub mod dl3047;
64pub mod dl3048;
65pub mod dl3049;
66pub mod dl3050;
67pub mod dl3051;
68pub mod dl3052;
69pub mod dl3053;
70pub mod dl3054;
71pub mod dl3055;
72pub mod dl3056;
73pub mod dl3057;
74pub mod dl3058;
75pub mod dl3059;
76pub mod dl3060;
77pub mod dl3061;
78pub mod dl3062;
79pub mod dl4000;
80pub mod dl4001;
81pub mod dl4003;
82pub mod dl4004;
83pub mod dl4005;
84pub mod dl4006;
85
86/// A rule that can check Dockerfile instructions.
87pub trait Rule: Send + Sync {
88    /// Check an instruction and potentially add failures to the state.
89    fn check(&self, state: &mut RuleState, line: u32, instruction: &Instruction, shell: Option<&ParsedShell>);
90
91    /// Finalize the rule and return any additional failures.
92    /// Called after all instructions have been processed.
93    fn finalize(&self, state: RuleState) -> Vec<CheckFailure> {
94        state.failures
95    }
96
97    /// Get the rule code.
98    fn code(&self) -> &RuleCode;
99
100    /// Get the default severity.
101    fn severity(&self) -> Severity;
102
103    /// Get the rule message.
104    fn message(&self) -> &str;
105}
106
107/// State for rule execution.
108#[derive(Debug, Clone, Default)]
109pub struct RuleState {
110    /// Accumulated failures.
111    pub failures: Vec<CheckFailure>,
112    /// Custom state data (serialized).
113    pub data: RuleData,
114}
115
116impl RuleState {
117    /// Create a new empty state.
118    pub fn new() -> Self {
119        Self::default()
120    }
121
122    /// Add a failure.
123    pub fn add_failure(&mut self, code: impl Into<RuleCode>, severity: Severity, message: impl Into<String>, line: u32) {
124        self.failures.push(CheckFailure::new(code, severity, message, line));
125    }
126}
127
128/// Custom data storage for stateful rules.
129#[derive(Debug, Clone, Default)]
130pub struct RuleData {
131    /// Integer values.
132    pub ints: std::collections::HashMap<&'static str, i64>,
133    /// Boolean values.
134    pub bools: std::collections::HashMap<&'static str, bool>,
135    /// String values.
136    pub strings: std::collections::HashMap<&'static str, String>,
137    /// String set values.
138    pub string_sets: std::collections::HashMap<&'static str, std::collections::HashSet<String>>,
139}
140
141impl RuleData {
142    pub fn get_int(&self, key: &'static str) -> i64 {
143        self.ints.get(key).copied().unwrap_or(0)
144    }
145
146    pub fn set_int(&mut self, key: &'static str, value: i64) {
147        self.ints.insert(key, value);
148    }
149
150    pub fn get_bool(&self, key: &'static str) -> bool {
151        self.bools.get(key).copied().unwrap_or(false)
152    }
153
154    pub fn set_bool(&mut self, key: &'static str, value: bool) {
155        self.bools.insert(key, value);
156    }
157
158    pub fn get_string(&self, key: &'static str) -> Option<&str> {
159        self.strings.get(key).map(|s| s.as_str())
160    }
161
162    pub fn set_string(&mut self, key: &'static str, value: impl Into<String>) {
163        self.strings.insert(key, value.into());
164    }
165
166    pub fn get_string_set(&self, key: &'static str) -> Option<&std::collections::HashSet<String>> {
167        self.string_sets.get(key)
168    }
169
170    pub fn insert_to_set(&mut self, key: &'static str, value: impl Into<String>) {
171        self.string_sets.entry(key).or_default().insert(value.into());
172    }
173
174    pub fn set_contains(&self, key: &'static str, value: &str) -> bool {
175        self.string_sets.get(key).map(|s| s.contains(value)).unwrap_or(false)
176    }
177}
178
179/// A simple stateless rule.
180pub struct SimpleRule<F>
181where
182    F: Fn(&Instruction, Option<&ParsedShell>) -> bool + Send + Sync,
183{
184    code: RuleCode,
185    severity: Severity,
186    message: String,
187    check_fn: F,
188}
189
190impl<F> SimpleRule<F>
191where
192    F: Fn(&Instruction, Option<&ParsedShell>) -> bool + Send + Sync,
193{
194    /// Create a new simple rule.
195    pub fn new(code: impl Into<RuleCode>, severity: Severity, message: impl Into<String>, check_fn: F) -> Self {
196        Self {
197            code: code.into(),
198            severity,
199            message: message.into(),
200            check_fn,
201        }
202    }
203}
204
205impl<F> Rule for SimpleRule<F>
206where
207    F: Fn(&Instruction, Option<&ParsedShell>) -> bool + Send + Sync,
208{
209    fn check(&self, state: &mut RuleState, line: u32, instruction: &Instruction, shell: Option<&ParsedShell>) {
210        if !(self.check_fn)(instruction, shell) {
211            state.add_failure(self.code.clone(), self.severity, self.message.clone(), line);
212        }
213    }
214
215    fn code(&self) -> &RuleCode {
216        &self.code
217    }
218
219    fn severity(&self) -> Severity {
220        self.severity
221    }
222
223    fn message(&self) -> &str {
224        &self.message
225    }
226}
227
228/// Create a simple stateless rule.
229pub fn simple_rule<F>(
230    code: impl Into<RuleCode>,
231    severity: Severity,
232    message: impl Into<String>,
233    check_fn: F,
234) -> SimpleRule<F>
235where
236    F: Fn(&Instruction, Option<&ParsedShell>) -> bool + Send + Sync,
237{
238    SimpleRule::new(code, severity, message, check_fn)
239}
240
241/// A stateful rule with custom step function.
242pub struct CustomRule<F>
243where
244    F: Fn(&mut RuleState, u32, &Instruction, Option<&ParsedShell>) + Send + Sync,
245{
246    code: RuleCode,
247    severity: Severity,
248    message: String,
249    step_fn: F,
250}
251
252impl<F> CustomRule<F>
253where
254    F: Fn(&mut RuleState, u32, &Instruction, Option<&ParsedShell>) + Send + Sync,
255{
256    /// Create a new custom rule.
257    pub fn new(code: impl Into<RuleCode>, severity: Severity, message: impl Into<String>, step_fn: F) -> Self {
258        Self {
259            code: code.into(),
260            severity,
261            message: message.into(),
262            step_fn,
263        }
264    }
265}
266
267impl<F> Rule for CustomRule<F>
268where
269    F: Fn(&mut RuleState, u32, &Instruction, Option<&ParsedShell>) + Send + Sync,
270{
271    fn check(&self, state: &mut RuleState, line: u32, instruction: &Instruction, shell: Option<&ParsedShell>) {
272        (self.step_fn)(state, line, instruction, shell);
273    }
274
275    fn code(&self) -> &RuleCode {
276        &self.code
277    }
278
279    fn severity(&self) -> Severity {
280        self.severity
281    }
282
283    fn message(&self) -> &str {
284        &self.message
285    }
286}
287
288/// Create a custom stateful rule.
289pub fn custom_rule<F>(
290    code: impl Into<RuleCode>,
291    severity: Severity,
292    message: impl Into<String>,
293    step_fn: F,
294) -> CustomRule<F>
295where
296    F: Fn(&mut RuleState, u32, &Instruction, Option<&ParsedShell>) + Send + Sync,
297{
298    CustomRule::new(code, severity, message, step_fn)
299}
300
301/// A rule with custom finalization.
302pub struct VeryCustomRule<F, D>
303where
304    F: Fn(&mut RuleState, u32, &Instruction, Option<&ParsedShell>) + Send + Sync,
305    D: Fn(RuleState) -> Vec<CheckFailure> + Send + Sync,
306{
307    code: RuleCode,
308    severity: Severity,
309    message: String,
310    step_fn: F,
311    done_fn: D,
312}
313
314impl<F, D> VeryCustomRule<F, D>
315where
316    F: Fn(&mut RuleState, u32, &Instruction, Option<&ParsedShell>) + Send + Sync,
317    D: Fn(RuleState) -> Vec<CheckFailure> + Send + Sync,
318{
319    /// Create a new very custom rule.
320    pub fn new(
321        code: impl Into<RuleCode>,
322        severity: Severity,
323        message: impl Into<String>,
324        step_fn: F,
325        done_fn: D,
326    ) -> Self {
327        Self {
328            code: code.into(),
329            severity,
330            message: message.into(),
331            step_fn,
332            done_fn,
333        }
334    }
335}
336
337impl<F, D> Rule for VeryCustomRule<F, D>
338where
339    F: Fn(&mut RuleState, u32, &Instruction, Option<&ParsedShell>) + Send + Sync,
340    D: Fn(RuleState) -> Vec<CheckFailure> + Send + Sync,
341{
342    fn check(&self, state: &mut RuleState, line: u32, instruction: &Instruction, shell: Option<&ParsedShell>) {
343        (self.step_fn)(state, line, instruction, shell);
344    }
345
346    fn finalize(&self, state: RuleState) -> Vec<CheckFailure> {
347        (self.done_fn)(state)
348    }
349
350    fn code(&self) -> &RuleCode {
351        &self.code
352    }
353
354    fn severity(&self) -> Severity {
355        self.severity
356    }
357
358    fn message(&self) -> &str {
359        &self.message
360    }
361}
362
363/// Create a rule with custom finalization.
364pub fn very_custom_rule<F, D>(
365    code: impl Into<RuleCode>,
366    severity: Severity,
367    message: impl Into<String>,
368    step_fn: F,
369    done_fn: D,
370) -> VeryCustomRule<F, D>
371where
372    F: Fn(&mut RuleState, u32, &Instruction, Option<&ParsedShell>) + Send + Sync,
373    D: Fn(RuleState) -> Vec<CheckFailure> + Send + Sync,
374{
375    VeryCustomRule::new(code, severity, message, step_fn, done_fn)
376}
377
378/// Get all enabled rules.
379pub fn all_rules() -> Vec<Box<dyn Rule>> {
380    vec![
381        // DL1xxx rules (deprecation warnings)
382        Box::new(dl1001::rule()),
383        // Simple DL3xxx rules
384        Box::new(dl3000::rule()),
385        Box::new(dl3001::rule()),
386        Box::new(dl3003::rule()),
387        Box::new(dl3004::rule()),
388        Box::new(dl3005::rule()),
389        Box::new(dl3007::rule()),
390        Box::new(dl3010::rule()),
391        Box::new(dl3011::rule()),
392        Box::new(dl3017::rule()),
393        Box::new(dl3020::rule()),
394        Box::new(dl3021::rule()),
395        Box::new(dl3025::rule()),
396        Box::new(dl3026::rule()),
397        Box::new(dl3027::rule()),
398        Box::new(dl3029::rule()),
399        Box::new(dl3031::rule()),
400        Box::new(dl3035::rule()),
401        Box::new(dl3039::rule()),
402        Box::new(dl3043::rule()),
403        Box::new(dl3044::rule()),
404        Box::new(dl3046::rule()),
405        Box::new(dl3048::rule()),
406        Box::new(dl3049::rule()),
407        Box::new(dl3050::rule()),
408        Box::new(dl3051::rule()),
409        Box::new(dl3052::rule()),
410        Box::new(dl3053::rule()),
411        Box::new(dl3054::rule()),
412        Box::new(dl3055::rule()),
413        Box::new(dl3056::rule()),
414        Box::new(dl3058::rule()),
415        Box::new(dl3061::rule()),
416        // DL4xxx simple rules
417        Box::new(dl4000::rule()),
418        Box::new(dl4005::rule()),
419        Box::new(dl4006::rule()),
420        // Stateful rules
421        Box::new(dl3002::rule()),
422        Box::new(dl3006::rule()),
423        Box::new(dl3012::rule()),
424        Box::new(dl3022::rule()),
425        Box::new(dl3023::rule()),
426        Box::new(dl3024::rule()),
427        Box::new(dl3045::rule()),
428        Box::new(dl3047::rule()),
429        Box::new(dl3057::rule()),
430        Box::new(dl3059::rule()),
431        Box::new(dl3062::rule()),
432        Box::new(dl4001::rule()),
433        Box::new(dl4003::rule()),
434        Box::new(dl4004::rule()),
435        // Shell-dependent rules
436        Box::new(dl3008::rule()),
437        Box::new(dl3009::rule()),
438        Box::new(dl3013::rule()),
439        Box::new(dl3014::rule()),
440        Box::new(dl3015::rule()),
441        Box::new(dl3016::rule()),
442        Box::new(dl3018::rule()),
443        Box::new(dl3019::rule()),
444        Box::new(dl3028::rule()),
445        Box::new(dl3030::rule()),
446        Box::new(dl3032::rule()),
447        Box::new(dl3033::rule()),
448        Box::new(dl3034::rule()),
449        Box::new(dl3036::rule()),
450        Box::new(dl3037::rule()),
451        Box::new(dl3038::rule()),
452        Box::new(dl3040::rule()),
453        Box::new(dl3041::rule()),
454        Box::new(dl3042::rule()),
455        Box::new(dl3060::rule()),
456    ]
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462
463    #[test]
464    fn test_simple_rule() {
465        let rule = simple_rule(
466            "TEST001",
467            Severity::Warning,
468            "Test message",
469            |instr, _| !matches!(instr, Instruction::Maintainer(_)),
470        );
471
472        let mut state = RuleState::new();
473        let instr = Instruction::Maintainer("test".to_string());
474        rule.check(&mut state, 1, &instr, None);
475
476        assert_eq!(state.failures.len(), 1);
477        assert_eq!(state.failures[0].code.as_str(), "TEST001");
478    }
479
480    #[test]
481    fn test_rule_data() {
482        let mut data = RuleData::default();
483
484        data.set_int("count", 5);
485        assert_eq!(data.get_int("count"), 5);
486
487        data.set_bool("seen", true);
488        assert!(data.get_bool("seen"));
489
490        data.set_string("name", "test");
491        assert_eq!(data.get_string("name"), Some("test"));
492
493        data.insert_to_set("aliases", "builder");
494        assert!(data.set_contains("aliases", "builder"));
495        assert!(!data.set_contains("aliases", "runner"));
496    }
497}