1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
//! MAKE020: Missing include guard
//!
//! **Rule**: Detect included Makefiles without include guards (double-inclusion prevention)
//!
//! **Why this matters**:
//! When Makefiles are included multiple times (directly or transitively), variables
//! can be redefined multiple times, rules can be duplicated, and builds become slower.
//! Include guards (like C header guards) prevent this by ensuring a file is only
//! processed once.
//!
//! **Auto-fix**: Add include guard pattern at top of file
//!
//! ## Examples
//!
//! ❌ **BAD** (no include guard - can be included multiple times):
//! ```makefile
//! # common.mk
//! CC = gcc
//! CFLAGS = -Wall
//! ```
//!
//! ✅ **GOOD** (with include guard):
//! ```makefile
//! # common.mk
//! ifndef COMMON_MK_INCLUDED
//! COMMON_MK_INCLUDED := 1
//!
//! CC = gcc
//! CFLAGS = -Wall
//!
//! endif
//! ```
use crate::linter::{Diagnostic, Fix, LintResult, Severity, Span};
/// Check for missing include guards in Makefiles meant for inclusion
pub fn check(source: &str) -> LintResult {
let mut result = LintResult::new();
// Empty Makefile doesn't need guard
if source.trim().is_empty() {
return result;
}
// If already has ifndef (even if not a proper guard), don't flag
// to avoid false positives
if has_ifndef(source) {
return result;
}
// Check if this Makefile has content that should be guarded
// (variable definitions that could be problematic if included multiple times)
if !should_have_guard(source) {
return result;
}
// Missing include guard - create diagnostic
let span = Span::new(1, 1, 1, 1); // Point to start of file
// Create fix by adding include guard
let fix_replacement = create_guard_fix(source);
let diag = Diagnostic::new(
"MAKE020",
Severity::Warning,
"Missing include guard - Makefile may be included multiple times (consider adding ifndef/endif guard)",
span,
)
.with_fix(Fix::new(&fix_replacement));
result.add(diag);
result
}
/// Check if Makefile has any ifndef directive
fn has_ifndef(source: &str) -> bool {
for line in source.lines() {
let trimmed = line.trim();
if trimmed.starts_with("ifndef ") {
return true;
}
}
false
}
/// Check if Makefile should have an include guard
/// (has variable definitions AND looks like an includable file, not a standalone Makefile)
fn should_have_guard(source: &str) -> bool {
let mut has_variables = false;
let mut has_targets = false;
for line in source.lines() {
let trimmed = line.trim();
// Skip comments and empty lines
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
// Skip recipe lines
if line.starts_with('\t') {
continue;
}
// Check for variable definitions (contains = but not :)
if trimmed.contains('=') && !trimmed.starts_with("export ") {
has_variables = true;
}
// Check for targets (lines with : that aren't variable assignments)
if trimmed.contains(':') {
// Distinguish between "VAR := value" and "target: deps"
if let Some(before_colon) = trimmed.split(':').next() {
// If there's an = before the :, it's an assignment (VAR := value)
if !before_colon.contains('=') {
has_targets = true;
}
}
}
}
// Only warn if has variables AND no targets (indicating include file)
// Standalone Makefiles with targets don't need guards
has_variables && !has_targets
}
/// Create fix by adding include guard around entire file
fn create_guard_fix(source: &str) -> String {
// Generate guard name based on typical convention
let guard_name = "MAKEFILE_INCLUDED";
format!(
"ifndef {}\n{} := 1\n\n{}\n\nendif",
guard_name, guard_name, source
)
}
#[cfg(test)]
mod tests {
use super::*;
// RED PHASE: Write failing tests first
#[test]
fn test_MAKE020_detects_missing_guard() {
let makefile = "# common.mk\nCC = gcc\nCFLAGS = -Wall";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 1);
let diag = &result.diagnostics[0];
assert_eq!(diag.code, "MAKE020");
assert_eq!(diag.severity, Severity::Warning);
assert!(
diag.message.to_lowercase().contains("guard")
|| diag.message.to_lowercase().contains("include")
);
}
#[test]
fn test_MAKE020_detects_makefile_with_variables() {
let makefile = "VERSION = 1.0\nPREFIX = /usr/local";
let result = check(makefile);
// Has variables but no guard
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_MAKE020_provides_fix() {
let makefile = "# common.mk\nCC = gcc\nCFLAGS = -Wall";
let result = check(makefile);
assert!(result.diagnostics[0].fix.is_some());
let fix = result.diagnostics[0].fix.as_ref().unwrap();
// Fix should add ifndef/endif guard
assert!(fix.replacement.contains("ifndef"));
assert!(fix.replacement.contains("endif"));
}
#[test]
fn test_MAKE020_no_warning_with_guard() {
let makefile = "ifndef COMMON_MK\nCOMMON_MK := 1\n\nCC = gcc\n\nendif";
let result = check(makefile);
// Has include guard - OK
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_MAKE020_no_warning_for_simple_targets_only() {
let makefile = "all:\n\t$(CC) main.c\n\nclean:\n\trm -f *.o";
let result = check(makefile);
// Only targets, no variables to guard - OK
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_MAKE020_detects_complex_makefile() {
let makefile =
"# config.mk\nCC = gcc\nCXX = g++\nAR = ar\n\nCFLAGS = -Wall\nLDFLAGS = -L/usr/lib";
let result = check(makefile);
// Complex config file needs guard
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_MAKE020_no_warning_with_ifndef_anywhere() {
let makefile = "CC = gcc\n\nifndef DEBUG\nCFLAGS = -O2\nendif";
let result = check(makefile);
// Has ifndef (even if not a guard) - don't flag to avoid false positives
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_MAKE020_empty_makefile() {
let makefile = "";
let result = check(makefile);
// Empty Makefile doesn't need guard
assert_eq!(result.diagnostics.len(), 0);
}
}