use crate::ast;
use crate::inspect;
use std::collections::HashSet;
use std::fmt;
use std::sync;
pub static WD_COMMANDS: sync::LazyLock<Vec<&str>> =
sync::LazyLock::new(|| vec!["cd", "pushd", "popd"]);
pub static LOWER_CONVENTIONAL_PHONY_TARGETS_PATTERN: sync::LazyLock<regex::Regex> =
sync::LazyLock::new(|| {
regex::Regex::new("^all|lint|install|uninstall|publish|(test.*)|(clean.*)$").unwrap()
});
pub static COMMAND_PREFIX_PATTERN: sync::LazyLock<regex::Regex> =
sync::LazyLock::new(|| regex::Regex::new(r"^(?P<prefix>[-+@]+)").unwrap());
pub static BLANK_COMMAND_PATTERN: sync::LazyLock<regex::Regex> =
sync::LazyLock::new(|| regex::Regex::new(r"^[-+@]+\s*$").unwrap());
pub static WHITESPACE_LEADING_COMMAND_PATTERN: sync::LazyLock<regex::Regex> =
sync::LazyLock::new(|| regex::Regex::new(r"^[-+@]*\s+").unwrap());
pub static RESERVED_TARGET_PATTERN: sync::LazyLock<regex::Regex> =
sync::LazyLock::new(|| regex::Regex::new(r"^.[A-Z]+").unwrap());
pub static CHECKS: sync::LazyLock<Vec<Check>> = sync::LazyLock::new(|| {
vec![
check_ub_late_posix_marker,
check_ub_ambiguous_include,
check_ub_makeflags_assignment,
check_ub_shell_macro,
check_silent_include,
check_strict_posix,
check_implementation_defined_target,
check_makefile_precedence,
check_curdir_assignment_nop,
check_wd_nop,
check_wait_nop,
check_phony_nop,
check_redundant_notparallel_wait,
check_redundant_silent_at,
check_redundant_ignore_minus,
check_global_ignore,
check_simplify_at,
check_simplify_minus,
check_command_comment,
check_phony_target,
check_repeated_command_prefix,
check_blank_command,
check_whitespace_leading_command,
check_no_rules,
check_reserved_target,
check_rule_all,
check_final_eol,
check_portable_assignment,
]
});
static WARNING_DEFAULT_PATH: &str = "-";
pub type Check = fn(&inspect::Metadata, &[ast::Gem]) -> Vec<Warning>;
#[derive(Debug, PartialEq)]
pub struct Warning {
pub path: String,
pub line: usize,
pub message: String,
}
impl Warning {
pub fn new() -> Warning {
Warning {
path: WARNING_DEFAULT_PATH.to_string(),
line: 0,
message: String::new(),
}
}
}
impl Default for Warning {
fn default() -> Self {
Warning::new()
}
}
impl fmt::Display for Warning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "warning: {}:", self.path)?;
if self.line > 0 {
write!(f, "{}:", self.line)?;
}
write!(f, " {}", self.message)
}
}
pub fn mock_md(pth: &str) -> inspect::Metadata {
inspect::Metadata {
path: pth.to_string(),
filename: pth.to_string(),
is_makefile: true,
build_system: String::new(),
is_machine_generated: false,
is_include_file: false,
is_empty: true,
lines: 0,
has_final_eol: false,
}
}
pub static UB_LATE_POSIX_MARKER: &str = "UB_LATE_POSIX_MARKER: the special rule \".POSIX:\" should be the first uncommented instruction in POSIX makefiles, or else absent from *.include.mk files";
fn check_ub_late_posix_marker(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
gems.iter()
.enumerate()
.filter(|(i, e)| match &e.n {
ast::Ore::Ru { ps: _, ts, cs: _ } => {
(metadata.is_include_file || i > &0) && ts == &vec![".POSIX"]
}
_ => false,
})
.map(|(_, e)| Warning {
path: metadata.path.to_string(),
line: e.l,
message: UB_LATE_POSIX_MARKER.to_string(),
})
.collect()
}
#[test]
fn test_late_posix_marker() {
assert!(
lint(&mock_md("-"), "PKG=curl\n.POSIX:\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&UB_LATE_POSIX_MARKER.to_string())
);
assert!(
!lint(&mock_md("-"), "# strict posix\n.POSIX:\nPKG=curl\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&UB_LATE_POSIX_MARKER.to_string())
);
assert!(
!lint(&mock_md("-"), ".POSIX:\nPKG=curl\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&UB_LATE_POSIX_MARKER.to_string())
);
let mut md_include = mock_md("provision.include.mk");
md_include.is_include_file = true;
assert!(
lint(&md_include, ".POSIX:\nPKG=curl\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&UB_LATE_POSIX_MARKER.to_string())
);
assert!(
!lint(&md_include, "PKG=curl\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&UB_LATE_POSIX_MARKER.to_string())
);
assert!(
lint(&mock_md("-"), ".POSIX:\ninclude =foo.mk\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&UB_AMBIGUOUS_INCLUDE.to_string())
);
}
pub static UB_AMBIGUOUS_INCLUDE: &str =
"UB_AMBIGUOUS_INCLUDE: unclear whether include line or macro definition";
fn check_ub_ambiguous_include(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
gems.iter()
.filter(|e| match &e.n {
ast::Ore::In { s: _, ps } => ps.iter().any(|e2| e2.starts_with('=')),
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: UB_AMBIGUOUS_INCLUDE.to_string(),
})
.collect()
}
#[test]
fn test_ub_ambiguous_include() {
assert!(
lint(&mock_md("-"), ".POSIX:\ninclude = foo.mk\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&UB_AMBIGUOUS_INCLUDE.to_string())
);
assert!(
lint(&mock_md("-"), ".POSIX:\ninclude =foo.mk\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&UB_AMBIGUOUS_INCLUDE.to_string())
);
assert!(
!lint(&mock_md("-"), ".POSIX:\ninclude=foo.mk\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&UB_AMBIGUOUS_INCLUDE.to_string())
);
}
pub static UB_MAKEFLAGS_ASSIGNMENT: &str = "UB_MAKEFLAGS_MACRO: do not modify MAKEFLAGS macro";
fn check_ub_makeflags_assignment(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
gems.iter()
.filter(|e| match &e.n {
ast::Ore::Mc { n, o: _, v: _ } => n == "MAKEFLAGS",
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: UB_MAKEFLAGS_ASSIGNMENT.to_string(),
})
.collect()
}
#[test]
fn test_ub_makeflags_assignment() {
assert!(
lint(&mock_md("-"), ".POSIX:\nMAKEFLAGS ?= -j\nMAKEFLAGS = -j\nMAKEFLAGS ::= -j\nMAKEFLAGS :::= -j\nMAKEFLAGS += -j\nMAKEFLAGS != echo \"-j\"\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&UB_MAKEFLAGS_ASSIGNMENT.to_string()));
assert!(
!lint(&mock_md("-"), ".POSIX:\nPKG = curl\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&UB_MAKEFLAGS_ASSIGNMENT.to_string())
);
}
pub static UB_SHELL_MACRO: &str = "UB_SHELL_MACRO: do not use or modify SHELL macro";
fn check_ub_shell_macro(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
gems.iter()
.filter(|e| match &e.n {
ast::Ore::Mc { n, o: _, v: _ } => n == "SHELL",
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: UB_SHELL_MACRO.to_string(),
})
.collect()
}
#[test]
fn test_ub_shell_macro() {
assert!(lint(
&mock_md("-"),
".POSIX:\nSHELL ?= sh\nSHELL = sh\nSHELL ::= sh\nSHELL :::= sh\nSHELL += sh\nSHELL != sh\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&UB_SHELL_MACRO.to_string()));
assert!(
!lint(&mock_md("-"), ".POSIX:\nPKG = curl\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&UB_SHELL_MACRO.to_string())
);
}
pub static SILENT_INCLUDE: &str = "SILENT_INCLUDE: dashed includes may obfuscate systemic errors";
fn check_silent_include(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
gems.iter()
.filter(|e| match &e.n {
ast::Ore::In { s, ps: _ } => *s,
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: SILENT_INCLUDE.to_string(),
})
.collect()
}
#[test]
fn test_silent_include() {
assert!(
lint(&mock_md("-"), "-include = foo.mk\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&SILENT_INCLUDE.to_string())
);
assert!(
!lint(&mock_md("-"), "include=foo.mk\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&SILENT_INCLUDE.to_string())
);
}
pub static MAKEFILE_PRECEDENCE: &str =
"MAKEFILE_PRECEDENCE: lowercase Makefile to makefile for launch speed";
fn check_makefile_precedence(metadata: &inspect::Metadata, _: &[ast::Gem]) -> Vec<Warning> {
if metadata.filename == "Makefile" {
return vec![Warning {
path: metadata.path.to_string(),
line: 0,
message: MAKEFILE_PRECEDENCE.to_string(),
}];
}
Vec::new()
}
#[test]
pub fn test_makefile_precedence() {
assert!(
lint(&mock_md("Makefile"), ".POSIX:\nPKG=curl\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&MAKEFILE_PRECEDENCE.to_string())
);
assert!(
!lint(&mock_md("makefile"), ".POSIX:\nPKG=curl\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&MAKEFILE_PRECEDENCE.to_string())
);
assert!(
!lint(&mock_md("foo.mk"), ".POSIX:\nPKG=curl\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&MAKEFILE_PRECEDENCE.to_string())
);
assert!(
!lint(&mock_md("foo.Makefile"), ".POSIX:\nPKG=curl\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&MAKEFILE_PRECEDENCE.to_string())
);
assert!(
!lint(&mock_md("foo.makefile"), ".POSIX:\nPKG=curl\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&MAKEFILE_PRECEDENCE.to_string())
);
}
pub static CURDIR_ASSIGNMENT_NOP: &str =
"CURDIR_ASSIGNMENT_NOP: CURDIR assignment does not change the make working directory";
fn check_curdir_assignment_nop(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
gems.iter()
.filter(|e| match &e.n {
ast::Ore::Mc { n, o: _, v: _ } => n == "CURDIR",
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: CURDIR_ASSIGNMENT_NOP.to_string(),
})
.collect()
}
#[test]
pub fn test_curdir_assignment_nop() {
assert!(
lint(&mock_md("-"), ".POSIX:\nCURDIR = build\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&CURDIR_ASSIGNMENT_NOP.to_string())
);
assert!(
!lint(&mock_md("-"), ".POSIX:\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&CURDIR_ASSIGNMENT_NOP.to_string())
);
}
pub static WD_NOP: &str =
"WD_NOP: change directory commands may not persist across successive commands or rules";
fn check_wd_nop(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
gems.iter()
.filter(|e| match &e.n {
ast::Ore::Ru { ps: _, ts: _, cs } => cs
.iter()
.any(|e2| WD_COMMANDS.contains(&e2.split_whitespace().next().unwrap_or(""))),
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: WD_NOP.to_string(),
})
.collect()
}
#[test]
pub fn test_wd_nop() {
assert!(
lint(
&mock_md("-"),
".POSIX:\n.PHONY: all\nall:\n\tcd foo\n\n\tpushd bar\n\tpopd\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&WD_NOP.to_string())
);
assert!(
!lint(
&mock_md("-"),
".POSIX:\n.PHONY: all\nall:\n\ttar -C foo czvf foo.tgz .\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&WD_NOP.to_string())
);
}
pub static WAIT_NOP: &str = "WAIT_NOP: .WAIT as a target has no effect";
fn check_wait_nop(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
gems.iter()
.filter(|e| match &e.n {
ast::Ore::Ru { ps: _, ts, cs: _ } => ts.contains(&".WAIT".to_string()),
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: WAIT_NOP.to_string(),
})
.collect()
}
#[test]
pub fn test_wait_nop() {
assert!(
lint(&mock_md("-"), ".POSIX:\n.WAIT:\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&WAIT_NOP.to_string())
);
assert!(
!lint(&mock_md("-"), ".POSIX:\n.PHONY: test test-1 test-2\ntest: test-1 .WAIT test-2\ntest-1:\n\techo \"Hello World!\"\ntest-2:\n\techo \"Hi World!\"\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&WAIT_NOP.to_string()));
}
pub static PHONY_NOP: &str = "PHONY_NOP: empty .PHONY has no effect";
fn check_phony_nop(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
gems.iter()
.filter(|e| match &e.n {
ast::Ore::Ru { ps, ts, cs: _ } => ts.contains(&".PHONY".to_string()) && ps.is_empty(),
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: PHONY_NOP.to_string(),
})
.collect()
}
#[test]
pub fn test_phony_nop() {
assert!(
lint(
&mock_md("-"),
".POSIX:\n.PHONY:\nfoo: foo.c\n\tgcc -o foo foo.c\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&PHONY_NOP.to_string())
);
assert!(
!lint(
&mock_md("-"),
".POSIX:\n.PHONY: test\ntest:\n\techo \"Hello World!\"\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&PHONY_NOP.to_string())
);
}
pub static REDUNDANT_NOTPARALLEL_WAIT: &str =
"REDUNDANT_NOTPARALLEL_WAIT: .NOTPARALLEL with .WAIT is redundant and superfluous";
fn check_redundant_notparallel_wait(
metadata: &inspect::Metadata,
gems: &[ast::Gem],
) -> Vec<Warning> {
let has_notparallel: bool = gems.iter().any(|e| match &e.n {
ast::Ore::Ru { ps: _, ts, cs: _ } => ts.contains(&".NOTPARALLEL".to_string()),
_ => false,
});
if !has_notparallel {
return Vec::new();
}
gems.iter()
.filter(|e| match &e.n {
ast::Ore::Ru { ps, ts: _, cs: _ } => ps.contains(&".WAIT".to_string()),
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: REDUNDANT_NOTPARALLEL_WAIT.to_string(),
})
.collect()
}
#[test]
pub fn test_redundant_nonparallel_wait() {
assert!(
lint(&mock_md("-"), ".POSIX:\n.NOTPARALLEL:\n.PHONY: test test-1 test-2\ntest: test-1 .WAIT test-2\ntest-1:\n\techo \"Hello World!\"\ntest-2:\n\techo \"Hi World!\"\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&REDUNDANT_NOTPARALLEL_WAIT.to_string()));
assert!(
!lint(&mock_md("-"), ".POSIX:\n.PHONY: test test-1 test-2\ntest: test-1 .WAIT test-2\ntest-1:\n\techo \"Hello World!\"\ntest-2:\n\techo \"Hi World!\"\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&REDUNDANT_NOTPARALLEL_WAIT.to_string()));
assert!(
!lint(&mock_md("-"), ".POSIX:\n.NOTPARALLEL:\n.PHONY: test test-1 test-2\ntest: test-1 test-2\ntest-1:\n\techo \"Hello World!\"\ntest-2:\n\techo \"Hi World!\"\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&REDUNDANT_NOTPARALLEL_WAIT.to_string()));
}
pub static REDUNDANT_SILENT_AT: &str =
"REDUNDANT_SILENT_AT: .SILENT with @ is redundant and superfluous";
fn check_redundant_silent_at(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
let mut has_global_silence: bool = false;
let mut marked_silent_targets: HashSet<&String> = HashSet::new();
for gem in gems {
if let ast::Ore::Ru { ps, ts, cs: _ } = &gem.n
&& ts.contains(&".SILENT".to_string())
{
if ps.is_empty() {
has_global_silence = true;
}
for p in ps {
marked_silent_targets.insert(p);
}
}
}
gems.iter()
.filter(|e| match &e.n {
ast::Ore::Ru { ps: _, ts, cs } => {
cs.iter().any(|e2| e2.starts_with('@'))
&& (has_global_silence
|| ts.iter().any(|e2| marked_silent_targets.contains(e2)))
}
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: REDUNDANT_SILENT_AT.to_string(),
})
.collect()
}
#[test]
pub fn test_redundant_silent_at() {
assert!(
lint(
&mock_md("-"),
".POSIX:\n.PHONY: lint\n.SILENT:\nlint:\n\t@unmake .\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&REDUNDANT_SILENT_AT.to_string())
);
assert!(
lint(
&mock_md("-"),
".POSIX:\n.PHONY: lint\n.SILENT: lint\nlint:\n\t@unmake .\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&REDUNDANT_SILENT_AT.to_string())
);
assert!(
!lint(
&mock_md("-"),
".POSIX:\n.PHONY: lint\n.SILENT: lint\nlint:\n\tunmake .\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&REDUNDANT_SILENT_AT.to_string())
);
assert!(
!lint(&mock_md("-"), ".POSIX:\n.PHONY: lint\nlint:\n\t@unmake .\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&REDUNDANT_SILENT_AT.to_string())
);
}
pub static REDUNDANT_IGNORE_MINUS: &str =
"REDUNDANT_IGNORE_MINUS: .IGNORE with - is redundant and superfluous";
fn check_redundant_ignore_minus(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
let mut marked_ignored_targets: HashSet<&String> = HashSet::new();
for gem in gems {
if let ast::Ore::Ru { ps, ts, cs: _ } = &gem.n
&& ts.contains(&".IGNORE".to_string())
{
for p in ps {
marked_ignored_targets.insert(p);
}
}
}
gems.iter()
.filter(|e| match &e.n {
ast::Ore::Ru { ps: _, ts, cs } => {
cs.iter().any(|e2| e2.starts_with('-'))
&& ts.iter().any(|e2| marked_ignored_targets.contains(e2))
}
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: REDUNDANT_IGNORE_MINUS.to_string(),
})
.collect()
}
#[test]
pub fn test_redundant_ignore_minus() {
assert!(
lint(
&mock_md("-"),
".POSIX:\n.PHONY: clean\n.IGNORE: clean\nclean:\n\t-rm -rf bin\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&REDUNDANT_IGNORE_MINUS.to_string())
);
assert!(
!lint(
&mock_md("-"),
".POSIX:\n.PHONY: clean\nclean:\n\t-rm -rf bin\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&REDUNDANT_IGNORE_MINUS.to_string())
);
}
pub static GLOBAL_IGNORE: &str =
"GLOBAL_IGNORE: .IGNORE without prerequisites may corrupt artifacts";
fn check_global_ignore(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
gems.iter()
.filter(|e| match &e.n {
ast::Ore::Ru { ps, ts, cs: _ } => ts.contains(&".IGNORE".to_string()) && ps.is_empty(),
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: GLOBAL_IGNORE.to_string(),
})
.collect()
}
#[test]
pub fn test_global_ignore() {
assert!(
lint(&mock_md("-"), ".POSIX:\n.IGNORE:\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&GLOBAL_IGNORE.to_string())
);
assert!(
!lint(
&mock_md("-"),
".POSIX:\n.PHONY: clean\n.IGNORE: clean\nclean:\n\trm -rf bin"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&GLOBAL_IGNORE.to_string())
);
assert!(
!lint(
&mock_md("-"),
".POSIX:\n.PHONY: clean\nclean:\n\t-rm -rf bin"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&GLOBAL_IGNORE.to_string())
);
}
pub static SIMPLIFY_AT: &str =
"SIMPLIFY_AT: replace individual at (@) signs with .SILENT target declaration(s)";
fn check_simplify_at(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
let mut has_global_silence: bool = false;
let mut marked_silent_targets: HashSet<&String> = HashSet::new();
for gem in gems {
if let ast::Ore::Ru { ps, ts, cs: _ } = &gem.n
&& ts.contains(&".SILENT".to_string())
{
if ps.is_empty() {
has_global_silence = true;
}
for p in ps {
marked_silent_targets.insert(p);
}
}
}
if has_global_silence {
return Vec::new();
}
gems.iter()
.filter(|e| match &e.n {
ast::Ore::Ru { ps: _, ts, cs } => {
cs.len() > 1
&& cs.iter().all(|e2| e2.starts_with('@'))
&& !ts.iter().any(|e2| marked_silent_targets.contains(e2))
}
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: SIMPLIFY_AT.to_string(),
})
.collect()
}
#[test]
pub fn test_simplify_at() {
assert!(
lint(
&mock_md("-"),
".POSIX:\nwelcome:\n\t@echo foo\n\t@echo bar\n\t@echo baz\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&SIMPLIFY_AT.to_string())
);
assert!(
!lint(
&mock_md("-"),
".POSIX:\nwelcome:\n\t@echo foo\n\t@echo bar\n\techo baz\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&SIMPLIFY_AT.to_string())
);
assert!(
!lint(
&mock_md("-"),
".POSIX:\n.SILENT: welcome\nwelcome:\n\techo foo\n\techo bar\n\techo baz\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&SIMPLIFY_AT.to_string())
);
}
pub static SIMPLIFY_MINUS: &str =
"SIMPLIFY_MINUS: replace individual hyphen-minus (-) signs with .IGNORE target declaration(s)";
fn check_simplify_minus(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
let mut has_global_ignore: bool = false;
let mut marked_ignored_targets: HashSet<&String> = HashSet::new();
for gem in gems {
if let ast::Ore::Ru { ps, ts, cs: _ } = &gem.n
&& ts.contains(&".IGNORE".to_string())
{
if ps.is_empty() {
has_global_ignore = true;
}
for p in ps {
marked_ignored_targets.insert(p);
}
}
}
if has_global_ignore {
return Vec::new();
}
gems.iter()
.filter(|e| match &e.n {
ast::Ore::Ru { ps: _, ts, cs } => {
cs.len() > 1
&& cs.iter().all(|e2| e2.starts_with('-'))
&& !ts.iter().any(|e2| marked_ignored_targets.contains(e2))
}
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: SIMPLIFY_MINUS.to_string(),
})
.collect()
}
#[test]
pub fn test_simplify_minus() {
assert!(
lint(
&mock_md("-"),
".POSIX:\nwelcome:\n\t-echo foo\n\t-echo bar\n\t-echo baz\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&SIMPLIFY_MINUS.to_string())
);
assert!(
!lint(
&mock_md("-"),
".POSIX:\nwelcome:\n\t-echo foo\n\t-echo bar\n\techo baz\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&SIMPLIFY_MINUS.to_string())
);
assert!(
!lint(
&mock_md("-"),
".POSIX:\n.IGNORE: welcome\nwelcome:\n\techo foo\n\techo bar\n\techo baz\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&SIMPLIFY_MINUS.to_string())
);
}
pub static STRICT_POSIX: &str = "STRICT_POSIX: lead makefiles with the \".POSIX:\" compliance marker, or else rename include files like *.include.mk";
fn check_strict_posix(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
if metadata.is_include_file {
return Vec::new();
}
let has_strict_posix: bool = gems.iter().any(|e| match &e.n {
ast::Ore::Ru { ps: _, ts, cs: _ } => ts.contains(&".POSIX".to_string()),
_ => false,
});
if !has_strict_posix {
return vec![Warning {
path: metadata.path.to_string(),
line: 1,
message: STRICT_POSIX.to_string(),
}];
}
Vec::new()
}
#[test]
pub fn test_strict_posix() {
let md_stdin: inspect::Metadata = mock_md("-");
assert!(
lint(&md_stdin, "PKG = curl\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&STRICT_POSIX.to_string())
);
assert!(
!lint(&md_stdin, ".POSIX:\nPKG = curl\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&STRICT_POSIX.to_string())
);
let mut md_sys: inspect::Metadata = mock_md("sys.mk");
md_sys.is_include_file = true;
assert!(
!lint(&md_sys, "PKG = curl\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&STRICT_POSIX.to_string())
);
let mut md_include_mk: inspect::Metadata = mock_md("foo.include.mk");
md_include_mk.is_include_file = true;
assert!(
!lint(&md_include_mk, "PKG = curl\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&STRICT_POSIX.to_string())
);
}
pub static IMPLEMENTATTION_DEFINED_TARGET: &str = "IMPLEMENTATTION_DEFINED_TARGET: non-portable percent (%) or double-quote (\") in target or prerequisite";
fn check_implementation_defined_target(
metadata: &inspect::Metadata,
gems: &[ast::Gem],
) -> Vec<Warning> {
gems.iter()
.filter(|e| match &e.n {
ast::Ore::Ru { ps, ts, cs: _ } => {
ps.iter().any(|e2| e2.contains('%') || e2.contains('\"'))
|| ts.iter().any(|e2| e2.contains('%') || e2.contains('\"'))
}
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: IMPLEMENTATTION_DEFINED_TARGET.to_string(),
})
.collect()
}
#[test]
pub fn test_implementation_defined_target() {
assert!(
lint(
&mock_md("-"),
".POSIX:\n.PHONY: all\nall: foo%\nfoo%: foo.c\n\tgcc -o foo% foo.c\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&IMPLEMENTATTION_DEFINED_TARGET.to_string())
);
assert!(
lint(
&mock_md("-"),
".POSIX:\n.PHONY: all\nall: \"foo\"\n\"foo\": foo.c\n\tgcc -o \"foo\" foo.c\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&IMPLEMENTATTION_DEFINED_TARGET.to_string())
);
assert!(
!lint(
&mock_md("-"),
".POSIX:\n.PHONY: all\nall: foo\nfoo: foo.c\n\tgcc -o foo foo.c\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&IMPLEMENTATTION_DEFINED_TARGET.to_string())
);
}
pub static COMMAND_COMMENT: &str =
"COMMAND_COMMENT: comment embedded inside commands will forward to the shell interpreter";
fn check_command_comment(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
gems.iter()
.filter(|e| match &e.n {
ast::Ore::Ru { ps: _, ts: _, cs } => cs.iter().any(|e2| e2.contains('#')),
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: COMMAND_COMMENT.to_string(),
})
.collect()
}
#[test]
pub fn test_command_comment() {
assert!(
lint(
&mock_md("-"),
".POSIX:\nfoo: foo.c\n\t#build foo\n\tgcc -o foo foo.c\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&COMMAND_COMMENT.to_string())
);
assert!(
lint(&mock_md("-"), ".POSIX:\nfoo: foo.c\n\t@#gcc -o foo foo.c\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&COMMAND_COMMENT.to_string())
);
assert!(
lint(&mock_md("-"), ".POSIX:\nfoo: foo.c\n\t-#gcc -o foo foo.c\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&COMMAND_COMMENT.to_string())
);
assert!(
lint(&mock_md("-"), ".POSIX:\nfoo: foo.c\n\t+#gcc -o foo foo.c\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&COMMAND_COMMENT.to_string())
);
assert!(
lint(
&mock_md("-"),
".POSIX:\nfoo: foo.c\n\tgcc \\\n#output file \\\n\t\t-o foo \\\n\t\tfoo.c\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&COMMAND_COMMENT.to_string())
);
assert!(
!lint(
&mock_md("-"),
".POSIX:\nfoo: foo.c\n#build foo\n\tgcc -o foo foo.c\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&COMMAND_COMMENT.to_string())
);
}
pub static REPEATED_COMMAND_PREFIX: &str =
"REPEATED_COMMAND_PREFIX: redundant prefixes are superfluous";
fn check_repeated_command_prefix(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
gems.iter()
.filter(|e| match &e.n {
ast::Ore::Ru { ps: _, ts: _, cs } => cs.iter().any(|e2| {
if BLANK_COMMAND_PATTERN.is_match(e2) {
return false;
}
let prefix: &str = COMMAND_PREFIX_PATTERN
.captures(e2)
.and_then(|e3| e3.name("prefix"))
.map(|e3| e3.as_str())
.unwrap_or("");
prefix.matches('@').count() > 1
|| prefix.matches('+').count() > 1
|| prefix.matches('-').count() > 1
}),
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: REPEATED_COMMAND_PREFIX.to_string(),
})
.collect()
}
#[test]
pub fn test_repeated_command_prefix() {
assert!(
lint(
&mock_md("-"),
".POSIX:\n.PHONY: test\ntest:\n\t@@echo \"Hello World!\"\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&REPEATED_COMMAND_PREFIX.to_string())
);
assert!(
lint(
&mock_md("-"),
".POSIX:\n.PHONY: test\ntest:\n\t--echo \"Hello World!\"\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&REPEATED_COMMAND_PREFIX.to_string())
);
assert!(
lint(
&mock_md("-"),
".POSIX:\n.PHONY: test\ntest:\n\t@-@echo \"Hello World!\"\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&REPEATED_COMMAND_PREFIX.to_string())
);
assert!(
!lint(
&mock_md("-"),
".POSIX:\n.PHONY: test\ntest:\n\t@+-echo \"Hello World!\"\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&REPEATED_COMMAND_PREFIX.to_string())
);
}
pub static BLANK_COMMAND: &str = "BLANK_COMMAND: indeterminate behavior when empty commands are sent to assorted shell interpreters";
fn check_blank_command(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
gems.iter()
.filter(|e| match &e.n {
ast::Ore::Ru { ps: _, ts: _, cs } => {
cs.iter().any(|e2| BLANK_COMMAND_PATTERN.is_match(e2))
}
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: BLANK_COMMAND.to_string(),
})
.collect()
}
#[test]
pub fn test_blank_command() {
assert!(
lint(&mock_md("-"), ".POSIX:\n.PHONY: test\ntest:\n\t@\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&BLANK_COMMAND.to_string())
);
assert!(
lint(&mock_md("-"), ".POSIX:\n.PHONY: test\ntest:\n\t-\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&BLANK_COMMAND.to_string())
);
assert!(
lint(&mock_md("-"), ".POSIX:\n.PHONY: test\ntest:\n\t+\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&BLANK_COMMAND.to_string())
);
assert!(
lint(&mock_md("-"), ".POSIX:\n.PHONY: test\ntest:\n\t@+- \n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&BLANK_COMMAND.to_string())
);
assert!(
!lint(
&mock_md("-"),
".POSIX:\n.PHONY: test\ntest:\n\techo \"Hello World!\"\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&BLANK_COMMAND.to_string())
);
}
pub static WHITESPACE_LEADING_COMMAND: &str =
"WHITESPACE_LEADING_COMMAND: questionable whitespace detected at the start of a command";
fn check_whitespace_leading_command(
metadata: &inspect::Metadata,
gems: &[ast::Gem],
) -> Vec<Warning> {
gems.iter()
.filter(|e| match &e.n {
ast::Ore::Ru { ps: _, ts: _, cs } => cs
.iter()
.any(|e2| WHITESPACE_LEADING_COMMAND_PATTERN.is_match(e2)),
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: WHITESPACE_LEADING_COMMAND.to_string(),
})
.collect()
}
#[test]
pub fn test_whitespace_leading_command() {
assert!(
lint(&mock_md("-"), "foo:\n\t gcc -o foo foo.c\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&WHITESPACE_LEADING_COMMAND.to_string())
);
assert!(
lint(&mock_md("-"), "foo:\n\t\tgcc -o foo foo.c\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&WHITESPACE_LEADING_COMMAND.to_string())
);
assert!(
lint(&mock_md("-"), "foo:\n\t@+- gcc -o foo foo.c\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&WHITESPACE_LEADING_COMMAND.to_string())
);
assert!(
!lint(&mock_md("-"), "foo:\n\tgcc -o foo foo.c\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&WHITESPACE_LEADING_COMMAND.to_string())
);
assert!(
!lint(&mock_md("-"), "foo:\n\t@+-gcc -o foo foo.c\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&WHITESPACE_LEADING_COMMAND.to_string())
);
assert!(
!lint(&mock_md("-"), "foo:\n\tgcc \\\n\t\t-o \\\n\t\tfoo foo.c\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&WHITESPACE_LEADING_COMMAND.to_string())
);
}
pub static MISSING_FINAL_EOL: &str =
"MISSING_FINAL_EOL: UNIX text files may process poorly without a final LF";
fn check_final_eol(metadata: &inspect::Metadata, _: &[ast::Gem]) -> Vec<Warning> {
if !metadata.is_empty && !metadata.has_final_eol {
return vec![Warning {
path: metadata.path.to_string(),
line: metadata.lines,
message: MISSING_FINAL_EOL.to_string(),
}];
}
Vec::new()
}
#[test]
pub fn test_final_eol() {
let mf_pkg: &str = ".POSIX:\nPKG = curl";
let mut md_pkg: inspect::Metadata = mock_md("-");
md_pkg.is_empty = &mf_pkg.len() == &0;
md_pkg.has_final_eol = &mf_pkg.chars().last().unwrap_or(' ') == &'\n';
assert!(
lint(&md_pkg, &mf_pkg)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&MISSING_FINAL_EOL.to_string())
);
let mf_pkg_final_eol: &str = ".POSIX:\nPKG = curl\n";
let mut md_pkg_final_eol: inspect::Metadata = mock_md("-");
md_pkg_final_eol.is_empty = &mf_pkg_final_eol.len() == &0;
md_pkg_final_eol.has_final_eol = &mf_pkg_final_eol.chars().last().unwrap_or(' ') == &'\n';
assert!(
!lint(&md_pkg_final_eol, &mf_pkg_final_eol)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&MISSING_FINAL_EOL.to_string())
);
let mf_empty: &str = "";
let mut md_empty: inspect::Metadata = mock_md("-");
md_empty.is_empty = &mf_empty.len() == &0;
md_empty.has_final_eol = &mf_empty.chars().last().unwrap_or(' ') == &'\n';
assert!(
!lint(&md_empty, &mf_empty)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&MISSING_FINAL_EOL.to_string())
);
}
pub static PHONY_TARGET: &str = "PHONY_TARGET: mark common artifactless rules as .PHONY";
fn check_phony_target(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
let mut marked_phony_targets: HashSet<&String> = HashSet::new();
for gem in gems {
if let ast::Ore::Ru { ps, ts, cs: _ } = &gem.n
&& ts.contains(&".PHONY".to_string())
{
for p in ps {
marked_phony_targets.insert(p);
}
}
}
gems.iter()
.filter(|e| match &e.n {
ast::Ore::Ru { ps: _, ts, cs: _ }
if !ts.iter().any(|e2| ast::SPECIAL_TARGETS.contains(e2))
&& !marked_phony_targets.iter().any(|e2| e2.starts_with('$'))
&& ts.iter().any(|e2| !marked_phony_targets.contains(e2)) =>
{
ts.iter().any(|e2| {
LOWER_CONVENTIONAL_PHONY_TARGETS_PATTERN.is_match(e2.to_lowercase().as_str())
})
}
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: PHONY_TARGET.to_string(),
})
.collect()
}
#[test]
pub fn test_phony_target() {
assert!(
lint(&mock_md("-"), ".POSIX:\nall:\n\techo \"Hello World!\"\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&PHONY_TARGET.to_string())
);
assert!(
lint(
&mock_md("-"),
".POSIX:\nlint:;\ninstall:;\nuninstall:;\npublish:;\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&PHONY_TARGET.to_string())
);
assert!(
!lint(
&mock_md("-"),
".POSIX:\n.PHONY: all\nall:\n\techo \"Hello World!\"\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&PHONY_TARGET.to_string())
);
assert!(
lint(
&mock_md("-"),
".POSIX:\ntest: test-1 test-2\ntest-1:\n\techo \"Hello World!\"\ntest-2:\n\techo \"Hi World!\"\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&PHONY_TARGET.to_string()));
assert!(
!lint(
&mock_md("-"),
".POSIX:\n.PHONY: test test-1 test-2\ntest: test-1 test-2\ntest-1:\n\techo \"Hello World!\"\ntest-2:\n\techo \"Hi World!\"\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&PHONY_TARGET.to_string()));
assert!(
!lint(
&mock_md("-"),
".POSIX:\n.PHONY: test\ntest: test-1 test-2\n.PHONY: test-1\ntest-1:\n\techo \"Hello World!\"\n.PHONY: test-2\ntest-2:\n\techo \"Hi World!\"\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&PHONY_TARGET.to_string()));
assert!(
lint(&mock_md("-"), ".POSIX:\nclean:\n\t-rm -rf bin\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&PHONY_TARGET.to_string())
);
assert!(
!lint(
&mock_md("-"),
".POSIX:\n.PHONY: clean\nclean:\n\t-rm -rf bin\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&PHONY_TARGET.to_string())
);
assert!(
!lint(&mock_md("-"), ".POSIX:\nport: cross-compile archive\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&PHONY_TARGET.to_string())
);
assert!(
!lint(
&mock_md("-"),
".POSIX:\n.PHONY: port\nport: cross-compile archive\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&PHONY_TARGET.to_string())
);
assert!(
!lint(&mock_md("-"), ".POSIX:\nempty:;\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&PHONY_TARGET.to_string())
);
assert!(
!lint(&mock_md("-"), ".POSIX:\nPKG = curl\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&PHONY_TARGET.to_string())
);
assert!(
!lint(&mock_md("-"), "ALLTARGETS!=ls -a\n.PHONY: $(ALLTARGETS)\nall: welcome\nwelcome:\n\techo \"Hello World!\"\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&PHONY_TARGET.to_string())
);
}
pub static NO_RULES: &str =
"NO_RULES: declare at least one non-special rule, or else rename to *.include.mk";
fn check_no_rules(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
if metadata.is_include_file {
return Vec::new();
}
let has_nonspecial_rule: bool = !gems
.iter()
.filter(|e| match &e.n {
ast::Ore::Ru { ps: _, ts, cs: _ } => {
ts.iter().any(|e2| !ast::SPECIAL_TARGETS.contains(e2))
}
_ => false,
})
.collect::<Vec<&ast::Gem>>()
.is_empty();
if !has_nonspecial_rule {
return vec![Warning {
path: metadata.path.to_string(),
line: 0,
message: NO_RULES.to_string(),
}];
}
Vec::new()
}
#[test]
pub fn test_no_rules() {
let md_stdin: inspect::Metadata = mock_md("-");
assert!(
lint(&md_stdin, ".POSIX:\nPKG = curl\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&NO_RULES.to_string())
);
let mut md_include: inspect::Metadata = mock_md("foo.include.mk");
md_include.is_include_file = true;
assert!(
!lint(&md_include, "PKG = curl\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&NO_RULES.to_string())
);
assert!(
!lint(&md_stdin, "all:\n\techo \"Hello World!\"\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&NO_RULES.to_string())
);
}
pub static RULE_ALL: &str = "RULE_ALL: makefiles conventionally name the first non-special, default rule \"all\", excepting certain *.include.mk files";
fn check_rule_all(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
if metadata.is_include_file {
return Vec::new();
}
let mut first_nonspecial_target: &str = "";
let mut found_nonspecial_target: bool = false;
for gem in gems {
match &gem.n {
ast::Ore::Ru { ps: _, ts, cs: _ }
if !ts.is_empty() && ts.iter().all(|e2| !ast::SPECIAL_TARGETS.contains(e2)) =>
{
found_nonspecial_target = true;
first_nonspecial_target = ts.first().unwrap();
break;
}
_ => (),
}
}
if found_nonspecial_target && first_nonspecial_target != "all" {
return vec![Warning {
path: metadata.path.to_string(),
line: 0,
message: RULE_ALL.to_string(),
}];
}
Vec::new()
}
#[test]
pub fn test_rule_all() {
assert!(
lint(&mock_md("-"), "build:\n\techo \"Hello World!\"\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&RULE_ALL.to_string())
);
assert!(
!lint(&mock_md("-"), "all:\n\techo \"Hello World!\"\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&RULE_ALL.to_string())
);
assert!(
!lint(
&mock_md("-"),
"all: build\nbuild:\n\techo \"Hello World!\"\n"
)
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&RULE_ALL.to_string())
);
assert!(
!lint(&mock_md("-"), "PKG = curl\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&RULE_ALL.to_string())
);
let mut md_include = mock_md("foo.include.mk");
md_include.is_include_file = true;
assert!(
!lint(&md_include, "build:\n\techo \"Hello World!\"\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&RULE_ALL.to_string())
);
}
pub static RESERVED_TARGET: &str =
"RESERVED_TARGET: non-special targets named like \".(A-Z)\"... are reserved";
fn check_reserved_target(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
gems.iter()
.filter(|e| match &e.n {
ast::Ore::Ru { ts, ps, cs: _ } => [&ts[..], &ps[..]].concat().iter().any(|e2| {
RESERVED_TARGET_PATTERN.is_match(e2) && !ast::SPECIAL_TARGETS.contains(e2)
}),
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: RESERVED_TARGET.to_string(),
})
.collect()
}
#[test]
fn test_reserved_target() {
assert!(lint(&mock_md("-"), ".POSIXX:\n").is_err());
assert!(
lint(&mock_md("-"), ".PHONYY: all\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&RESERVED_TARGET.to_string())
);
assert!(
lint(&mock_md("-"), ".TEST:\n\techo \"Hello World!\"\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&RESERVED_TARGET.to_string())
);
assert!(
lint(&mock_md("-"), "test: .TEST-UNIT .TEST-INTEGRATION\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&RESERVED_TARGET.to_string())
);
assert!(
!lint(&mock_md("-"), "test:\n\t./foo\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&RESERVED_TARGET.to_string())
);
}
pub static NONPORTABLE_ASSIGNMENT: &str =
"NONPORTABLE_ASSIGNMENT: single colon equals (:=) assignment is nonportable";
fn check_portable_assignment(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
gems.iter()
.filter(|e| match &e.n {
ast::Ore::Mc { n: _, o, v: _ } => o == ":=",
_ => false,
})
.map(|e| Warning {
path: metadata.path.to_string(),
line: e.l,
message: NONPORTABLE_ASSIGNMENT.to_string(),
})
.collect()
}
#[test]
fn test_nonportable_assignment() {
assert!(
lint(&mock_md("-"), "CLIENT:=curl --version\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&NONPORTABLE_ASSIGNMENT.to_string())
);
assert!(
!lint(&mock_md("-"), "CLIENT::=curl --version\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&NONPORTABLE_ASSIGNMENT.to_string())
);
assert!(
!lint(&mock_md("-"), "CLIENT:::=curl --version\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&NONPORTABLE_ASSIGNMENT.to_string())
);
assert!(
!lint(&mock_md("-"), "CLIENT?=curl --version\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&NONPORTABLE_ASSIGNMENT.to_string())
);
assert!(
!lint(&mock_md("-"), "CLIENT!=curl --version\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&NONPORTABLE_ASSIGNMENT.to_string())
);
assert!(
!lint(&mock_md("-"), "CLIENT+=curl --version\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&NONPORTABLE_ASSIGNMENT.to_string())
);
assert!(
!lint(&mock_md("-"), "CLIENT=curl --version\n")
.unwrap()
.into_iter()
.map(|e| e.message)
.collect::<Vec<String>>()
.contains(&NONPORTABLE_ASSIGNMENT.to_string())
);
}
pub fn lint(metadata: &inspect::Metadata, makefile: &str) -> Result<Vec<Warning>, String> {
let gems: Vec<ast::Gem> = ast::parse_posix(&metadata.path, makefile)?.ns;
let mut warnings: Vec<Warning> = Vec::new();
for check in CHECKS.iter() {
warnings.extend(check(metadata, &gems));
}
Ok(warnings)
}
#[test]
pub fn test_line_numbers() {
let md: inspect::Metadata = mock_md("-");
assert_eq!(
check_ub_late_posix_marker(
&md,
&ast::parse_posix(md.path.as_str(), "PKG=curl\n.POSIX:\n")
.unwrap()
.ns
),
vec![Warning {
path: WARNING_DEFAULT_PATH.to_string(),
line: 2,
message: UB_LATE_POSIX_MARKER.to_string(),
},]
);
}