use crate::linter::{Diagnostic, Fix, LintResult, Severity, Span};
const RECURSIVE_MAKE_PATTERNS: &[&str] = &["$(MAKE)", "${MAKE}", "make -C", "make --directory"];
pub fn check(source: &str) -> LintResult {
let mut result = LintResult::new();
for (line_num, line) in source.lines().enumerate() {
if !line.starts_with('\t') {
continue;
}
for pattern in RECURSIVE_MAKE_PATTERNS {
if line.contains(pattern) {
let span = Span::new(line_num + 1, 1, line_num + 1, line.len() + 1);
let fix_replacement = create_fix(line);
let diag = Diagnostic::new(
"MAKE012",
Severity::Warning,
"Recursive make invocation - consider using 'include' directives (see 'Recursive Make Considered Harmful')",
span,
)
.with_fix(Fix::new(&fix_replacement));
result.add(diag);
break; }
}
}
result
}
fn create_fix(line: &str) -> String {
let subdir = extract_subdir(line);
if let Some(dir) = subdir {
format!("# Consider: include {}/module.mk", dir)
} else {
"# Consider: include subdirs.mk or use non-recursive make".to_string()
}
}
fn extract_subdir(line: &str) -> Option<String> {
if let Some(pos) = line.find("-C ") {
let after_c = &line[pos + 3..];
let dir = after_c.split_whitespace().next()?;
return Some(dir.to_string());
}
if let Some(pos) = line.find("--directory=") {
let after_dir = &line[pos + 12..];
let dir = after_dir.split_whitespace().next()?;
return Some(dir.to_string());
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_MAKE012_detects_dollar_make() {
let makefile = "subdirs:\n\t$(MAKE) -C subdir";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 1);
let diag = &result.diagnostics[0];
assert_eq!(diag.code, "MAKE012");
assert_eq!(diag.severity, Severity::Warning);
assert!(diag.message.to_lowercase().contains("recursive"));
}
#[test]
fn test_MAKE012_detects_curly_brace_make() {
let makefile = "subdirs:\n\t${MAKE} -C subdir";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_MAKE012_detects_make_dash_c() {
let makefile = "subdirs:\n\tmake -C subdir";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_MAKE012_provides_fix() {
let makefile = "subdirs:\n\t$(MAKE) -C subdir";
let result = check(makefile);
assert!(result.diagnostics[0].fix.is_some());
let fix = result.diagnostics[0].fix.as_ref().unwrap();
assert!(fix.replacement.contains("include"));
}
#[test]
fn test_MAKE012_detects_multiple_recursive_makes() {
let makefile = "subdirs:\n\t$(MAKE) -C sub1\n\t$(MAKE) -C sub2";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 2);
}
#[test]
fn test_MAKE012_no_warning_for_regular_commands() {
let makefile = "build:\n\tgcc main.c -o app";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_MAKE012_detects_make_directory() {
let makefile = "subdirs:\n\tmake --directory=subdir all";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_MAKE012_empty_makefile() {
let makefile = "";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 0);
}
}