use thiserror::Error;
use tree_sitter::{Node, Parser, Point, Tree};
#[derive(Debug)]
pub struct SyntaxTree {
source: String,
tree: Tree,
errors: Vec<SyntaxError>,
}
impl SyntaxTree {
pub fn source(&self) -> &str {
&self.source
}
pub fn tree(&self) -> &Tree {
&self.tree
}
pub fn root_node(&self) -> Node<'_> {
self.tree.root_node()
}
pub fn range_for(&self, node: Node<'_>) -> TextRange {
TextRange::from_node(node)
}
pub fn text_for<'a>(&'a self, node: Node<'_>) -> &'a str {
&self.source[node.byte_range()]
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn errors(&self) -> &[SyntaxError] {
&self.errors
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SyntaxError {
kind: SyntaxErrorKind,
range: TextRange,
}
impl SyntaxError {
pub fn kind(&self) -> SyntaxErrorKind {
self.kind
}
pub fn range(&self) -> TextRange {
self.range
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyntaxErrorKind {
Missing,
Unexpected,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TextPosition {
pub row: usize,
pub column: usize,
}
impl From<Point> for TextPosition {
fn from(point: Point) -> Self {
Self {
row: point.row,
column: point.column,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TextRange {
pub start_byte: usize,
pub end_byte: usize,
pub start_position: TextPosition,
pub end_position: TextPosition,
}
impl TextRange {
fn from_node(node: Node<'_>) -> Self {
Self {
start_byte: node.start_byte(),
end_byte: node.end_byte(),
start_position: node.start_position().into(),
end_position: node.end_position().into(),
}
}
}
#[derive(Debug, Error)]
pub enum ParseError {
#[error("failed to configure the Achitek parser: {0}")]
Language(#[from] tree_sitter::LanguageError),
#[error("tree-sitter did not produce a parse tree")]
ParseCancelled,
}
pub fn parse(source: &str) -> Result<SyntaxTree, ParseError> {
let mut parser = Parser::new();
parser.set_language(&tree_sitter_achitekfile::LANGUAGE.into())?;
let tree = parser
.parse(source, None)
.ok_or(ParseError::ParseCancelled)?;
let errors = collect_errors(tree.root_node());
Ok(SyntaxTree {
source: source.to_owned(),
tree,
errors,
})
}
fn collect_errors(root: Node<'_>) -> Vec<SyntaxError> {
let mut errors = Vec::new();
collect_errors_from_node(root, &mut errors);
errors
}
fn collect_errors_from_node(node: Node<'_>, errors: &mut Vec<SyntaxError>) -> bool {
let mut child_has_error = false;
for index in 0..node.child_count() {
let child = node
.child(u32::try_from(index).expect("child index should fit into u32"))
.expect("child index should be valid");
child_has_error |= collect_errors_from_node(child, errors);
}
if node.is_missing() {
errors.push(SyntaxError {
kind: SyntaxErrorKind::Missing,
range: TextRange::from_node(node),
});
return true;
}
if node.is_error() && !child_has_error {
errors.push(SyntaxError {
kind: SyntaxErrorKind::Unexpected,
range: TextRange::from_node(node),
});
return true;
}
child_has_error || node.has_error()
}
#[cfg(test)]
mod tests {
use super::{SyntaxErrorKind, parse};
use indoc::indoc;
#[test]
fn parses_valid_achitek_source() {
let source = indoc! {r#"
blueprint {
version = "1.0.0"
name = "minimal"
}
prompt "project_name" {
type = string
help = "Project name"
}
"#};
let tree = parse(source).expect("valid source should parse");
assert_eq!(tree.root_node().kind(), "file");
assert!(!tree.has_errors());
assert!(tree.errors().is_empty());
}
#[test]
fn reports_syntax_errors_for_invalid_source() {
let source = indoc! {r#"
blueprint {
version = "1.0.0"
name = "broken"
prompt "project_name" {
type = string
}
"#};
let tree = parse(source).expect("tree-sitter should still produce a tree");
assert!(tree.has_errors());
assert!(!tree.errors().is_empty());
assert!(
tree.errors()
.iter()
.any(|error| error.kind() == SyntaxErrorKind::Missing
|| error.kind() == SyntaxErrorKind::Unexpected)
);
}
}