Skip to main content

squawk_syntax/
lib.rs

1// via https://github.com/rust-lang/rust-analyzer/blob/d8887c0758bbd2d5f752d5bd405d4491e90e7ed6/crates/syntax/src/lib.rs
2//
3// Permission is hereby granted, free of charge, to any
4// person obtaining a copy of this software and associated
5// documentation files (the "Software"), to deal in the
6// Software without restriction, including without
7// limitation the rights to use, copy, modify, merge,
8// publish, distribute, sublicense, and/or sell copies of
9// the Software, and to permit persons to whom the Software
10// is furnished to do so, subject to the following
11// conditions:
12//
13// The above copyright notice and this permission notice
14// shall be included in all copies or substantial portions
15// of the Software.
16//
17// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
18// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
20// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
21// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
24// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25// DEALINGS IN THE SOFTWARE.
26
27pub mod ast;
28pub mod body;
29pub mod column_name;
30pub mod decoded_text;
31mod generated;
32mod parsing;
33pub mod plpgsql;
34mod ptr;
35pub mod quote;
36pub mod sql_body;
37pub mod syntax_error;
38mod syntax_node;
39mod token_text;
40pub mod unescape;
41mod validation;
42
43#[cfg(test)]
44mod test;
45
46use std::{marker::PhantomData, sync::Arc};
47
48pub use squawk_parser::SyntaxKind;
49
50use ast::AstNode;
51pub use ptr::{AstPtr, SyntaxNodePtr};
52use rowan::GreenNode;
53use syntax_error::SyntaxError;
54pub use syntax_node::{SyntaxElement, SyntaxNode, SyntaxToken};
55pub use token_text::TokenText;
56
57/// `Parse` is the result of the parsing: a syntax tree and a collection of
58/// errors.
59///
60/// Note that we always produce a syntax tree, even for completely invalid
61/// files.
62#[derive(Debug, PartialEq, Eq)]
63pub struct Parse<T> {
64    green: GreenNode,
65    errors: Option<Arc<[SyntaxError]>>,
66    _ty: PhantomData<fn() -> T>,
67}
68
69impl<T> Clone for Parse<T> {
70    fn clone(&self) -> Parse<T> {
71        Parse {
72            green: self.green.clone(),
73            errors: self.errors.clone(),
74            _ty: PhantomData,
75        }
76    }
77}
78
79impl<T> Parse<T> {
80    fn new(green: GreenNode, errors: Vec<SyntaxError>) -> Parse<T> {
81        Parse {
82            green,
83            errors: if errors.is_empty() {
84                None
85            } else {
86                Some(errors.into())
87            },
88            _ty: PhantomData,
89        }
90    }
91
92    pub fn syntax_node(&self) -> SyntaxNode {
93        SyntaxNode::new_root(self.green.clone())
94    }
95
96    pub fn errors(&self) -> Vec<SyntaxError> {
97        let mut errors = if let Some(e) = self.errors.as_deref() {
98            e.to_vec()
99        } else {
100            vec![]
101        };
102        validation::validate(&self.syntax_node(), &mut errors);
103        let file = SourceFile::cast(self.syntax_node()).expect("parse root is always a SourceFile");
104        errors.extend(file.sql_body_errors());
105        errors.sort_by_key(|error| error.range().start());
106        errors
107    }
108}
109
110impl<T: AstNode> Parse<T> {
111    /// Converts this parse result into a parse result for an untyped syntax tree.
112    pub fn to_syntax(self) -> Parse<SyntaxNode> {
113        Parse {
114            green: self.green,
115            errors: self.errors,
116            _ty: PhantomData,
117        }
118    }
119
120    /// Gets the parsed syntax tree as a typed ast node.
121    ///
122    /// # Panics
123    ///
124    /// Panics if the root node cannot be casted into the typed ast node
125    /// (e.g. if it's an `ERROR` node).
126    pub fn tree(&self) -> T {
127        T::cast(self.syntax_node()).unwrap()
128    }
129
130    /// Converts from `Parse<T>` to [`Result<T, Vec<SyntaxError>>`].
131    pub fn ok(self) -> Result<T, Vec<SyntaxError>> {
132        match self.errors() {
133            errors if !errors.is_empty() => Err(errors),
134            _ => Ok(self.tree()),
135        }
136    }
137}
138
139impl Parse<SyntaxNode> {
140    pub fn cast<N: AstNode>(self) -> Option<Parse<N>> {
141        if N::cast(self.syntax_node()).is_some() {
142            Some(Parse {
143                green: self.green,
144                errors: self.errors,
145                _ty: PhantomData,
146            })
147        } else {
148            None
149        }
150    }
151}
152
153/// `SourceFile` represents a parse tree for a single SQL file.
154pub use crate::ast::SourceFile;
155
156impl SourceFile {
157    pub fn parse(text: &str) -> Parse<SourceFile> {
158        let (green, errors) = parsing::parse_text(text);
159        let root = SyntaxNode::new_root(green.clone());
160
161        assert_eq!(root.kind(), SyntaxKind::SOURCE_FILE);
162        Parse::new(green, errors)
163    }
164}
165
166/// Matches a `SyntaxNode` against an `ast` type.
167///
168/// # Example:
169///
170/// ```ignore
171/// match_ast! {
172///     match node {
173///         ast::CallExpr(it) => { ... },
174///         ast::MethodCallExpr(it) => { ... },
175///         ast::MacroCall(it) => { ... },
176///         _ => None,
177///     }
178/// }
179/// ```
180#[macro_export]
181macro_rules! match_ast {
182    (match $node:ident { $($tt:tt)* }) => { $crate::match_ast!(match ($node) { $($tt)* }) };
183
184    (match ($node:expr) {
185        $( $( $path:ident )::+ ($it:pat) => $res:expr, )*
186        _ => $catch_all:expr $(,)?
187    }) => {{
188        $( if let Some($it) = $($path::)+cast($node.clone()) { $res } else )*
189        { $catch_all }
190    }};
191}
192
193/// This test does not assert anything and instead just shows off the crate's
194/// API.
195#[test]
196fn api_walkthrough() {
197    use ast::SourceFile;
198    use rowan::{Direction, NodeOrToken, SyntaxText, TextRange, WalkEvent};
199    use std::fmt::Write;
200
201    let source_code = "
202        create function foo(p int8)
203        returns int
204        as 'select 1 + 1'
205        language sql;
206    ";
207    // `SourceFile` is the main entry point.
208    //
209    // The `parse` method returns a `Parse` -- a pair of syntax tree and a list
210    // of errors. That is, syntax tree is constructed even in presence of errors.
211    let parse = SourceFile::parse(source_code);
212    assert!(parse.errors().is_empty());
213
214    // The `tree` method returns an owned syntax node of type `SourceFile`.
215    // Owned nodes are cheap: inside, they are `Rc` handles to the underling data.
216    let file: SourceFile = parse.tree();
217
218    // `SourceFile` is the root of the syntax tree. We can iterate file's items.
219    // Let's fetch the `foo` function.
220    let mut func = None;
221    for stmt in file.stmts() {
222        match stmt {
223            ast::Stmt::CreateFunction(f) => func = Some(f),
224            _ => unreachable!(),
225        }
226    }
227    let func: ast::CreateFunction = func.unwrap();
228
229    // Each AST node has a bunch of getters for children. All getters return
230    // `Option`s though, to account for incomplete code. Some getters are common
231    // for several kinds of node. In this case, a trait like `ast::NameOwner`
232    // usually exists. By convention, all ast types should be used with `ast::`
233    // qualifier.
234    let path: Option<ast::Path> = func.name().and_then(|name| name.path());
235    let name: ast::PathSegment = path.unwrap().segment().unwrap();
236    assert_eq!(name.text(), "foo");
237
238    // return
239    let ret_type: Option<ast::RetType> = func.ret_type();
240    let r_ty = ret_type.unwrap().func_type().unwrap();
241    let type_: ast::PathType = match r_ty {
242        ast::FuncType::Type(ast::Type::PathType(r)) => r,
243        _ => unreachable!(),
244    };
245    let type_path: ast::PathRef = type_.path_ref().unwrap();
246    assert_eq!(type_path.syntax().to_string(), "int");
247
248    // params
249    let param_list: ast::ParamList = func.param_list().unwrap();
250    let param: ast::Param = param_list.params().next().unwrap();
251
252    let param_name: ast::ParamName = param.name().unwrap();
253    assert_eq!(param_name.syntax().to_string(), "p");
254
255    let param_ty: ast::FuncType = param.func_type().unwrap();
256    assert_eq!(param_ty.syntax().to_string(), "int8");
257
258    let func_option_list: ast::FuncOptionList = func.option_list().unwrap();
259
260    // Enums are used to group related ast nodes together, and can be used for
261    // matching. However, because there are no public fields, it's possible to
262    // match only the top level enum: that is the price we pay for increased API
263    // flexibility
264    let func_option = func_option_list.options().next().unwrap();
265    let option: &ast::AsFuncOption = match &func_option {
266        ast::FuncOption::AsFuncOption(o) => o,
267        _ => unreachable!(),
268    };
269    let as_definition: ast::AsDefinition = match option.as_func_target().unwrap() {
270        ast::AsFuncTarget::AsDefinition(d) => d,
271        _ => unreachable!(),
272    };
273    let definition: ast::Literal = as_definition.literal().unwrap();
274    assert_eq!(definition.syntax().to_string(), "'select 1 + 1'");
275
276    // Besides the "typed" AST API, there's an untyped CST one as well.
277    // To switch from AST to CST, call `.syntax()` method:
278    let func_option_syntax = func_option.syntax();
279
280    // Note how `func_option_syntax` and `option` are in fact the same node underneath:
281    assert!(func_option_syntax == option.syntax());
282
283    // To go from CST to AST, `AstNode::cast` function is used:
284    let _expr: ast::FuncOption = match ast::FuncOption::cast(func_option_syntax.clone()) {
285        Some(e) => e,
286        None => unreachable!(),
287    };
288
289    // The two properties each syntax node has is a `SyntaxKind`:
290    assert_eq!(func_option_syntax.kind(), SyntaxKind::AS_FUNC_OPTION);
291
292    // And text range:
293    assert_eq!(
294        func_option_syntax.text_range(),
295        TextRange::new(65.into(), 82.into())
296    );
297
298    // You can get node's text as a `SyntaxText` object, which will traverse the
299    // tree collecting token's text:
300    let text: SyntaxText = func_option_syntax.text();
301    assert_eq!(text.to_string(), "as 'select 1 + 1'");
302
303    // There's a bunch of traversal methods on `SyntaxNode`:
304    assert_eq!(
305        func_option_syntax.parent().as_ref(),
306        Some(func_option_list.syntax())
307    );
308    assert_eq!(
309        param_list
310            .syntax()
311            .first_child_or_token()
312            .map(|it| it.kind()),
313        Some(SyntaxKind::L_PAREN)
314    );
315    assert_eq!(
316        func_option_syntax
317            .next_sibling_or_token()
318            .map(|it| it.kind()),
319        Some(SyntaxKind::WHITESPACE)
320    );
321
322    // As well as some iterator helpers:
323    let f = func_option_syntax
324        .ancestors()
325        .find_map(ast::CreateFunction::cast);
326    assert_eq!(f, Some(func));
327    assert!(
328        param
329            .syntax()
330            .siblings_with_tokens(Direction::Next)
331            .any(|it| it.kind() == SyntaxKind::R_PAREN)
332    );
333    assert_eq!(
334        func_option_syntax.descendants_with_tokens().count(),
335        6, // 1 the node itself: `as 'select 1 + 1'`
336           // 2 tokens: `as`, ` `
337           // 2 child nodes: `AsDefinition`, `Literal`
338           // 1 token: `'select 1 + 1'`
339    );
340
341    // There's also a `preorder` method with a more fine-grained iteration control:
342    let mut buf = String::new();
343    let mut indent = 0;
344    for event in func_option_syntax.preorder_with_tokens() {
345        match event {
346            WalkEvent::Enter(node) => {
347                let text = match &node {
348                    NodeOrToken::Node(it) => it.text().to_string(),
349                    NodeOrToken::Token(it) => it.text().to_owned(),
350                };
351                buf.write_fmt(format_args!(
352                    "{:indent$}{:?} {:?}\n",
353                    " ",
354                    text,
355                    node.kind(),
356                    indent = indent
357                ))
358                .unwrap();
359                indent += 2;
360            }
361            WalkEvent::Leave(_) => indent -= 2,
362        }
363    }
364    assert_eq!(indent, 0);
365    assert_eq!(
366        buf.trim(),
367        r#"
368"as 'select 1 + 1'" AS_FUNC_OPTION
369  "as" AS_KW
370  " " WHITESPACE
371  "'select 1 + 1'" AS_DEFINITION
372    "'select 1 + 1'" LITERAL
373      "'select 1 + 1'" STRING
374    "#
375        .trim()
376    );
377
378    // To recursively process the tree, there are three approaches:
379    // 1. explicitly call getter methods on AST nodes.
380    // 2. use descendants and `AstNode::cast`.
381    // 3. use descendants and `match_ast!`.
382    //
383    // Here's how the first one looks like:
384    let exprs_cast: Vec<String> = file
385        .syntax()
386        .descendants()
387        .filter_map(ast::FuncOption::cast)
388        .map(|expr| expr.syntax().text().to_string())
389        .collect();
390
391    // An alternative is to use a macro.
392    let mut exprs_visit = Vec::new();
393    for node in file.syntax().descendants() {
394        match_ast! {
395            match node {
396                ast::FuncOption(it) => {
397                    let res = it.syntax().text().to_string();
398                    exprs_visit.push(res);
399                },
400                _ => (),
401            }
402        }
403    }
404    assert_eq!(exprs_cast, exprs_visit);
405}
406
407#[test]
408fn create_table() {
409    use insta::assert_debug_snapshot;
410
411    let source_code = "
412        create table users (
413            id int8 primary key,
414            name varchar(255) not null,
415            email text,
416            created_at timestamp default now()
417        );
418        
419        create table posts (
420            id serial primary key,
421            title varchar(500),
422            content text,
423            user_id int8 references users(id)
424        );
425    ";
426
427    let parse = SourceFile::parse(source_code);
428    assert!(parse.errors().is_empty());
429    let file: SourceFile = parse.tree();
430
431    let mut tables: Vec<(String, Vec<(String, String)>)> = vec![];
432
433    for stmt in file.stmts() {
434        if let ast::Stmt::CreateTable(create_table) = stmt {
435            let table_name = create_table
436                .table_name()
437                .and_then(|table| table.path())
438                .unwrap()
439                .syntax()
440                .to_string();
441            let mut columns = vec![];
442            for arg in create_table.table_arg_list().unwrap().args() {
443                match arg {
444                    ast::TableArg::Column(column) => {
445                        let column_name = column.name().unwrap();
446                        let column_type = column.ty().unwrap();
447                        columns.push((
448                            column_name.syntax().to_string(),
449                            column_type.syntax().to_string(),
450                        ));
451                    }
452                    ast::TableArg::TableConstraint(_) | ast::TableArg::LikeClause(_) => (),
453                }
454            }
455            tables.push((table_name, columns));
456        }
457    }
458
459    assert_debug_snapshot!(tables, @r#"
460    [
461        (
462            "users",
463            [
464                (
465                    "id",
466                    "int8",
467                ),
468                (
469                    "name",
470                    "varchar(255)",
471                ),
472                (
473                    "email",
474                    "text",
475                ),
476                (
477                    "created_at",
478                    "timestamp",
479                ),
480            ],
481        ),
482        (
483            "posts",
484            [
485                (
486                    "id",
487                    "serial",
488                ),
489                (
490                    "title",
491                    "varchar(500)",
492                ),
493                (
494                    "content",
495                    "text",
496                ),
497                (
498                    "user_id",
499                    "int8",
500                ),
501            ],
502        ),
503    ]
504    "#)
505}
506
507#[test]
508fn bin_expr() {
509    use insta::assert_debug_snapshot;
510
511    let source_code = "select 1 is not null;";
512    let parse = SourceFile::parse(source_code);
513    assert!(parse.errors().is_empty());
514    let file: SourceFile = parse.tree();
515
516    let ast::Stmt::Select(select) = file.stmts().next().unwrap() else {
517        unreachable!()
518    };
519
520    let target_list = select.select_clause().unwrap().target_list().unwrap();
521    let target = target_list.targets().next().unwrap();
522    let ast::Expr::BinExpr(bin_expr) = target.expr().unwrap() else {
523        unreachable!()
524    };
525
526    let lhs = bin_expr.lhs();
527    let op = bin_expr.op();
528    let rhs = bin_expr.rhs();
529
530    assert_debug_snapshot!(lhs, @r#"
531    Some(
532        Literal(
533            Literal {
534                syntax: LITERAL@7..8
535                  INT_NUMBER@7..8 "1"
536                ,
537            },
538        ),
539    )
540    "#);
541    assert_debug_snapshot!(op, @r#"
542    Some(
543        IsNot(
544            IsNot {
545                syntax: IS_NOT@9..15
546                  IS_KW@9..11 "is"
547                  WHITESPACE@11..12 " "
548                  NOT_KW@12..15 "not"
549                ,
550            },
551        ),
552    )
553    "#);
554    assert_debug_snapshot!(rhs, @r#"
555    Some(
556        Literal(
557            Literal {
558                syntax: LITERAL@16..20
559                  NULL_KW@16..20 "null"
560                ,
561            },
562        ),
563    )
564    "#);
565}