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(
90        &self,
91        state: &mut RuleState,
92        line: u32,
93        instruction: &Instruction,
94        shell: Option<&ParsedShell>,
95    );
96
97    /// Finalize the rule and return any additional failures.
98    /// Called after all instructions have been processed.
99    fn finalize(&self, state: RuleState) -> Vec<CheckFailure> {
100        state.failures
101    }
102
103    /// Get the rule code.
104    fn code(&self) -> &RuleCode;
105
106    /// Get the default severity.
107    fn severity(&self) -> Severity;
108
109    /// Get the rule message.
110    fn message(&self) -> &str;
111}
112
113/// State for rule execution.
114#[derive(Debug, Clone, Default)]
115pub struct RuleState {
116    /// Accumulated failures.
117    pub failures: Vec<CheckFailure>,
118    /// Custom state data (serialized).
119    pub data: RuleData,
120}
121
122impl RuleState {
123    /// Create a new empty state.
124    pub fn new() -> Self {
125        Self::default()
126    }
127
128    /// Add a failure.
129    pub fn add_failure(
130        &mut self,
131        code: impl Into<RuleCode>,
132        severity: Severity,
133        message: impl Into<String>,
134        line: u32,
135    ) {
136        self.failures
137            .push(CheckFailure::new(code, severity, message, line));
138    }
139}
140
141/// Custom data storage for stateful rules.
142#[derive(Debug, Clone, Default)]
143pub struct RuleData {
144    /// Integer values.
145    pub ints: std::collections::HashMap<&'static str, i64>,
146    /// Boolean values.
147    pub bools: std::collections::HashMap<&'static str, bool>,
148    /// String values.
149    pub strings: std::collections::HashMap<&'static str, String>,
150    /// String set values.
151    pub string_sets: std::collections::HashMap<&'static str, std::collections::HashSet<String>>,
152}
153
154impl RuleData {
155    pub fn get_int(&self, key: &'static str) -> i64 {
156        self.ints.get(key).copied().unwrap_or(0)
157    }
158
159    pub fn set_int(&mut self, key: &'static str, value: i64) {
160        self.ints.insert(key, value);
161    }
162
163    pub fn get_bool(&self, key: &'static str) -> bool {
164        self.bools.get(key).copied().unwrap_or(false)
165    }
166
167    pub fn set_bool(&mut self, key: &'static str, value: bool) {
168        self.bools.insert(key, value);
169    }
170
171    pub fn get_string(&self, key: &'static str) -> Option<&str> {
172        self.strings.get(key).map(|s| s.as_str())
173    }
174
175    pub fn set_string(&mut self, key: &'static str, value: impl Into<String>) {
176        self.strings.insert(key, value.into());
177    }
178
179    pub fn get_string_set(&self, key: &'static str) -> Option<&std::collections::HashSet<String>> {
180        self.string_sets.get(key)
181    }
182
183    pub fn insert_to_set(&mut self, key: &'static str, value: impl Into<String>) {
184        self.string_sets
185            .entry(key)
186            .or_default()
187            .insert(value.into());
188    }
189
190    pub fn set_contains(&self, key: &'static str, value: &str) -> bool {
191        self.string_sets
192            .get(key)
193            .map(|s| s.contains(value))
194            .unwrap_or(false)
195    }
196}
197
198/// A simple stateless rule.
199pub struct SimpleRule<F>
200where
201    F: Fn(&Instruction, Option<&ParsedShell>) -> bool + Send + Sync,
202{
203    code: RuleCode,
204    severity: Severity,
205    message: String,
206    check_fn: F,
207}
208
209impl<F> SimpleRule<F>
210where
211    F: Fn(&Instruction, Option<&ParsedShell>) -> bool + Send + Sync,
212{
213    /// Create a new simple rule.
214    pub fn new(
215        code: impl Into<RuleCode>,
216        severity: Severity,
217        message: impl Into<String>,
218        check_fn: F,
219    ) -> Self {
220        Self {
221            code: code.into(),
222            severity,
223            message: message.into(),
224            check_fn,
225        }
226    }
227}
228
229impl<F> Rule for SimpleRule<F>
230where
231    F: Fn(&Instruction, Option<&ParsedShell>) -> bool + Send + Sync,
232{
233    fn check(
234        &self,
235        state: &mut RuleState,
236        line: u32,
237        instruction: &Instruction,
238        shell: Option<&ParsedShell>,
239    ) {
240        if !(self.check_fn)(instruction, shell) {
241            state.add_failure(self.code.clone(), self.severity, self.message.clone(), line);
242        }
243    }
244
245    fn code(&self) -> &RuleCode {
246        &self.code
247    }
248
249    fn severity(&self) -> Severity {
250        self.severity
251    }
252
253    fn message(&self) -> &str {
254        &self.message
255    }
256}
257
258/// Create a simple stateless rule.
259pub fn simple_rule<F>(
260    code: impl Into<RuleCode>,
261    severity: Severity,
262    message: impl Into<String>,
263    check_fn: F,
264) -> SimpleRule<F>
265where
266    F: Fn(&Instruction, Option<&ParsedShell>) -> bool + Send + Sync,
267{
268    SimpleRule::new(code, severity, message, check_fn)
269}
270
271/// A stateful rule with custom step function.
272pub struct CustomRule<F>
273where
274    F: Fn(&mut RuleState, u32, &Instruction, Option<&ParsedShell>) + Send + Sync,
275{
276    code: RuleCode,
277    severity: Severity,
278    message: String,
279    step_fn: F,
280}
281
282impl<F> CustomRule<F>
283where
284    F: Fn(&mut RuleState, u32, &Instruction, Option<&ParsedShell>) + Send + Sync,
285{
286    /// Create a new custom rule.
287    pub fn new(
288        code: impl Into<RuleCode>,
289        severity: Severity,
290        message: impl Into<String>,
291        step_fn: F,
292    ) -> Self {
293        Self {
294            code: code.into(),
295            severity,
296            message: message.into(),
297            step_fn,
298        }
299    }
300}
301
302impl<F> Rule for CustomRule<F>
303where
304    F: Fn(&mut RuleState, u32, &Instruction, Option<&ParsedShell>) + Send + Sync,
305{
306    fn check(
307        &self,
308        state: &mut RuleState,
309        line: u32,
310        instruction: &Instruction,
311        shell: Option<&ParsedShell>,
312    ) {
313        (self.step_fn)(state, line, instruction, shell);
314    }
315
316    fn code(&self) -> &RuleCode {
317        &self.code
318    }
319
320    fn severity(&self) -> Severity {
321        self.severity
322    }
323
324    fn message(&self) -> &str {
325        &self.message
326    }
327}
328
329/// Create a custom stateful rule.
330pub fn custom_rule<F>(
331    code: impl Into<RuleCode>,
332    severity: Severity,
333    message: impl Into<String>,
334    step_fn: F,
335) -> CustomRule<F>
336where
337    F: Fn(&mut RuleState, u32, &Instruction, Option<&ParsedShell>) + Send + Sync,
338{
339    CustomRule::new(code, severity, message, step_fn)
340}
341
342/// A rule with custom finalization.
343pub struct VeryCustomRule<F, D>
344where
345    F: Fn(&mut RuleState, u32, &Instruction, Option<&ParsedShell>) + Send + Sync,
346    D: Fn(RuleState) -> Vec<CheckFailure> + Send + Sync,
347{
348    code: RuleCode,
349    severity: Severity,
350    message: String,
351    step_fn: F,
352    done_fn: D,
353}
354
355impl<F, D> VeryCustomRule<F, D>
356where
357    F: Fn(&mut RuleState, u32, &Instruction, Option<&ParsedShell>) + Send + Sync,
358    D: Fn(RuleState) -> Vec<CheckFailure> + Send + Sync,
359{
360    /// Create a new very custom rule.
361    pub fn new(
362        code: impl Into<RuleCode>,
363        severity: Severity,
364        message: impl Into<String>,
365        step_fn: F,
366        done_fn: D,
367    ) -> Self {
368        Self {
369            code: code.into(),
370            severity,
371            message: message.into(),
372            step_fn,
373            done_fn,
374        }
375    }
376}
377
378impl<F, D> Rule for VeryCustomRule<F, D>
379where
380    F: Fn(&mut RuleState, u32, &Instruction, Option<&ParsedShell>) + Send + Sync,
381    D: Fn(RuleState) -> Vec<CheckFailure> + Send + Sync,
382{
383    fn check(
384        &self,
385        state: &mut RuleState,
386        line: u32,
387        instruction: &Instruction,
388        shell: Option<&ParsedShell>,
389    ) {
390        (self.step_fn)(state, line, instruction, shell);
391    }
392
393    fn finalize(&self, state: RuleState) -> Vec<CheckFailure> {
394        (self.done_fn)(state)
395    }
396
397    fn code(&self) -> &RuleCode {
398        &self.code
399    }
400
401    fn severity(&self) -> Severity {
402        self.severity
403    }
404
405    fn message(&self) -> &str {
406        &self.message
407    }
408}
409
410/// Create a rule with custom finalization.
411pub fn very_custom_rule<F, D>(
412    code: impl Into<RuleCode>,
413    severity: Severity,
414    message: impl Into<String>,
415    step_fn: F,
416    done_fn: D,
417) -> VeryCustomRule<F, D>
418where
419    F: Fn(&mut RuleState, u32, &Instruction, Option<&ParsedShell>) + Send + Sync,
420    D: Fn(RuleState) -> Vec<CheckFailure> + Send + Sync,
421{
422    VeryCustomRule::new(code, severity, message, step_fn, done_fn)
423}
424
425/// Get all enabled rules.
426pub fn all_rules() -> Vec<Box<dyn Rule>> {
427    vec![
428        // DL1xxx rules (deprecation warnings)
429        Box::new(dl1001::rule()),
430        // Simple DL3xxx rules
431        Box::new(dl3000::rule()),
432        Box::new(dl3001::rule()),
433        Box::new(dl3003::rule()),
434        Box::new(dl3004::rule()),
435        Box::new(dl3005::rule()),
436        Box::new(dl3007::rule()),
437        Box::new(dl3010::rule()),
438        Box::new(dl3011::rule()),
439        Box::new(dl3017::rule()),
440        Box::new(dl3020::rule()),
441        Box::new(dl3021::rule()),
442        Box::new(dl3025::rule()),
443        Box::new(dl3026::rule()),
444        Box::new(dl3027::rule()),
445        Box::new(dl3029::rule()),
446        Box::new(dl3031::rule()),
447        Box::new(dl3035::rule()),
448        Box::new(dl3039::rule()),
449        Box::new(dl3043::rule()),
450        Box::new(dl3044::rule()),
451        Box::new(dl3046::rule()),
452        Box::new(dl3048::rule()),
453        Box::new(dl3049::rule()),
454        Box::new(dl3050::rule()),
455        Box::new(dl3051::rule()),
456        Box::new(dl3052::rule()),
457        Box::new(dl3053::rule()),
458        Box::new(dl3054::rule()),
459        Box::new(dl3055::rule()),
460        Box::new(dl3056::rule()),
461        Box::new(dl3058::rule()),
462        Box::new(dl3061::rule()),
463        // DL4xxx simple rules
464        Box::new(dl4000::rule()),
465        Box::new(dl4005::rule()),
466        Box::new(dl4006::rule()),
467        // Stateful rules
468        Box::new(dl3002::rule()),
469        Box::new(dl3006::rule()),
470        Box::new(dl3012::rule()),
471        Box::new(dl3022::rule()),
472        Box::new(dl3023::rule()),
473        Box::new(dl3024::rule()),
474        Box::new(dl3045::rule()),
475        Box::new(dl3047::rule()),
476        Box::new(dl3057::rule()),
477        Box::new(dl3059::rule()),
478        Box::new(dl3062::rule()),
479        Box::new(dl4001::rule()),
480        Box::new(dl4003::rule()),
481        Box::new(dl4004::rule()),
482        // Shell-dependent rules
483        Box::new(dl3008::rule()),
484        Box::new(dl3009::rule()),
485        Box::new(dl3013::rule()),
486        Box::new(dl3014::rule()),
487        Box::new(dl3015::rule()),
488        Box::new(dl3016::rule()),
489        Box::new(dl3018::rule()),
490        Box::new(dl3019::rule()),
491        Box::new(dl3028::rule()),
492        Box::new(dl3030::rule()),
493        Box::new(dl3032::rule()),
494        Box::new(dl3033::rule()),
495        Box::new(dl3034::rule()),
496        Box::new(dl3036::rule()),
497        Box::new(dl3037::rule()),
498        Box::new(dl3038::rule()),
499        Box::new(dl3040::rule()),
500        Box::new(dl3041::rule()),
501        Box::new(dl3042::rule()),
502        Box::new(dl3060::rule()),
503    ]
504}
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509
510    #[test]
511    fn test_simple_rule() {
512        let rule = simple_rule("TEST001", Severity::Warning, "Test message", |instr, _| {
513            !matches!(instr, Instruction::Maintainer(_))
514        });
515
516        let mut state = RuleState::new();
517        let instr = Instruction::Maintainer("test".to_string());
518        rule.check(&mut state, 1, &instr, None);
519
520        assert_eq!(state.failures.len(), 1);
521        assert_eq!(state.failures[0].code.as_str(), "TEST001");
522    }
523
524    #[test]
525    fn test_rule_data() {
526        let mut data = RuleData::default();
527
528        data.set_int("count", 5);
529        assert_eq!(data.get_int("count"), 5);
530
531        data.set_bool("seen", true);
532        assert!(data.get_bool("seen"));
533
534        data.set_string("name", "test");
535        assert_eq!(data.get_string("name"), Some("test"));
536
537        data.insert_to_set("aliases", "builder");
538        assert!(data.set_contains("aliases", "builder"));
539        assert!(!data.set_contains("aliases", "runner"));
540    }
541}