use std::marker::PhantomData;
use thiserror::Error;
use crate::errors::AntlrError;
use crate::tree::{MissingChildError, Node, ParsedFile, RuleNodeView};
pub struct ValidatedTree<Grammar> {
parsed: ParsedFile,
grammar: PhantomData<Grammar>,
}
impl<Grammar> ValidatedTree<Grammar> {
#[doc(hidden)]
#[must_use]
pub const fn __new(parsed: ParsedFile) -> Self {
Self {
parsed,
grammar: PhantomData,
}
}
#[must_use]
pub fn tree(&self) -> ValidatedRuleNode<'_, Grammar> {
let Some(rule) = self.parsed.tree().as_rule() else {
unreachable!("validated parse root was checked as a rule node")
};
ValidatedRuleNode {
node: rule,
grammar: PhantomData,
}
}
#[must_use]
pub const fn parsed_file(&self) -> &ParsedFile {
&self.parsed
}
#[must_use]
pub fn into_parsed_file(self) -> ParsedFile {
self.parsed
}
}
impl<Grammar> std::fmt::Debug for ValidatedTree<Grammar> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ValidatedTree")
.field("parsed", &self.parsed)
.finish()
}
}
pub struct ValidatedRuleNode<'a, Grammar> {
node: RuleNodeView<'a>,
grammar: PhantomData<Grammar>,
}
impl<'a, Grammar> ValidatedRuleNode<'a, Grammar> {
#[doc(hidden)]
#[must_use]
pub const fn __new(node: RuleNodeView<'a>) -> Self {
Self {
node,
grammar: PhantomData,
}
}
#[must_use]
pub const fn rule_node(self) -> RuleNodeView<'a> {
self.node
}
#[must_use]
pub const fn node(self) -> Node<'a> {
self.node.node()
}
#[must_use]
pub fn rule_index(self) -> usize {
self.node.rule_index()
}
#[must_use]
pub fn text(self) -> String {
self.node.text()
}
#[must_use]
pub fn downcast_ref<T: FromValidatedRuleNode<'a, Grammar = Grammar>>(self) -> Option<T> {
T::from_validated_rule_node(self)
}
}
impl<Grammar> std::fmt::Debug for ValidatedRuleNode<'_, Grammar> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ValidatedRuleNode")
.field("node", &self.node)
.finish()
}
}
impl<Grammar> Clone for ValidatedRuleNode<'_, Grammar> {
fn clone(&self) -> Self {
*self
}
}
impl<Grammar> Copy for ValidatedRuleNode<'_, Grammar> {}
pub trait FromValidatedRuleNode<'a>: Sized {
type Grammar;
fn from_validated_rule_node(node: ValidatedRuleNode<'a, Self::Grammar>) -> Option<Self>;
}
#[derive(Clone, Debug, Eq, PartialEq, Error)]
pub enum ValidationError {
#[error("parse failed: {0}")]
Recognition(#[from] AntlrError),
#[error("parse produced {lexer} lexer and {parser} parser syntax errors")]
SyntaxErrors { lexer: usize, parser: usize },
#[error("{0}")]
MissingChild(#[from] MissingChildError),
#[error(
"required child {child} occurs {actual} times in {context}; expected at least {minimum}"
)]
InvalidChildCount {
context: &'static str,
child: &'static str,
minimum: usize,
actual: usize,
},
#[error("recovered error node at {line}:{column}: {text}")]
RecoveredErrorNode {
line: usize,
column: usize,
text: String,
},
#[error("validated parse root is not a rule node")]
InvalidRoot,
#[error("parse tree contains unknown rule index {rule_index}")]
UnknownRule { rule_index: usize },
}
pub const fn require_min_count(
actual: usize,
minimum: usize,
context: &'static str,
child: &'static str,
) -> Result<(), ValidationError> {
if actual < minimum {
return Err(ValidationError::InvalidChildCount {
context,
child,
minimum,
actual,
});
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)] mod tests {
use std::error::Error as _;
use super::*;
fn every_variant() -> Vec<ValidationError> {
vec![
ValidationError::Recognition(AntlrError::LexerError {
line: 3,
column: 7,
message: "token recognition error at: '#'".to_owned(),
}),
ValidationError::SyntaxErrors {
lexer: 1,
parser: 2,
},
ValidationError::MissingChild(MissingChildError::new("StartContext", "atom")),
ValidationError::InvalidChildCount {
context: "StartContext",
child: "atom",
minimum: 2,
actual: 1,
},
ValidationError::RecoveredErrorNode {
line: 4,
column: 9,
text: "<missing ';'>".to_owned(),
},
ValidationError::InvalidRoot,
ValidationError::UnknownRule { rule_index: 41 },
]
}
#[test]
fn validation_error_display_texts() {
let rendered = every_variant()
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n");
insta::assert_snapshot!("validation_error_display_texts", rendered);
}
#[test]
fn validation_error_sources() {
for error in every_variant() {
let expects_source = matches!(
error,
ValidationError::Recognition(_) | ValidationError::MissingChild(_)
);
assert_eq!(
error.source().is_some(),
expects_source,
"source() mismatch for {error:?}"
);
}
}
#[test]
fn validation_error_from_conversions() {
let recognition = AntlrError::LexerError {
line: 1,
column: 0,
message: "boom".to_owned(),
};
assert_eq!(
ValidationError::from(recognition.clone()),
ValidationError::Recognition(recognition)
);
let missing = MissingChildError::new("StartContext", "atom");
assert_eq!(
ValidationError::from(missing),
ValidationError::MissingChild(missing)
);
}
#[test]
fn require_min_count_accepts_satisfied_minimums() {
assert_eq!(require_min_count(2, 2, "StartContext", "atom"), Ok(()));
assert_eq!(require_min_count(3, 0, "StartContext", "atom"), Ok(()));
}
#[test]
fn require_min_count_reports_the_violated_site() {
assert_eq!(
require_min_count(1, 2, "StartContext", "atom"),
Err(ValidationError::InvalidChildCount {
context: "StartContext",
child: "atom",
minimum: 2,
actual: 1,
})
);
}
}