syncable_cli/analyzer/dclint/rules/
dcl010.rs1use crate::analyzer::dclint::rules::{FixableRule, LintContext, Rule, make_failure};
6use crate::analyzer::dclint::types::{CheckFailure, RuleCategory, Severity};
7
8const CODE: &str = "DCL010";
9const NAME: &str = "service-dependencies-alphabetical-order";
10const DESCRIPTION: &str = "Service dependencies should be sorted alphabetically.";
11const URL: &str = "https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/service-dependencies-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 for (service_name, service) in &ctx.compose.services {
30 if service.depends_on.len() > 1 {
31 let mut sorted = service.depends_on.clone();
32 sorted.sort();
33
34 if service.depends_on != sorted {
35 let line = service
36 .depends_on_pos
37 .map(|p| p.line)
38 .unwrap_or(service.position.line);
39
40 let message = format!(
41 "Dependencies in service \"{}\" are not in alphabetical order. Expected: [{}], got: [{}].",
42 service_name,
43 sorted.join(", "),
44 service.depends_on.join(", ")
45 );
46
47 failures.push(
48 make_failure(
49 &CODE.into(),
50 NAME,
51 Severity::Style,
52 RuleCategory::Style,
53 message,
54 line,
55 1,
56 true,
57 )
58 .with_data("serviceName", service_name.clone())
59 .with_data("expected", sorted.join(", "))
60 .with_data("actual", service.depends_on.join(", ")),
61 );
62 }
63 }
64 }
65
66 failures
67}
68
69fn fix(source: &str) -> Option<String> {
70 let mut result = String::new();
73 let mut modified = false;
74 let mut in_depends_on = false;
75 let mut depends_on_indent = 0;
76 let mut deps: Vec<String> = Vec::new();
77 let mut _deps_start_line = 0;
78 let mut collected_lines: Vec<String> = Vec::new();
79
80 for (idx, line) in source.lines().enumerate() {
81 let trimmed = line.trim();
82 let indent = line.len() - line.trim_start().len();
83
84 if trimmed.starts_with("depends_on:") {
86 in_depends_on = true;
87 depends_on_indent = indent;
88 _deps_start_line = idx;
89 deps.clear();
90 result.push_str(line);
91 result.push('\n');
92 continue;
93 }
94
95 if in_depends_on && trimmed.starts_with('-') && indent > depends_on_indent {
97 let dep = trimmed.trim_start_matches('-').trim().to_string();
98 deps.push(dep);
99 collected_lines.push(line.to_string());
100 continue;
101 }
102
103 if in_depends_on && (!trimmed.starts_with('-') || indent <= depends_on_indent) {
105 let mut sorted_deps = deps.clone();
107 sorted_deps.sort();
108
109 if deps != sorted_deps {
110 modified = true;
111 for dep in &sorted_deps {
112 result.push_str(&" ".repeat(depends_on_indent + 2));
113 result.push_str("- ");
114 result.push_str(dep);
115 result.push('\n');
116 }
117 } else {
118 for dep_line in &collected_lines {
119 result.push_str(dep_line);
120 result.push('\n');
121 }
122 }
123
124 deps.clear();
125 collected_lines.clear();
126 in_depends_on = false;
127 }
128
129 result.push_str(line);
130 result.push('\n');
131 }
132
133 if in_depends_on && !deps.is_empty() {
135 let mut sorted_deps = deps.clone();
136 sorted_deps.sort();
137
138 if deps != sorted_deps {
139 modified = true;
140 for dep in &sorted_deps {
141 result.push_str(&" ".repeat(depends_on_indent + 2));
142 result.push_str("- ");
143 result.push_str(dep);
144 result.push('\n');
145 }
146 } else {
147 for dep_line in &collected_lines {
148 result.push_str(dep_line);
149 result.push('\n');
150 }
151 }
152 }
153
154 if modified {
155 if !source.ends_with('\n') {
156 result.pop();
157 }
158 Some(result)
159 } else {
160 None
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use crate::analyzer::dclint::parser::parse_compose;
168
169 fn check_yaml(yaml: &str) -> Vec<CheckFailure> {
170 let compose = parse_compose(yaml).unwrap();
171 let ctx = LintContext::new(&compose, yaml, "docker-compose.yml");
172 check(&ctx)
173 }
174
175 #[test]
176 fn test_no_violation_sorted() {
177 let yaml = r#"
178services:
179 web:
180 image: nginx
181 depends_on:
182 - cache
183 - db
184"#;
185 assert!(check_yaml(yaml).is_empty());
186 }
187
188 #[test]
189 fn test_no_violation_single_dep() {
190 let yaml = r#"
191services:
192 web:
193 image: nginx
194 depends_on:
195 - db
196"#;
197 assert!(check_yaml(yaml).is_empty());
198 }
199
200 #[test]
201 fn test_violation_unsorted() {
202 let yaml = r#"
203services:
204 web:
205 image: nginx
206 depends_on:
207 - db
208 - cache
209"#;
210 let failures = check_yaml(yaml);
211 assert_eq!(failures.len(), 1);
212 assert!(failures[0].message.contains("alphabetical"));
213 }
214
215 #[test]
216 fn test_violation_multiple_unsorted() {
217 let yaml = r#"
218services:
219 web:
220 image: nginx
221 depends_on:
222 - redis
223 - db
224 - cache
225"#;
226 let failures = check_yaml(yaml);
227 assert_eq!(failures.len(), 1);
228 }
229}