use crate::linter::{Diagnostic, Fix, LintResult, Severity, Span};
pub fn check(source: &str) -> LintResult {
let mut result = LintResult::new();
for (line_num, line) in source.lines().enumerate() {
if line.contains("$(wildcard") && !line.contains("$(sort") {
if let Some(col) = line.find("$(wildcard") {
let (span, fix_replacement) = if let Some(end) = find_matching_paren(line, col + 2)
{
let wildcard_expr = &line[col..=end];
let span = Span::new(
line_num + 1,
col + 1,
line_num + 1,
col + wildcard_expr.len() + 1,
);
let replacement = format!("$(sort {})", wildcard_expr);
(span, replacement)
} else {
let span = Span::new(
line_num + 1,
col + 1,
line_num + 1,
col + 11, );
(span, "$(sort $(wildcard ...))".to_string())
};
let diag = Diagnostic::new(
"MAKE001",
Severity::Warning,
"Non-deterministic $(wildcard) - results may vary between runs",
span,
)
.with_fix(Fix::new(&fix_replacement));
result.add(diag);
}
}
}
result
}
#[allow(clippy::needless_range_loop)]
fn find_matching_paren(line: &str, start: usize) -> Option<usize> {
let chars: Vec<char> = line.chars().collect();
let mut depth = 1;
for i in start..chars.len() {
match chars[i] {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_MAKE001_detects_wildcard_basic() {
let makefile = "SOURCES = $(wildcard *.c)";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 1);
let diag = &result.diagnostics[0];
assert_eq!(diag.code, "MAKE001");
assert_eq!(diag.severity, Severity::Warning);
assert!(diag.message.contains("Non-deterministic"));
}
#[test]
fn test_MAKE001_detects_wildcard_with_path() {
let makefile = "HEADERS = $(wildcard include/*.h)";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].code, "MAKE001");
}
#[test]
fn test_MAKE001_no_warning_with_sort() {
let makefile = "SOURCES = $(sort $(wildcard *.c))";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_MAKE001_no_warning_without_wildcard() {
let makefile = "SOURCES = main.c utils.c";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_MAKE001_provides_fix() {
let makefile = "SOURCES = $(wildcard *.c)";
let result = check(makefile);
assert!(result.diagnostics[0].fix.is_some());
let fix = result.diagnostics[0].fix.as_ref().unwrap();
assert!(fix.replacement.contains("$(sort"));
assert!(fix.replacement.contains("$(wildcard *.c)"));
}
#[test]
fn test_MAKE001_detects_multiple_wildcards() {
let makefile = "SOURCES = $(wildcard *.c)\nHEADERS = $(wildcard *.h)";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 2);
}
#[test]
fn test_MAKE001_no_false_positive_in_comment() {
let makefile = "# SOURCES = $(wildcard *.c)\nSOURCES = main.c";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_MAKE001_nested_parens() {
let makefile = "SOURCES = $(wildcard src/**/*.c)";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 1);
let fix = result.diagnostics[0].fix.as_ref().unwrap();
assert!(fix.replacement.starts_with("$(sort"));
}
}