syncable_cli/analyzer/dclint/rules/
dcl012.rs

1//! DCL012: service-keys-order
2//!
3//! Service keys should be in a standard order.
4
5use crate::analyzer::dclint::rules::{FixableRule, LintContext, Rule, make_failure};
6use crate::analyzer::dclint::types::{CheckFailure, RuleCategory, Severity};
7
8const CODE: &str = "DCL012";
9const NAME: &str = "service-keys-order";
10const DESCRIPTION: &str = "Service keys should follow a standard ordering convention.";
11const URL: &str = "https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/service-keys-order-rule.md";
12
13// Standard key order for services
14const KEY_ORDER: &[&str] = &[
15    "image",
16    "build",
17    "container_name",
18    "hostname",
19    "restart",
20    "depends_on",
21    "links",
22    "ports",
23    "expose",
24    "volumes",
25    "volumes_from",
26    "environment",
27    "env_file",
28    "secrets",
29    "configs",
30    "labels",
31    "logging",
32    "network_mode",
33    "networks",
34    "extra_hosts",
35    "dns",
36    "dns_search",
37    "healthcheck",
38    "deploy",
39    "command",
40    "entrypoint",
41    "working_dir",
42    "user",
43    "privileged",
44    "cap_add",
45    "cap_drop",
46    "security_opt",
47    "tmpfs",
48    "stdin_open",
49    "tty",
50    "ulimits",
51    "sysctls",
52    "extends",
53    "profiles",
54];
55
56pub fn rule() -> impl Rule {
57    FixableRule::new(
58        CODE,
59        NAME,
60        Severity::Style,
61        RuleCategory::Style,
62        DESCRIPTION,
63        URL,
64        check,
65        fix,
66    )
67}
68
69fn get_key_order(key: &str) -> usize {
70    KEY_ORDER
71        .iter()
72        .position(|&k| k == key)
73        .unwrap_or(KEY_ORDER.len())
74}
75
76fn check(ctx: &LintContext) -> Vec<CheckFailure> {
77    let mut failures = Vec::new();
78
79    for (service_name, service) in &ctx.compose.services {
80        if service.keys.len() > 1 {
81            // Check if keys are in the expected order
82            let mut sorted_keys = service.keys.clone();
83            sorted_keys.sort_by_key(|k| get_key_order(k));
84
85            if service.keys != sorted_keys {
86                let line = service.position.line;
87
88                // Find the first out-of-order key
89                let mut first_wrong = None;
90                for (i, key) in service.keys.iter().enumerate() {
91                    if i < sorted_keys.len() && key != &sorted_keys[i] {
92                        first_wrong = Some(key.clone());
93                        break;
94                    }
95                }
96
97                let message = format!(
98                    "Service \"{}\" has keys in non-standard order. Consider reordering for consistency.",
99                    service_name
100                );
101
102                failures.push(
103                    make_failure(
104                        &CODE.into(),
105                        NAME,
106                        Severity::Style,
107                        RuleCategory::Style,
108                        message,
109                        line,
110                        1,
111                        true,
112                    )
113                    .with_data("serviceName", service_name.clone())
114                    .with_data("firstWrongKey", first_wrong.unwrap_or_default()),
115                );
116            }
117        }
118    }
119
120    failures
121}
122
123fn fix(_source: &str) -> Option<String> {
124    // Full YAML key reordering requires proper YAML AST manipulation
125    // This is a placeholder - a full implementation would need yaml-rust2's Document API
126    None
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use crate::analyzer::dclint::parser::parse_compose;
133
134    fn check_yaml(yaml: &str) -> Vec<CheckFailure> {
135        let compose = parse_compose(yaml).unwrap();
136        let ctx = LintContext::new(&compose, yaml, "docker-compose.yml");
137        check(&ctx)
138    }
139
140    #[test]
141    fn test_no_violation_correct_order() {
142        let yaml = r#"
143services:
144  web:
145    image: nginx
146    container_name: web
147    ports:
148      - "80:80"
149    environment:
150      - DEBUG=true
151"#;
152        assert!(check_yaml(yaml).is_empty());
153    }
154
155    #[test]
156    fn test_violation_wrong_order() {
157        let yaml = r#"
158services:
159  web:
160    environment:
161      - DEBUG=true
162    image: nginx
163    ports:
164      - "80:80"
165"#;
166        let failures = check_yaml(yaml);
167        assert_eq!(failures.len(), 1);
168        assert!(failures[0].message.contains("non-standard order"));
169    }
170
171    #[test]
172    fn test_no_violation_single_key() {
173        let yaml = r#"
174services:
175  web:
176    image: nginx
177"#;
178        assert!(check_yaml(yaml).is_empty());
179    }
180}