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

kaidokert / xacro / 21198192262

21 Jan 2026 05:16AM UTC coverage: 89.645%. First build
21198192262

Pull #85

github

web-flow
Merge 1dfdb2ec1 into 1efd978d7
Pull Request #85: Refactor scope.rs into focused submodules

872 of 947 new or added lines in 6 files covered. (92.08%)

5333 of 5949 relevant lines covered (89.65%)

283.15 hits per line

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

96.34
/src/eval/scope/scope_stack.rs
1
//! Scope stack management for property shadowing and lookup
2
//!
3
//! Provides scope manipulation methods for the `EvalContext` struct.
4
//! Scopes are used during macro expansion to shadow global properties
5
//! with macro parameters.
6

7
use super::{EvalContext, PropertyScope};
8
use std::collections::HashMap;
9

10
impl<const MAX_SUBSTITUTION_DEPTH: usize> EvalContext<MAX_SUBSTITUTION_DEPTH> {
11
    /// Push a new scope for macro parameter bindings
12
    ///
13
    /// Used during macro expansion to create a temporary scope where macro parameters
14
    /// shadow global properties. The scope must be popped when the macro expansion completes.
15
    ///
16
    /// # Example
17
    /// ```ignore
18
    /// // Illustrative example (requires internal access for setup)
19
    /// use std::collections::HashMap;
20
    ///
21
    /// // Global: x=10
22
    /// let mut scope = HashMap::new();
23
    /// scope.insert("x".to_string(), "5".to_string());
24
    /// processor.push_scope(scope);
25
    ///
26
    /// // Now ${x} resolves to "5" (shadowing global x=10)
27
    /// processor.pop_scope();
28
    /// // Now ${x} resolves to "10" again
29
    /// ```
30
    pub fn push_scope(
205✔
31
        &self,
205✔
32
        bindings: HashMap<String, String>,
205✔
33
    ) {
205✔
34
        self.scope_stack.borrow_mut().push(bindings);
205✔
35
    }
205✔
36

37
    /// Pop the innermost scope
38
    ///
39
    /// Should be called when a macro expansion completes to restore the previous scope.
40
    /// Panics if the scope stack is empty (indicates a programming error).
41
    pub fn pop_scope(&self) {
205✔
42
        // Get properties from the scope being popped before we pop it
43
        let properties_to_clear: Vec<String> = self
205✔
44
            .scope_stack
205✔
45
            .borrow()
205✔
46
            .last()
205✔
47
            .map(|scope| scope.keys().cloned().collect())
205✔
48
            .unwrap_or_default();
205✔
49

50
        // Clear cache entries for properties that were in the popped scope
51
        // This prevents scoped properties from leaking via the cache
52
        let mut cache = self.evaluated_cache.borrow_mut();
205✔
53
        for prop_name in &properties_to_clear {
397✔
54
            cache.remove(prop_name);
192✔
55
        }
192✔
56
        drop(cache);
205✔
57

58
        #[cfg(feature = "compat")]
59
        {
60
            // Clean up metadata for the scope being popped
61
            let scope_depth = self.scope_stack.borrow().len();
205✔
62
            let prefix = format!("{}:", scope_depth);
205✔
63
            self.property_metadata
205✔
64
                .borrow_mut()
205✔
65
                .retain(|key, _| !key.starts_with(&prefix));
405✔
66
        }
67

68
        self.scope_stack
205✔
69
            .borrow_mut()
205✔
70
            .pop()
205✔
71
            .expect("Scope stack underflow - mismatched push/pop");
205✔
72
    }
205✔
73

74
    /// Add a property to the current (innermost) macro scope
75
    ///
76
    /// Used to bind macro parameter values when entering a macro.
77
    /// Different from `add_raw_property` which adds to the global scope.
78
    ///
79
    /// # Arguments
80
    /// * `name` - Property name (macro parameter)
81
    /// * `value` - Property value (from macro call site)
82
    ///
83
    /// # Panics
84
    /// Panics if the scope stack is empty (no scope has been pushed)
85
    pub fn add_to_current_scope(
137✔
86
        &self,
137✔
87
        name: String,
137✔
88
        value: String,
137✔
89
    ) {
137✔
90
        // Compat feature: Track float and boolean metadata for scoped properties
91
        #[cfg(feature = "compat")]
92
        {
137✔
93
            let metadata = self.compute_property_metadata(&value);
137✔
94
            let scope_depth = self.scope_stack.borrow().len();
137✔
95
            let scoped_key = format!("{}:{}", scope_depth, name);
137✔
96
            self.property_metadata
137✔
97
                .borrow_mut()
137✔
98
                .insert(scoped_key, metadata);
137✔
99
        }
137✔
100

101
        self.scope_stack
137✔
102
            .borrow_mut()
137✔
103
            .last_mut()
137✔
104
            .expect("No scope to add to - push a scope first")
137✔
105
            .insert(name, value);
137✔
106
    }
137✔
107

108
    /// Check if a property exists in a specific scope
109
    ///
110
    /// Used by macro parameter binding to determine where to look for properties.
111
    /// The `^` operator uses this to find properties in parent/global scopes.
112
    ///
113
    /// # Arguments
114
    /// * `name` - Property name to look up
115
    /// * `scope` - Which scope to search (Local, Parent, or Global)
116
    ///
117
    /// # Returns
118
    /// * `true` if property exists in the target scope
119
    /// * `false` if property does not exist in the target scope
120
    pub fn has_property_in_scope(
4✔
121
        &self,
4✔
122
        name: &str,
4✔
123
        scope: PropertyScope,
4✔
124
    ) -> bool {
4✔
125
        match scope {
4✔
126
            PropertyScope::Local => {
127
                // Check current scope (top of stack or global if no stack)
128
                let stack = self.scope_stack.borrow();
3✔
129
                if let Some(top) = stack.last() {
3✔
NEW
130
                    top.contains_key(name)
×
131
                } else {
132
                    self.raw_properties.borrow().contains_key(name)
3✔
133
                }
134
            }
135
            PropertyScope::Parent => {
136
                // Check parent scope (stack[len-2] or global if at first macro)
137
                let stack = self.scope_stack.borrow();
1✔
138
                if stack.len() >= 2 {
1✔
139
                    let parent_idx = stack.len() - 2;
1✔
140
                    stack[parent_idx].contains_key(name)
1✔
141
                } else {
142
                    // Parent is global scope
NEW
143
                    self.raw_properties.borrow().contains_key(name)
×
144
                }
145
            }
146
            PropertyScope::Global => {
147
                // Check global scope only
NEW
148
                self.raw_properties.borrow().contains_key(name)
×
149
            }
150
        }
151
    }
4✔
152

153
    /// Get current scope depth
154
    ///
155
    /// Returns the number of active macro scopes:
156
    /// - 0 = global scope only (no macros running)
157
    /// - 1 = inside first macro
158
    /// - 2 = inside nested macro, etc.
159
    ///
160
    /// Used for scope manipulation (^ operator and scope="parent/global")
161
    pub fn scope_depth(&self) -> usize {
202✔
162
        self.scope_stack.borrow().len()
202✔
163
    }
202✔
164

165
    /// Look up property starting from a specific depth downwards
166
    ///
167
    /// Used by `^` operator to search parent scope and below.
168
    /// Searches from `depth` down through the scope stack to global.
169
    ///
170
    /// # Arguments
171
    /// * `key` - Property name to look up
172
    /// * `depth` - Scope depth to start search from (1-based, where depth 0 = global only)
173
    ///
174
    /// # Returns
175
    /// * `Some(value)` if found in any scope from depth downward
176
    /// * `None` if not found anywhere
177
    ///
178
    /// # Example
179
    /// ```ignore
180
    /// // scope_stack = [Scope1, Scope2]  (depth = 2)
181
    /// // To look up in Scope1 and below:
182
    /// ctx.lookup_at_depth("x", 1)  // Searches: Scope1 -> Global
183
    /// ```
184
    pub fn lookup_at_depth(
36✔
185
        &self,
36✔
186
        key: &str,
36✔
187
        depth: usize,
36✔
188
    ) -> Option<String> {
36✔
189
        let stack = self.scope_stack.borrow();
36✔
190

191
        // Search stack from depth down to 1
192
        // depth=1 means scope_stack[0], depth=2 means scope_stack[1], etc.
193
        let start_index = depth.min(stack.len()).saturating_sub(1);
36✔
194
        if depth > 0 {
36✔
195
            for i in (0..=start_index).rev() {
20✔
196
                if let Some(value) = stack[i].get(key) {
20✔
197
                    return Some(value.clone());
11✔
198
                }
9✔
199
            }
200
        }
17✔
201

202
        // Fallback to global (depth 0)
203
        self.raw_properties.borrow().get(key).cloned()
25✔
204
    }
36✔
205
}
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