use std::fmt;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq)]
pub struct SourceLocation {
pub file: Option<String>,
pub line: usize,
pub column: Option<usize>,
pub source_line: Option<String>,
}
impl SourceLocation {
pub fn new(line: usize) -> Self {
Self {
file: None,
line,
column: None,
source_line: None,
}
}
pub fn with_file(mut self, file: String) -> Self {
self.file = Some(file);
self
}
pub fn with_column(mut self, column: usize) -> Self {
self.column = Some(column);
self
}
pub fn with_source_line(mut self, source_line: String) -> Self {
self.source_line = Some(source_line);
self
}
}
impl fmt::Display for SourceLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(file) = &self.file {
write!(f, "{}:{}", file, self.line)?;
} else {
write!(f, "line {}", self.line)?;
}
if let Some(col) = self.column {
write!(f, ":{}", col)?;
}
Ok(())
}
}
#[derive(Error, Debug)]
pub enum MakeParseError {
#[error("Invalid variable assignment at {location}")]
InvalidVariableAssignment {
location: SourceLocation,
found: String,
},
#[error("Empty variable name at {location}")]
EmptyVariableName { location: SourceLocation },
#[error("No assignment operator found at {location}")]
NoAssignmentOperator {
location: SourceLocation,
found: String,
},
#[error("Invalid include syntax at {location}")]
InvalidIncludeSyntax {
location: SourceLocation,
found: String,
},
#[error("Invalid conditional syntax at {location}")]
InvalidConditionalSyntax {
location: SourceLocation,
directive: String,
found: String,
},
#[error("Conditional requires arguments at {location}")]
MissingConditionalArguments {
location: SourceLocation,
directive: String,
expected_args: usize,
found_args: usize,
},
#[error("Missing variable name in {directive} at {location}")]
MissingVariableName {
location: SourceLocation,
directive: String,
},
#[error("Unknown conditional directive at {location}")]
UnknownConditional {
location: SourceLocation,
found: String,
},
#[error("Invalid target rule syntax at {location}")]
InvalidTargetRule {
location: SourceLocation,
found: String,
},
#[error("Empty target name at {location}")]
EmptyTargetName { location: SourceLocation },
#[error("Unterminated define block for variable '{var_name}' at {location}")]
UnterminatedDefine {
location: SourceLocation,
var_name: String,
},
#[error("Unexpected end of file")]
UnexpectedEof,
}
impl MakeParseError {
pub fn location(&self) -> Option<&SourceLocation> {
match self {
Self::InvalidVariableAssignment { location, .. } => Some(location),
Self::EmptyVariableName { location } => Some(location),
Self::NoAssignmentOperator { location, .. } => Some(location),
Self::InvalidIncludeSyntax { location, .. } => Some(location),
Self::InvalidConditionalSyntax { location, .. } => Some(location),
Self::MissingConditionalArguments { location, .. } => Some(location),
Self::MissingVariableName { location, .. } => Some(location),
Self::UnknownConditional { location, .. } => Some(location),
Self::InvalidTargetRule { location, .. } => Some(location),
Self::EmptyTargetName { location } => Some(location),
Self::UnterminatedDefine { location, .. } => Some(location),
Self::UnexpectedEof => None,
}
}
pub fn note(&self) -> String {
match self {
Self::InvalidVariableAssignment { .. } => {
"Variable assignments must use one of the assignment operators: =, :=, ?=, +=, !=".to_string()
}
Self::EmptyVariableName { .. } => {
"Variable names cannot be empty. A valid variable name must contain at least one character.".to_string()
}
Self::NoAssignmentOperator { .. } => {
"Variable assignments require an assignment operator (=, :=, ?=, +=, or !=)".to_string()
}
Self::InvalidIncludeSyntax { .. } => {
"Include directives must be: 'include file', '-include file', or 'sinclude file'".to_string()
}
Self::InvalidConditionalSyntax { directive, .. } => {
match directive.as_str() {
"ifeq" | "ifneq" => {
format!("{} requires arguments in parentheses with a comma separator", directive)
}
"ifdef" | "ifndef" => {
format!("{} requires a variable name argument", directive)
}
_ => "Conditional directives must follow GNU Make syntax".to_string(),
}
}
Self::MissingConditionalArguments { directive, expected_args, found_args, .. } => {
format!("{} requires {} argument(s), but found {}", directive, expected_args, found_args)
}
Self::MissingVariableName { directive, .. } => {
format!("{} requires a variable name to test", directive)
}
Self::UnknownConditional { .. } => {
"Supported conditional directives are: ifeq, ifneq, ifdef, ifndef".to_string()
}
Self::InvalidTargetRule { .. } => {
"Target rules must have the format: target: prerequisites".to_string()
}
Self::EmptyTargetName { .. } => {
"Target names cannot be empty. A valid target must have a name before the colon.".to_string()
}
Self::UnterminatedDefine { .. } => {
"define blocks must be terminated with 'endef'".to_string()
}
Self::UnexpectedEof => {
"The Makefile ended unexpectedly. Check for unclosed conditional blocks or incomplete rules.".to_string()
}
}
}
pub fn help(&self) -> String {
match self {
Self::InvalidVariableAssignment { .. } => {
"Example: VAR = value\n VAR := value\n VAR ?= value".to_string()
}
Self::EmptyVariableName { .. } => {
"Provide a variable name before the assignment operator.\nExample: MY_VAR = value".to_string()
}
Self::NoAssignmentOperator { .. } => {
"Use one of the following assignment operators:\n = (recursive expansion)\n := (simple expansion)\n ?= (conditional assignment)\n += (append)\n != (shell assignment)".to_string()
}
Self::InvalidIncludeSyntax { .. } => {
"Use: include filename.mk\nOr for optional includes:\n -include filename.mk\n sinclude filename.mk".to_string()
}
Self::InvalidConditionalSyntax { directive, .. } => {
match directive.as_str() {
"ifeq" => "Use: ifeq ($(VAR),value)\nOr: ifeq (arg1,arg2)".to_string(),
"ifneq" => "Use: ifneq ($(VAR),value)\nOr: ifneq (arg1,arg2)".to_string(),
"ifdef" => "Use: ifdef VARIABLE_NAME".to_string(),
"ifndef" => "Use: ifndef VARIABLE_NAME".to_string(),
_ => "Check the GNU Make manual for conditional syntax".to_string(),
}
}
Self::MissingConditionalArguments { directive, .. } => {
match directive.as_str() {
"ifeq" | "ifneq" => format!("Use: {} (arg1,arg2)", directive),
"ifdef" | "ifndef" => format!("Use: {} VAR_NAME", directive),
_ => "Provide the required arguments for the conditional".to_string(),
}
}
Self::MissingVariableName { directive, .. } => {
format!("Provide a variable name after {}.\nExample: {} DEBUG", directive, directive)
}
Self::UnknownConditional { found, .. } => {
format!("Did you mean one of: ifeq, ifneq, ifdef, ifndef?\nFound: {}", found)
}
Self::InvalidTargetRule { .. } => {
"Use the format: target: prerequisite1 prerequisite2\nFollowed by tab-indented recipe lines".to_string()
}
Self::EmptyTargetName { .. } => {
"Provide a target name before the colon.\nExample: build: main.c\n\t$(CC) -o build main.c".to_string()
}
Self::UnterminatedDefine { .. } => {
"Ensure all define blocks are closed with 'endef'.\nExample:\ndefine VAR_NAME\ncontent\nendef".to_string()
}
Self::UnexpectedEof => {
"Ensure all conditional blocks (ifeq/ifdef/etc.) are closed with 'endif'.\nCheck that all target rules are complete.".to_string()
}
}
}
pub fn to_detailed_string(&self) -> String {
let mut output = String::new();
output.push_str("error: ");
output.push_str(&self.to_string());
output.push('\n');
if let Some(location) = self.location() {
if let Some(source_line) = &location.source_line {
output.push('\n');
output.push_str(&format!("{} | {}\n", location.line, source_line));
if let Some(col) = location.column {
let line_num_width = format!("{}", location.line).len();
let spaces = " ".repeat(line_num_width + 3 + col.saturating_sub(1));
output.push_str(&format!("{}^\n", spaces));
}
}
}
output.push('\n');
output.push_str("note: ");
output.push_str(&self.note());
output.push('\n');
output.push('\n');
output.push_str("help: ");
output.push_str(&self.help());
output.push('\n');
output
}
pub fn quality_score(&self) -> f32 {
let mut score = 0.0;
score += 1.0;
score += 2.5;
score += 2.5;
if let Some(location) = self.location() {
if location.file.is_some() {
score += 1.0;
}
score += 0.25;
if location.column.is_some() {
score += 0.25;
}
if location.source_line.is_some() {
score += 1.0;
}
}
score / 8.5 }
}
#[cfg(test)]
#[path = "error_tests_source_locat.rs"]
mod tests_extracted;