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