• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kaidokert / xacro / 20725572640

05 Jan 2026 06:43PM UTC coverage: 87.568%. First build
20725572640

Pull #19

github

web-flow
Merge 4243bd79c into f16353670
Pull Request #19: Refactor: Implement Single-Pass Recursive Expander architecture

531 of 598 new or added lines in 5 files covered. (88.8%)

1296 of 1480 relevant lines covered (87.57%)

141.07 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.82
/src/processor.rs
1
use crate::{
2
    error::XacroError,
3
    expander::{expand_node, XacroContext},
4
    utils::xml::{extract_xacro_namespace, is_known_xacro_uri},
5
};
6
use xmltree::XMLNode;
7

8
pub struct XacroProcessor {
9
    /// Maximum recursion depth for macro expansion and insert_block
10
    /// Default: 50 (set conservatively to prevent stack overflow)
11
    max_recursion_depth: usize,
12
}
13

14
impl XacroProcessor {
15
    /// Create a new xacro processor with default settings
16
    #[allow(clippy::new_without_default)]
17
    pub fn new() -> Self {
110✔
18
        Self {
110✔
19
            max_recursion_depth: 50,
110✔
20
        }
110✔
21
    }
110✔
22

23
    /// Create a new xacro processor with custom max recursion depth
24
    ///
25
    /// # Arguments
26
    /// * `max_depth` - Maximum recursion depth before triggering error (recommended: 50-100)
27
    ///
28
    /// # Example
29
    /// ```
30
    /// use xacro::XacroProcessor;
31
    /// let processor = XacroProcessor::new_with_depth(100);
32
    /// ```
33
    pub fn new_with_depth(max_depth: usize) -> Self {
1✔
34
        Self {
1✔
35
            max_recursion_depth: max_depth,
1✔
36
        }
1✔
37
    }
1✔
38

39
    /// Process xacro content from a file path
40
    pub fn run<P: AsRef<std::path::Path>>(
9✔
41
        &self,
9✔
42
        path: P,
9✔
43
    ) -> Result<String, XacroError> {
9✔
44
        let xml = XacroProcessor::parse_file(&path)?;
9✔
45
        self.run_impl(
9✔
46
            xml,
9✔
47
            path.as_ref()
9✔
48
                .parent()
9✔
49
                .unwrap_or_else(|| std::path::Path::new(".")),
9✔
50
        )
51
    }
9✔
52

53
    /// Process xacro content from a string
54
    ///
55
    /// # Note
56
    /// Any `<xacro:include>` directives with relative paths will be resolved
57
    /// relative to the current working directory.
58
    pub fn run_from_string(
102✔
59
        &self,
102✔
60
        content: &str,
102✔
61
    ) -> Result<String, XacroError> {
102✔
62
        let xml = xmltree::Element::parse(content.as_bytes())?;
102✔
63
        // Use current directory as base path for any includes in test content
64
        self.run_impl(xml, std::path::Path::new("."))
102✔
65
    }
102✔
66

67
    /// Internal implementation
68
    fn run_impl(
111✔
69
        &self,
111✔
70
        mut root: xmltree::Element,
111✔
71
        base_path: &std::path::Path,
111✔
72
    ) -> Result<String, XacroError> {
111✔
73
        // Extract xacro namespace from document root (if present)
74
        // Strategy:
75
        // 1. Try standard "xacro" prefix (e.g., xmlns:xacro="...")
76
        // 2. Fallback: search for any prefix bound to a known xacro URI
77
        // 3. If not found, use empty string (lazy checking - only error if xacro elements actually used)
78
        //
79
        // Documents with NO xacro elements don't need xacro namespace declaration.
80
        // Only error during finalize_tree if xacro elements are found.
81
        let xacro_ns = extract_xacro_namespace(&root)?;
111✔
82

83
        // Create expansion context
84
        // Math constants (pi, e, tau, etc.) are automatically initialized by PropertyProcessor::new()
85
        let mut ctx = XacroContext::new(base_path.to_path_buf(), xacro_ns.clone());
110✔
86
        ctx.set_max_recursion_depth(self.max_recursion_depth);
110✔
87

88
        // Expand the root element itself. This will handle attributes on the root
89
        // and any xacro directives at the root level (though unlikely).
90
        let expanded_nodes = expand_node(XMLNode::Element(root), &ctx)?;
110✔
91

92
        // The expansion of the root must result in a single root element.
93
        if expanded_nodes.len() != 1 {
94✔
NEW
94
            return Err(XacroError::InvalidRoot(format!(
×
NEW
95
                "Root element expanded to {} nodes, expected 1",
×
NEW
96
                expanded_nodes.len()
×
NEW
97
            )));
×
98
        }
94✔
99

100
        root = match expanded_nodes.into_iter().next().unwrap() {
94✔
101
            XMLNode::Element(elem) => elem,
94✔
102
            _ => {
NEW
103
                return Err(XacroError::InvalidRoot(
×
NEW
104
                    "Root element expanded to a non-element node (e.g., text or comment)"
×
NEW
105
                        .to_string(),
×
NEW
106
                ))
×
107
            }
108
        };
109

110
        // Final cleanup: check for unprocessed xacro elements and remove namespace
111
        Self::finalize_tree(&mut root, &xacro_ns)?;
94✔
112

113
        XacroProcessor::serialize(&root)
92✔
114
    }
111✔
115

116
    fn finalize_tree(
281✔
117
        element: &mut xmltree::Element,
281✔
118
        xacro_ns: &str,
281✔
119
    ) -> Result<(), XacroError> {
281✔
120
        // Check if this element is in the xacro namespace (indicates unprocessed feature)
121
        // Must check namespace URI, not prefix, to handle namespace aliasing (e.g., xmlns:x="...")
122

123
        // Case 1: Element has namespace and matches declared xacro namespace
124
        if !xacro_ns.is_empty() && element.namespace.as_deref() == Some(xacro_ns) {
281✔
125
            // Use centralized feature lists for consistent error messages
126
            use crate::error::{IMPLEMENTED_FEATURES, UNIMPLEMENTED_FEATURES};
127
            return Err(XacroError::UnimplementedFeature(format!(
1✔
128
                "<xacro:{}>\n\
1✔
129
                     This element was not processed. Either:\n\
1✔
130
                     1. The feature is not implemented yet (known unimplemented: {})\n\
1✔
131
                     2. There's a bug in the processor\n\
1✔
132
                     \n\
1✔
133
                     Currently implemented: {}",
1✔
134
                element.name,
1✔
135
                UNIMPLEMENTED_FEATURES.join(", "),
1✔
136
                IMPLEMENTED_FEATURES.join(", ")
1✔
137
            )));
1✔
138
        }
280✔
139

140
        // Case 2: Element has a known xacro namespace but no namespace was declared in root
141
        // This is the lazy checking: only error if xacro elements are actually used
142
        if xacro_ns.is_empty() {
280✔
143
            if let Some(elem_ns) = element.namespace.as_deref() {
7✔
144
                if is_known_xacro_uri(elem_ns) {
1✔
145
                    return Err(XacroError::MissingNamespace(format!(
1✔
146
                        "Found xacro element <{}> with namespace '{}', but no xacro namespace declared in document root. \
1✔
147
                         Please add xmlns:xacro=\"{}\" to your root element.",
1✔
148
                        element.name, elem_ns, elem_ns
1✔
149
                    )));
1✔
150
                }
×
151
            }
6✔
152
        }
273✔
153

154
        // Remove xacro namespace declaration (if namespace was declared)
155
        // Find and remove whichever prefix is bound to the xacro namespace URI
156
        // This handles both standard (xmlns:xacro="...") and non-standard (xmlns:foo="...") prefixes
157
        if !xacro_ns.is_empty() {
279✔
158
            if let Some(ref mut ns) = element.namespaces {
273✔
159
                // Find all prefixes bound to the xacro namespace URI
160
                let prefixes_to_remove: Vec<String> =
271✔
161
                    ns.0.iter()
271✔
162
                        .filter(|(_, uri)| uri.as_str() == xacro_ns)
1,114✔
163
                        .map(|(prefix, _)| prefix.clone())
271✔
164
                        .collect();
271✔
165

166
                // Remove all found prefixes
167
                for prefix in prefixes_to_remove {
542✔
168
                    ns.0.remove(&prefix);
271✔
169
                }
271✔
170
            }
2✔
171
        }
6✔
172

173
        // Recursively process children
174
        for child in &mut element.children {
490✔
175
            if let Some(child_elem) = child.as_mut_element() {
213✔
176
                Self::finalize_tree(child_elem, xacro_ns)?;
187✔
177
            }
26✔
178
        }
179

180
        Ok(())
277✔
181
    }
281✔
182
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc