surrealguard_syntax/
parse.rs1use 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)]
13pub struct ParsedSource {
16 source_id: SourceId,
17 text: Arc<str>,
18 tree: Tree,
19 syntax_diagnostics: Vec<SyntaxDiagnostic>,
20}
21
22impl ParsedSource {
23 pub fn source_id(&self) -> &SourceId {
25 &self.source_id
26 }
27
28 pub fn text(&self) -> &str {
30 &self.text
31 }
32
33 pub fn tree(&self) -> &Tree {
35 &self.tree
36 }
37
38 pub fn root_kind(&self) -> &str {
40 self.tree.root_node().kind()
41 }
42
43 pub fn has_error(&self) -> bool {
45 self.tree.root_node().has_error()
46 }
47
48 pub fn syntax_diagnostics(&self) -> &[SyntaxDiagnostic] {
50 &self.syntax_diagnostics
51 }
52}
53
54#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
55pub struct SyntaxDiagnostic {
57 kind: SyntaxDiagnosticKind,
58 span: SourceSpan,
59 message: String,
60}
61
62impl SyntaxDiagnostic {
63 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 pub fn kind(&self) -> SyntaxDiagnosticKind {
74 self.kind
75 }
76
77 pub fn span(&self) -> &SourceSpan {
79 &self.span
80 }
81
82 pub fn message(&self) -> &str {
84 &self.message
85 }
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
89pub enum SyntaxDiagnosticKind {
92 ErrorNode,
94 MissingNode,
96}
97
98#[derive(Clone, Debug, PartialEq, Eq)]
99pub enum ParseError {
102 LanguageError,
104 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
121pub 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 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}