use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct ErrorContext {
pub file: Option<PathBuf>,
pub macro_stack: Vec<String>,
pub include_stack: Vec<PathBuf>,
}
impl From<crate::eval::LocationContext> for ErrorContext {
fn from(loc: crate::eval::LocationContext) -> Self {
ErrorContext {
file: loc.file,
macro_stack: loc.macro_stack,
include_stack: loc.include_stack,
}
}
}
impl core::fmt::Display for ErrorContext {
fn fmt(
&self,
f: &mut core::fmt::Formatter<'_>,
) -> core::fmt::Result {
if !self.macro_stack.is_empty() {
writeln!(f, " Macro stack:")?;
for (i, macro_name) in self.macro_stack.iter().rev().enumerate() {
if i == 0 {
writeln!(f, " in macro: {}", macro_name)?;
} else {
writeln!(f, " called from: {}", macro_name)?;
}
}
}
if !self.include_stack.is_empty() {
writeln!(f, " File stack:")?;
for (i, file_path) in self.include_stack.iter().rev().enumerate() {
let file_str = file_path.to_str().unwrap_or("???");
if i == 0 {
writeln!(f, " in file: {}", file_str)?;
} else {
writeln!(f, " included from: {}", file_str)?;
}
}
} else if let Some(file) = &self.file {
writeln!(f, " File: {}", file.display())?;
}
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum XacroError {
#[error("{source}\n\nContext:\n{context}")]
WithContext {
#[source]
source: Box<XacroError>,
context: ErrorContext,
},
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("XML error: {0}")]
Xml(#[from] xmltree::ParseError),
#[error("Include error: {0}")]
Include(String),
#[error("Macro error: {0}")]
UndefinedMacro(String),
#[error("Missing parameter '{param}' in macro '{macro_name}'")]
MissingParameter { macro_name: String, param: String },
#[error("Missing attribute '{attribute}' in element '{element}'")]
MissingAttribute { element: String, attribute: String },
#[error("Macro error: {0}")]
PropertyNotFound(String),
#[error("Evaluation error in '{expr}': {source}")]
EvalError {
expr: String,
#[source]
source: crate::eval::EvalError,
},
#[error("XML write error: {0}")]
XmlWrite(#[from] xmltree::Error),
#[error("UTF-8 conversion error: {0}")]
Utf8(#[from] std::string::FromUtf8Error),
#[error("Macro recursion limit exceeded: depth {depth} > {limit} (possible infinite loop)")]
MacroRecursionLimit { depth: usize, limit: usize },
#[error("Block parameter '{param}' cannot have a default value")]
BlockParameterWithDefault { param: String },
#[error("Invalid parameter name: '{param}' (parameter names cannot be empty)")]
InvalidParameterName { param: String },
#[error("Unbalanced quote in macro parameters: unclosed {quote_char} quote in '{params_str}'")]
UnbalancedQuote {
quote_char: char,
params_str: String,
},
#[error("Missing block parameter '{param}' in macro '{macro_name}'")]
MissingBlockParameter { macro_name: String, param: String },
#[error("Unused block in macro '{macro_name}' (provided {extra_count} extra child elements)")]
UnusedBlock {
macro_name: String,
extra_count: usize,
},
#[error("Undefined block '{name}'")]
UndefinedBlock { name: String },
#[error("Duplicate parameter declaration: '{param}'\n\nParameter names must be unique. Duplicate parameters are ambiguous and can lead to\nunexpected behavior in other xacro implementations.\n\nTo accept duplicates (last declaration wins), use:\n xacro --compat <file>")]
DuplicateParamDeclaration { param: String },
#[error("Block parameter '{param}' cannot be specified as an attribute (it must be provided as a child element)")]
BlockParameterAttributeCollision { param: String },
#[error("Invalid macro parameter '{param}': {reason}")]
InvalidMacroParameter { param: String, reason: String },
#[error("Invalid forward syntax in parameter '{param}': {hint}")]
InvalidForwardSyntax { param: String, hint: String },
#[error("Macro '{macro_name}' parameter '{param}' declared with ^ to forward '{forward_name}' but not found in parent scope")]
UndefinedPropertyToForward {
macro_name: String,
param: String,
forward_name: String,
},
#[error(
"Invalid scope attribute '{scope}' for property '{property}': must be 'parent' or 'global'"
)]
InvalidScopeAttribute { property: String, scope: String },
#[cfg(feature = "yaml")]
#[error("Failed to load YAML file '{path}': {source}")]
YamlLoadError {
path: String,
#[source]
source: std::io::Error,
},
#[cfg(feature = "yaml")]
#[error("Failed to parse YAML file '{path}': {message}")]
YamlParseError { path: String, message: String },
#[cfg(not(feature = "yaml"))]
#[error(
"load_yaml() requires 'yaml' feature.\n\
\n\
To enable YAML support, rebuild with:\n\
cargo build --features yaml"
)]
YamlFeatureDisabled,
#[error("Unimplemented xacro feature: {0}")]
UnimplementedFeature(String),
#[error("Missing xacro namespace declaration: {0}")]
MissingNamespace(String),
#[error("Circular property dependency detected: {chain}")]
CircularPropertyDependency { chain: String },
#[error("Undefined property: '{0}'")]
UndefinedProperty(String),
#[error(
"Undefined argument: '{name}'.\n\
\n\
To fix this:\n\
1. Define it in XML: <xacro:arg name=\"{name}\" default=\"...\"/>\n\
2. Or pass it via CLI: {name}:=value"
)]
UndefinedArgument { name: String },
#[error("Unknown extension type: '$({} ...)'", ext_type)]
UnknownExtension { ext_type: String },
#[error(
"Failed to resolve extension: '$({})'.\n\
\n\
{}",
content,
reason
)]
InvalidExtension { content: String, reason: String },
#[error("Property substitution exceeded maximum depth of {depth} iterations. Remaining unresolved expressions in: {snippet}")]
MaxSubstitutionDepth { depth: usize, snippet: String },
#[error("Invalid root element: {0}")]
InvalidRoot(String),
#[error("Invalid XML: {0}")]
InvalidXml(String),
}
pub use crate::eval::EvalError;
impl From<crate::eval::EvalError> for XacroError {
fn from(e: crate::eval::EvalError) -> Self {
XacroError::EvalError {
expr: match &e {
crate::eval::EvalError::PyishEval { expr, .. } => expr.clone(),
crate::eval::EvalError::InvalidBoolean { condition, .. } => condition.clone(),
},
source: e,
}
}
}
impl XacroError {
pub fn with_context(
self,
context: ErrorContext,
) -> Self {
if matches!(self, XacroError::WithContext { .. }) {
return self;
}
XacroError::WithContext {
source: Box::new(self),
context,
}
}
}
pub trait EnrichError<T> {
fn with_loc(
self,
loc: &crate::eval::LocationContext,
) -> Result<T, XacroError>;
}
impl<T> EnrichError<T> for Result<T, XacroError> {
fn with_loc(
self,
loc: &crate::eval::LocationContext,
) -> Result<T, XacroError> {
self.map_err(|e| e.with_context(loc.clone().into()))
}
}
pub use crate::directives::{IMPLEMENTED_FEATURES, UNIMPLEMENTED_FEATURES};
pub fn unimplemented_feature_error(feature: &str) -> XacroError {
XacroError::UnimplementedFeature(format!(
"<xacro:{}> is not implemented yet.\n\
\n\
Currently implemented: {}\n\
Not yet implemented: {}",
feature,
IMPLEMENTED_FEATURES.join(", "),
UNIMPLEMENTED_FEATURES.join(", ")
))
}