syncable_cli/analyzer/hadolint/rules/
dl3024.rs

1//! DL3024: FROM aliases must be unique
2//!
3//! Each FROM instruction should have a unique alias.
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        "DL3024",
13        Severity::Error,
14        "`FROM` aliases (stage names) must be unique.",
15        |state, line, instr, _shell| {
16            if let Instruction::From(base) = instr {
17                if let Some(alias) = &base.alias {
18                    let alias_str = alias.as_str();
19                    if state.data.set_contains("seen_aliases", alias_str) {
20                        state.add_failure(
21                            "DL3024",
22                            Severity::Error,
23                            format!("Duplicate `FROM` alias `{}`.", alias_str),
24                            line,
25                        );
26                    } else {
27                        state.data.insert_to_set("seen_aliases", alias_str);
28                    }
29                }
30            }
31        },
32    )
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38    use crate::analyzer::hadolint::lint::{lint, LintResult};
39    use crate::analyzer::hadolint::config::HadolintConfig;
40
41    fn lint_dockerfile(content: &str) -> LintResult {
42        lint(content, &HadolintConfig::default())
43    }
44
45    #[test]
46    fn test_duplicate_alias() {
47        let result = lint_dockerfile(
48            "FROM node:18 AS builder\nRUN npm ci\nFROM node:18-alpine AS builder\nRUN echo done"
49        );
50        assert!(result.failures.iter().any(|f| f.code.as_str() == "DL3024"));
51    }
52
53    #[test]
54    fn test_unique_aliases() {
55        let result = lint_dockerfile(
56            "FROM node:18 AS builder\nRUN npm ci\nFROM node:18-alpine AS runner\nRUN echo done"
57        );
58        assert!(!result.failures.iter().any(|f| f.code.as_str() == "DL3024"));
59    }
60
61    #[test]
62    fn test_no_aliases() {
63        let result = lint_dockerfile(
64            "FROM node:18\nRUN npm ci\nFROM node:18-alpine\nRUN echo done"
65        );
66        assert!(!result.failures.iter().any(|f| f.code.as_str() == "DL3024"));
67    }
68
69    #[test]
70    fn test_single_stage() {
71        let result = lint_dockerfile("FROM node:18 AS builder\nRUN npm ci");
72        assert!(!result.failures.iter().any(|f| f.code.as_str() == "DL3024"));
73    }
74}