syncable_cli/analyzer/dclint/rules/
dcl004.rs1use crate::analyzer::dclint::rules::{FixableRule, LintContext, Rule, make_failure};
6use crate::analyzer::dclint::types::{CheckFailure, RuleCategory, Severity};
7
8const CODE: &str = "DCL004";
9const NAME: &str = "no-quotes-in-volumes";
10const DESCRIPTION: &str = "Volume paths should not contain quotes.";
11const URL: &str = "https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-quotes-in-volumes-rule.md";
12
13pub fn rule() -> impl Rule {
14 FixableRule::new(
15 CODE,
16 NAME,
17 Severity::Warning,
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 for volume in &service.volumes {
31 if volume.raw.contains('"') || volume.raw.contains('\'') {
33 let message = format!(
34 "Volume \"{}\" in service \"{}\" contains quotes that may be interpreted literally.",
35 volume.raw, service_name
36 );
37
38 failures.push(
39 make_failure(
40 &CODE.into(),
41 NAME,
42 Severity::Warning,
43 RuleCategory::Style,
44 message,
45 volume.position.line,
46 volume.position.column,
47 true,
48 )
49 .with_data("serviceName", service_name.clone())
50 .with_data("volume", volume.raw.clone()),
51 );
52 }
53 }
54 }
55
56 failures
57}
58
59fn fix(source: &str) -> Option<String> {
60 let mut modified = false;
61 let mut result = String::new();
62
63 for line in source.lines() {
64 let trimmed = line.trim();
65
66 if trimmed.starts_with('-') {
68 let after_dash = trimmed.trim_start_matches('-').trim();
69
70 if (after_dash.starts_with('"') && after_dash.ends_with('"'))
72 || (after_dash.starts_with('\'') && after_dash.ends_with('\''))
73 {
74 let unquoted = &after_dash[1..after_dash.len() - 1];
76 if unquoted.contains(':') || unquoted.starts_with('/') || unquoted.starts_with('.')
77 {
78 let indent = line.len() - line.trim_start().len();
80 result.push_str(&" ".repeat(indent));
81 result.push_str("- ");
82 result.push_str(unquoted);
83 result.push('\n');
84 modified = true;
85 continue;
86 }
87 }
88 }
89
90 result.push_str(line);
91 result.push('\n');
92 }
93
94 if modified {
95 if !source.ends_with('\n') {
97 result.pop();
98 }
99 Some(result)
100 } else {
101 None
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::analyzer::dclint::parser::parse_compose;
109
110 fn check_yaml(yaml: &str) -> Vec<CheckFailure> {
111 let compose = parse_compose(yaml).unwrap();
112 let ctx = LintContext::new(&compose, yaml, "docker-compose.yml");
113 check(&ctx)
114 }
115
116 #[test]
117 fn test_no_violation_unquoted() {
118 let yaml = r#"
119services:
120 web:
121 image: nginx
122 volumes:
123 - ./data:/data
124 - /host/path:/container/path
125"#;
126 assert!(check_yaml(yaml).is_empty());
127 }
128
129 #[test]
130 fn test_violation_quoted_volume() {
131 let yaml = r#"
132services:
133 web:
134 image: nginx
135 volumes:
136 - "./data:/data"
137"#;
138 let failures = check_yaml(yaml);
142 assert!(failures.is_empty());
144 }
145}