syncable_cli/analyzer/dclint/rules/
dcl006.rs

1//! DCL006: no-version-field
2//!
3//! The `version` field is deprecated and should be removed.
4
5use 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        // Skip version line
61        if trimmed.starts_with("version:") {
62            modified = true;
63            skip_next_empty = true;
64            continue;
65        }
66
67        // Skip empty line after version
68        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}