Skip to main content

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}