syncable_cli/analyzer/dclint/rules/
dcl014.rs1use crate::analyzer::dclint::rules::{FixableRule, LintContext, Rule, make_failure};
6use crate::analyzer::dclint::types::{CheckFailure, RuleCategory, Severity};
7
8const CODE: &str = "DCL014";
9const NAME: &str = "services-alphabetical-order";
10const DESCRIPTION: &str = "Services should be defined in alphabetical order.";
11const URL: &str = "https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/services-alphabetical-order-rule.md";
12
13pub fn rule() -> impl Rule {
14 FixableRule::new(
15 CODE,
16 NAME,
17 Severity::Style,
18 RuleCategory::Style,
19 DESCRIPTION,
20 URL,
21 check,
22 fix,
23 )
24}
25
26fn check(ctx: &LintContext) -> Vec<CheckFailure> {
27 let mut failures = Vec::new();
28
29 let service_names: Vec<String> = ctx.compose.services.keys().cloned().collect();
30
31 if service_names.len() > 1 {
32 let mut sorted_names = service_names.clone();
33 sorted_names.sort();
34
35 let current_order: Vec<String> = {
37 let mut names: Vec<_> = ctx.compose.services.keys().cloned().collect();
40 names.sort_by_key(|name| {
41 ctx.compose
42 .services
43 .get(name)
44 .map(|s| s.position.line)
45 .unwrap_or(u32::MAX)
46 });
47 names
48 };
49
50 if current_order != sorted_names {
51 let line = ctx.compose.services_pos.map(|p| p.line).unwrap_or(1);
52
53 let message = format!(
54 "Services are not in alphabetical order. Expected: [{}], got: [{}].",
55 sorted_names.join(", "),
56 current_order.join(", ")
57 );
58
59 failures.push(
60 make_failure(
61 &CODE.into(),
62 NAME,
63 Severity::Style,
64 RuleCategory::Style,
65 message,
66 line,
67 1,
68 true,
69 )
70 .with_data("expected", sorted_names.join(", "))
71 .with_data("actual", current_order.join(", ")),
72 );
73 }
74 }
75
76 failures
77}
78
79fn fix(_source: &str) -> Option<String> {
80 None
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use crate::analyzer::dclint::parser::parse_compose;
89
90 fn check_yaml(yaml: &str) -> Vec<CheckFailure> {
91 let compose = parse_compose(yaml).unwrap();
92 let ctx = LintContext::new(&compose, yaml, "docker-compose.yml");
93 check(&ctx)
94 }
95
96 #[test]
97 fn test_no_violation_sorted() {
98 let yaml = r#"
99services:
100 api:
101 image: api
102 db:
103 image: postgres
104 web:
105 image: nginx
106"#;
107 assert!(check_yaml(yaml).is_empty());
108 }
109
110 #[test]
111 fn test_violation_unsorted() {
112 let yaml = r#"
113services:
114 web:
115 image: nginx
116 api:
117 image: api
118 db:
119 image: postgres
120"#;
121 let failures = check_yaml(yaml);
122 assert_eq!(failures.len(), 1);
123 assert!(failures[0].message.contains("alphabetical"));
124 }
125
126 #[test]
127 fn test_no_violation_single_service() {
128 let yaml = r#"
129services:
130 web:
131 image: nginx
132"#;
133 assert!(check_yaml(yaml).is_empty());
134 }
135}