use crate::linter::{Diagnostic, Fix, LintResult, Severity, Span};
fn is_recipe_with_mkdir(line: &str) -> bool {
line.starts_with('\t') && line.contains("mkdir")
}
fn has_p_flag(line: &str, mkdir_pos: usize) -> bool {
let after_mkdir = &line[mkdir_pos + 5..];
after_mkdir.trim_start().starts_with("-p")
}
fn create_mkdir_diagnostic(line_num: usize, mkdir_pos: usize) -> Diagnostic {
let span = Span::new(
line_num + 1,
mkdir_pos + 1,
line_num + 1,
mkdir_pos + 6, );
Diagnostic::new(
"MAKE002",
Severity::Warning,
"Non-idempotent mkdir - will fail if directory exists",
span,
)
.with_fix(Fix::new("mkdir -p"))
}
pub fn check(source: &str) -> LintResult {
let mut result = LintResult::new();
for (line_num, line) in source.lines().enumerate() {
if !is_recipe_with_mkdir(line) {
continue;
}
if let Some(mkdir_pos) = line.find("mkdir") {
if has_p_flag(line, mkdir_pos) {
continue;
}
let diag = create_mkdir_diagnostic(line_num, mkdir_pos);
result.add(diag);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_MAKE002_detects_mkdir_without_p() {
let makefile = "build:\n\tmkdir build";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 1);
let diag = &result.diagnostics[0];
assert_eq!(diag.code, "MAKE002");
assert_eq!(diag.severity, Severity::Warning);
assert!(diag.message.contains("Non-idempotent"));
}
#[test]
fn test_MAKE002_no_warning_with_p_flag() {
let makefile = "build:\n\tmkdir -p build";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_MAKE002_no_warning_outside_recipe() {
let makefile = "# mkdir without -p in comment\nDIR = mkdir";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_MAKE002_provides_fix() {
let makefile = "build:\n\tmkdir build";
let result = check(makefile);
assert!(result.diagnostics[0].fix.is_some());
let fix = result.diagnostics[0].fix.as_ref().unwrap();
assert_eq!(fix.replacement, "mkdir -p");
}
#[test]
fn test_MAKE002_detects_multiple_mkdir() {
let makefile = "build:\n\tmkdir build\n\tmkdir dist";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 2);
}
#[test]
fn test_MAKE002_with_path_argument() {
let makefile = "install:\n\tmkdir /usr/local/bin";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_MAKE002_mkdir_with_other_flags() {
let makefile = "build:\n\tmkdir -m 755 build";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_MAKE002_multiple_recipes() {
let makefile = r#"build:
mkdir build
gcc main.c
install:
mkdir -p /usr/local/bin
cp app /usr/local/bin"#;
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 1);
}
}