syncable_cli/analyzer/dclint/rules/
dcl002.rs

1//! DCL002: no-duplicate-container-names
2//!
3//! Container names must be unique across all services.
4
5use std::collections::HashMap;
6
7use crate::analyzer::dclint::rules::{LintContext, Rule, SimpleRule, make_failure};
8use crate::analyzer::dclint::types::{CheckFailure, RuleCategory, Severity};
9
10const CODE: &str = "DCL002";
11const NAME: &str = "no-duplicate-container-names";
12const DESCRIPTION: &str = "Container names must be unique across all services.";
13const URL: &str = "https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-duplicate-container-names-rule.md";
14
15pub fn rule() -> impl Rule {
16    SimpleRule::new(
17        CODE,
18        NAME,
19        Severity::Error,
20        RuleCategory::BestPractice,
21        DESCRIPTION,
22        URL,
23        check,
24    )
25}
26
27fn check(ctx: &LintContext) -> Vec<CheckFailure> {
28    let mut failures = Vec::new();
29    let mut container_names: HashMap<String, Vec<(String, u32)>> = HashMap::new();
30
31    // Collect all container names with their service names and positions
32    for (service_name, service) in &ctx.compose.services {
33        if let Some(container_name) = &service.container_name {
34            let line = service
35                .container_name_pos
36                .map(|p| p.line)
37                .unwrap_or(service.position.line);
38
39            container_names
40                .entry(container_name.clone())
41                .or_default()
42                .push((service_name.clone(), line));
43        }
44    }
45
46    // Report duplicates
47    for (container_name, services) in container_names {
48        if services.len() > 1 {
49            for (service_name, line) in &services {
50                let other_services: Vec<&str> = services
51                    .iter()
52                    .filter(|(name, _)| name != service_name)
53                    .map(|(name, _)| name.as_str())
54                    .collect();
55
56                let message = format!(
57                    "Container name \"{}\" is used by multiple services: \"{}\" and \"{}\".",
58                    container_name,
59                    service_name,
60                    other_services.join("\", \"")
61                );
62
63                failures.push(
64                    make_failure(
65                        &CODE.into(),
66                        NAME,
67                        Severity::Error,
68                        RuleCategory::BestPractice,
69                        message,
70                        *line,
71                        1,
72                        false,
73                    )
74                    .with_data("containerName", container_name.clone())
75                    .with_data("serviceName", service_name.clone()),
76                );
77            }
78        }
79    }
80
81    failures
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use crate::analyzer::dclint::parser::parse_compose;
88
89    fn check_yaml(yaml: &str) -> Vec<CheckFailure> {
90        let compose = parse_compose(yaml).unwrap();
91        let ctx = LintContext::new(&compose, yaml, "docker-compose.yml");
92        check(&ctx)
93    }
94
95    #[test]
96    fn test_no_violation_unique_names() {
97        let yaml = r#"
98services:
99  web:
100    image: nginx
101    container_name: my-web
102  db:
103    image: postgres
104    container_name: my-db
105"#;
106        assert!(check_yaml(yaml).is_empty());
107    }
108
109    #[test]
110    fn test_no_violation_no_container_names() {
111        let yaml = r#"
112services:
113  web:
114    image: nginx
115  db:
116    image: postgres
117"#;
118        assert!(check_yaml(yaml).is_empty());
119    }
120
121    #[test]
122    fn test_violation_duplicate_names() {
123        let yaml = r#"
124services:
125  web:
126    image: nginx
127    container_name: my-container
128  api:
129    image: node
130    container_name: my-container
131"#;
132        let failures = check_yaml(yaml);
133        assert_eq!(failures.len(), 2); // One failure per service with duplicate
134        assert!(failures[0].message.contains("my-container"));
135    }
136
137    #[test]
138    fn test_violation_multiple_duplicates() {
139        let yaml = r#"
140services:
141  web:
142    image: nginx
143    container_name: shared-name
144  api:
145    image: node
146    container_name: shared-name
147  worker:
148    image: worker
149    container_name: shared-name
150"#;
151        let failures = check_yaml(yaml);
152        assert_eq!(failures.len(), 3); // One failure per service
153    }
154}