Skip to main content

surrealguard_syntax/
parse.rs

1//! Parsing entry points and tree-sitter facade types.
2
3use std::fmt;
4use std::sync::Arc;
5
6use serde::{Deserialize, Serialize};
7use tree_sitter::{Node, Parser, Tree};
8
9use crate::source::SourceId;
10use crate::span::{ByteRange, SourceSpan};
11
12#[derive(Debug)]
13/// A source file plus its parse tree; the input to lowering and to the
14/// syntax-diagnostic pass.
15pub struct ParsedSource {
16    source_id: SourceId,
17    text: Arc<str>,
18    tree: Tree,
19    syntax_diagnostics: Vec<SyntaxDiagnostic>,
20}
21
22impl ParsedSource {
23    /// The identity of the parsed source.
24    pub fn source_id(&self) -> &SourceId {
25        &self.source_id
26    }
27
28    /// The original source text.
29    pub fn text(&self) -> &str {
30        &self.text
31    }
32
33    /// The tree-sitter parse tree.
34    pub fn tree(&self) -> &Tree {
35        &self.tree
36    }
37
38    /// Kind of the root CST node (`"SurrealQL"` for a well-formed parse).
39    pub fn root_kind(&self) -> &str {
40        self.tree.root_node().kind()
41    }
42
43    /// Whether the tree contains any error or missing nodes.
44    pub fn has_error(&self) -> bool {
45        self.tree.root_node().has_error()
46    }
47
48    /// The recoverable syntax problems collected during parsing.
49    pub fn syntax_diagnostics(&self) -> &[SyntaxDiagnostic] {
50        &self.syntax_diagnostics
51    }
52}
53
54#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
55/// A parse-level problem (an `ERROR` or `MISSING` region) with its span.
56pub struct SyntaxDiagnostic {
57    kind: SyntaxDiagnosticKind,
58    span: SourceSpan,
59    message: String,
60}
61
62impl SyntaxDiagnostic {
63    /// Builds a syntax diagnostic from its kind, span, and message.
64    pub fn new(kind: SyntaxDiagnosticKind, span: SourceSpan, message: impl Into<String>) -> Self {
65        Self {
66            kind,
67            span,
68            message: message.into(),
69        }
70    }
71
72    /// How the parser recovered at this location.
73    pub fn kind(&self) -> SyntaxDiagnosticKind {
74        self.kind
75    }
76
77    /// Where the problem is in the source.
78    pub fn span(&self) -> &SourceSpan {
79        &self.span
80    }
81
82    /// The human-readable description of the problem.
83    pub fn message(&self) -> &str {
84        &self.message
85    }
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
89/// Whether the parser recovered by skipping input or by inserting a
90/// missing token.
91pub enum SyntaxDiagnosticKind {
92    /// The parser recovered by skipping input it could not match.
93    ErrorNode,
94    /// The parser recovered by inserting a token the grammar required.
95    MissingNode,
96}
97
98#[derive(Clone, Debug, PartialEq, Eq)]
99/// Failure to produce a tree at all (as opposed to a recoverable
100/// [`SyntaxDiagnostic`]).
101pub enum ParseError {
102    /// The SurrealQL tree-sitter language failed to load.
103    LanguageError,
104    /// tree-sitter returned no tree for the input.
105    ParseFailed,
106}
107
108impl fmt::Display for ParseError {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        match self {
111            ParseError::LanguageError => {
112                f.write_str("failed to load SurrealQL tree-sitter language")
113            }
114            ParseError::ParseFailed => f.write_str("tree-sitter parse returned None"),
115        }
116    }
117}
118
119impl std::error::Error for ParseError {}
120
121/// Parses `text` into a [`ParsedSource`], collecting recoverable syntax
122/// diagnostics rather than failing on imperfect input.
123pub fn parse_source(
124    source_id: SourceId,
125    text: impl Into<Arc<str>>,
126) -> Result<ParsedSource, ParseError> {
127    let text = text.into();
128    let mut parser = Parser::new();
129    parser
130        .set_language(&tree_sitter_surrealql::LANGUAGE.into())
131        .map_err(|_| ParseError::LanguageError)?;
132
133    let tree = parser
134        .parse(text.as_ref(), None)
135        .ok_or(ParseError::ParseFailed)?;
136    let mut syntax_diagnostics = Vec::new();
137    collect_syntax_diagnostics(tree.root_node(), &source_id, &mut syntax_diagnostics);
138
139    Ok(ParsedSource {
140        source_id,
141        text,
142        tree,
143        syntax_diagnostics,
144    })
145}
146
147fn collect_syntax_diagnostics(
148    node: Node<'_>,
149    source_id: &SourceId,
150    diagnostics: &mut Vec<SyntaxDiagnostic>,
151) {
152    // Captured before this node emits anything so the `has_error` fallback
153    // below fires only when neither this node nor any descendant produced a
154    // diagnostic — otherwise an `ERROR` node would be reported twice (once
155    // here, once by the fallback, since `has_error()` is true for it).
156    let diagnostics_before = diagnostics.len();
157
158    if node.is_error() {
159        diagnostics.push(SyntaxDiagnostic::new(
160            SyntaxDiagnosticKind::ErrorNode,
161            node_source_span(node, source_id.clone()),
162            "SurrealQL syntax error",
163        ));
164    }
165
166    if node.is_missing() {
167        diagnostics.push(SyntaxDiagnostic::new(
168            SyntaxDiagnosticKind::MissingNode,
169            node_source_span(node, source_id.clone()),
170            format!("missing SurrealQL syntax node `{}`", node.kind()),
171        ));
172    }
173
174    let mut cursor = node.walk();
175    for child in node.children(&mut cursor) {
176        collect_syntax_diagnostics(child, source_id, diagnostics);
177    }
178
179    if node.has_error() && diagnostics.len() == diagnostics_before {
180        diagnostics.push(SyntaxDiagnostic::new(
181            SyntaxDiagnosticKind::ErrorNode,
182            node_source_span(node, source_id.clone()),
183            "SurrealQL syntax error",
184        ));
185    }
186}
187
188fn node_source_span(node: Node<'_>, source_id: SourceId) -> SourceSpan {
189    let start = node.start_byte().min(u32::MAX as usize) as u32;
190    let end = node.end_byte().min(u32::MAX as usize) as u32;
191    let range = ByteRange::new(start, end).expect("tree-sitter node byte ranges are ordered");
192    SourceSpan::new(source_id, range)
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use crate::source::SourceId;
199
200    #[test]
201    fn parse_source_keeps_text_source_and_tree() {
202        let parsed = parse_source(SourceId::new("query:001"), "SELECT * FROM person;")
203            .expect("valid query should parse");
204
205        assert_eq!(parsed.source_id().as_str(), "query:001");
206        assert_eq!(parsed.text(), "SELECT * FROM person;");
207        assert_eq!(parsed.root_kind(), "SurrealQL");
208        assert!(!parsed.has_error());
209        assert!(parsed.syntax_diagnostics().is_empty());
210    }
211
212    #[test]
213    fn parse_source_collects_error_nodes_as_diagnostics() {
214        let parsed = parse_source(SourceId::new("query:bad"), "SELECT * FROM ;")
215            .expect("tree-sitter should still return a partial tree");
216
217        assert!(parsed.has_error());
218        assert!(!parsed.syntax_diagnostics().is_empty());
219        assert!(parsed
220            .syntax_diagnostics()
221            .iter()
222            .all(|diagnostic| diagnostic.span().source().as_str() == "query:bad"));
223    }
224}