syncable_cli/analyzer/dclint/rules/
dcl015.rs

1//! DCL015: top-level-properties-order
2//!
3//! Top-level properties should be in a standard order.
4
5use crate::analyzer::dclint::rules::{FixableRule, LintContext, Rule, make_failure};
6use crate::analyzer::dclint::types::{CheckFailure, RuleCategory, Severity};
7
8const CODE: &str = "DCL015";
9const NAME: &str = "top-level-properties-order";
10const DESCRIPTION: &str = "Top-level properties should follow a standard ordering convention.";
11const URL: &str = "https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/top-level-properties-order-rule.md";
12
13// Standard top-level key order
14const KEY_ORDER: &[&str] = &[
15    "version", // Deprecated but may exist
16    "name", "services", "networks", "volumes", "configs", "secrets",
17];
18
19pub fn rule() -> impl Rule {
20    FixableRule::new(
21        CODE,
22        NAME,
23        Severity::Style,
24        RuleCategory::Style,
25        DESCRIPTION,
26        URL,
27        check,
28        fix,
29    )
30}
31
32fn get_key_order(key: &str) -> usize {
33    KEY_ORDER
34        .iter()
35        .position(|&k| k == key)
36        .unwrap_or(KEY_ORDER.len())
37}
38
39fn check(ctx: &LintContext) -> Vec<CheckFailure> {
40    let mut failures = Vec::new();
41
42    if ctx.compose.top_level_keys.len() > 1 {
43        let mut sorted_keys = ctx.compose.top_level_keys.clone();
44        sorted_keys.sort_by_key(|k| get_key_order(k));
45
46        if ctx.compose.top_level_keys != sorted_keys {
47            let message = format!(
48                "Top-level properties are not in standard order. Expected: [{}], got: [{}].",
49                sorted_keys.join(", "),
50                ctx.compose.top_level_keys.join(", ")
51            );
52
53            failures.push(
54                make_failure(
55                    &CODE.into(),
56                    NAME,
57                    Severity::Style,
58                    RuleCategory::Style,
59                    message,
60                    1,
61                    1,
62                    true,
63                )
64                .with_data("expected", sorted_keys.join(", "))
65                .with_data("actual", ctx.compose.top_level_keys.join(", ")),
66            );
67        }
68    }
69
70    failures
71}
72
73fn fix(_source: &str) -> Option<String> {
74    // Full reordering requires proper YAML AST manipulation
75    None
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use crate::analyzer::dclint::parser::parse_compose;
82
83    fn check_yaml(yaml: &str) -> Vec<CheckFailure> {
84        let compose = parse_compose(yaml).unwrap();
85        let ctx = LintContext::new(&compose, yaml, "docker-compose.yml");
86        check(&ctx)
87    }
88
89    #[test]
90    fn test_no_violation_correct_order() {
91        let yaml = r#"
92name: myproject
93services:
94  web:
95    image: nginx
96networks:
97  default:
98volumes:
99  data:
100"#;
101        assert!(check_yaml(yaml).is_empty());
102    }
103
104    #[test]
105    fn test_violation_wrong_order() {
106        let yaml = r#"
107services:
108  web:
109    image: nginx
110name: myproject
111"#;
112        let failures = check_yaml(yaml);
113        assert_eq!(failures.len(), 1);
114        assert!(failures[0].message.contains("standard order"));
115    }
116
117    #[test]
118    fn test_no_violation_single_key() {
119        let yaml = r#"
120services:
121  web:
122    image: nginx
123"#;
124        assert!(check_yaml(yaml).is_empty());
125    }
126
127    #[test]
128    fn test_violation_volumes_before_services() {
129        let yaml = r#"
130volumes:
131  data:
132services:
133  web:
134    image: nginx
135"#;
136        let failures = check_yaml(yaml);
137        assert_eq!(failures.len(), 1);
138    }
139}