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)]
mod tests {
use super::*;
#[test]
fn test_source_location_new() {
let loc = SourceLocation::new(42);
assert_eq!(loc.line, 42);
assert!(loc.file.is_none());
assert!(loc.column.is_none());
assert!(loc.source_line.is_none());
}
#[test]
fn test_source_location_with_file() {
let loc = SourceLocation::new(10).with_file("Makefile".to_string());
assert_eq!(loc.file, Some("Makefile".to_string()));
assert_eq!(loc.line, 10);
}
#[test]
fn test_source_location_with_column() {
let loc = SourceLocation::new(5).with_column(15);
assert_eq!(loc.column, Some(15));
}
#[test]
fn test_source_location_with_source_line() {
let loc = SourceLocation::new(1).with_source_line("CC := gcc".to_string());
assert_eq!(loc.source_line, Some("CC := gcc".to_string()));
}
#[test]
fn test_source_location_chained_builder() {
let loc = SourceLocation::new(42)
.with_file("test.mk".to_string())
.with_column(8)
.with_source_line("ifeq ($(X),Y)".to_string());
assert_eq!(loc.line, 42);
assert_eq!(loc.file, Some("test.mk".to_string()));
assert_eq!(loc.column, Some(8));
assert_eq!(loc.source_line, Some("ifeq ($(X),Y)".to_string()));
}
#[test]
fn test_source_location_display_no_file() {
let loc = SourceLocation::new(15);
let display = format!("{}", loc);
assert_eq!(display, "line 15");
}
#[test]
fn test_source_location_display_with_file() {
let loc = SourceLocation::new(15).with_file("Makefile".to_string());
let display = format!("{}", loc);
assert_eq!(display, "Makefile:15");
}
#[test]
fn test_source_location_display_with_column() {
let loc = SourceLocation::new(15)
.with_file("Makefile".to_string())
.with_column(8);
let display = format!("{}", loc);
assert_eq!(display, "Makefile:15:8");
}
#[test]
fn test_source_location_display_no_file_with_column() {
let loc = SourceLocation::new(15).with_column(8);
let display = format!("{}", loc);
assert_eq!(display, "line 15:8");
}
#[test]
fn test_source_location_equality() {
let loc1 = SourceLocation::new(10).with_file("a.mk".to_string());
let loc2 = SourceLocation::new(10).with_file("a.mk".to_string());
let loc3 = SourceLocation::new(20).with_file("a.mk".to_string());
assert_eq!(loc1, loc2);
assert_ne!(loc1, loc3);
}
#[test]
fn test_quality_score_minimum() {
let error = MakeParseError::UnexpectedEof;
let score = error.quality_score();
assert!(score >= 0.7, "Score {} should be ≥0.7", score);
assert!(score < 0.75, "Score {} should be <0.75", score);
}
#[test]
fn test_quality_score_with_location() {
let location = SourceLocation::new(15);
let error = MakeParseError::EmptyVariableName { location };
let score = error.quality_score();
assert!(score >= 0.73, "Score {} should be ≥0.73", score);
assert!(score < 0.75, "Score {} should be <0.75", score);
}
#[test]
fn test_quality_score_with_file_and_column() {
let location = SourceLocation::new(15)
.with_file("Makefile".to_string())
.with_column(8);
let error = MakeParseError::EmptyTargetName { location };
let score = error.quality_score();
assert!(score >= 0.88, "Score {} should be ≥0.88", score);
assert!(score < 0.89, "Score {} should be <0.89", score);
}
#[test]
fn test_quality_score_with_snippet() {
let location = SourceLocation::new(15)
.with_file("Makefile".to_string())
.with_column(8)
.with_source_line("ifeq $(VAR) value".to_string());
let error = MakeParseError::InvalidConditionalSyntax {
location,
directive: "ifeq".to_string(),
found: "$(VAR) value".to_string(),
};
let score = error.quality_score();
}
}
include!("error_part2_incl2.rs");