Skip to main content

xacro_rs/
error.rs

1use std::path::PathBuf;
2
3/// Context information for error reporting
4///
5/// Provides a snapshot of the processor state when an error occurs,
6/// including the macro call stack and file include hierarchy.
7#[derive(Debug, Clone)]
8pub struct ErrorContext {
9    /// Current file being processed
10    pub file: Option<PathBuf>,
11    /// Macro call stack (most recent last)
12    pub macro_stack: Vec<String>,
13    /// Include stack showing file hierarchy
14    pub include_stack: Vec<PathBuf>,
15}
16
17impl From<crate::eval::LocationContext> for ErrorContext {
18    fn from(loc: crate::eval::LocationContext) -> Self {
19        ErrorContext {
20            file: loc.file,
21            macro_stack: loc.macro_stack,
22            include_stack: loc.include_stack,
23        }
24    }
25}
26
27impl core::fmt::Display for ErrorContext {
28    fn fmt(
29        &self,
30        f: &mut core::fmt::Formatter<'_>,
31    ) -> core::fmt::Result {
32        // Print macro stack (reversed, most recent first)
33        if !self.macro_stack.is_empty() {
34            writeln!(f, "  Macro stack:")?;
35            for (i, macro_name) in self.macro_stack.iter().rev().enumerate() {
36                if i == 0 {
37                    writeln!(f, "    in macro: {}", macro_name)?;
38                } else {
39                    writeln!(f, "    called from: {}", macro_name)?;
40                }
41            }
42        }
43
44        // Print file/include stack (reversed, most recent first)
45        if !self.include_stack.is_empty() {
46            writeln!(f, "  File stack:")?;
47            for (i, file_path) in self.include_stack.iter().rev().enumerate() {
48                let file_str = file_path.to_str().unwrap_or("???");
49                if i == 0 {
50                    writeln!(f, "    in file: {}", file_str)?;
51                } else {
52                    writeln!(f, "    included from: {}", file_str)?;
53                }
54            }
55        } else if let Some(file) = &self.file {
56            writeln!(f, "  File: {}", file.display())?;
57        }
58
59        Ok(())
60    }
61}
62
63#[derive(Debug, thiserror::Error)]
64pub enum XacroError {
65    /// Error with added location context
66    ///
67    /// Wraps any other XacroError variant with location information (file, macro stack,
68    /// include stack). This allows errors to be enriched with context as they bubble up.
69    #[error("{source}\n\nContext:\n{context}")]
70    WithContext {
71        #[source]
72        source: Box<XacroError>,
73        context: ErrorContext,
74    },
75    #[error("IO error: {0}")]
76    Io(#[from] std::io::Error),
77
78    #[error("XML error: {0}")]
79    Xml(#[from] xmltree::ParseError),
80
81    #[error("Include error: {0}")]
82    Include(String),
83
84    #[error("Macro error: {0}")]
85    UndefinedMacro(String),
86
87    #[error("Missing parameter '{param}' in macro '{macro_name}'")]
88    MissingParameter { macro_name: String, param: String },
89
90    #[error("Missing attribute '{attribute}' in element '{element}'")]
91    MissingAttribute { element: String, attribute: String },
92
93    #[error("Macro error: {0}")]
94    PropertyNotFound(String),
95
96    #[error("Evaluation error in '{expr}': {source}")]
97    EvalError {
98        expr: String,
99        #[source]
100        source: crate::eval::EvalError,
101    },
102
103    #[error("XML write error: {0}")]
104    XmlWrite(#[from] xmltree::Error),
105
106    #[error("UTF-8 conversion error: {0}")]
107    Utf8(#[from] std::string::FromUtf8Error),
108
109    #[error("Macro recursion limit exceeded: depth {depth} > {limit} (possible infinite loop)")]
110    MacroRecursionLimit { depth: usize, limit: usize },
111
112    #[error("Block parameter '{param}' cannot have a default value")]
113    BlockParameterWithDefault { param: String },
114
115    #[error("Invalid parameter name: '{param}' (parameter names cannot be empty)")]
116    InvalidParameterName { param: String },
117
118    #[error("Unbalanced quote in macro parameters: unclosed {quote_char} quote in '{params_str}'")]
119    UnbalancedQuote {
120        quote_char: char,
121        params_str: String,
122    },
123
124    #[error("Missing block parameter '{param}' in macro '{macro_name}'")]
125    MissingBlockParameter { macro_name: String, param: String },
126
127    #[error("Unused block in macro '{macro_name}' (provided {extra_count} extra child elements)")]
128    UnusedBlock {
129        macro_name: String,
130        extra_count: usize,
131    },
132
133    #[error("Undefined block '{name}'")]
134    UndefinedBlock { name: String },
135
136    #[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>")]
137    DuplicateParamDeclaration { param: String },
138
139    #[error("Block parameter '{param}' cannot be specified as an attribute (it must be provided as a child element)")]
140    BlockParameterAttributeCollision { param: String },
141
142    #[error("Invalid macro parameter '{param}': {reason}")]
143    InvalidMacroParameter { param: String, reason: String },
144
145    #[error("Invalid forward syntax in parameter '{param}': {hint}")]
146    InvalidForwardSyntax { param: String, hint: String },
147
148    #[error("Macro '{macro_name}' parameter '{param}' declared with ^ to forward '{forward_name}' but not found in parent scope")]
149    UndefinedPropertyToForward {
150        macro_name: String,
151        param: String,
152        forward_name: String,
153    },
154
155    #[error(
156        "Invalid scope attribute '{scope}' for property '{property}': must be 'parent' or 'global'"
157    )]
158    InvalidScopeAttribute { property: String, scope: String },
159
160    /// YAML file loading failed
161    #[cfg(feature = "yaml")]
162    #[error("Failed to load YAML file '{path}': {source}")]
163    YamlLoadError {
164        path: String,
165        #[source]
166        source: std::io::Error,
167    },
168
169    /// YAML parsing failed
170    #[cfg(feature = "yaml")]
171    #[error("Failed to parse YAML file '{path}': {message}")]
172    YamlParseError { path: String, message: String },
173
174    /// YAML feature not enabled
175    #[cfg(not(feature = "yaml"))]
176    #[error(
177        "load_yaml() requires 'yaml' feature.\n\
178         \n\
179         To enable YAML support, rebuild with:\n\
180         cargo build --features yaml"
181    )]
182    YamlFeatureDisabled,
183
184    #[error("Unimplemented xacro feature: {0}")]
185    UnimplementedFeature(String),
186
187    #[error("Missing xacro namespace declaration: {0}")]
188    MissingNamespace(String),
189
190    /// Circular property dependency detected during lazy evaluation
191    ///
192    /// The `chain` field contains the dependency path formatted as "a -> b -> c -> a"
193    /// showing how the circular reference was formed.
194    #[error("Circular property dependency detected: {chain}")]
195    CircularPropertyDependency { chain: String },
196
197    #[error("Undefined property: '{0}'")]
198    UndefinedProperty(String),
199
200    /// Undefined argument accessed via $(arg name)
201    ///
202    /// The user tried to access an argument that was not defined in XML
203    /// and was not provided via CLI.
204    #[error(
205        "Undefined argument: '{name}'.\n\
206             \n\
207             To fix this:\n\
208             1. Define it in XML: <xacro:arg name=\"{name}\" default=\"...\"/>\n\
209             2. Or pass it via CLI: {name}:=value"
210    )]
211    UndefinedArgument { name: String },
212
213    /// Unknown extension type
214    ///
215    /// This error occurs when a $(command ...) substitution uses an unrecognized command.
216    /// The set of available extensions depends on how the processor was configured.
217    ///
218    /// Core extensions (always available):
219    /// - $(arg name)  - Access xacro argument
220    /// - $(cwd)       - Get current working directory
221    /// - $(env VAR)   - Get environment variable
222    ///
223    /// Additional extensions may be available if explicitly added via builder pattern.
224    #[error("Unknown extension type: '$({} ...)'", ext_type)]
225    UnknownExtension { ext_type: String },
226
227    /// Extension resolution failed
228    #[error(
229        "Failed to resolve extension: '$({})'.\n\
230             \n\
231             {}",
232        content,
233        reason
234    )]
235    InvalidExtension { content: String, reason: String },
236
237    /// Property substitution exceeded maximum depth
238    ///
239    /// Indicates that iterative property substitution did not converge within the
240    /// allowed number of iterations. This usually means circular or self-referential
241    /// property definitions that cannot be fully resolved.
242    #[error("Property substitution exceeded maximum depth of {depth} iterations. Remaining unresolved expressions in: {snippet}")]
243    MaxSubstitutionDepth { depth: usize, snippet: String },
244
245    /// Invalid root element after expansion
246    ///
247    /// The root element must expand to exactly one element node. This error indicates
248    /// that expansion resulted in multiple nodes, zero nodes, or a non-element node.
249    #[error("Invalid root element: {0}")]
250    InvalidRoot(String),
251
252    /// Invalid XML content
253    ///
254    /// The content violates XML specification rules (e.g., forbidden sequences in comments,
255    /// CDATA sections, or processing instructions).
256    #[error("Invalid XML: {0}")]
257    InvalidXml(String),
258}
259
260// Implement From trait for EvalError to avoid duplicated error mapping
261pub use crate::eval::EvalError;
262impl From<crate::eval::EvalError> for XacroError {
263    fn from(e: crate::eval::EvalError) -> Self {
264        XacroError::EvalError {
265            expr: match &e {
266                crate::eval::EvalError::PyishEval { expr, .. } => expr.clone(),
267                crate::eval::EvalError::InvalidBoolean { condition, .. } => condition.clone(),
268            },
269            source: e,
270        }
271    }
272}
273
274impl XacroError {
275    /// Enrich this error with location context
276    ///
277    /// Wraps the error in a WithContext variant that adds file, macro stack, and include
278    /// stack information. This is useful for providing better error messages that show
279    /// where the error occurred.
280    ///
281    /// # Example
282    ///
283    /// ```ignore
284    /// let err = XacroError::UndefinedProperty("foo".to_string());
285    /// let enriched = err.with_context(location_ctx.to_error_context());
286    /// ```
287    pub fn with_context(
288        self,
289        context: ErrorContext,
290    ) -> Self {
291        // Don't double-wrap WithContext errors
292        if matches!(self, XacroError::WithContext { .. }) {
293            return self;
294        }
295        XacroError::WithContext {
296            source: Box::new(self),
297            context,
298        }
299    }
300}
301
302/// Trait extension for enriching errors with location context
303///
304/// Provides a concise `.with_loc(&loc)?` syntax for attaching location context to errors.
305/// Similar to anyhow's `.context()` method but specifically for xacro error context.
306pub trait EnrichError<T> {
307    /// Enrich an error Result with location context
308    ///
309    /// # Example
310    ///
311    /// ```ignore
312    /// let loc = ctx.get_location_context();
313    /// let result = ctx.properties.substitute_all(val, Some(&loc)).with_loc(&loc)?;
314    /// ```
315    fn with_loc(
316        self,
317        loc: &crate::eval::LocationContext,
318    ) -> Result<T, XacroError>;
319}
320
321impl<T> EnrichError<T> for Result<T, XacroError> {
322    fn with_loc(
323        self,
324        loc: &crate::eval::LocationContext,
325    ) -> Result<T, XacroError> {
326        self.map_err(|e| e.with_context(loc.clone().into()))
327    }
328}
329
330// Feature lists for consistent error messages
331// Re-exported from directives module (single source of truth)
332pub use crate::directives::{IMPLEMENTED_FEATURES, UNIMPLEMENTED_FEATURES};
333
334/// Helper function to create consistent UnimplementedFeature error messages
335pub fn unimplemented_feature_error(feature: &str) -> XacroError {
336    XacroError::UnimplementedFeature(format!(
337        "<xacro:{}> is not implemented yet.\n\
338         \n\
339         Currently implemented: {}\n\
340         Not yet implemented: {}",
341        feature,
342        IMPLEMENTED_FEATURES.join(", "),
343        UNIMPLEMENTED_FEATURES.join(", ")
344    ))
345}