syncable_cli/analyzer/hadolint/rules/
dl3020.rs

1//! DL3020: Use COPY instead of ADD for files/dirs
2//!
3//! ADD has special behaviors (URL download, tar extraction) that make it
4//! less predictable. Use COPY for simply copying files.
5
6use crate::analyzer::hadolint::parser::instruction::Instruction;
7use crate::analyzer::hadolint::rules::{simple_rule, SimpleRule};
8use crate::analyzer::hadolint::shell::ParsedShell;
9use crate::analyzer::hadolint::types::Severity;
10
11pub fn rule() -> SimpleRule<impl Fn(&Instruction, Option<&ParsedShell>) -> bool + Send + Sync> {
12    simple_rule(
13        "DL3020",
14        Severity::Error,
15        "Use COPY instead of ADD for files and folders",
16        |instr, _shell| {
17            match instr {
18                Instruction::Add(args, _) => {
19                    // ADD is OK for URLs and archives
20                    args.has_url() || args.has_archive()
21                }
22                _ => true,
23            }
24        },
25    )
26}
27
28#[cfg(test)]
29mod tests {
30    use super::*;
31    use crate::analyzer::hadolint::parser::instruction::{AddArgs, AddFlags};
32    use crate::analyzer::hadolint::rules::{Rule, RuleState};
33
34    #[test]
35    fn test_add_file() {
36        let rule = rule();
37        let mut state = RuleState::new();
38
39        let args = AddArgs::new(vec!["app.js".to_string()], "/app/");
40        let instr = Instruction::Add(args, AddFlags::default());
41        rule.check(&mut state, 1, &instr, None);
42        assert_eq!(state.failures.len(), 1);
43        assert_eq!(state.failures[0].code.as_str(), "DL3020");
44    }
45
46    #[test]
47    fn test_add_url() {
48        let rule = rule();
49        let mut state = RuleState::new();
50
51        let args = AddArgs::new(vec!["https://example.com/file.tar.gz".to_string()], "/app/");
52        let instr = Instruction::Add(args, AddFlags::default());
53        rule.check(&mut state, 1, &instr, None);
54        assert!(state.failures.is_empty());
55    }
56
57    #[test]
58    fn test_add_archive() {
59        let rule = rule();
60        let mut state = RuleState::new();
61
62        let args = AddArgs::new(vec!["app.tar.gz".to_string()], "/app/");
63        let instr = Instruction::Add(args, AddFlags::default());
64        rule.check(&mut state, 1, &instr, None);
65        assert!(state.failures.is_empty());
66    }
67
68    #[test]
69    fn test_copy_ok() {
70        let rule = rule();
71        let mut state = RuleState::new();
72
73        // COPY is always OK
74        let instr = Instruction::Workdir("/app".to_string()); // Different instruction
75        rule.check(&mut state, 1, &instr, None);
76        assert!(state.failures.is_empty());
77    }
78}