1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
//! Expansion state and context management
use crate::{
error::XacroError, eval::EvalContext, extensions::ExtensionHandler,
parse::macro_def::MacroDefinition, processor::CompatMode,
};
use core::cell::RefCell;
use std::{collections::HashMap, path::PathBuf, rc::Rc};
use xmltree::XMLNode;
pub struct XacroContext {
/// Property processor with scope support
pub properties: EvalContext,
/// Macro definitions wrapped in Rc - uses RefCell for interior mutability
pub macros: RefCell<HashMap<String, Rc<MacroDefinition>>>,
/// CLI arguments (shared with EvalContext for $(arg) resolution)
/// Wrapped in Rc<RefCell<...>> for shared mutable access
pub args: Rc<RefCell<HashMap<String, String>>>,
/// Include stack for circular include detection (uses RefCell for interior mutability)
pub include_stack: RefCell<Vec<PathBuf>>,
/// All included files (for --deps output)
pub all_includes: RefCell<Vec<PathBuf>>,
/// Namespace stack: (file_path, xacro_namespace_prefix) (uses RefCell for interior mutability)
pub namespace_stack: RefCell<Vec<(PathBuf, String)>>,
/// Block stack for insert_block arguments (uses RefCell for interior mutability)
/// Stores pre-expanded XMLNode content for each block parameter
pub block_stack: RefCell<Vec<HashMap<String, Vec<XMLNode>>>>,
/// Current base path for resolving relative includes (uses RefCell for interior mutability)
pub base_path: RefCell<PathBuf>,
/// Current overall recursion depth (uses RefCell for interior mutability to enable RAII guards)
pub recursion_depth: RefCell<usize>,
/// Maximum recursion depth before triggering error
/// Set conservatively to prevent stack overflow before the check triggers
pub max_recursion_depth: usize,
/// Macro call stack for error reporting and debugging (uses RefCell for interior mutability)
/// Tracks which macro called which (most recent last)
pub macro_call_stack: RefCell<Vec<String>>,
/// Python xacro compatibility modes
pub compat_mode: CompatMode,
}
impl XacroContext {
/// Default maximum recursion depth
/// Set conservatively to prevent stack overflow before the check triggers
pub const DEFAULT_MAX_DEPTH: usize = 50;
/// Create a new context with the given base path (for testing).
///
/// This is a minimal constructor used by unit tests. Production code should use
/// `new_with_extensions()` which properly integrates with the extension system.
#[cfg(test)]
pub(crate) fn new(
base_path: PathBuf,
xacro_ns: String,
) -> Self {
let args = Rc::new(RefCell::new(HashMap::new()));
let extensions = Rc::new(Vec::new()); // Empty extensions for tests
XacroContext {
properties: EvalContext::new_with_extensions(
args.clone(),
extensions,
#[cfg(feature = "yaml")]
Rc::new(crate::eval::yaml_tag_handler::YamlTagHandlerRegistry::new()),
),
macros: RefCell::new(HashMap::new()),
args,
include_stack: RefCell::new(Vec::new()),
all_includes: RefCell::new(Vec::new()),
namespace_stack: RefCell::new(vec![(base_path.clone(), xacro_ns)]),
block_stack: RefCell::new(Vec::new()),
base_path: RefCell::new(base_path),
recursion_depth: RefCell::new(0),
max_recursion_depth: Self::DEFAULT_MAX_DEPTH,
macro_call_stack: RefCell::new(Vec::new()),
compat_mode: CompatMode::none(),
}
}
/// Create a new context with custom extensions
///
/// This constructor allows providing custom extension handlers and YAML tag handlers.
///
/// # Arguments
/// * `base_path` - Base path for resolving relative includes
/// * `xacro_ns` - Xacro namespace prefix
/// * `args` - Shared reference to CLI arguments (wrapped in Rc<RefCell<...>>)
/// * `compat_mode` - Compatibility mode
/// * `extensions` - Custom extension handlers (wrapped in Rc for sharing)
/// * `yaml_tag_handlers` - YAML tag handler registry (wrapped in Rc for sharing)
pub fn new_with_extensions(
base_path: PathBuf,
xacro_ns: String,
args: Rc<RefCell<HashMap<String, String>>>,
compat_mode: CompatMode,
extensions: Rc<Vec<Box<dyn ExtensionHandler>>>,
#[cfg(feature = "yaml")] yaml_tag_handlers: Rc<
crate::eval::yaml_tag_handler::YamlTagHandlerRegistry,
>,
) -> Self {
XacroContext {
properties: EvalContext::new_with_extensions(
args.clone(),
extensions,
#[cfg(feature = "yaml")]
yaml_tag_handlers,
),
macros: RefCell::new(HashMap::new()),
args,
include_stack: RefCell::new(Vec::new()),
all_includes: RefCell::new(Vec::new()),
namespace_stack: RefCell::new(vec![(base_path.clone(), xacro_ns)]),
block_stack: RefCell::new(Vec::new()),
base_path: RefCell::new(base_path),
recursion_depth: RefCell::new(0),
max_recursion_depth: Self::DEFAULT_MAX_DEPTH,
macro_call_stack: RefCell::new(Vec::new()),
compat_mode,
}
}
/// Get the current xacro namespace prefix
///
/// Returns the namespace prefix from the top of the namespace stack.
/// This is the prefix used for xacro directives in the current file.
pub fn current_xacro_ns(&self) -> String {
self.namespace_stack
.borrow()
.last()
.map(|(_, ns)| ns.clone())
.expect("namespace_stack should never be empty - initialized in XacroContext::new()")
}
/// Set the initial source file path
///
/// Updates the namespace_stack with the actual file path (instead of directory).
/// Should be called immediately after construction when processing a file.
pub fn set_source_file(
&self,
file_path: std::path::PathBuf,
) {
let mut ns_stack = self.namespace_stack.borrow_mut();
if let Some((path, _)) = ns_stack.first_mut() {
*path = file_path;
}
}
/// Get current location context for error reporting
///
/// Creates a snapshot of location information (file path, macro stack, include stack)
/// for passing to the evaluation layer. Clones the data to avoid RefCell lifetime issues.
pub fn get_location_context(&self) -> crate::eval::LocationContext {
// Get current file from namespace_stack (stores actual file paths)
let ns_stack = self.namespace_stack.borrow();
let current_file = ns_stack.last().map(|(path, _ns)| path.clone());
crate::eval::LocationContext {
file: current_file,
macro_stack: self.macro_call_stack.borrow().clone(),
include_stack: self.include_stack.borrow().clone(),
}
}
/// Look up a named block from the current macro scope
/// Returns pre-expanded XMLNodes
pub fn lookup_block(
&self,
name: &str,
) -> Result<Vec<XMLNode>, XacroError> {
// FIX Bug #2: Search entire block stack (most recent to oldest)
// This allows nested macros to access block parameters from parent macros
let stack = self.block_stack.borrow();
for blocks in stack.iter().rev() {
if let Some(nodes) = blocks.get(name) {
return Ok(nodes.clone());
}
}
// Not found in block stack
Err(XacroError::UndefinedBlock {
name: name.to_string(),
})
}
/// Set the maximum recursion depth
pub fn set_max_recursion_depth(
&mut self,
depth: usize,
) {
self.max_recursion_depth = depth;
}
/// Get all included files (for --deps output)
///
/// Returns a sorted, deduplicated list of all files included during processing.
/// Sorting ensures deterministic output (Python xacro's set() has non-deterministic
/// hash-based ordering). This improves on Python's behavior for reproducibility.
pub fn get_all_includes(&self) -> Vec<PathBuf> {
let mut includes = self.all_includes.borrow().clone();
includes.sort();
includes.dedup(); // Safety: remove any duplicates after sorting
includes
}
}