Skip to main content

rdom_core/
validate.rs

1//! Debug-only arena validator.
2//!
3//! `Dom::validate()` walks every occupied slot, checks every invariant from
4//! the design RFC, and returns the list of violations (not just a bool, so
5//! failures are actionable). Used by fuzz / property tests in Phase 4; the
6//! current (Phase 1) test suite also calls it at the end of non-trivial
7//! scenarios as a second check.
8//!
9//! Never enabled in release builds — it's O(n) and walks every pointer.
10
11use crate::dom::Dom;
12use crate::node::NodeData;
13use crate::node_id::NodeId;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16#[non_exhaustive]
17pub enum InvariantViolation {
18    /// A `parent` pointer on some node doesn't appear in that parent's
19    /// children (chain from first_child via next_sibling).
20    ParentNotContainingChild { parent: NodeId, child: NodeId },
21    /// Sibling chain is not doubly linked: `a.next_sibling == Some(b)` but
22    /// `b.prev_sibling != Some(a)`.
23    SiblingChainBroken { a: NodeId, b: NodeId },
24    /// Parent's `first_child` doesn't point to a node whose `prev_sibling` is None.
25    FirstChildNotLeader { parent: NodeId },
26    /// Parent's `last_child` doesn't point to a node whose `next_sibling` is None.
27    LastChildNotTail { parent: NodeId },
28    /// A node appears in a parent's child chain but its own `parent` is wrong.
29    WrongParentPointer { child: NodeId, expected: NodeId },
30    /// Freed slot referenced from a live node's pointer.
31    DanglingPointer { from: NodeId, to: NodeId },
32    /// An index entry references a node that no longer has the indexed
33    /// attribute (id/tag/class). Index out-of-sync with node state.
34    IndexOrphan {
35        kind: &'static str,
36        key: String,
37        id: NodeId,
38    },
39    /// A live Element has an attribute/class that should be indexed but
40    /// is not present in the index.
41    IndexMissing {
42        kind: &'static str,
43        key: String,
44        id: NodeId,
45    },
46}
47
48impl<Ext> Dom<Ext> {
49    /// Walk the arena, check every invariant, return all violations.
50    /// Intended for tests and debug builds. Release builds of callers can
51    /// simply skip this call.
52    pub fn validate(&self) -> Vec<InvariantViolation> {
53        let mut out = Vec::new();
54
55        for (idx, slot) in self.nodes.iter().enumerate() {
56            let Some(node) = &slot.node else { continue };
57            let id = NodeId::from_parts(idx, slot.generation);
58
59            // Pointer reachability — freed slots not referenced.
60            for (label, target) in [
61                ("parent", node.parent),
62                ("first_child", node.first_child),
63                ("last_child", node.last_child),
64                ("prev_sibling", node.prev_sibling),
65                ("next_sibling", node.next_sibling),
66            ] {
67                if let Some(t) = target
68                    && self.get_node(t).is_none()
69                {
70                    let _ = label; // informational
71                    out.push(InvariantViolation::DanglingPointer { from: id, to: t });
72                }
73            }
74
75            // Sibling doubly-linked.
76            if let Some(next) = node.next_sibling
77                && let Some(next_node) = self.get_node(next)
78                && next_node.prev_sibling != Some(id)
79            {
80                out.push(InvariantViolation::SiblingChainBroken { a: id, b: next });
81            }
82            if let Some(prev) = node.prev_sibling
83                && let Some(prev_node) = self.get_node(prev)
84                && prev_node.next_sibling != Some(id)
85            {
86                out.push(InvariantViolation::SiblingChainBroken { a: prev, b: id });
87            }
88
89            // If this node has a parent, parent's child chain must contain it.
90            if let Some(parent) = node.parent {
91                let mut found = false;
92                let mut cur = self.get_node(parent).and_then(|n| n.first_child);
93                while let Some(c) = cur {
94                    if c == id {
95                        found = true;
96                        break;
97                    }
98                    cur = self.get_node(c).and_then(|n| n.next_sibling);
99                }
100                if !found {
101                    out.push(InvariantViolation::ParentNotContainingChild { parent, child: id });
102                }
103            }
104
105            // First-child: its prev_sibling should be None.
106            if let Some(first) = node.first_child
107                && let Some(fn_) = self.get_node(first)
108            {
109                if fn_.prev_sibling.is_some() {
110                    out.push(InvariantViolation::FirstChildNotLeader { parent: id });
111                }
112                if fn_.parent != Some(id) {
113                    out.push(InvariantViolation::WrongParentPointer {
114                        child: first,
115                        expected: id,
116                    });
117                }
118            }
119            if let Some(last) = node.last_child
120                && let Some(ln) = self.get_node(last)
121            {
122                if ln.next_sibling.is_some() {
123                    out.push(InvariantViolation::LastChildNotTail { parent: id });
124                }
125                if ln.parent != Some(id) {
126                    out.push(InvariantViolation::WrongParentPointer {
127                        child: last,
128                        expected: id,
129                    });
130                }
131            }
132
133            // Index invariants: for every live Element, its tag / id / classes
134            // must be present in the corresponding index.
135            if let NodeData::Element {
136                tag,
137                attrs,
138                classes,
139                ..
140            } = &node.data
141            {
142                if !self
143                    .indexes
144                    .by_tag
145                    .get(tag)
146                    .is_some_and(|v| v.contains(&id))
147                {
148                    out.push(InvariantViolation::IndexMissing {
149                        kind: "tag",
150                        key: tag.clone(),
151                        id,
152                    });
153                }
154                if let Some(id_attr) = attrs.get("id")
155                    && !id_attr.is_empty()
156                    && !self
157                        .indexes
158                        .by_id
159                        .get(id_attr)
160                        .is_some_and(|v| v.contains(&id))
161                {
162                    out.push(InvariantViolation::IndexMissing {
163                        kind: "id",
164                        key: id_attr.clone(),
165                        id,
166                    });
167                }
168                for c in classes {
169                    if !self
170                        .indexes
171                        .by_class
172                        .get(c)
173                        .is_some_and(|v| v.contains(&id))
174                    {
175                        out.push(InvariantViolation::IndexMissing {
176                            kind: "class",
177                            key: c.clone(),
178                            id,
179                        });
180                    }
181                }
182            }
183        }
184
185        // Reverse direction: every index entry must reference a live
186        // Element that still carries the attribute/class/tag.
187        for (tag, ids) in &self.indexes.by_tag {
188            for &iid in ids {
189                match self.get_node(iid).map(|n| &n.data) {
190                    Some(NodeData::Element { tag: t, .. }) if t == tag => {}
191                    _ => out.push(InvariantViolation::IndexOrphan {
192                        kind: "tag",
193                        key: tag.clone(),
194                        id: iid,
195                    }),
196                }
197            }
198        }
199        for (key, ids) in &self.indexes.by_id {
200            for &iid in ids {
201                let has_id = matches!(
202                    self.get_node(iid).map(|n| &n.data),
203                    Some(NodeData::Element { attrs, .. }) if attrs.get("id") == Some(key)
204                );
205                if !has_id {
206                    out.push(InvariantViolation::IndexOrphan {
207                        kind: "id",
208                        key: key.clone(),
209                        id: iid,
210                    });
211                }
212            }
213        }
214        for (cls, ids) in &self.indexes.by_class {
215            for &iid in ids {
216                let has_cls = matches!(
217                    self.get_node(iid).map(|n| &n.data),
218                    Some(NodeData::Element { classes, .. }) if classes.contains(cls)
219                );
220                if !has_cls {
221                    out.push(InvariantViolation::IndexOrphan {
222                        kind: "class",
223                        key: cls.clone(),
224                        id: iid,
225                    });
226                }
227            }
228        }
229
230        out
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use crate::Dom;
237
238    #[test]
239    fn empty_dom_validates() {
240        let dom: Dom = Dom::new();
241        assert!(dom.validate().is_empty());
242    }
243
244    #[test]
245    fn simple_tree_validates() {
246        let mut dom: Dom = Dom::new();
247        let root = dom.root();
248        let a = dom.create_element("a");
249        let b = dom.create_element("b");
250        let c = dom.create_element("c");
251        dom.append_child(root, a).unwrap();
252        dom.append_child(root, b).unwrap();
253        dom.append_child(a, c).unwrap();
254        assert!(dom.validate().is_empty());
255    }
256
257    #[test]
258    fn deep_tree_validates() {
259        let mut dom: Dom = Dom::new();
260        let mut cur = dom.root();
261        for _ in 0..100 {
262            let el = dom.create_element("div");
263            dom.append_child(cur, el).unwrap();
264            cur = el;
265        }
266        assert!(dom.validate().is_empty());
267    }
268
269    #[test]
270    fn mutations_preserve_invariants() {
271        let mut dom: Dom = Dom::new();
272        let root = dom.root();
273        let a = dom.create_element("a");
274        let b = dom.create_element("b");
275        let c = dom.create_element("c");
276        dom.append_child(root, a).unwrap();
277        dom.append_child(root, b).unwrap();
278        dom.append_child(root, c).unwrap();
279
280        dom.remove_child(root, b).unwrap();
281        assert!(dom.validate().is_empty());
282
283        let d = dom.create_element("d");
284        dom.insert_before(root, d, Some(c)).unwrap();
285        assert!(dom.validate().is_empty());
286
287        dom.replace_child(root, a, b).unwrap();
288        assert!(dom.validate().is_empty());
289    }
290}