From 78ab9cae35b30de996f9cc0718d5df15c00c866d Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sun, 24 May 2026 15:53:00 -0400 Subject: [PATCH 1/2] fmt: literals & binary operators --- crates/squawk_fmt/src/fmt.rs | 147 +++++++++++++----- .../squawk_fmt/tests/after/select_expr.snap | 32 ++++ .../tests/after/select_literals.snap | 60 +++++++ .../squawk_fmt/tests/before/select_expr.sql | 32 ++++ .../tests/before/select_literals.sql | 45 ++++++ crates/squawk_fmt/tests/tests.rs | 21 +-- crates/squawk_ide/src/hover.rs | 3 + crates/squawk_syntax/src/ast/node_ext.rs | 18 ++- 8 files changed, 305 insertions(+), 53 deletions(-) create mode 100644 crates/squawk_fmt/tests/after/select_literals.snap create mode 100644 crates/squawk_fmt/tests/before/select_literals.sql diff --git a/crates/squawk_fmt/src/fmt.rs b/crates/squawk_fmt/src/fmt.rs index 5dea8990..703ae042 100644 --- a/crates/squawk_fmt/src/fmt.rs +++ b/crates/squawk_fmt/src/fmt.rs @@ -1,6 +1,6 @@ use itertools::Itertools; use rowan::Direction; -use squawk_syntax::ast::{self, AstNode}; +use squawk_syntax::ast::{self, AstNode, LitKind}; use squawk_syntax::quote::quote_column_alias; use squawk_syntax::{SyntaxKind, SyntaxNode, SyntaxToken}; use tiny_pretty::Doc; @@ -79,7 +79,7 @@ fn build_create_table<'a>(create_table: &ast::CreateTable) -> Doc<'a> { fn build_table_arg<'a>(create_table: ast::TableArg) -> Doc<'a> { match create_table { - ast::TableArg::Column(column) => Doc::text(column.name().unwrap().syntax().to_string()) + ast::TableArg::Column(column) => build_name(column.name().unwrap()) .append(Doc::space()) .append(Doc::text(column.ty().unwrap().syntax().to_string())), ast::TableArg::LikeClause(_like_clause) => todo!(), @@ -250,7 +250,7 @@ fn build_expr<'a>(expr: ast::Expr) -> Doc<'a> { } // ast::Expr::FieldExpr(field_expr) => todo!(), // ast::Expr::IndexExpr(index_expr) => todo!(), - // ast::Expr::Literal(literal) => todo!(), + ast::Expr::Literal(literal) => build_literal(literal), // ast::Expr::NameRef(name_ref) => todo!(), // ast::Expr::ParenExpr(paren_expr) => todo!(), ast::Expr::PostfixExpr(postfix_expr) => { @@ -402,48 +402,121 @@ fn build_unicode_normal_form<'a>(form: ast::UnicodeNormalForm) -> Doc<'a> { } } +fn build_keyword_node<'a>(node: &SyntaxNode) -> Doc<'a> { + let mut docs: Vec> = vec![]; + for el in node.children_with_tokens() { + match el { + rowan::NodeOrToken::Token(token) => match token.kind() { + SyntaxKind::WHITESPACE => continue, + SyntaxKind::COMMENT => { + if !docs.is_empty() { + docs.push(Doc::space()); + } + docs.push(Doc::text(token.text().to_string())); + } + _ => { + if !docs.is_empty() { + docs.push(Doc::space()); + } + docs.push(Doc::text(token.text().to_ascii_lowercase())); + } + }, + rowan::NodeOrToken::Node(_) => (), + } + } + Doc::list(docs) +} + fn build_op<'a>(op: ast::BinOp) -> Doc<'a> { match op { - ast::BinOp::And(_) => todo!(), - ast::BinOp::AtTimeZone(_) => todo!(), - ast::BinOp::Caret(_) => todo!(), - ast::BinOp::Collate(_) => todo!(), - ast::BinOp::ColonColon(_) => todo!(), - ast::BinOp::ColonEq(_) => todo!(), + ast::BinOp::And(_) => Doc::text("and"), + ast::BinOp::AtTimeZone(n) => build_keyword_node(n.syntax()), + ast::BinOp::Caret(_) => Doc::text("^"), + ast::BinOp::Collate(_) => Doc::text("collate"), + ast::BinOp::ColonColon(_) => Doc::text("::"), + ast::BinOp::ColonEq(_) => Doc::text(":="), ast::BinOp::CustomOp(custom_op) => Doc::text(custom_op.syntax().to_string()), - ast::BinOp::Eq(_) => todo!(), - ast::BinOp::FatArrow(_) => todo!(), - ast::BinOp::Gteq(_) => todo!(), - ast::BinOp::Ilike(_) => todo!(), - ast::BinOp::In(_) => todo!(), - ast::BinOp::Is(_) => todo!(), - ast::BinOp::IsDistinctFrom(_) => todo!(), - ast::BinOp::IsNot(_) => todo!(), - ast::BinOp::IsNotDistinctFrom(_) => todo!(), - ast::BinOp::LAngle(_) => todo!(), - ast::BinOp::Like(_) => todo!(), - ast::BinOp::Lteq(_) => todo!(), - ast::BinOp::Minus(_) => todo!(), - ast::BinOp::Neq(_) => todo!(), - ast::BinOp::Neqb(_) => todo!(), - ast::BinOp::NotIlike(_) => todo!(), - ast::BinOp::NotIn(_) => todo!(), - ast::BinOp::NotLike(_) => todo!(), - ast::BinOp::NotSimilarTo(_) => todo!(), - ast::BinOp::OperatorCall(_) => todo!(), - ast::BinOp::Or(_) => todo!(), - ast::BinOp::Overlaps(_) => todo!(), - ast::BinOp::Percent(_) => todo!(), + ast::BinOp::Eq(_) => Doc::text("="), + ast::BinOp::FatArrow(_) => Doc::text("=>"), + ast::BinOp::Gteq(_) => Doc::text(">="), + ast::BinOp::Ilike(_) => Doc::text("ilike"), + ast::BinOp::In(_) => Doc::text("in"), + ast::BinOp::Is(_) => Doc::text("is"), + ast::BinOp::IsDistinctFrom(n) => build_keyword_node(n.syntax()), + ast::BinOp::IsNot(n) => build_keyword_node(n.syntax()), + ast::BinOp::IsNotDistinctFrom(n) => build_keyword_node(n.syntax()), + ast::BinOp::LAngle(_) => Doc::text("<"), + ast::BinOp::Like(_) => Doc::text("like"), + ast::BinOp::Lteq(_) => Doc::text("<="), + ast::BinOp::Minus(_) => Doc::text("-"), + ast::BinOp::Neq(_) => Doc::text("!="), + ast::BinOp::Neqb(_) => Doc::text("<>"), + ast::BinOp::NotIlike(n) => build_keyword_node(n.syntax()), + ast::BinOp::NotIn(n) => build_keyword_node(n.syntax()), + ast::BinOp::NotLike(n) => build_keyword_node(n.syntax()), + ast::BinOp::NotSimilarTo(n) => build_keyword_node(n.syntax()), + ast::BinOp::OperatorCall(op) => Doc::text(op.syntax().to_string()), + ast::BinOp::Or(_) => Doc::text("or"), + ast::BinOp::Overlaps(_) => Doc::text("overlaps"), + ast::BinOp::Percent(_) => Doc::text("%"), ast::BinOp::Plus(_) => Doc::text("+"), - ast::BinOp::RAngle(_) => todo!(), - ast::BinOp::SimilarTo(_) => todo!(), - ast::BinOp::Slash(_) => todo!(), - ast::BinOp::Star(_) => todo!(), + ast::BinOp::RAngle(_) => Doc::text(">"), + ast::BinOp::SimilarTo(n) => build_keyword_node(n.syntax()), + ast::BinOp::Slash(_) => Doc::text("/"), + ast::BinOp::Star(_) => Doc::text("*"), } } fn build_literal<'a>(lit: ast::Literal) -> Doc<'a> { - Doc::text(lit.syntax().to_string()) + let Some(kind) = lit.kind() else { + return Doc::nil(); + }; + match kind { + LitKind::Default(_) => Doc::text("default"), + LitKind::False(_) => Doc::text("false"), + LitKind::IntNumber(t) => Doc::text(t.text().to_string()), + LitKind::Null(_) => Doc::text("null"), + LitKind::NumericNumber(t) => Doc::text(t.text().to_string()), + LitKind::PositionalParam(t) => Doc::text(t.text().to_string()), + LitKind::True(_) => Doc::text("true"), + LitKind::BitString(_) + | LitKind::ByteString(_) + | LitKind::DollarQuotedString(_) + | LitKind::EscString(_) + | LitKind::String(_) + | LitKind::UnicodeEscString(_) => build_string_literal(&lit), + } +} + +fn build_string_literal<'a>(lit: &ast::Literal) -> Doc<'a> { + let parts: Vec> = lit + .syntax() + .children_with_tokens() + .filter_map(|el| match el { + rowan::NodeOrToken::Token(t) if t.kind() != SyntaxKind::WHITESPACE => { + Some(Doc::text(format_string_token(&t))) + } + _ => None, + }) + .collect(); + Doc::list(Itertools::intersperse(parts.into_iter(), Doc::hard_line()).collect()) +} + +fn format_string_token(t: &SyntaxToken) -> String { + let text = t.text(); + if matches!(t.kind(), SyntaxKind::STRING | SyntaxKind::DOLLAR_QUOTED_STRING) { + return text.to_string(); + } + match text.find('\'') { + Some(idx) => { + let (prefix, rest) = text.split_at(idx); + let mut s = String::with_capacity(text.len()); + s.push_str(&prefix.to_ascii_lowercase()); + s.push_str(rest); + s + } + None => text.to_string(), + } } fn build_name<'a>(name: ast::Name) -> Doc<'a> { diff --git a/crates/squawk_fmt/tests/after/select_expr.snap b/crates/squawk_fmt/tests/after/select_expr.snap index e0793356..e2f6f3e3 100644 --- a/crates/squawk_fmt/tests/after/select_expr.snap +++ b/crates/squawk_fmt/tests/after/select_expr.snap @@ -14,6 +14,38 @@ select -- bin expr 1 + 1, 2 @@@ 2, + true and false, + ts at time zone 'UTC', + 2 ^ 3, + name collate "C", + 1 = 1, + 1 >= 1, + 'foo' ilike 'F%', + 1 in (1, 2), + x is true, + 1 is distinct from 2, + 1 is /* comment */ distinct from 2, + x is not true, + 1 is not distinct from 2, + 1 < 2, + 'foo' like 'f%', + 1 <= 2, + 1 - 1, + 1 != 2, + 1 <> 2, + 'foo' not ilike 'F%', + 1 not in (1, 2), + 'foo' not like 'f%', + 'foo' not similar to 'f%', + 1 operator(+) 1, + 1 operator(public.+) 1, + true or false, + (1, 2) overlaps (3, 4), + 10 % 3, + 2 > 1, + 'foo' similar to 'f%', + 6 / 2, + 2 * 3, -- call expr date_trunc('month', now()), -- case expr diff --git a/crates/squawk_fmt/tests/after/select_literals.snap b/crates/squawk_fmt/tests/after/select_literals.snap new file mode 100644 index 00000000..530d06c7 --- /dev/null +++ b/crates/squawk_fmt/tests/after/select_literals.snap @@ -0,0 +1,60 @@ +--- +source: crates/squawk_fmt/tests/tests.rs +input_file: crates/squawk_fmt/tests/before/select_literals.sql +--- +-- null +select null; +-- true +select true; +-- false +select false; +-- int number +select + 42; +-- numeric number +select 3.14; +-- string +select 'hello'; +-- esc string +select + e'esc\n'; +-- unicode esc string +select u&'unicode'; +-- dollar quoted string +select + $$dollar$$; +-- dollar quoted string with tag +select + $tag$body$tag$; +-- bit string +select b'1010'; +-- byte string +select + x'AF'; +-- positional param +select $1; +-- string continuation +select + 'foo' + 'bar'; +-- string continuation, multiple +select + 'one' + 'two' + 'three'; +-- esc string continuation +select + e'esc\n' + 'tail'; +-- unicode esc string continuation +select + u&'uni' + 'tail'; +-- bit string continuation +select + b'1010' + '0101'; +-- byte string continuation +select + x'AF' + 'BE'; diff --git a/crates/squawk_fmt/tests/before/select_expr.sql b/crates/squawk_fmt/tests/before/select_expr.sql index 96629a03..ff75cfdc 100644 --- a/crates/squawk_fmt/tests/before/select_expr.sql +++ b/crates/squawk_fmt/tests/before/select_expr.sql @@ -10,6 +10,38 @@ select -- bin expr 1 + 1, 2@@@2, + true and false, + ts at time zone 'UTC', + 2 ^ 3, + name collate "C", + 1 = 1, + 1 >= 1, + 'foo' ilike 'F%', + 1 in (1, 2), + x is true, + 1 is distinct from 2, + 1 is /* comment */ distinct from 2, + x is not true, + 1 is not distinct from 2, + 1 < 2, + 'foo' like 'f%', + 1 <= 2, + 1 - 1, + 1 != 2, + 1 <> 2, + 'foo' not ilike 'F%', + 1 not in (1, 2), + 'foo' not like 'f%', + 'foo' not similar to 'f%', + 1 operator(+) 1, + 1 operator(public.+) 1, + true or false, + (1, 2) overlaps (3, 4), + 10 % 3, + 2 > 1, + 'foo' similar to 'f%', + 6 / 2, + 2 * 3, -- call expr date_trunc('month', now()), -- case expr diff --git a/crates/squawk_fmt/tests/before/select_literals.sql b/crates/squawk_fmt/tests/before/select_literals.sql new file mode 100644 index 00000000..74077f09 --- /dev/null +++ b/crates/squawk_fmt/tests/before/select_literals.sql @@ -0,0 +1,45 @@ +-- null +select null; +-- true +select true; +-- false +select false; +-- int number +select 42; +-- numeric number +select 3.14; +-- string +select 'hello'; +-- esc string +select E'esc\n'; +-- unicode esc string +select U&'unicode'; +-- dollar quoted string +select $$dollar$$; +-- dollar quoted string with tag +select $tag$body$tag$; +-- bit string +select B'1010'; +-- byte string +select X'AF'; +-- positional param +select $1; +-- string continuation +select 'foo' + 'bar'; +-- string continuation, multiple +select 'one' + 'two' + 'three'; +-- esc string continuation +select E'esc\n' + 'tail'; +-- unicode esc string continuation +select U&'uni' + 'tail'; +-- bit string continuation +select B'1010' + '0101'; +-- byte string continuation +select X'AF' + 'BE'; diff --git a/crates/squawk_fmt/tests/tests.rs b/crates/squawk_fmt/tests/tests.rs index 67ebcf62..2a99cbdd 100644 --- a/crates/squawk_fmt/tests/tests.rs +++ b/crates/squawk_fmt/tests/tests.rs @@ -52,17 +52,18 @@ fn tokens_equivalent(before: (TokenKind, &str), after: (TokenKind, &str)) -> boo // We convert `select 1 "foo"` to `select 1 foo` so we need to do some quote // munging - let unquote = |kind: TokenKind, text: &str| match kind { - TokenKind::QuotedIdent { .. } => text - .strip_prefix('"') - .and_then(|t| t.strip_suffix('"')) - .map(|t| t.to_string()), - TokenKind::Ident => Some(text.to_string()), - _ => None, - }; + fn unquote<'a>(kind: &TokenKind, text: &'a str) -> Option<&'a str> { + match kind { + TokenKind::QuotedIdent { .. } => { + text.strip_prefix('"').and_then(|t| t.strip_suffix('"')) + } + TokenKind::Ident => Some(text), + _ => None, + } + } - match (unquote(bkind, btext), unquote(akind, atext)) { - (Some(b), Some(a)) => b.eq_ignore_ascii_case(&a), + match (unquote(&bkind, btext), unquote(&akind, atext)) { + (Some(b), Some(a)) => b.eq_ignore_ascii_case(a), _ => false, } } diff --git a/crates/squawk_ide/src/hover.rs b/crates/squawk_ide/src/hover.rs index 70054127..9ef33294 100644 --- a/crates/squawk_ide/src/hover.rs +++ b/crates/squawk_ide/src/hover.rs @@ -181,10 +181,13 @@ fn hover_literal(literal: &ast::Literal) -> Option { } None => format!("value of literal: {}", markdown_inline_code(&value)), }, + LitKind::Default(_) => return None, + LitKind::False(_) => return None, LitKind::IntNumber(_) => return None, LitKind::Null(_) => return None, LitKind::NumericNumber(_) => return None, LitKind::PositionalParam(_) => return None, + LitKind::True(_) => return None, }; Some(Hover::new(ty, comment)) diff --git a/crates/squawk_syntax/src/ast/node_ext.rs b/crates/squawk_syntax/src/ast/node_ext.rs index 12f1b711..f2e3e8bb 100644 --- a/crates/squawk_syntax/src/ast/node_ext.rs +++ b/crates/squawk_syntax/src/ast/node_ext.rs @@ -45,13 +45,16 @@ use super::support; pub enum LitKind { BitString(SyntaxToken), ByteString(SyntaxToken), + Default(SyntaxToken), DollarQuotedString(SyntaxToken), EscString(SyntaxToken), + False(SyntaxToken), IntNumber(SyntaxToken), Null(SyntaxToken), NumericNumber(SyntaxToken), PositionalParam(SyntaxToken), String(SyntaxToken), + True(SyntaxToken), UnicodeEscString(SyntaxToken), } @@ -59,16 +62,19 @@ impl ast::Literal { pub fn kind(&self) -> Option { let token = self.syntax().first_child_or_token()?.into_token()?; let kind = match token.kind() { - SyntaxKind::STRING => LitKind::String(token), - SyntaxKind::NULL_KW => LitKind::Null(token), - SyntaxKind::NUMERIC_NUMBER => LitKind::NumericNumber(token), - SyntaxKind::INT_NUMBER => LitKind::IntNumber(token), - SyntaxKind::BYTE_STRING => LitKind::ByteString(token), SyntaxKind::BIT_STRING => LitKind::BitString(token), + SyntaxKind::BYTE_STRING => LitKind::ByteString(token), + SyntaxKind::DEFAULT_KW => LitKind::Default(token), SyntaxKind::DOLLAR_QUOTED_STRING => LitKind::DollarQuotedString(token), - SyntaxKind::UNICODE_ESC_STRING => LitKind::UnicodeEscString(token), SyntaxKind::ESC_STRING => LitKind::EscString(token), + SyntaxKind::FALSE_KW => LitKind::False(token), + SyntaxKind::INT_NUMBER => LitKind::IntNumber(token), + SyntaxKind::NULL_KW => LitKind::Null(token), + SyntaxKind::NUMERIC_NUMBER => LitKind::NumericNumber(token), SyntaxKind::POSITIONAL_PARAM => LitKind::PositionalParam(token), + SyntaxKind::STRING => LitKind::String(token), + SyntaxKind::TRUE_KW => LitKind::True(token), + SyntaxKind::UNICODE_ESC_STRING => LitKind::UnicodeEscString(token), _ => return None, }; Some(kind) From c585d01682cfbb894704517e32bdacadda452747 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sun, 24 May 2026 15:53:32 -0400 Subject: [PATCH 2/2] fmt --- crates/squawk_fmt/src/fmt.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/squawk_fmt/src/fmt.rs b/crates/squawk_fmt/src/fmt.rs index 703ae042..10f9bab5 100644 --- a/crates/squawk_fmt/src/fmt.rs +++ b/crates/squawk_fmt/src/fmt.rs @@ -504,7 +504,10 @@ fn build_string_literal<'a>(lit: &ast::Literal) -> Doc<'a> { fn format_string_token(t: &SyntaxToken) -> String { let text = t.text(); - if matches!(t.kind(), SyntaxKind::STRING | SyntaxKind::DOLLAR_QUOTED_STRING) { + if matches!( + t.kind(), + SyntaxKind::STRING | SyntaxKind::DOLLAR_QUOTED_STRING + ) { return text.to_string(); } match text.find('\'') {