1use crate::ast;
4use crate::inspect;
5use std::collections::HashSet;
6use std::fmt;
7
8lazy_static::lazy_static! {
9 pub static ref WD_COMMANDS: Vec<&'static str> = vec![
11 "cd",
12 "pushd",
13 "popd",
14 ];
15
16 pub static ref LOWER_CONVENTIONAL_PHONY_TARGETS_PATTERN: regex::Regex = regex::Regex::new(
19 "^all|lint|install|uninstall|publish|(test.*)|(clean.*)$"
20 ).unwrap();
21
22 pub static ref COMMAND_PREFIX_PATTERN: regex::Regex = regex::Regex::new(r"^(?P<prefix>[-+@]+)").unwrap();
24
25 pub static ref BLANK_COMMAND_PATTERN: regex::Regex = regex::Regex::new(r"^[-+@]+\s*$").unwrap();
29
30 pub static ref WHITESPACE_LEADING_COMMAND_PATTERN: regex::Regex = regex::Regex::new(r"^[-+@]*\s+").unwrap();
32
33 pub static ref RESERVED_TARGET_PATTERN: regex::Regex = regex::Regex::new(r"^.[A-Z]+").unwrap();
35
36 static ref WARNING_DEFAULT_PATH: String = "-".to_string();
38
39 pub static ref CHECKS: Vec<Check> = vec![
41 check_ub_late_posix_marker,
42 check_ub_ambiguous_include,
43 check_ub_makeflags_assignment,
44 check_ub_shell_macro,
45 check_silent_include,
46 check_strict_posix,
47 check_implementation_defined_target,
48 check_makefile_precedence,
49 check_curdir_assignment_nop,
50 check_wd_nop,
51 check_wait_nop,
52 check_phony_nop,
53 check_redundant_notparallel_wait,
54 check_redundant_silent_at,
55 check_redundant_ignore_minus,
56 check_global_ignore,
57 check_simplify_at,
58 check_simplify_minus,
59 check_command_comment,
60 check_phony_target,
61 check_repeated_command_prefix,
62 check_blank_command,
63 check_whitespace_leading_command,
64 check_no_rules,
65 check_reserved_target,
66 check_rule_all,
67 check_final_eol,
68 check_portable_assignment,
69 ];
70}
71
72pub type Check = fn(&inspect::Metadata, &[ast::Gem]) -> Vec<Warning>;
74
75#[derive(Debug, PartialEq)]
77pub struct Warning {
78 pub path: String,
80
81 pub line: usize,
83
84 pub message: String,
86}
87
88impl Warning {
89 pub fn new() -> Warning {
91 Warning {
92 path: WARNING_DEFAULT_PATH.to_string(),
93 line: 0,
94 message: String::new(),
95 }
96 }
97}
98
99impl Default for Warning {
100 fn default() -> Self {
102 Warning::new()
103 }
104}
105
106impl fmt::Display for Warning {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 write!(f, "warning: {}:", self.path)?;
110
111 if self.line > 0 {
112 write!(f, "{}:", self.line)?;
113 }
114
115 write!(f, " {}", self.message)
116 }
117}
118
119pub fn mock_md(pth: &str) -> inspect::Metadata {
125 inspect::Metadata {
126 path: pth.to_string(),
127 filename: pth.to_string(),
128 is_makefile: true,
129 build_system: String::new(),
130 is_machine_generated: false,
131 is_include_file: false,
132 is_empty: true,
133 lines: 0,
134 has_final_eol: false,
135 }
136}
137
138pub 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";
139
140fn check_ub_late_posix_marker(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
142 gems.iter()
143 .enumerate()
144 .filter(|(i, e)| match &e.n {
145 ast::Ore::Ru { ps: _, ts, cs: _ } => {
146 (metadata.is_include_file || i > &0) && ts == &vec![".POSIX"]
147 }
148 _ => false,
149 })
150 .map(|(_, e)| Warning {
151 path: metadata.path.to_string(),
152 line: e.l,
153 message: UB_LATE_POSIX_MARKER.to_string(),
154 })
155 .collect()
156}
157
158#[test]
159fn test_late_posix_marker() {
160 assert!(
161 lint(&mock_md("-"), "PKG=curl\n.POSIX:\n")
162 .unwrap()
163 .into_iter()
164 .map(|e| e.message)
165 .collect::<Vec<String>>()
166 .contains(&UB_LATE_POSIX_MARKER.to_string())
167 );
168
169 assert!(
170 !lint(&mock_md("-"), "# strict posix\n.POSIX:\nPKG=curl\n")
171 .unwrap()
172 .into_iter()
173 .map(|e| e.message)
174 .collect::<Vec<String>>()
175 .contains(&UB_LATE_POSIX_MARKER.to_string())
176 );
177
178 assert!(
179 !lint(&mock_md("-"), ".POSIX:\nPKG=curl\n")
180 .unwrap()
181 .into_iter()
182 .map(|e| e.message)
183 .collect::<Vec<String>>()
184 .contains(&UB_LATE_POSIX_MARKER.to_string())
185 );
186
187 let mut md_include = mock_md("provision.include.mk");
188 md_include.is_include_file = true;
189
190 assert!(
191 lint(&md_include, ".POSIX:\nPKG=curl\n")
192 .unwrap()
193 .into_iter()
194 .map(|e| e.message)
195 .collect::<Vec<String>>()
196 .contains(&UB_LATE_POSIX_MARKER.to_string())
197 );
198
199 assert!(
200 !lint(&md_include, "PKG=curl\n")
201 .unwrap()
202 .into_iter()
203 .map(|e| e.message)
204 .collect::<Vec<String>>()
205 .contains(&UB_LATE_POSIX_MARKER.to_string())
206 );
207
208 assert!(
209 lint(&mock_md("-"), ".POSIX:\ninclude =foo.mk\n")
210 .unwrap()
211 .into_iter()
212 .map(|e| e.message)
213 .collect::<Vec<String>>()
214 .contains(&UB_AMBIGUOUS_INCLUDE.to_string())
215 );
216}
217
218pub static UB_AMBIGUOUS_INCLUDE: &str =
219 "UB_AMBIGUOUS_INCLUDE: unclear whether include line or macro definition";
220
221fn check_ub_ambiguous_include(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
223 gems.iter()
224 .filter(|e| match &e.n {
225 ast::Ore::In { s: _, ps } => ps.iter().any(|e2| e2.starts_with('=')),
226 _ => false,
227 })
228 .map(|e| Warning {
229 path: metadata.path.to_string(),
230 line: e.l,
231 message: UB_AMBIGUOUS_INCLUDE.to_string(),
232 })
233 .collect()
234}
235
236#[test]
237fn test_ub_ambiguous_include() {
238 assert!(
239 lint(&mock_md("-"), ".POSIX:\ninclude = foo.mk\n")
240 .unwrap()
241 .into_iter()
242 .map(|e| e.message)
243 .collect::<Vec<String>>()
244 .contains(&UB_AMBIGUOUS_INCLUDE.to_string())
245 );
246
247 assert!(
248 lint(&mock_md("-"), ".POSIX:\ninclude =foo.mk\n")
249 .unwrap()
250 .into_iter()
251 .map(|e| e.message)
252 .collect::<Vec<String>>()
253 .contains(&UB_AMBIGUOUS_INCLUDE.to_string())
254 );
255
256 assert!(
257 !lint(&mock_md("-"), ".POSIX:\ninclude=foo.mk\n")
258 .unwrap()
259 .into_iter()
260 .map(|e| e.message)
261 .collect::<Vec<String>>()
262 .contains(&UB_AMBIGUOUS_INCLUDE.to_string())
263 );
264}
265
266pub static UB_MAKEFLAGS_ASSIGNMENT: &str = "UB_MAKEFLAGS_MACRO: do not modify MAKEFLAGS macro";
267
268fn check_ub_makeflags_assignment(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
270 gems.iter()
271 .filter(|e| match &e.n {
272 ast::Ore::Mc { n, o: _, v: _ } => n == "MAKEFLAGS",
273 _ => false,
274 })
275 .map(|e| Warning {
276 path: metadata.path.to_string(),
277 line: e.l,
278 message: UB_MAKEFLAGS_ASSIGNMENT.to_string(),
279 })
280 .collect()
281}
282
283#[test]
284fn test_ub_makeflags_assignment() {
285 assert!(
286 lint(&mock_md("-"), ".POSIX:\nMAKEFLAGS ?= -j\nMAKEFLAGS = -j\nMAKEFLAGS ::= -j\nMAKEFLAGS :::= -j\nMAKEFLAGS += -j\nMAKEFLAGS != echo \"-j\"\n")
287 .unwrap()
288 .into_iter()
289 .map(|e| e.message)
290 .collect::<Vec<String>>()
291 .contains(&UB_MAKEFLAGS_ASSIGNMENT.to_string()));
292
293 assert!(
294 !lint(&mock_md("-"), ".POSIX:\nPKG = curl\n")
295 .unwrap()
296 .into_iter()
297 .map(|e| e.message)
298 .collect::<Vec<String>>()
299 .contains(&UB_MAKEFLAGS_ASSIGNMENT.to_string())
300 );
301}
302
303pub static UB_SHELL_MACRO: &str = "UB_SHELL_MACRO: do not use or modify SHELL macro";
304
305fn check_ub_shell_macro(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
307 gems.iter()
308 .filter(|e| match &e.n {
309 ast::Ore::Mc { n, o: _, v: _ } => n == "SHELL",
310 _ => false,
311 })
312 .map(|e| Warning {
313 path: metadata.path.to_string(),
314 line: e.l,
315 message: UB_SHELL_MACRO.to_string(),
316 })
317 .collect()
318}
319
320#[test]
321fn test_ub_shell_macro() {
322 assert!(lint(
323 &mock_md("-"),
324 ".POSIX:\nSHELL ?= sh\nSHELL = sh\nSHELL ::= sh\nSHELL :::= sh\nSHELL += sh\nSHELL != sh\n"
325 )
326 .unwrap()
327 .into_iter()
328 .map(|e| e.message)
329 .collect::<Vec<String>>()
330 .contains(&UB_SHELL_MACRO.to_string()));
331
332 assert!(
333 !lint(&mock_md("-"), ".POSIX:\nPKG = curl\n")
334 .unwrap()
335 .into_iter()
336 .map(|e| e.message)
337 .collect::<Vec<String>>()
338 .contains(&UB_SHELL_MACRO.to_string())
339 );
340}
341
342pub static SILENT_INCLUDE: &str = "SILENT_INCLUDE: dashed includes may obfuscate systemic errors";
343
344fn check_silent_include(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
346 gems.iter()
347 .filter(|e| match &e.n {
348 ast::Ore::In { s, ps: _ } => *s,
349 _ => false,
350 })
351 .map(|e| Warning {
352 path: metadata.path.to_string(),
353 line: e.l,
354 message: SILENT_INCLUDE.to_string(),
355 })
356 .collect()
357}
358
359#[test]
360fn test_silent_include() {
361 assert!(
362 lint(&mock_md("-"), "-include = foo.mk\n")
363 .unwrap()
364 .into_iter()
365 .map(|e| e.message)
366 .collect::<Vec<String>>()
367 .contains(&SILENT_INCLUDE.to_string())
368 );
369
370 assert!(
371 !lint(&mock_md("-"), "include=foo.mk\n")
372 .unwrap()
373 .into_iter()
374 .map(|e| e.message)
375 .collect::<Vec<String>>()
376 .contains(&SILENT_INCLUDE.to_string())
377 );
378}
379
380pub static MAKEFILE_PRECEDENCE: &str =
381 "MAKEFILE_PRECEDENCE: lowercase Makefile to makefile for launch speed";
382
383fn check_makefile_precedence(metadata: &inspect::Metadata, _: &[ast::Gem]) -> Vec<Warning> {
385 if metadata.filename == "Makefile" {
386 return vec![Warning {
387 path: metadata.path.to_string(),
388 line: 0,
389 message: MAKEFILE_PRECEDENCE.to_string(),
390 }];
391 }
392
393 Vec::new()
394}
395
396#[test]
397pub fn test_makefile_precedence() {
398 assert!(
399 lint(&mock_md("Makefile"), ".POSIX:\nPKG=curl\n")
400 .unwrap()
401 .into_iter()
402 .map(|e| e.message)
403 .collect::<Vec<String>>()
404 .contains(&MAKEFILE_PRECEDENCE.to_string())
405 );
406
407 assert!(
408 !lint(&mock_md("makefile"), ".POSIX:\nPKG=curl\n")
409 .unwrap()
410 .into_iter()
411 .map(|e| e.message)
412 .collect::<Vec<String>>()
413 .contains(&MAKEFILE_PRECEDENCE.to_string())
414 );
415
416 assert!(
417 !lint(&mock_md("foo.mk"), ".POSIX:\nPKG=curl\n")
418 .unwrap()
419 .into_iter()
420 .map(|e| e.message)
421 .collect::<Vec<String>>()
422 .contains(&MAKEFILE_PRECEDENCE.to_string())
423 );
424
425 assert!(
426 !lint(&mock_md("foo.Makefile"), ".POSIX:\nPKG=curl\n")
427 .unwrap()
428 .into_iter()
429 .map(|e| e.message)
430 .collect::<Vec<String>>()
431 .contains(&MAKEFILE_PRECEDENCE.to_string())
432 );
433
434 assert!(
435 !lint(&mock_md("foo.makefile"), ".POSIX:\nPKG=curl\n")
436 .unwrap()
437 .into_iter()
438 .map(|e| e.message)
439 .collect::<Vec<String>>()
440 .contains(&MAKEFILE_PRECEDENCE.to_string())
441 );
442}
443
444pub static CURDIR_ASSIGNMENT_NOP: &str =
445 "CURDIR_ASSIGNMENT_NOP: CURDIR assignment does not change the make working directory";
446
447fn check_curdir_assignment_nop(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
449 gems.iter()
450 .filter(|e| match &e.n {
451 ast::Ore::Mc { n, o: _, v: _ } => n == "CURDIR",
452 _ => false,
453 })
454 .map(|e| Warning {
455 path: metadata.path.to_string(),
456 line: e.l,
457 message: CURDIR_ASSIGNMENT_NOP.to_string(),
458 })
459 .collect()
460}
461
462#[test]
463pub fn test_curdir_assignment_nop() {
464 assert!(
465 lint(&mock_md("-"), ".POSIX:\nCURDIR = build\n")
466 .unwrap()
467 .into_iter()
468 .map(|e| e.message)
469 .collect::<Vec<String>>()
470 .contains(&CURDIR_ASSIGNMENT_NOP.to_string())
471 );
472
473 assert!(
474 !lint(&mock_md("-"), ".POSIX:\n")
475 .unwrap()
476 .into_iter()
477 .map(|e| e.message)
478 .collect::<Vec<String>>()
479 .contains(&CURDIR_ASSIGNMENT_NOP.to_string())
480 );
481}
482
483pub static WD_NOP: &str =
484 "WD_NOP: change directory commands may not persist across successive commands or rules";
485
486fn check_wd_nop(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
488 gems.iter()
489 .filter(|e| match &e.n {
490 ast::Ore::Ru { ps: _, ts: _, cs } => cs
491 .iter()
492 .any(|e2| WD_COMMANDS.contains(&e2.split_whitespace().next().unwrap_or(""))),
493 _ => false,
494 })
495 .map(|e| Warning {
496 path: metadata.path.to_string(),
497 line: e.l,
498 message: WD_NOP.to_string(),
499 })
500 .collect()
501}
502
503#[test]
504pub fn test_wd_nop() {
505 assert!(
506 lint(
507 &mock_md("-"),
508 ".POSIX:\n.PHONY: all\nall:\n\tcd foo\n\n\tpushd bar\n\tpopd\n"
509 )
510 .unwrap()
511 .into_iter()
512 .map(|e| e.message)
513 .collect::<Vec<String>>()
514 .contains(&WD_NOP.to_string())
515 );
516
517 assert!(
518 !lint(
519 &mock_md("-"),
520 ".POSIX:\n.PHONY: all\nall:\n\ttar -C foo czvf foo.tgz .\n"
521 )
522 .unwrap()
523 .into_iter()
524 .map(|e| e.message)
525 .collect::<Vec<String>>()
526 .contains(&WD_NOP.to_string())
527 );
528}
529
530pub static WAIT_NOP: &str = "WAIT_NOP: .WAIT as a target has no effect";
531
532fn check_wait_nop(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
534 gems.iter()
535 .filter(|e| match &e.n {
536 ast::Ore::Ru { ps: _, ts, cs: _ } => ts.contains(&".WAIT".to_string()),
537 _ => false,
538 })
539 .map(|e| Warning {
540 path: metadata.path.to_string(),
541 line: e.l,
542 message: WAIT_NOP.to_string(),
543 })
544 .collect()
545}
546
547#[test]
548pub fn test_wait_nop() {
549 assert!(
550 lint(&mock_md("-"), ".POSIX:\n.WAIT:\n")
551 .unwrap()
552 .into_iter()
553 .map(|e| e.message)
554 .collect::<Vec<String>>()
555 .contains(&WAIT_NOP.to_string())
556 );
557
558 assert!(
559 !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")
560 .unwrap()
561 .into_iter()
562 .map(|e| e.message)
563 .collect::<Vec<String>>()
564 .contains(&WAIT_NOP.to_string()));
565}
566
567pub static PHONY_NOP: &str = "PHONY_NOP: empty .PHONY has no effect";
568
569fn check_phony_nop(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
571 gems.iter()
572 .filter(|e| match &e.n {
573 ast::Ore::Ru { ps, ts, cs: _ } => ts.contains(&".PHONY".to_string()) && ps.is_empty(),
574 _ => false,
575 })
576 .map(|e| Warning {
577 path: metadata.path.to_string(),
578 line: e.l,
579 message: PHONY_NOP.to_string(),
580 })
581 .collect()
582}
583
584#[test]
585pub fn test_phony_nop() {
586 assert!(
587 lint(
588 &mock_md("-"),
589 ".POSIX:\n.PHONY:\nfoo: foo.c\n\tgcc -o foo foo.c\n"
590 )
591 .unwrap()
592 .into_iter()
593 .map(|e| e.message)
594 .collect::<Vec<String>>()
595 .contains(&PHONY_NOP.to_string())
596 );
597
598 assert!(
599 !lint(
600 &mock_md("-"),
601 ".POSIX:\n.PHONY: test\ntest:\n\techo \"Hello World!\"\n"
602 )
603 .unwrap()
604 .into_iter()
605 .map(|e| e.message)
606 .collect::<Vec<String>>()
607 .contains(&PHONY_NOP.to_string())
608 );
609}
610
611pub static REDUNDANT_NOTPARALLEL_WAIT: &str =
612 "REDUNDANT_NOTPARALLEL_WAIT: .NOTPARALLEL with .WAIT is redundant and superfluous";
613
614fn check_redundant_notparallel_wait(
616 metadata: &inspect::Metadata,
617 gems: &[ast::Gem],
618) -> Vec<Warning> {
619 let has_notparallel: bool = gems.iter().any(|e| match &e.n {
620 ast::Ore::Ru { ps: _, ts, cs: _ } => ts.contains(&".NOTPARALLEL".to_string()),
621 _ => false,
622 });
623
624 if !has_notparallel {
625 return Vec::new();
626 }
627
628 gems.iter()
629 .filter(|e| match &e.n {
630 ast::Ore::Ru { ps, ts: _, cs: _ } => ps.contains(&".WAIT".to_string()),
631 _ => false,
632 })
633 .map(|e| Warning {
634 path: metadata.path.to_string(),
635 line: e.l,
636 message: REDUNDANT_NOTPARALLEL_WAIT.to_string(),
637 })
638 .collect()
639}
640
641#[test]
642pub fn test_redundant_nonparallel_wait() {
643 assert!(
644 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")
645 .unwrap()
646 .into_iter()
647 .map(|e| e.message)
648 .collect::<Vec<String>>()
649 .contains(&REDUNDANT_NOTPARALLEL_WAIT.to_string()));
650
651 assert!(
652 !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")
653 .unwrap()
654 .into_iter()
655 .map(|e| e.message)
656 .collect::<Vec<String>>()
657 .contains(&REDUNDANT_NOTPARALLEL_WAIT.to_string()));
658
659 assert!(
660 !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")
661 .unwrap()
662 .into_iter()
663 .map(|e| e.message)
664 .collect::<Vec<String>>()
665 .contains(&REDUNDANT_NOTPARALLEL_WAIT.to_string()));
666}
667
668pub static REDUNDANT_SILENT_AT: &str =
669 "REDUNDANT_SILENT_AT: .SILENT with @ is redundant and superfluous";
670
671fn check_redundant_silent_at(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
673 let mut has_global_silence: bool = false;
674 let mut marked_silent_targets: HashSet<&String> = HashSet::new();
675
676 for gem in gems {
677 if let ast::Ore::Ru { ps, ts, cs: _ } = &gem.n {
678 if ts.contains(&".SILENT".to_string()) {
679 if ps.is_empty() {
680 has_global_silence = true;
681 }
682
683 for p in ps {
684 marked_silent_targets.insert(p);
685 }
686 }
687 }
688 }
689
690 gems.iter()
691 .filter(|e| match &e.n {
692 ast::Ore::Ru { ps: _, ts, cs } => {
693 cs.iter().any(|e2| e2.starts_with('@'))
694 && (has_global_silence
695 || ts.iter().any(|e2| marked_silent_targets.contains(e2)))
696 }
697 _ => false,
698 })
699 .map(|e| Warning {
700 path: metadata.path.to_string(),
701 line: e.l,
702 message: REDUNDANT_SILENT_AT.to_string(),
703 })
704 .collect()
705}
706
707#[test]
708pub fn test_redundant_silent_at() {
709 assert!(
710 lint(
711 &mock_md("-"),
712 ".POSIX:\n.PHONY: lint\n.SILENT:\nlint:\n\t@unmake .\n"
713 )
714 .unwrap()
715 .into_iter()
716 .map(|e| e.message)
717 .collect::<Vec<String>>()
718 .contains(&REDUNDANT_SILENT_AT.to_string())
719 );
720
721 assert!(
722 lint(
723 &mock_md("-"),
724 ".POSIX:\n.PHONY: lint\n.SILENT: lint\nlint:\n\t@unmake .\n"
725 )
726 .unwrap()
727 .into_iter()
728 .map(|e| e.message)
729 .collect::<Vec<String>>()
730 .contains(&REDUNDANT_SILENT_AT.to_string())
731 );
732
733 assert!(
734 !lint(
735 &mock_md("-"),
736 ".POSIX:\n.PHONY: lint\n.SILENT: lint\nlint:\n\tunmake .\n"
737 )
738 .unwrap()
739 .into_iter()
740 .map(|e| e.message)
741 .collect::<Vec<String>>()
742 .contains(&REDUNDANT_SILENT_AT.to_string())
743 );
744
745 assert!(
746 !lint(&mock_md("-"), ".POSIX:\n.PHONY: lint\nlint:\n\t@unmake .\n")
747 .unwrap()
748 .into_iter()
749 .map(|e| e.message)
750 .collect::<Vec<String>>()
751 .contains(&REDUNDANT_SILENT_AT.to_string())
752 );
753}
754
755pub static REDUNDANT_IGNORE_MINUS: &str =
756 "REDUNDANT_IGNORE_MINUS: .IGNORE with - is redundant and superfluous";
757
758fn check_redundant_ignore_minus(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
760 let mut marked_ignored_targets: HashSet<&String> = HashSet::new();
761 for gem in gems {
762 if let ast::Ore::Ru { ps, ts, cs: _ } = &gem.n {
763 if ts.contains(&".IGNORE".to_string()) {
764 for p in ps {
765 marked_ignored_targets.insert(p);
766 }
767 }
768 }
769 }
770
771 gems.iter()
772 .filter(|e| match &e.n {
773 ast::Ore::Ru { ps: _, ts, cs } => {
774 cs.iter().any(|e2| e2.starts_with('-'))
775 && ts.iter().any(|e2| marked_ignored_targets.contains(e2))
776 }
777 _ => false,
778 })
779 .map(|e| Warning {
780 path: metadata.path.to_string(),
781 line: e.l,
782 message: REDUNDANT_IGNORE_MINUS.to_string(),
783 })
784 .collect()
785}
786
787#[test]
788pub fn test_redundant_ignore_minus() {
789 assert!(
790 lint(
791 &mock_md("-"),
792 ".POSIX:\n.PHONY: clean\n.IGNORE: clean\nclean:\n\t-rm -rf bin\n"
793 )
794 .unwrap()
795 .into_iter()
796 .map(|e| e.message)
797 .collect::<Vec<String>>()
798 .contains(&REDUNDANT_IGNORE_MINUS.to_string())
799 );
800
801 assert!(
802 !lint(
803 &mock_md("-"),
804 ".POSIX:\n.PHONY: clean\nclean:\n\t-rm -rf bin\n"
805 )
806 .unwrap()
807 .into_iter()
808 .map(|e| e.message)
809 .collect::<Vec<String>>()
810 .contains(&REDUNDANT_IGNORE_MINUS.to_string())
811 );
812}
813
814pub static GLOBAL_IGNORE: &str =
815 "GLOBAL_IGNORE: .IGNORE without prerequisites may corrupt artifacts";
816
817fn check_global_ignore(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
819 gems.iter()
820 .filter(|e| match &e.n {
821 ast::Ore::Ru { ps, ts, cs: _ } => ts.contains(&".IGNORE".to_string()) && ps.is_empty(),
822 _ => false,
823 })
824 .map(|e| Warning {
825 path: metadata.path.to_string(),
826 line: e.l,
827 message: GLOBAL_IGNORE.to_string(),
828 })
829 .collect()
830}
831
832#[test]
833pub fn test_global_ignore() {
834 assert!(
835 lint(&mock_md("-"), ".POSIX:\n.IGNORE:\n")
836 .unwrap()
837 .into_iter()
838 .map(|e| e.message)
839 .collect::<Vec<String>>()
840 .contains(&GLOBAL_IGNORE.to_string())
841 );
842
843 assert!(
844 !lint(
845 &mock_md("-"),
846 ".POSIX:\n.PHONY: clean\n.IGNORE: clean\nclean:\n\trm -rf bin"
847 )
848 .unwrap()
849 .into_iter()
850 .map(|e| e.message)
851 .collect::<Vec<String>>()
852 .contains(&GLOBAL_IGNORE.to_string())
853 );
854
855 assert!(
856 !lint(
857 &mock_md("-"),
858 ".POSIX:\n.PHONY: clean\nclean:\n\t-rm -rf bin"
859 )
860 .unwrap()
861 .into_iter()
862 .map(|e| e.message)
863 .collect::<Vec<String>>()
864 .contains(&GLOBAL_IGNORE.to_string())
865 );
866}
867
868pub static SIMPLIFY_AT: &str =
869 "SIMPLIFY_AT: replace individual at (@) signs with .SILENT target declaration(s)";
870
871fn check_simplify_at(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
873 let mut has_global_silence: bool = false;
874 let mut marked_silent_targets: HashSet<&String> = HashSet::new();
875
876 for gem in gems {
877 if let ast::Ore::Ru { ps, ts, cs: _ } = &gem.n {
878 if ts.contains(&".SILENT".to_string()) {
879 if ps.is_empty() {
880 has_global_silence = true;
881 }
882
883 for p in ps {
884 marked_silent_targets.insert(p);
885 }
886 }
887 }
888 }
889
890 if has_global_silence {
891 return Vec::new();
892 }
893
894 gems.iter()
895 .filter(|e| match &e.n {
896 ast::Ore::Ru { ps: _, ts, cs } => {
897 cs.len() > 1
898 && cs.iter().all(|e2| e2.starts_with('@'))
899 && !ts.iter().any(|e2| marked_silent_targets.contains(e2))
900 }
901 _ => false,
902 })
903 .map(|e| Warning {
904 path: metadata.path.to_string(),
905 line: e.l,
906 message: SIMPLIFY_AT.to_string(),
907 })
908 .collect()
909}
910
911#[test]
912pub fn test_simplify_at() {
913 assert!(
914 lint(
915 &mock_md("-"),
916 ".POSIX:\nwelcome:\n\t@echo foo\n\t@echo bar\n\t@echo baz\n"
917 )
918 .unwrap()
919 .into_iter()
920 .map(|e| e.message)
921 .collect::<Vec<String>>()
922 .contains(&SIMPLIFY_AT.to_string())
923 );
924
925 assert!(
926 !lint(
927 &mock_md("-"),
928 ".POSIX:\nwelcome:\n\t@echo foo\n\t@echo bar\n\techo baz\n"
929 )
930 .unwrap()
931 .into_iter()
932 .map(|e| e.message)
933 .collect::<Vec<String>>()
934 .contains(&SIMPLIFY_AT.to_string())
935 );
936
937 assert!(
938 !lint(
939 &mock_md("-"),
940 ".POSIX:\n.SILENT: welcome\nwelcome:\n\techo foo\n\techo bar\n\techo baz\n"
941 )
942 .unwrap()
943 .into_iter()
944 .map(|e| e.message)
945 .collect::<Vec<String>>()
946 .contains(&SIMPLIFY_AT.to_string())
947 );
948}
949
950pub static SIMPLIFY_MINUS: &str =
951 "SIMPLIFY_MINUS: replace individual hyphen-minus (-) signs with .IGNORE target declaration(s)";
952
953fn check_simplify_minus(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
955 let mut has_global_ignore: bool = false;
956 let mut marked_ignored_targets: HashSet<&String> = HashSet::new();
957
958 for gem in gems {
959 if let ast::Ore::Ru { ps, ts, cs: _ } = &gem.n {
960 if ts.contains(&".IGNORE".to_string()) {
961 if ps.is_empty() {
962 has_global_ignore = true;
963 }
964
965 for p in ps {
966 marked_ignored_targets.insert(p);
967 }
968 }
969 }
970 }
971
972 if has_global_ignore {
973 return Vec::new();
974 }
975
976 gems.iter()
977 .filter(|e| match &e.n {
978 ast::Ore::Ru { ps: _, ts, cs } => {
979 cs.len() > 1
980 && cs.iter().all(|e2| e2.starts_with('-'))
981 && !ts.iter().any(|e2| marked_ignored_targets.contains(e2))
982 }
983 _ => false,
984 })
985 .map(|e| Warning {
986 path: metadata.path.to_string(),
987 line: e.l,
988 message: SIMPLIFY_MINUS.to_string(),
989 })
990 .collect()
991}
992
993#[test]
994pub fn test_simplify_minus() {
995 assert!(
996 lint(
997 &mock_md("-"),
998 ".POSIX:\nwelcome:\n\t-echo foo\n\t-echo bar\n\t-echo baz\n"
999 )
1000 .unwrap()
1001 .into_iter()
1002 .map(|e| e.message)
1003 .collect::<Vec<String>>()
1004 .contains(&SIMPLIFY_MINUS.to_string())
1005 );
1006
1007 assert!(
1008 !lint(
1009 &mock_md("-"),
1010 ".POSIX:\nwelcome:\n\t-echo foo\n\t-echo bar\n\techo baz\n"
1011 )
1012 .unwrap()
1013 .into_iter()
1014 .map(|e| e.message)
1015 .collect::<Vec<String>>()
1016 .contains(&SIMPLIFY_MINUS.to_string())
1017 );
1018
1019 assert!(
1020 !lint(
1021 &mock_md("-"),
1022 ".POSIX:\n.IGNORE: welcome\nwelcome:\n\techo foo\n\techo bar\n\techo baz\n"
1023 )
1024 .unwrap()
1025 .into_iter()
1026 .map(|e| e.message)
1027 .collect::<Vec<String>>()
1028 .contains(&SIMPLIFY_MINUS.to_string())
1029 );
1030}
1031
1032pub static STRICT_POSIX: &str = "STRICT_POSIX: lead makefiles with the \".POSIX:\" compliance marker, or else rename include files like *.include.mk";
1033
1034fn check_strict_posix(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
1036 if metadata.is_include_file {
1037 return Vec::new();
1038 }
1039
1040 let has_strict_posix: bool = gems.iter().any(|e| match &e.n {
1041 ast::Ore::Ru { ps: _, ts, cs: _ } => ts.contains(&".POSIX".to_string()),
1042 _ => false,
1043 });
1044
1045 if !has_strict_posix {
1046 return vec![Warning {
1047 path: metadata.path.to_string(),
1048 line: 1,
1049 message: STRICT_POSIX.to_string(),
1050 }];
1051 }
1052
1053 Vec::new()
1054}
1055
1056#[test]
1057pub fn test_strict_posix() {
1058 let md_stdin: inspect::Metadata = mock_md("-");
1059
1060 assert!(
1061 lint(&md_stdin, "PKG = curl\n")
1062 .unwrap()
1063 .into_iter()
1064 .map(|e| e.message)
1065 .collect::<Vec<String>>()
1066 .contains(&STRICT_POSIX.to_string())
1067 );
1068
1069 assert!(
1070 !lint(&md_stdin, ".POSIX:\nPKG = curl\n")
1071 .unwrap()
1072 .into_iter()
1073 .map(|e| e.message)
1074 .collect::<Vec<String>>()
1075 .contains(&STRICT_POSIX.to_string())
1076 );
1077
1078 let mut md_sys: inspect::Metadata = mock_md("sys.mk");
1079 md_sys.is_include_file = true;
1080
1081 assert!(
1082 !lint(&md_sys, "PKG = curl\n")
1083 .unwrap()
1084 .into_iter()
1085 .map(|e| e.message)
1086 .collect::<Vec<String>>()
1087 .contains(&STRICT_POSIX.to_string())
1088 );
1089
1090 let mut md_include_mk: inspect::Metadata = mock_md("foo.include.mk");
1091 md_include_mk.is_include_file = true;
1092
1093 assert!(
1094 !lint(&md_include_mk, "PKG = curl\n")
1095 .unwrap()
1096 .into_iter()
1097 .map(|e| e.message)
1098 .collect::<Vec<String>>()
1099 .contains(&STRICT_POSIX.to_string())
1100 );
1101}
1102
1103pub static IMPLEMENTATTION_DEFINED_TARGET: &str = "IMPLEMENTATTION_DEFINED_TARGET: non-portable percent (%) or double-quote (\") in target or prerequisite";
1104
1105fn check_implementation_defined_target(
1107 metadata: &inspect::Metadata,
1108 gems: &[ast::Gem],
1109) -> Vec<Warning> {
1110 gems.iter()
1111 .filter(|e| match &e.n {
1112 ast::Ore::Ru { ps, ts, cs: _ } => {
1113 ps.iter().any(|e2| e2.contains('%') || e2.contains('\"'))
1114 || ts.iter().any(|e2| e2.contains('%') || e2.contains('\"'))
1115 }
1116 _ => false,
1117 })
1118 .map(|e| Warning {
1119 path: metadata.path.to_string(),
1120 line: e.l,
1121 message: IMPLEMENTATTION_DEFINED_TARGET.to_string(),
1122 })
1123 .collect()
1124}
1125
1126#[test]
1127pub fn test_implementation_defined_target() {
1128 assert!(
1129 lint(
1130 &mock_md("-"),
1131 ".POSIX:\n.PHONY: all\nall: foo%\nfoo%: foo.c\n\tgcc -o foo% foo.c\n"
1132 )
1133 .unwrap()
1134 .into_iter()
1135 .map(|e| e.message)
1136 .collect::<Vec<String>>()
1137 .contains(&IMPLEMENTATTION_DEFINED_TARGET.to_string())
1138 );
1139
1140 assert!(
1141 lint(
1142 &mock_md("-"),
1143 ".POSIX:\n.PHONY: all\nall: \"foo\"\n\"foo\": foo.c\n\tgcc -o \"foo\" foo.c\n"
1144 )
1145 .unwrap()
1146 .into_iter()
1147 .map(|e| e.message)
1148 .collect::<Vec<String>>()
1149 .contains(&IMPLEMENTATTION_DEFINED_TARGET.to_string())
1150 );
1151
1152 assert!(
1153 !lint(
1154 &mock_md("-"),
1155 ".POSIX:\n.PHONY: all\nall: foo\nfoo: foo.c\n\tgcc -o foo foo.c\n"
1156 )
1157 .unwrap()
1158 .into_iter()
1159 .map(|e| e.message)
1160 .collect::<Vec<String>>()
1161 .contains(&IMPLEMENTATTION_DEFINED_TARGET.to_string())
1162 );
1163}
1164
1165pub static COMMAND_COMMENT: &str =
1166 "COMMAND_COMMENT: comment embedded inside commands will forward to the shell interpreter";
1167
1168fn check_command_comment(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
1170 gems.iter()
1171 .filter(|e| match &e.n {
1172 ast::Ore::Ru { ps: _, ts: _, cs } => cs.iter().any(|e2| e2.contains('#')),
1173 _ => false,
1174 })
1175 .map(|e| Warning {
1176 path: metadata.path.to_string(),
1177 line: e.l,
1178 message: COMMAND_COMMENT.to_string(),
1179 })
1180 .collect()
1181}
1182
1183#[test]
1184pub fn test_command_comment() {
1185 assert!(
1186 lint(
1187 &mock_md("-"),
1188 ".POSIX:\nfoo: foo.c\n\t#build foo\n\tgcc -o foo foo.c\n"
1189 )
1190 .unwrap()
1191 .into_iter()
1192 .map(|e| e.message)
1193 .collect::<Vec<String>>()
1194 .contains(&COMMAND_COMMENT.to_string())
1195 );
1196
1197 assert!(
1198 lint(&mock_md("-"), ".POSIX:\nfoo: foo.c\n\t@#gcc -o foo foo.c\n")
1199 .unwrap()
1200 .into_iter()
1201 .map(|e| e.message)
1202 .collect::<Vec<String>>()
1203 .contains(&COMMAND_COMMENT.to_string())
1204 );
1205
1206 assert!(
1207 lint(&mock_md("-"), ".POSIX:\nfoo: foo.c\n\t-#gcc -o foo foo.c\n")
1208 .unwrap()
1209 .into_iter()
1210 .map(|e| e.message)
1211 .collect::<Vec<String>>()
1212 .contains(&COMMAND_COMMENT.to_string())
1213 );
1214
1215 assert!(
1216 lint(&mock_md("-"), ".POSIX:\nfoo: foo.c\n\t+#gcc -o foo foo.c\n")
1217 .unwrap()
1218 .into_iter()
1219 .map(|e| e.message)
1220 .collect::<Vec<String>>()
1221 .contains(&COMMAND_COMMENT.to_string())
1222 );
1223
1224 assert!(
1225 lint(
1226 &mock_md("-"),
1227 ".POSIX:\nfoo: foo.c\n\tgcc \\\n#output file \\\n\t\t-o foo \\\n\t\tfoo.c\n"
1228 )
1229 .unwrap()
1230 .into_iter()
1231 .map(|e| e.message)
1232 .collect::<Vec<String>>()
1233 .contains(&COMMAND_COMMENT.to_string())
1234 );
1235
1236 assert!(
1237 !lint(
1238 &mock_md("-"),
1239 ".POSIX:\nfoo: foo.c\n#build foo\n\tgcc -o foo foo.c\n"
1240 )
1241 .unwrap()
1242 .into_iter()
1243 .map(|e| e.message)
1244 .collect::<Vec<String>>()
1245 .contains(&COMMAND_COMMENT.to_string())
1246 );
1247}
1248
1249pub static REPEATED_COMMAND_PREFIX: &str =
1250 "REPEATED_COMMAND_PREFIX: redundant prefixes are superfluous";
1251
1252fn check_repeated_command_prefix(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
1254 gems.iter()
1255 .filter(|e| match &e.n {
1256 ast::Ore::Ru { ps: _, ts: _, cs } => cs.iter().any(|e2| {
1257 if BLANK_COMMAND_PATTERN.is_match(e2) {
1258 return false;
1259 }
1260
1261 let prefix: &str = COMMAND_PREFIX_PATTERN
1262 .captures(e2)
1263 .and_then(|e3| e3.name("prefix"))
1264 .map(|e3| e3.as_str())
1265 .unwrap_or("");
1266
1267 prefix.matches('@').count() > 1
1268 || prefix.matches('+').count() > 1
1269 || prefix.matches('-').count() > 1
1270 }),
1271 _ => false,
1272 })
1273 .map(|e| Warning {
1274 path: metadata.path.to_string(),
1275 line: e.l,
1276 message: REPEATED_COMMAND_PREFIX.to_string(),
1277 })
1278 .collect()
1279}
1280
1281#[test]
1282pub fn test_repeated_command_prefix() {
1283 assert!(
1284 lint(
1285 &mock_md("-"),
1286 ".POSIX:\n.PHONY: test\ntest:\n\t@@echo \"Hello World!\"\n"
1287 )
1288 .unwrap()
1289 .into_iter()
1290 .map(|e| e.message)
1291 .collect::<Vec<String>>()
1292 .contains(&REPEATED_COMMAND_PREFIX.to_string())
1293 );
1294
1295 assert!(
1296 lint(
1297 &mock_md("-"),
1298 ".POSIX:\n.PHONY: test\ntest:\n\t--echo \"Hello World!\"\n"
1299 )
1300 .unwrap()
1301 .into_iter()
1302 .map(|e| e.message)
1303 .collect::<Vec<String>>()
1304 .contains(&REPEATED_COMMAND_PREFIX.to_string())
1305 );
1306
1307 assert!(
1308 lint(
1309 &mock_md("-"),
1310 ".POSIX:\n.PHONY: test\ntest:\n\t@-@echo \"Hello World!\"\n"
1311 )
1312 .unwrap()
1313 .into_iter()
1314 .map(|e| e.message)
1315 .collect::<Vec<String>>()
1316 .contains(&REPEATED_COMMAND_PREFIX.to_string())
1317 );
1318
1319 assert!(
1320 !lint(
1321 &mock_md("-"),
1322 ".POSIX:\n.PHONY: test\ntest:\n\t@+-echo \"Hello World!\"\n"
1323 )
1324 .unwrap()
1325 .into_iter()
1326 .map(|e| e.message)
1327 .collect::<Vec<String>>()
1328 .contains(&REPEATED_COMMAND_PREFIX.to_string())
1329 );
1330}
1331
1332pub static BLANK_COMMAND: &str = "BLANK_COMMAND: indeterminate behavior when empty commands are sent to assorted shell interpreters";
1333
1334fn check_blank_command(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
1336 gems.iter()
1337 .filter(|e| match &e.n {
1338 ast::Ore::Ru { ps: _, ts: _, cs } => {
1339 cs.iter().any(|e2| BLANK_COMMAND_PATTERN.is_match(e2))
1340 }
1341 _ => false,
1342 })
1343 .map(|e| Warning {
1344 path: metadata.path.to_string(),
1345 line: e.l,
1346 message: BLANK_COMMAND.to_string(),
1347 })
1348 .collect()
1349}
1350
1351#[test]
1352pub fn test_blank_command() {
1353 assert!(
1354 lint(&mock_md("-"), ".POSIX:\n.PHONY: test\ntest:\n\t@\n")
1355 .unwrap()
1356 .into_iter()
1357 .map(|e| e.message)
1358 .collect::<Vec<String>>()
1359 .contains(&BLANK_COMMAND.to_string())
1360 );
1361
1362 assert!(
1363 lint(&mock_md("-"), ".POSIX:\n.PHONY: test\ntest:\n\t-\n")
1364 .unwrap()
1365 .into_iter()
1366 .map(|e| e.message)
1367 .collect::<Vec<String>>()
1368 .contains(&BLANK_COMMAND.to_string())
1369 );
1370
1371 assert!(
1372 lint(&mock_md("-"), ".POSIX:\n.PHONY: test\ntest:\n\t+\n")
1373 .unwrap()
1374 .into_iter()
1375 .map(|e| e.message)
1376 .collect::<Vec<String>>()
1377 .contains(&BLANK_COMMAND.to_string())
1378 );
1379
1380 assert!(
1381 lint(&mock_md("-"), ".POSIX:\n.PHONY: test\ntest:\n\t@+- \n")
1382 .unwrap()
1383 .into_iter()
1384 .map(|e| e.message)
1385 .collect::<Vec<String>>()
1386 .contains(&BLANK_COMMAND.to_string())
1387 );
1388
1389 assert!(
1390 !lint(
1391 &mock_md("-"),
1392 ".POSIX:\n.PHONY: test\ntest:\n\techo \"Hello World!\"\n"
1393 )
1394 .unwrap()
1395 .into_iter()
1396 .map(|e| e.message)
1397 .collect::<Vec<String>>()
1398 .contains(&BLANK_COMMAND.to_string())
1399 );
1400}
1401
1402pub static WHITESPACE_LEADING_COMMAND: &str =
1403 "WHITESPACE_LEADING_COMMAND: questionable whitespace detected at the start of a command";
1404
1405fn check_whitespace_leading_command(
1407 metadata: &inspect::Metadata,
1408 gems: &[ast::Gem],
1409) -> Vec<Warning> {
1410 gems.iter()
1411 .filter(|e| match &e.n {
1412 ast::Ore::Ru { ps: _, ts: _, cs } => cs
1413 .iter()
1414 .any(|e2| WHITESPACE_LEADING_COMMAND_PATTERN.is_match(e2)),
1415 _ => false,
1416 })
1417 .map(|e| Warning {
1418 path: metadata.path.to_string(),
1419 line: e.l,
1420 message: WHITESPACE_LEADING_COMMAND.to_string(),
1421 })
1422 .collect()
1423}
1424
1425#[test]
1426pub fn test_whitespace_leading_command() {
1427 assert!(
1428 lint(&mock_md("-"), "foo:\n\t gcc -o foo foo.c\n")
1429 .unwrap()
1430 .into_iter()
1431 .map(|e| e.message)
1432 .collect::<Vec<String>>()
1433 .contains(&WHITESPACE_LEADING_COMMAND.to_string())
1434 );
1435
1436 assert!(
1437 lint(&mock_md("-"), "foo:\n\t\tgcc -o foo foo.c\n")
1438 .unwrap()
1439 .into_iter()
1440 .map(|e| e.message)
1441 .collect::<Vec<String>>()
1442 .contains(&WHITESPACE_LEADING_COMMAND.to_string())
1443 );
1444
1445 assert!(
1446 lint(&mock_md("-"), "foo:\n\t@+- gcc -o foo foo.c\n")
1447 .unwrap()
1448 .into_iter()
1449 .map(|e| e.message)
1450 .collect::<Vec<String>>()
1451 .contains(&WHITESPACE_LEADING_COMMAND.to_string())
1452 );
1453
1454 assert!(
1455 !lint(&mock_md("-"), "foo:\n\tgcc -o foo foo.c\n")
1456 .unwrap()
1457 .into_iter()
1458 .map(|e| e.message)
1459 .collect::<Vec<String>>()
1460 .contains(&WHITESPACE_LEADING_COMMAND.to_string())
1461 );
1462
1463 assert!(
1464 !lint(&mock_md("-"), "foo:\n\t@+-gcc -o foo foo.c\n")
1465 .unwrap()
1466 .into_iter()
1467 .map(|e| e.message)
1468 .collect::<Vec<String>>()
1469 .contains(&WHITESPACE_LEADING_COMMAND.to_string())
1470 );
1471
1472 assert!(
1473 !lint(&mock_md("-"), "foo:\n\tgcc \\\n\t\t-o \\\n\t\tfoo foo.c\n")
1474 .unwrap()
1475 .into_iter()
1476 .map(|e| e.message)
1477 .collect::<Vec<String>>()
1478 .contains(&WHITESPACE_LEADING_COMMAND.to_string())
1479 );
1480}
1481
1482pub static MISSING_FINAL_EOL: &str =
1483 "MISSING_FINAL_EOL: UNIX text files may process poorly without a final LF";
1484
1485fn check_final_eol(metadata: &inspect::Metadata, _: &[ast::Gem]) -> Vec<Warning> {
1487 if !metadata.is_empty && !metadata.has_final_eol {
1488 return vec![Warning {
1489 path: metadata.path.to_string(),
1490 line: metadata.lines,
1491 message: MISSING_FINAL_EOL.to_string(),
1492 }];
1493 }
1494
1495 Vec::new()
1496}
1497
1498#[test]
1499pub fn test_final_eol() {
1500 let mf_pkg: &str = ".POSIX:\nPKG = curl";
1501 let mut md_pkg: inspect::Metadata = mock_md("-");
1502 md_pkg.is_empty = &mf_pkg.len() == &0;
1503 md_pkg.has_final_eol = &mf_pkg.chars().last().unwrap_or(' ') == &'\n';
1504
1505 assert!(
1506 lint(&md_pkg, &mf_pkg)
1507 .unwrap()
1508 .into_iter()
1509 .map(|e| e.message)
1510 .collect::<Vec<String>>()
1511 .contains(&MISSING_FINAL_EOL.to_string())
1512 );
1513
1514 let mf_pkg_final_eol: &str = ".POSIX:\nPKG = curl\n";
1515 let mut md_pkg_final_eol: inspect::Metadata = mock_md("-");
1516 md_pkg_final_eol.is_empty = &mf_pkg_final_eol.len() == &0;
1517 md_pkg_final_eol.has_final_eol = &mf_pkg_final_eol.chars().last().unwrap_or(' ') == &'\n';
1518
1519 assert!(
1520 !lint(&md_pkg_final_eol, &mf_pkg_final_eol)
1521 .unwrap()
1522 .into_iter()
1523 .map(|e| e.message)
1524 .collect::<Vec<String>>()
1525 .contains(&MISSING_FINAL_EOL.to_string())
1526 );
1527
1528 let mf_empty: &str = "";
1529 let mut md_empty: inspect::Metadata = mock_md("-");
1530 md_empty.is_empty = &mf_empty.len() == &0;
1531 md_empty.has_final_eol = &mf_empty.chars().last().unwrap_or(' ') == &'\n';
1532
1533 assert!(
1534 !lint(&md_empty, &mf_empty)
1535 .unwrap()
1536 .into_iter()
1537 .map(|e| e.message)
1538 .collect::<Vec<String>>()
1539 .contains(&MISSING_FINAL_EOL.to_string())
1540 );
1541}
1542
1543pub static PHONY_TARGET: &str = "PHONY_TARGET: mark common artifactless rules as .PHONY";
1544
1545fn check_phony_target(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
1547 let mut marked_phony_targets: HashSet<&String> = HashSet::new();
1548 for gem in gems {
1549 if let ast::Ore::Ru { ps, ts, cs: _ } = &gem.n {
1550 if ts.contains(&".PHONY".to_string()) {
1551 for p in ps {
1552 marked_phony_targets.insert(p);
1553 }
1554 }
1555 }
1556 }
1557
1558 gems.iter()
1559 .filter(|e| match &e.n {
1560 ast::Ore::Ru { ps: _, ts, cs: _ }
1561 if !ts.iter().any(|e2| ast::SPECIAL_TARGETS.contains(e2))
1562 && !marked_phony_targets.iter().any(|e2| e2.starts_with('$'))
1563 && ts.iter().any(|e2| !marked_phony_targets.contains(e2)) =>
1564 {
1565 ts.iter().any(|e2| {
1566 LOWER_CONVENTIONAL_PHONY_TARGETS_PATTERN.is_match(e2.to_lowercase().as_str())
1567 })
1568 }
1569 _ => false,
1570 })
1571 .map(|e| Warning {
1572 path: metadata.path.to_string(),
1573 line: e.l,
1574 message: PHONY_TARGET.to_string(),
1575 })
1576 .collect()
1577}
1578
1579#[test]
1580pub fn test_phony_target() {
1581 assert!(
1582 lint(&mock_md("-"), ".POSIX:\nall:\n\techo \"Hello World!\"\n")
1583 .unwrap()
1584 .into_iter()
1585 .map(|e| e.message)
1586 .collect::<Vec<String>>()
1587 .contains(&PHONY_TARGET.to_string())
1588 );
1589
1590 assert!(
1591 lint(
1592 &mock_md("-"),
1593 ".POSIX:\nlint:;\ninstall:;\nuninstall:;\npublish:;\n"
1594 )
1595 .unwrap()
1596 .into_iter()
1597 .map(|e| e.message)
1598 .collect::<Vec<String>>()
1599 .contains(&PHONY_TARGET.to_string())
1600 );
1601
1602 assert!(
1603 !lint(
1604 &mock_md("-"),
1605 ".POSIX:\n.PHONY: all\nall:\n\techo \"Hello World!\"\n"
1606 )
1607 .unwrap()
1608 .into_iter()
1609 .map(|e| e.message)
1610 .collect::<Vec<String>>()
1611 .contains(&PHONY_TARGET.to_string())
1612 );
1613
1614 assert!(
1615 lint(
1616 &mock_md("-"),
1617 ".POSIX:\ntest: test-1 test-2\ntest-1:\n\techo \"Hello World!\"\ntest-2:\n\techo \"Hi World!\"\n"
1618 )
1619 .unwrap()
1620 .into_iter()
1621 .map(|e| e.message)
1622 .collect::<Vec<String>>()
1623 .contains(&PHONY_TARGET.to_string()));
1624
1625 assert!(
1626 !lint(
1627 &mock_md("-"),
1628 ".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"
1629 )
1630 .unwrap()
1631 .into_iter()
1632 .map(|e| e.message)
1633 .collect::<Vec<String>>()
1634 .contains(&PHONY_TARGET.to_string()));
1635
1636 assert!(
1637 !lint(
1638 &mock_md("-"),
1639 ".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"
1640 )
1641 .unwrap()
1642 .into_iter()
1643 .map(|e| e.message)
1644 .collect::<Vec<String>>()
1645 .contains(&PHONY_TARGET.to_string()));
1646
1647 assert!(
1648 lint(&mock_md("-"), ".POSIX:\nclean:\n\t-rm -rf bin\n")
1649 .unwrap()
1650 .into_iter()
1651 .map(|e| e.message)
1652 .collect::<Vec<String>>()
1653 .contains(&PHONY_TARGET.to_string())
1654 );
1655
1656 assert!(
1657 !lint(
1658 &mock_md("-"),
1659 ".POSIX:\n.PHONY: clean\nclean:\n\t-rm -rf bin\n"
1660 )
1661 .unwrap()
1662 .into_iter()
1663 .map(|e| e.message)
1664 .collect::<Vec<String>>()
1665 .contains(&PHONY_TARGET.to_string())
1666 );
1667
1668 assert!(
1669 !lint(&mock_md("-"), ".POSIX:\nport: cross-compile archive\n")
1670 .unwrap()
1671 .into_iter()
1672 .map(|e| e.message)
1673 .collect::<Vec<String>>()
1674 .contains(&PHONY_TARGET.to_string())
1675 );
1676
1677 assert!(
1678 !lint(
1679 &mock_md("-"),
1680 ".POSIX:\n.PHONY: port\nport: cross-compile archive\n"
1681 )
1682 .unwrap()
1683 .into_iter()
1684 .map(|e| e.message)
1685 .collect::<Vec<String>>()
1686 .contains(&PHONY_TARGET.to_string())
1687 );
1688
1689 assert!(
1690 !lint(&mock_md("-"), ".POSIX:\nempty:;\n")
1691 .unwrap()
1692 .into_iter()
1693 .map(|e| e.message)
1694 .collect::<Vec<String>>()
1695 .contains(&PHONY_TARGET.to_string())
1696 );
1697
1698 assert!(
1701 !lint(&mock_md("-"), ".POSIX:\nPKG = curl\n")
1702 .unwrap()
1703 .into_iter()
1704 .map(|e| e.message)
1705 .collect::<Vec<String>>()
1706 .contains(&PHONY_TARGET.to_string())
1707 );
1708
1709 assert!(
1712 !lint(&mock_md("-"), "ALLTARGETS!=ls -a\n.PHONY: $(ALLTARGETS)\nall: welcome\nwelcome:\n\techo \"Hello World!\"\n")
1713 .unwrap()
1714 .into_iter()
1715 .map(|e| e.message)
1716 .collect::<Vec<String>>()
1717 .contains(&PHONY_TARGET.to_string())
1718 );
1719}
1720
1721pub static NO_RULES: &str =
1722 "NO_RULES: declare at least one non-special rule, or else rename to *.include.mk";
1723
1724fn check_no_rules(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
1726 if metadata.is_include_file {
1727 return Vec::new();
1728 }
1729
1730 let has_nonspecial_rule: bool = !gems
1731 .iter()
1732 .filter(|e| match &e.n {
1733 ast::Ore::Ru { ps: _, ts, cs: _ } => {
1734 ts.iter().any(|e2| !ast::SPECIAL_TARGETS.contains(e2))
1735 }
1736 _ => false,
1737 })
1738 .collect::<Vec<&ast::Gem>>()
1739 .is_empty();
1740
1741 if !has_nonspecial_rule {
1742 return vec![Warning {
1743 path: metadata.path.to_string(),
1744 line: 0,
1745 message: NO_RULES.to_string(),
1746 }];
1747 }
1748
1749 Vec::new()
1750}
1751
1752#[test]
1753pub fn test_no_rules() {
1754 let md_stdin: inspect::Metadata = mock_md("-");
1755
1756 assert!(
1757 lint(&md_stdin, ".POSIX:\nPKG = curl\n")
1758 .unwrap()
1759 .into_iter()
1760 .map(|e| e.message)
1761 .collect::<Vec<String>>()
1762 .contains(&NO_RULES.to_string())
1763 );
1764
1765 let mut md_include: inspect::Metadata = mock_md("foo.include.mk");
1766 md_include.is_include_file = true;
1767
1768 assert!(
1769 !lint(&md_include, "PKG = curl\n")
1770 .unwrap()
1771 .into_iter()
1772 .map(|e| e.message)
1773 .collect::<Vec<String>>()
1774 .contains(&NO_RULES.to_string())
1775 );
1776
1777 assert!(
1778 !lint(&md_stdin, "all:\n\techo \"Hello World!\"\n")
1779 .unwrap()
1780 .into_iter()
1781 .map(|e| e.message)
1782 .collect::<Vec<String>>()
1783 .contains(&NO_RULES.to_string())
1784 );
1785}
1786
1787pub static RULE_ALL: &str = "RULE_ALL: makefiles conventionally name the first non-special, default rule \"all\", excepting certain *.include.mk files";
1788
1789fn check_rule_all(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
1791 if metadata.is_include_file {
1792 return Vec::new();
1793 }
1794
1795 let mut first_nonspecial_target: &str = "";
1796 let mut found_nonspecial_target: bool = false;
1797
1798 for gem in gems {
1799 match &gem.n {
1800 ast::Ore::Ru { ps: _, ts, cs: _ }
1801 if !ts.is_empty() && ts.iter().all(|e2| !ast::SPECIAL_TARGETS.contains(e2)) =>
1802 {
1803 found_nonspecial_target = true;
1804 first_nonspecial_target = ts.first().unwrap();
1805 break;
1806 }
1807 _ => (),
1808 }
1809 }
1810
1811 if found_nonspecial_target && first_nonspecial_target != "all" {
1812 return vec![Warning {
1813 path: metadata.path.to_string(),
1814 line: 0,
1815 message: RULE_ALL.to_string(),
1816 }];
1817 }
1818
1819 Vec::new()
1820}
1821
1822#[test]
1823pub fn test_rule_all() {
1824 assert!(
1825 lint(&mock_md("-"), "build:\n\techo \"Hello World!\"\n")
1826 .unwrap()
1827 .into_iter()
1828 .map(|e| e.message)
1829 .collect::<Vec<String>>()
1830 .contains(&RULE_ALL.to_string())
1831 );
1832
1833 assert!(
1834 !lint(&mock_md("-"), "all:\n\techo \"Hello World!\"\n")
1835 .unwrap()
1836 .into_iter()
1837 .map(|e| e.message)
1838 .collect::<Vec<String>>()
1839 .contains(&RULE_ALL.to_string())
1840 );
1841
1842 assert!(
1843 !lint(
1844 &mock_md("-"),
1845 "all: build\nbuild:\n\techo \"Hello World!\"\n"
1846 )
1847 .unwrap()
1848 .into_iter()
1849 .map(|e| e.message)
1850 .collect::<Vec<String>>()
1851 .contains(&RULE_ALL.to_string())
1852 );
1853
1854 assert!(
1855 !lint(&mock_md("-"), "PKG = curl\n")
1856 .unwrap()
1857 .into_iter()
1858 .map(|e| e.message)
1859 .collect::<Vec<String>>()
1860 .contains(&RULE_ALL.to_string())
1861 );
1862
1863 let mut md_include = mock_md("foo.include.mk");
1864 md_include.is_include_file = true;
1865
1866 assert!(
1867 !lint(&md_include, "build:\n\techo \"Hello World!\"\n")
1868 .unwrap()
1869 .into_iter()
1870 .map(|e| e.message)
1871 .collect::<Vec<String>>()
1872 .contains(&RULE_ALL.to_string())
1873 );
1874}
1875
1876pub static RESERVED_TARGET: &str =
1877 "RESERVED_TARGET: non-special targets named like \".(A-Z)\"... are reserved";
1878
1879fn check_reserved_target(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
1881 gems.iter()
1882 .filter(|e| match &e.n {
1883 ast::Ore::Ru { ts, ps, cs: _ } => [&ts[..], &ps[..]].concat().iter().any(|e2| {
1884 RESERVED_TARGET_PATTERN.is_match(e2) && !ast::SPECIAL_TARGETS.contains(e2)
1885 }),
1886 _ => false,
1887 })
1888 .map(|e| Warning {
1889 path: metadata.path.to_string(),
1890 line: e.l,
1891 message: RESERVED_TARGET.to_string(),
1892 })
1893 .collect()
1894}
1895
1896#[test]
1897fn test_reserved_target() {
1898 assert!(lint(&mock_md("-"), ".POSIXX:\n").is_err());
1899
1900 assert!(
1901 lint(&mock_md("-"), ".PHONYY: all\n")
1902 .unwrap()
1903 .into_iter()
1904 .map(|e| e.message)
1905 .collect::<Vec<String>>()
1906 .contains(&RESERVED_TARGET.to_string())
1907 );
1908
1909 assert!(
1910 lint(&mock_md("-"), ".TEST:\n\techo \"Hello World!\"\n")
1911 .unwrap()
1912 .into_iter()
1913 .map(|e| e.message)
1914 .collect::<Vec<String>>()
1915 .contains(&RESERVED_TARGET.to_string())
1916 );
1917
1918 assert!(
1919 lint(&mock_md("-"), "test: .TEST-UNIT .TEST-INTEGRATION\n")
1920 .unwrap()
1921 .into_iter()
1922 .map(|e| e.message)
1923 .collect::<Vec<String>>()
1924 .contains(&RESERVED_TARGET.to_string())
1925 );
1926
1927 assert!(
1928 !lint(&mock_md("-"), "test:\n\t./foo\n")
1929 .unwrap()
1930 .into_iter()
1931 .map(|e| e.message)
1932 .collect::<Vec<String>>()
1933 .contains(&RESERVED_TARGET.to_string())
1934 );
1935}
1936
1937pub static NONPORTABLE_ASSIGNMENT: &str =
1938 "NONPORTABLE_ASSIGNMENT: single colon equals (:=) assignment is nonportable";
1939
1940fn check_portable_assignment(metadata: &inspect::Metadata, gems: &[ast::Gem]) -> Vec<Warning> {
1942 gems.iter()
1943 .filter(|e| match &e.n {
1944 ast::Ore::Mc { n: _, o, v: _ } => o == ":=",
1945 _ => false,
1946 })
1947 .map(|e| Warning {
1948 path: metadata.path.to_string(),
1949 line: e.l,
1950 message: NONPORTABLE_ASSIGNMENT.to_string(),
1951 })
1952 .collect()
1953}
1954
1955#[test]
1956fn test_nonportable_assignment() {
1957 assert!(
1958 lint(&mock_md("-"), "CLIENT:=curl --version\n")
1959 .unwrap()
1960 .into_iter()
1961 .map(|e| e.message)
1962 .collect::<Vec<String>>()
1963 .contains(&NONPORTABLE_ASSIGNMENT.to_string())
1964 );
1965
1966 assert!(
1967 !lint(&mock_md("-"), "CLIENT::=curl --version\n")
1968 .unwrap()
1969 .into_iter()
1970 .map(|e| e.message)
1971 .collect::<Vec<String>>()
1972 .contains(&NONPORTABLE_ASSIGNMENT.to_string())
1973 );
1974
1975 assert!(
1976 !lint(&mock_md("-"), "CLIENT:::=curl --version\n")
1977 .unwrap()
1978 .into_iter()
1979 .map(|e| e.message)
1980 .collect::<Vec<String>>()
1981 .contains(&NONPORTABLE_ASSIGNMENT.to_string())
1982 );
1983
1984 assert!(
1985 !lint(&mock_md("-"), "CLIENT?=curl --version\n")
1986 .unwrap()
1987 .into_iter()
1988 .map(|e| e.message)
1989 .collect::<Vec<String>>()
1990 .contains(&NONPORTABLE_ASSIGNMENT.to_string())
1991 );
1992
1993 assert!(
1994 !lint(&mock_md("-"), "CLIENT!=curl --version\n")
1995 .unwrap()
1996 .into_iter()
1997 .map(|e| e.message)
1998 .collect::<Vec<String>>()
1999 .contains(&NONPORTABLE_ASSIGNMENT.to_string())
2000 );
2001
2002 assert!(
2003 !lint(&mock_md("-"), "CLIENT+=curl --version\n")
2004 .unwrap()
2005 .into_iter()
2006 .map(|e| e.message)
2007 .collect::<Vec<String>>()
2008 .contains(&NONPORTABLE_ASSIGNMENT.to_string())
2009 );
2010
2011 assert!(
2012 !lint(&mock_md("-"), "CLIENT=curl --version\n")
2013 .unwrap()
2014 .into_iter()
2015 .map(|e| e.message)
2016 .collect::<Vec<String>>()
2017 .contains(&NONPORTABLE_ASSIGNMENT.to_string())
2018 );
2019}
2020
2021pub fn lint(metadata: &inspect::Metadata, makefile: &str) -> Result<Vec<Warning>, String> {
2023 let gems: Vec<ast::Gem> = ast::parse_posix(&metadata.path, makefile)?.ns;
2024 let mut warnings: Vec<Warning> = Vec::new();
2025
2026 for check in CHECKS.iter() {
2027 warnings.extend(check(metadata, &gems));
2028 }
2029
2030 Ok(warnings)
2031}
2032
2033#[test]
2034pub fn test_line_numbers() {
2035 let md: inspect::Metadata = mock_md("-");
2036
2037 assert_eq!(
2038 check_ub_late_posix_marker(
2039 &md,
2040 &ast::parse_posix(md.path.as_str(), "PKG=curl\n.POSIX:\n")
2041 .unwrap()
2042 .ns
2043 ),
2044 vec![Warning {
2045 path: WARNING_DEFAULT_PATH.to_string(),
2046 line: 2,
2047 message: UB_LATE_POSIX_MARKER.to_string(),
2048 },]
2049 );
2050}