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