syncable_cli/analyzer/dclint/rules/
dcl012.rs1use 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
13const 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 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 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 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}