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
//! MAKE019: Environment variable pollution
//!
//! **Rule**: Detect unnecessary export statements that pollute the environment
//!
//! **Why this matters**:
//! Using `export` in Make exports variables to all sub-processes, which can:
//! - Pollute the environment with unnecessary variables
//! - Slow down Make (exported vars passed to every command)
//! - Cause unexpected behavior in subprocesses
//! - Make builds less reproducible
//!
//! Only export variables that subprocesses actually need.
//!
//! **Auto-fix**: Remove export keyword (keep variable assignment)
//!
//! ## Examples
//!
//! ❌ **BAD** (unnecessary export):
//! ```makefile
//! export CC = gcc
//! export CFLAGS = -Wall
//! ```
//!
//! ✅ **GOOD** (no export - variables only for Make):
//! ```makefile
//! CC = gcc
//! CFLAGS = -Wall
//! ```
//!
//! ✅ **GOOD** (export when needed by subprocesses):
//! ```makefile
//! export PATH := $(PATH):/usr/local/bin
//! ```
use crate::linter::{Diagnostic, Fix, LintResult, Severity, Span};
/// Variables that commonly should NOT be exported (Make-internal)
const INTERNAL_VARS: &[&str] = &[
"CC", "CXX", "AR", "LD", "AS", // Compilers/linkers
"CFLAGS", "CXXFLAGS", "LDFLAGS", // Compiler flags
"SOURCES", "OBJECTS", "TARGET", // Build artifacts
"PREFIX", "DESTDIR", "BINDIR", // Installation paths
];
/// Check for unnecessary export statements
pub fn check(source: &str) -> LintResult {
let mut result = LintResult::new();
for (line_num, line) in source.lines().enumerate() {
// Check if line starts with "export "
let trimmed = line.trim();
if !trimmed.starts_with("export ") {
continue;
}
// Extract variable name from export statement
if let Some(var_name) = extract_var_name(trimmed) {
// Check if this is an internal variable that shouldn't be exported
if is_internal_variable(&var_name) {
let span = Span::new(line_num + 1, 1, line_num + 1, line.len());
let fix_replacement = create_fix(line);
let diag = Diagnostic::new(
"MAKE019",
Severity::Warning,
format!("Unnecessary export of '{}' - variable is Make-internal and doesn't need to be in environment", var_name),
span,
)
.with_fix(Fix::new(&fix_replacement));
result.add(diag);
}
}
}
result
}
/// Extract variable name from export statement
/// e.g., "export CC = gcc" → "CC"
fn extract_var_name(line: &str) -> Option<String> {
// Remove "export " prefix
let after_export = line.strip_prefix("export ")?;
// Extract variable name (before = or :=)
if let Some(eq_pos) = after_export.find('=') {
let var_name = after_export[..eq_pos].trim();
return Some(var_name.to_string());
}
None
}
/// Check if variable is internal (shouldn't be exported)
fn is_internal_variable(var_name: &str) -> bool {
// Check against list of known internal variables
for internal_var in INTERNAL_VARS {
if var_name == *internal_var {
return true;
}
}
// PATH and other environment variables should be allowed to export
// Only flag Make-specific build variables
false
}
/// Create a fix by removing "export " keyword
fn create_fix(line: &str) -> String {
line.replace("export ", "")
}
#[cfg(test)]
mod tests {
use super::*;
// RED PHASE: Write failing tests first
#[test]
fn test_MAKE019_detects_exported_cc() {
let makefile = "export CC = gcc\n\nall:\n\t$(CC) main.c";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 1);
let diag = &result.diagnostics[0];
assert_eq!(diag.code, "MAKE019");
assert_eq!(diag.severity, Severity::Warning);
assert!(
diag.message.to_lowercase().contains("export")
|| diag.message.to_lowercase().contains("variable")
);
}
#[test]
fn test_MAKE019_detects_exported_cflags() {
let makefile = "export CFLAGS = -Wall\n\nall:\n\t$(CC) $(CFLAGS) main.c";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_MAKE019_detects_multiple_exports() {
let makefile = "export CC = gcc\nexport CFLAGS = -Wall\n\nall:\n\t$(CC) $(CFLAGS) main.c";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 2);
}
#[test]
fn test_MAKE019_provides_fix() {
let makefile = "export CC = gcc\n\nall:\n\t$(CC) main.c";
let result = check(makefile);
assert!(result.diagnostics[0].fix.is_some());
let fix = result.diagnostics[0].fix.as_ref().unwrap();
// Fix should remove export keyword
assert!(!fix.replacement.contains("export CC"));
assert!(fix.replacement.contains("CC = gcc"));
}
#[test]
fn test_MAKE019_no_warning_for_non_exported() {
let makefile = "CC = gcc\nCFLAGS = -Wall\n\nall:\n\t$(CC) $(CFLAGS) main.c";
let result = check(makefile);
// No export - OK
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_MAKE019_no_warning_for_path() {
let makefile = "export PATH := $(PATH):/usr/local/bin\n\nall:\n\t./script.sh";
let result = check(makefile);
// PATH export is OK (commonly needed by subprocesses)
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_MAKE019_detects_exported_sources() {
let makefile = "export SOURCES = main.c utils.c\n\nall:\n\t$(CC) $(SOURCES)";
let result = check(makefile);
// SOURCES is Make-internal, shouldn't be exported
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_MAKE019_empty_makefile() {
let makefile = "";
let result = check(makefile);
assert_eq!(result.diagnostics.len(), 0);
}
}