pub struct UndefinedVariableRule;
impl Default for UndefinedVariableRule {
fn default() -> Self {
Self
}
}
impl MakefileRule for UndefinedVariableRule {
fn id(&self) -> &'static str {
"undefinedvariable"
}
fn check(&self, ast: &MakefileAst) -> Vec<Violation> {
let mut violations = Vec::new();
let mut defined_vars = HashSet::new();
for (name, _, _) in ast.get_variables() {
defined_vars.insert(name.clone());
}
for builtin in &["CC", "CXX", "CFLAGS", "LDFLAGS", "MAKE", "SHELL", "PWD"] {
defined_vars.insert((*builtin).to_string());
}
for node in &ast.nodes {
match &node.data {
NodeData::Variable { value, .. } => {
check_undefined_in_text(value, &defined_vars, &mut violations, node.span);
}
NodeData::Recipe { lines } => {
for line in lines {
check_undefined_in_text(
&line.text,
&defined_vars,
&mut violations,
node.span,
);
}
}
_ => {}
}
}
violations
}
}
#[derive(Debug)]
struct VariableRef {
name: String,
position: usize,
ref_type: VarRefType,
}
#[derive(Debug, PartialEq)]
enum VarRefType {
Parenthesized, Braced, Single, }
struct VariableScanner<'a> {
text: &'a str,
bytes: &'a [u8],
position: usize,
}
fn check_undefined_in_text(
text: &str,
defined_vars: &HashSet<String>,
violations: &mut Vec<Violation>,
span: SourceSpan,
) {
let scanner = VariableScanner::new(text);
for var_ref in scanner {
if should_check_variable(&var_ref) && !defined_vars.contains(&var_ref.name) {
violations.push(create_undefined_violation(&var_ref.name, span));
}
}
}
impl<'a> VariableScanner<'a> {
fn new(text: &'a str) -> Self {
Self {
text,
bytes: text.as_bytes(),
position: 0,
}
}
fn find_next_dollar(&mut self) -> Option<usize> {
while self.position < self.bytes.len() {
if self.bytes[self.position] == b'$' {
return Some(self.position);
}
self.position += 1;
}
None
}
fn parse_parenthesized_var(&mut self, start: usize) -> Option<VariableRef> {
let content_start = start + 2;
if content_start >= self.text.len() {
return None;
}
let remaining = self.text.get(content_start..).unwrap_or_default();
if let Some(end) = remaining.find(')') {
let var_content = remaining.get(..end).unwrap_or_default();
let var_name = extract_var_name(var_content);
self.position = content_start + end + 1;
Some(VariableRef {
name: var_name,
position: start,
ref_type: VarRefType::Parenthesized,
})
} else {
None
}
}
fn parse_braced_var(&mut self, start: usize) -> Option<VariableRef> {
let content_start = start + 2;
if content_start >= self.text.len() {
return None;
}
let remaining = self.text.get(content_start..).unwrap_or_default();
if let Some(end) = remaining.find('}') {
let var_name = remaining.get(..end).unwrap_or_default().to_string();
self.position = content_start + end + 1;
Some(VariableRef {
name: var_name,
position: start,
ref_type: VarRefType::Braced,
})
} else {
None
}
}
fn parse_single_char_var(&mut self, start: usize) -> Option<VariableRef> {
if start + 1 >= self.bytes.len() {
return None;
}
let ch = self.bytes[start + 1];
if ch.is_ascii_alphanumeric() || ch == b'_' {
let var_name = std::str::from_utf8(&[ch])
.expect("internal error")
.to_string();
self.position = start + 2;
Some(VariableRef {
name: var_name,
position: start,
ref_type: VarRefType::Single,
})
} else {
None
}
}
}
impl Iterator for VariableScanner<'_> {
type Item = VariableRef;
fn next(&mut self) -> Option<Self::Item> {
loop {
let dollar_pos = self.find_next_dollar()?;
if dollar_pos + 1 >= self.bytes.len() {
return None;
}
let next_char = self.bytes[dollar_pos + 1];
if next_char == b'$' {
self.position = dollar_pos + 2;
continue;
}
let var_ref = match next_char {
b'(' => self.parse_parenthesized_var(dollar_pos),
b'{' => self.parse_braced_var(dollar_pos),
_ => self.parse_single_char_var(dollar_pos),
};
if let Some(ref_) = var_ref {
return Some(ref_);
}
self.position = dollar_pos + 1;
}
}
}
fn extract_from_default_value(var_content: &str) -> Option<String> {
if var_content.contains(":-") {
if let Some(pos) = var_content.find(":-") {
return Some(
var_content
.get(..pos)
.unwrap_or_default()
.trim()
.to_string(),
);
}
}
None
}
fn extract_from_alternative_value(var_content: &str) -> Option<String> {
if var_content.contains(":+") {
if let Some(pos) = var_content.find(":+") {
return Some(
var_content
.get(..pos)
.unwrap_or_default()
.trim()
.to_string(),
);
}
}
None
}
fn extract_from_pattern_substitution(var_content: &str) -> Option<String> {
if let Some(colon_pos) = var_content.find(':') {
let before_colon = var_content.get(..colon_pos).unwrap_or_default();
if !contains_shell_indicators(before_colon) {
return Some(before_colon.trim().to_string());
}
}
None
}
fn contains_shell_indicators(text: &str) -> bool {
text.contains(' ') || text.contains('|') || text.contains('{')
}
fn contains_shell_operators(var_content: &str) -> bool {
var_content.contains('|') || var_content.contains('>') || var_content.contains('<')
}
fn extract_var_name(var_content: &str) -> String {
if let Some(var_name) = extract_from_default_value(var_content) {
return var_name;
}
if let Some(var_name) = extract_from_alternative_value(var_content) {
return var_name;
}
if let Some(var_name) = extract_from_pattern_substitution(var_content) {
return var_name;
}
if contains_shell_operators(var_content) {
return String::new(); }
var_content.trim().to_string()
}
fn should_check_variable(var_ref: &VariableRef) -> bool {
if var_ref.name.is_empty() {
return false;
}
if is_automatic_var(&var_ref.name) {
return false;
}
if var_ref.ref_type == VarRefType::Parenthesized && is_function_call(&var_ref.name) {
return false;
}
if var_ref.name.contains(' ') || var_ref.name.contains(';') || var_ref.name.contains('&') {
return false;
}
if var_ref.name.len() == 1 && var_ref.name.chars().all(char::is_lowercase) {
return false;
}
true
}
fn create_undefined_violation(var_name: &str, span: SourceSpan) -> Violation {
Violation {
rule: "undefinedvariable".to_string(),
severity: Severity::Warning,
span,
message: format!("Variable '{var_name}' may be undefined"),
fix_hint: Some(format!("Define '{var_name}' before use")),
}
}
fn is_automatic_var(var: &str) -> bool {
matches!(var, "@" | "<" | "^" | "?" | "*" | "%" | "+" | "|" | "$")
}
fn is_function_call(text: &str) -> bool {
const FUNCTION_PREFIXES: &[&str] = &[
"shell ",
"wildcard ",
"patsubst ",
"subst ",
"strip ",
"findstring ",
"filter ",
"sort ",
"word ",
"dir ",
"notdir ",
"suffix ",
"basename ",
"addprefix ",
"addsuffix ",
"join ",
"foreach ",
"if ",
"or ",
"and ",
"call ",
"eval ",
"origin ",
"error ",
"warning ",
"info ",
];
FUNCTION_PREFIXES
.iter()
.any(|prefix| text.starts_with(prefix))
}