1use std::marker::PhantomData;
25
26use thiserror::Error;
27
28use crate::errors::AntlrError;
29use crate::tree::{MissingChildError, Node, ParsedFile, RuleNodeView};
30
31pub struct ValidatedTree<Grammar> {
41 parsed: ParsedFile,
42 grammar: PhantomData<Grammar>,
43}
44
45impl<Grammar> ValidatedTree<Grammar> {
46 #[doc(hidden)]
55 #[must_use]
56 pub const fn __new(parsed: ParsedFile) -> Self {
57 Self {
58 parsed,
59 grammar: PhantomData,
60 }
61 }
62
63 #[must_use]
65 pub fn tree(&self) -> ValidatedRuleNode<'_, Grammar> {
66 let Some(rule) = self.parsed.tree().as_rule() else {
67 unreachable!("validated parse root was checked as a rule node")
68 };
69 ValidatedRuleNode {
70 node: rule,
71 grammar: PhantomData,
72 }
73 }
74
75 #[must_use]
77 pub const fn parsed_file(&self) -> &ParsedFile {
78 &self.parsed
79 }
80
81 #[must_use]
84 pub fn into_parsed_file(self) -> ParsedFile {
85 self.parsed
86 }
87}
88
89impl<Grammar> std::fmt::Debug for ValidatedTree<Grammar> {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 f.debug_struct("ValidatedTree")
92 .field("parsed", &self.parsed)
93 .finish()
94 }
95}
96
97pub struct ValidatedRuleNode<'a, Grammar> {
100 node: RuleNodeView<'a>,
101 grammar: PhantomData<Grammar>,
102}
103
104impl<'a, Grammar> ValidatedRuleNode<'a, Grammar> {
105 #[doc(hidden)]
114 #[must_use]
115 pub const fn __new(node: RuleNodeView<'a>) -> Self {
116 Self {
117 node,
118 grammar: PhantomData,
119 }
120 }
121
122 #[must_use]
123 pub const fn rule_node(self) -> RuleNodeView<'a> {
124 self.node
125 }
126
127 #[must_use]
128 pub const fn node(self) -> Node<'a> {
129 self.node.node()
130 }
131
132 #[must_use]
133 pub fn rule_index(self) -> usize {
134 self.node.rule_index()
135 }
136
137 #[must_use]
138 pub fn text(self) -> String {
139 self.node.text()
140 }
141
142 #[must_use]
147 pub fn downcast_ref<T: FromValidatedRuleNode<'a, Grammar = Grammar>>(self) -> Option<T> {
148 T::from_validated_rule_node(self)
149 }
150}
151
152impl<Grammar> std::fmt::Debug for ValidatedRuleNode<'_, Grammar> {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 f.debug_struct("ValidatedRuleNode")
155 .field("node", &self.node)
156 .finish()
157 }
158}
159
160impl<Grammar> Clone for ValidatedRuleNode<'_, Grammar> {
161 fn clone(&self) -> Self {
162 *self
163 }
164}
165
166impl<Grammar> Copy for ValidatedRuleNode<'_, Grammar> {}
167
168pub trait FromValidatedRuleNode<'a>: Sized {
171 type Grammar;
173
174 fn from_validated_rule_node(node: ValidatedRuleNode<'a, Self::Grammar>) -> Option<Self>;
175}
176
177#[derive(Clone, Debug, Eq, PartialEq, Error)]
183pub enum ValidationError {
184 #[error("parse failed: {0}")]
185 Recognition(#[from] AntlrError),
186 #[error("parse produced {lexer} lexer and {parser} parser syntax errors")]
187 SyntaxErrors { lexer: usize, parser: usize },
188 #[error("{0}")]
189 MissingChild(#[from] MissingChildError),
190 #[error(
191 "required child {child} occurs {actual} times in {context}; expected at least {minimum}"
192 )]
193 InvalidChildCount {
194 context: &'static str,
195 child: &'static str,
196 minimum: usize,
197 actual: usize,
198 },
199 #[error("recovered error node at {line}:{column}: {text}")]
200 RecoveredErrorNode {
201 line: usize,
202 column: usize,
203 text: String,
204 },
205 #[error("validated parse root is not a rule node")]
206 InvalidRoot,
207 #[error("parse tree contains unknown rule index {rule_index}")]
208 UnknownRule { rule_index: usize },
209}
210
211pub const fn require_min_count(
221 actual: usize,
222 minimum: usize,
223 context: &'static str,
224 child: &'static str,
225) -> Result<(), ValidationError> {
226 if actual < minimum {
227 return Err(ValidationError::InvalidChildCount {
228 context,
229 child,
230 minimum,
231 actual,
232 });
233 }
234 Ok(())
235}
236
237#[cfg(test)]
238#[allow(clippy::disallowed_methods)] mod tests {
240 use std::error::Error as _;
241
242 use super::*;
243
244 fn every_variant() -> Vec<ValidationError> {
245 vec![
246 ValidationError::Recognition(AntlrError::LexerError {
247 line: 3,
248 column: 7,
249 message: "token recognition error at: '#'".to_owned(),
250 }),
251 ValidationError::SyntaxErrors {
252 lexer: 1,
253 parser: 2,
254 },
255 ValidationError::MissingChild(MissingChildError::new("StartContext", "atom")),
256 ValidationError::InvalidChildCount {
257 context: "StartContext",
258 child: "atom",
259 minimum: 2,
260 actual: 1,
261 },
262 ValidationError::RecoveredErrorNode {
263 line: 4,
264 column: 9,
265 text: "<missing ';'>".to_owned(),
266 },
267 ValidationError::InvalidRoot,
268 ValidationError::UnknownRule { rule_index: 41 },
269 ]
270 }
271
272 #[test]
273 fn validation_error_display_texts() {
274 let rendered = every_variant()
275 .iter()
276 .map(ToString::to_string)
277 .collect::<Vec<_>>()
278 .join("\n");
279 insta::assert_snapshot!("validation_error_display_texts", rendered);
280 }
281
282 #[test]
283 fn validation_error_sources() {
284 for error in every_variant() {
285 let expects_source = matches!(
286 error,
287 ValidationError::Recognition(_) | ValidationError::MissingChild(_)
288 );
289 assert_eq!(
290 error.source().is_some(),
291 expects_source,
292 "source() mismatch for {error:?}"
293 );
294 }
295 }
296
297 #[test]
298 fn validation_error_from_conversions() {
299 let recognition = AntlrError::LexerError {
300 line: 1,
301 column: 0,
302 message: "boom".to_owned(),
303 };
304 assert_eq!(
305 ValidationError::from(recognition.clone()),
306 ValidationError::Recognition(recognition)
307 );
308
309 let missing = MissingChildError::new("StartContext", "atom");
310 assert_eq!(
311 ValidationError::from(missing),
312 ValidationError::MissingChild(missing)
313 );
314 }
315
316 #[test]
317 fn require_min_count_accepts_satisfied_minimums() {
318 assert_eq!(require_min_count(2, 2, "StartContext", "atom"), Ok(()));
319 assert_eq!(require_min_count(3, 0, "StartContext", "atom"), Ok(()));
320 }
321
322 #[test]
323 fn require_min_count_reports_the_violated_site() {
324 assert_eq!(
325 require_min_count(1, 2, "StartContext", "atom"),
326 Err(ValidationError::InvalidChildCount {
327 context: "StartContext",
328 child: "atom",
329 minimum: 2,
330 actual: 1,
331 })
332 );
333 }
334}