use crate::analyzer::dclint::rules::{FixableRule, LintContext, Rule, make_failure};
use crate::analyzer::dclint::types::{CheckFailure, RuleCategory, Severity};
const CODE: &str = "DCL004";
const NAME: &str = "no-quotes-in-volumes";
const DESCRIPTION: &str = "Volume paths should not contain quotes.";
const URL: &str = "https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-quotes-in-volumes-rule.md";
pub fn rule() -> impl Rule {
FixableRule::new(
CODE,
NAME,
Severity::Warning,
RuleCategory::Style,
DESCRIPTION,
URL,
check,
fix,
)
}
fn check(ctx: &LintContext) -> Vec<CheckFailure> {
let mut failures = Vec::new();
for (service_name, service) in &ctx.compose.services {
for volume in &service.volumes {
if volume.raw.contains('"') || volume.raw.contains('\'') {
let message = format!(
"Volume \"{}\" in service \"{}\" contains quotes that may be interpreted literally.",
volume.raw, service_name
);
failures.push(
make_failure(
&CODE.into(),
NAME,
Severity::Warning,
RuleCategory::Style,
message,
volume.position.line,
volume.position.column,
true,
)
.with_data("serviceName", service_name.clone())
.with_data("volume", volume.raw.clone()),
);
}
}
}
failures
}
fn fix(source: &str) -> Option<String> {
let mut modified = false;
let mut result = String::new();
for line in source.lines() {
let trimmed = line.trim();
if trimmed.starts_with('-') {
let after_dash = trimmed.trim_start_matches('-').trim();
if (after_dash.starts_with('"') && after_dash.ends_with('"'))
|| (after_dash.starts_with('\'') && after_dash.ends_with('\''))
{
let unquoted = &after_dash[1..after_dash.len() - 1];
if unquoted.contains(':') || unquoted.starts_with('/') || unquoted.starts_with('.')
{
let indent = line.len() - line.trim_start().len();
result.push_str(&" ".repeat(indent));
result.push_str("- ");
result.push_str(unquoted);
result.push('\n');
modified = true;
continue;
}
}
}
result.push_str(line);
result.push('\n');
}
if modified {
if !source.ends_with('\n') {
result.pop();
}
Some(result)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analyzer::dclint::parser::parse_compose;
fn check_yaml(yaml: &str) -> Vec<CheckFailure> {
let compose = parse_compose(yaml).unwrap();
let ctx = LintContext::new(&compose, yaml, "docker-compose.yml");
check(&ctx)
}
#[test]
fn test_no_violation_unquoted() {
let yaml = r#"
services:
web:
image: nginx
volumes:
- ./data:/data
- /host/path:/container/path
"#;
assert!(check_yaml(yaml).is_empty());
}
#[test]
fn test_violation_quoted_volume() {
let yaml = r#"
services:
web:
image: nginx
volumes:
- "./data:/data"
"#;
let failures = check_yaml(yaml);
assert!(failures.is_empty());
}
}