syncable_cli/analyzer/dclint/rules/
dcl001.rs1use crate::analyzer::dclint::rules::{LintContext, Rule, SimpleRule, make_failure};
6use crate::analyzer::dclint::types::{CheckFailure, RuleCategory, Severity};
7
8const CODE: &str = "DCL001";
9const NAME: &str = "no-build-and-image";
10const DESCRIPTION: &str = "Each service must use either `build` or `image`, not both.";
11const URL: &str = "https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-build-and-image-rule.md";
12
13pub fn rule() -> impl Rule {
14 SimpleRule::new(
15 CODE,
16 NAME,
17 Severity::Error,
18 RuleCategory::BestPractice,
19 DESCRIPTION,
20 URL,
21 check,
22 )
23}
24
25fn check(ctx: &LintContext) -> Vec<CheckFailure> {
26 let mut failures = Vec::new();
27
28 for (service_name, service) in &ctx.compose.services {
29 let has_build = service.build.is_some();
31 let has_image = service.image.is_some();
32 let has_pull_policy = service.pull_policy.is_some();
33
34 if has_build && has_image && !has_pull_policy {
36 let line = service
37 .build_pos
38 .map(|p| p.line)
39 .or(service.position.line.into())
40 .unwrap_or(1);
41
42 let message = format!(
43 "Service \"{}\" is using both \"build\" and \"image\". Use one of them, but not both.",
44 service_name
45 );
46
47 failures.push(
48 make_failure(
49 &CODE.into(),
50 NAME,
51 Severity::Error,
52 RuleCategory::BestPractice,
53 message,
54 line,
55 1,
56 false,
57 )
58 .with_data("serviceName", service_name.clone()),
59 );
60 }
61 }
62
63 failures
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use crate::analyzer::dclint::parser::parse_compose;
70
71 fn check_yaml(yaml: &str) -> Vec<CheckFailure> {
72 let compose = parse_compose(yaml).unwrap();
73 let ctx = LintContext::new(&compose, yaml, "docker-compose.yml");
74 check(&ctx)
75 }
76
77 #[test]
78 fn test_no_violation_image_only() {
79 let yaml = r#"
80services:
81 web:
82 image: nginx:latest
83"#;
84 assert!(check_yaml(yaml).is_empty());
85 }
86
87 #[test]
88 fn test_no_violation_build_only() {
89 let yaml = r#"
90services:
91 web:
92 build: .
93"#;
94 assert!(check_yaml(yaml).is_empty());
95 }
96
97 #[test]
98 fn test_violation_build_and_image() {
99 let yaml = r#"
100services:
101 web:
102 build: .
103 image: myapp:latest
104"#;
105 let failures = check_yaml(yaml);
106 assert_eq!(failures.len(), 1);
107 assert!(failures[0].message.contains("web"));
108 assert!(failures[0].message.contains("build"));
109 assert!(failures[0].message.contains("image"));
110 }
111
112 #[test]
113 fn test_no_violation_with_pull_policy() {
114 let yaml = r#"
115services:
116 web:
117 build: .
118 image: myapp:latest
119 pull_policy: build
120"#;
121 assert!(check_yaml(yaml).is_empty());
122 }
123
124 #[test]
125 fn test_multiple_services() {
126 let yaml = r#"
127services:
128 web:
129 build: .
130 image: myapp:latest
131 db:
132 image: postgres:15
133 api:
134 build: ./api
135 image: myapi:v1
136"#;
137 let failures = check_yaml(yaml);
138 assert_eq!(failures.len(), 2);
139 }
140}