syncable_cli/analyzer/dclint/rules/
dcl006.rs1use crate::analyzer::dclint::rules::{FixableRule, LintContext, Rule, make_failure};
6use crate::analyzer::dclint::types::{CheckFailure, RuleCategory, Severity};
7
8const CODE: &str = "DCL006";
9const NAME: &str = "no-version-field";
10const DESCRIPTION: &str = "The `version` field is deprecated in Docker Compose.";
11const URL: &str = "https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-version-field-rule.md";
12
13pub fn rule() -> impl Rule {
14 FixableRule::new(
15 CODE,
16 NAME,
17 Severity::Info,
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 if ctx.compose.version.is_some() {
30 let line = ctx.compose.version_pos.map(|p| p.line).unwrap_or(1);
31
32 let message = "The `version` field is obsolete and should be removed. Docker Compose now infers the version from the file structure.".to_string();
33
34 failures.push(
35 make_failure(
36 &CODE.into(),
37 NAME,
38 Severity::Info,
39 RuleCategory::Style,
40 message,
41 line,
42 1,
43 true,
44 )
45 .with_data("version", ctx.compose.version.clone().unwrap_or_default()),
46 );
47 }
48
49 failures
50}
51
52fn fix(source: &str) -> Option<String> {
53 let mut result = Vec::new();
54 let mut modified = false;
55 let mut skip_next_empty = false;
56
57 for line in source.lines() {
58 let trimmed = line.trim();
59
60 if trimmed.starts_with("version:") {
62 modified = true;
63 skip_next_empty = true;
64 continue;
65 }
66
67 if skip_next_empty && trimmed.is_empty() {
69 skip_next_empty = false;
70 continue;
71 }
72 skip_next_empty = false;
73
74 result.push(line);
75 }
76
77 if modified {
78 let mut output = result.join("\n");
79 if source.ends_with('\n') {
80 output.push('\n');
81 }
82 Some(output)
83 } else {
84 None
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::analyzer::dclint::parser::parse_compose;
92
93 fn check_yaml(yaml: &str) -> Vec<CheckFailure> {
94 let compose = parse_compose(yaml).unwrap();
95 let ctx = LintContext::new(&compose, yaml, "docker-compose.yml");
96 check(&ctx)
97 }
98
99 #[test]
100 fn test_no_violation_no_version() {
101 let yaml = r#"
102services:
103 web:
104 image: nginx
105"#;
106 assert!(check_yaml(yaml).is_empty());
107 }
108
109 #[test]
110 fn test_violation_has_version() {
111 let yaml = r#"
112version: "3.8"
113services:
114 web:
115 image: nginx
116"#;
117 let failures = check_yaml(yaml);
118 assert_eq!(failures.len(), 1);
119 assert!(failures[0].message.contains("obsolete"));
120 }
121
122 #[test]
123 fn test_fix_removes_version() {
124 let yaml = r#"version: "3.8"
125
126services:
127 web:
128 image: nginx
129"#;
130 let fixed = fix(yaml).unwrap();
131 assert!(!fixed.contains("version"));
132 assert!(fixed.contains("services"));
133 }
134
135 #[test]
136 fn test_fix_no_change_when_no_version() {
137 let yaml = r#"services:
138 web:
139 image: nginx
140"#;
141 assert!(fix(yaml).is_none());
142 }
143}