sql_dialect_fmt_parser/lib.rs
1//! Error-resilient, event-based recursive-descent parser for Snowflake SQL.
2//!
3//! [`parse`] turns source text into a lossless `rowan` CST plus a list of [`ParseError`]s.
4//! Parsing never fails: malformed input is wrapped in `ERROR` nodes and recovery continues, so
5//! the tree always round-trips byte-for-byte (the basis for formatting and highlighting).
6//!
7//! ## Pipeline
8//! `tokenize` -> `Input` -> `Parser` event stream -> `build_tree`.
9//!
10//! ## Modules
11//! * `event` / `input` / `parser` / `grammar` / `builder` — the parsing pipeline.
12//! * `ast` — typed accessors over the untyped tree.
13
14mod ast;
15mod builder;
16mod contextual;
17mod event;
18mod grammar;
19mod input;
20mod parser;
21
22pub use ast::*;
23pub use sql_dialect_fmt_syntax::{Dialect, SyntaxKind, SyntaxNode};
24pub use sql_dialect_fmt_text::LineColumn;
25
26/// A diagnostic produced while parsing, located at a byte span into the source.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct ParseError {
29 pub message: String,
30 /// Byte offset into the source where the offending token begins.
31 pub offset: usize,
32 /// Byte length of the offending token's text, so a diagnostic underlines the whole token
33 /// rather than a single character. `0` for a zero-width point (e.g. an error at end of input,
34 /// where there is no token to point at).
35 pub len: usize,
36 /// One-based line/column position, when the full source text was available.
37 pub line_column: Option<LineColumn>,
38}
39
40impl ParseError {
41 /// The byte range `offset..offset + len` this error covers in the source.
42 pub fn range(&self) -> std::ops::Range<usize> {
43 self.offset..self.offset + self.len
44 }
45}
46
47impl std::fmt::Display for ParseError {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 match self.line_column {
50 Some(pos) => write!(
51 f,
52 "{} at line {}, column {} (byte {})",
53 self.message, pos.line, pos.column, self.offset
54 ),
55 None => write!(f, "{} at byte {}", self.message, self.offset),
56 }
57 }
58}
59
60impl std::error::Error for ParseError {}
61
62/// The result of parsing: a lossless green tree plus any diagnostics.
63#[derive(Clone)]
64pub struct Parse {
65 green: rowan::GreenNode,
66 errors: Vec<ParseError>,
67}
68
69impl Parse {
70 /// The typed root of the syntax tree.
71 pub fn syntax(&self) -> SyntaxNode {
72 SyntaxNode::new_root(self.green.clone())
73 }
74
75 /// Diagnostics gathered during parsing (empty for fully valid input).
76 pub fn errors(&self) -> &[ParseError] {
77 &self.errors
78 }
79}
80
81/// Parse Snowflake SQL source into a lossless CST. Never panics; never loses input.
82///
83/// Equivalent to [`parse_with_dialect`] with [`Dialect::Snowflake`].
84pub fn parse(text: &str) -> Parse {
85 parse_with_dialect(text, Dialect::Snowflake)
86}
87
88/// Parse `text` as `dialect` into a lossless CST. Never panics; never loses input.
89///
90/// The dialect drives tokenization (so the lexer's quoting/special-token rules match) and gates
91/// which dialect-specific statements and operators the grammar accepts.
92pub fn parse_with_dialect(text: &str, dialect: Dialect) -> Parse {
93 let lexed = sql_dialect_fmt_lexer::tokenize_for_dialect(text, dialect);
94 parse_lexed(text, dialect, lexed)
95}
96
97/// Parse an already-tokenized SQL source. This is primarily for callers that need lexer-level
98/// checks and parse diagnostics from the same token stream.
99#[doc(hidden)]
100pub fn parse_lexed<'a>(
101 text: &'a str,
102 dialect: Dialect,
103 lexed: sql_dialect_fmt_lexer::Lexed<'a>,
104) -> Parse {
105 let input = input::Input::new(lexed);
106 let (events, mut errors) = parser::Parser::new(&input, dialect).parse();
107 let line_index = sql_dialect_fmt_text::LineIndex::new(text);
108 for error in &mut errors {
109 error.line_column = Some(line_index.line_column(error.offset));
110 }
111 let green = builder::build_tree(input.all(), events);
112 Parse { green, errors }
113}