syncable_cli/analyzer/hadolint/rules/
dl3059.rs

1//! DL3059: Multiple consecutive RUN instructions
2//!
3//! Combine consecutive RUN instructions to reduce the number of layers.
4
5use crate::analyzer::hadolint::parser::instruction::Instruction;
6use crate::analyzer::hadolint::rules::{CustomRule, RuleState, custom_rule};
7use crate::analyzer::hadolint::shell::ParsedShell;
8use crate::analyzer::hadolint::types::Severity;
9
10pub fn rule()
11-> CustomRule<impl Fn(&mut RuleState, u32, &Instruction, Option<&ParsedShell>) + Send + Sync> {
12    custom_rule(
13        "DL3059",
14        Severity::Info,
15        "Multiple consecutive `RUN` instructions. Consider consolidation.",
16        |state, line, instr, _shell| {
17            match instr {
18                Instruction::From(_) => {
19                    // Reset tracking for new stage
20                    state.data.set_int("consecutive_runs", 0);
21                    state.data.set_int("last_run_line", 0);
22                }
23                Instruction::Run(_) => {
24                    let consecutive = state.data.get_int("consecutive_runs");
25                    state.data.set_int("consecutive_runs", consecutive + 1);
26                    state.data.set_int("last_run_line", line as i64);
27
28                    // Report on the second consecutive RUN
29                    if consecutive >= 1 {
30                        state.add_failure(
31                            "DL3059",
32                            Severity::Info,
33                            "Multiple consecutive `RUN` instructions. Consider consolidation.",
34                            line,
35                        );
36                    }
37                }
38                // Other instructions reset the counter
39                _ => {
40                    state.data.set_int("consecutive_runs", 0);
41                }
42            }
43        },
44    )
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use crate::analyzer::hadolint::config::HadolintConfig;
51    use crate::analyzer::hadolint::lint::{LintResult, lint};
52
53    fn lint_dockerfile(content: &str) -> LintResult {
54        lint(content, &HadolintConfig::default())
55    }
56
57    #[test]
58    fn test_consecutive_runs() {
59        let result =
60            lint_dockerfile("FROM ubuntu:20.04\nRUN apt-get update\nRUN apt-get install -y nginx");
61        assert!(result.failures.iter().any(|f| f.code.as_str() == "DL3059"));
62    }
63
64    #[test]
65    fn test_single_run() {
66        let result =
67            lint_dockerfile("FROM ubuntu:20.04\nRUN apt-get update && apt-get install -y nginx");
68        assert!(!result.failures.iter().any(|f| f.code.as_str() == "DL3059"));
69    }
70
71    #[test]
72    fn test_runs_separated_by_other() {
73        let result = lint_dockerfile(
74            "FROM ubuntu:20.04\nRUN apt-get update\nENV DEBIAN_FRONTEND=noninteractive\nRUN apt-get install -y nginx",
75        );
76        assert!(!result.failures.iter().any(|f| f.code.as_str() == "DL3059"));
77    }
78
79    #[test]
80    fn test_three_consecutive_runs() {
81        let result = lint_dockerfile("FROM ubuntu:20.04\nRUN echo 1\nRUN echo 2\nRUN echo 3");
82        // Should report on 2nd and 3rd RUN
83        let count = result
84            .failures
85            .iter()
86            .filter(|f| f.code.as_str() == "DL3059")
87            .count();
88        assert_eq!(count, 2);
89    }
90
91    #[test]
92    fn test_different_stages() {
93        let result = lint_dockerfile(
94            "FROM ubuntu:20.04 AS stage1\nRUN echo 1\nFROM ubuntu:20.04 AS stage2\nRUN echo 2",
95        );
96        // Different stages, no consecutive RUNs
97        assert!(!result.failures.iter().any(|f| f.code.as_str() == "DL3059"));
98    }
99}