Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/squawk_fmt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ tiny_pretty.workspace = true
itertools.workspace = true
squawk-syntax.workspace = true
squawk-line-index.workspace = true
squawk-lexer.workspace = true
rowan.workspace = true
clap.workspace = true
anyhow.workspace = true
Expand All @@ -32,7 +33,6 @@ annotate-snippets.workspace = true
insta.workspace = true
dir-test.workspace = true
camino.workspace = true
squawk-lexer.workspace = true

[lints]
workspace = true
37 changes: 25 additions & 12 deletions crates/squawk_fmt/src/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1331,7 +1331,7 @@ fn build_accessor<'a>(accessor: ast::Accessor) -> Doc<'a> {
} else if let Some(name) = field.composite_field_ref() {
doc = doc
.append(leading_comments(name.syntax()))
.append(build_name(name.syntax()));
.append(build_column_label(name.syntax()));
}
doc
}
Expand Down Expand Up @@ -3225,7 +3225,7 @@ fn build_copy_option_list<'a>(list: ast::CopyOptionList) -> Doc<'a> {
fn build_copy_option<'a>(option: ast::CopyOption) -> Doc<'a> {
let mut doc = option
.copy_option_key()
.map(|key| build_keyword_node(key.syntax()))
.map(|key| build_column_label(key.syntax()))
.unwrap_or_else(Doc::nil);
if let Some(value) = option.copy_option_value() {
doc = doc
Expand Down Expand Up @@ -8727,18 +8727,31 @@ fn build_path_parts<'a>(
.append(build_path_ref(&qualifier))
.append(trailing_comments(qualifier.syntax()));
}
if dot.is_some() {
let is_qualified = dot.is_some();
if is_qualified {
doc = doc.append(Doc::text("."));
}
if let Some(segment) = segment {
doc = doc
.append(leading_comments(segment.syntax()))
.append(build_name(segment.syntax()));
.append(if is_qualified {
build_column_label(segment.syntax())
} else {
build_name(segment.syntax())
});
}
doc
}

fn build_name<'a>(node: &SyntaxNode) -> Doc<'a> {
build_name_with(node, quote_ident)
}

fn build_column_label<'a>(node: &SyntaxNode) -> Doc<'a> {
build_name_with(node, quote_column_alias)
}

fn build_name_with<'a>(node: &SyntaxNode, quote: fn(&str) -> String) -> Doc<'a> {
let mut tokens = node
.children_with_tokens()
.filter_map(|el| el.into_token())
Expand All @@ -8763,7 +8776,7 @@ fn build_name<'a>(node: &SyntaxNode) -> Doc<'a> {
return doc;
}

Doc::text(quote_ident(&normalize_name_node(node)))
Doc::text(quote(&normalize_name_node(node)))
}

fn is_unicode_escape(text: &str) -> bool {
Expand Down Expand Up @@ -9553,7 +9566,7 @@ fn build_attribute_list_with_layout<'a>(list: &ast::AttributeList, multiline: bo
let items = list.attribute_options().map(|option| {
let mut item = option
.namespace()
.map(|namespace| build_name(namespace.syntax()))
.map(|namespace| build_column_label(namespace.syntax()))
.unwrap_or_else(Doc::nil);
if let Some(dot) = option.dot_token() {
item = item.append(comments_before(dot)).append(Doc::text("."));
Expand All @@ -9564,7 +9577,7 @@ fn build_attribute_list_with_layout<'a>(list: &ast::AttributeList, multiline: bo
} else if name.join_token().is_some() {
Doc::text("join")
} else {
build_name(name.syntax())
build_column_label(name.syntax())
};
item = item
.append(leading_comments(name.syntax()))
Expand Down Expand Up @@ -19385,7 +19398,7 @@ fn build_field_expr<'a>(field_expr: ast::FieldExpr) -> Doc<'a> {
} else if let Some(field) = field_expr.field() {
doc = doc
.append(leading_comments(field.syntax()))
.append(build_name(field.syntax()));
.append(build_column_label(field.syntax()));
}

doc
Expand Down Expand Up @@ -19992,7 +20005,7 @@ fn build_xml_element_fn<'a>(xml_element_fn: ast::XmlElementFn) -> Doc<'a> {
body = body
.append(Doc::space())
.append(leading_comments(tag.syntax()))
.append(build_name(tag.syntax()));
.append(build_column_label(tag.syntax()));

let mut items = Vec::new();
if let Some(attrs) = xml_element_fn.expr_as_xml_attr_list() {
Expand Down Expand Up @@ -20049,7 +20062,7 @@ fn build_expr_as_xml_attr_list<'a>(attrs: ast::ExprAsXmlAttrList) -> Doc<'a> {
item = item
.append(Doc::space())
.append(leading_comments(name.syntax()))
.append(build_name(name.syntax()));
.append(build_column_label(name.syntax()));
}
(
leading_comments(attr.syntax()).append(item),
Expand Down Expand Up @@ -20146,7 +20159,7 @@ fn build_expr_as_element_tag_list<'a>(list: ast::ExprAsElementTagList) -> Doc<'a
item_doc = item_doc
.append(Doc::space())
.append(leading_comments(tag.syntax()))
.append(build_name(tag.syntax()));
.append(build_column_label(tag.syntax()));
}
(
leading_comments(item.syntax()).append(item_doc),
Expand Down Expand Up @@ -20212,7 +20225,7 @@ fn build_xml_pi_fn<'a>(xml_pi_fn: ast::XmlPiFn) -> Doc<'a> {
body = body
.append(Doc::space())
.append(leading_comments(target.syntax()))
.append(build_name(target.syntax()));
.append(build_column_label(target.syntax()));
}
if let Some(expr) = xml_pi_fn.expr() {
if let Some(comma) = xml_pi_fn.comma_token() {
Expand Down
1 change: 1 addition & 0 deletions crates/squawk_fmt/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod fmt;
pub mod token_compare;

pub use fmt::{fmt, fmt_str};
18 changes: 17 additions & 1 deletion crates/squawk_fmt/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::process::ExitCode;
use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet, renderer::DecorStyle};
use anyhow::Result;
use clap::Parser;
use squawk_fmt::token_compare::assert_no_dropped_tokens;
use squawk_syntax::SourceFile;

#[derive(Parser)]
Expand Down Expand Up @@ -51,6 +52,21 @@ fn main() -> Result<ExitCode> {
return Ok(ExitCode::FAILURE);
}

write!(io::stdout().lock(), "{}", squawk_fmt::fmt_str(&input)?)?;
let formatted = squawk_fmt::fmt_str(&input)?;
assert_no_dropped_tokens(&input, &formatted);

let reparse = SourceFile::parse(&formatted);
assert!(
reparse.errors().is_empty(),
"formatted output has syntax errors:\n{}\n\nformatted output:\n{formatted}",
reparse
.errors()
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n")
);

write!(io::stdout().lock(), "{formatted}")?;
Ok(ExitCode::SUCCESS)
}
90 changes: 90 additions & 0 deletions crates/squawk_fmt/src/token_compare.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use squawk_lexer::{Token, TokenKind, tokenize};

fn meaningful_tokens(text: &str) -> Vec<(TokenKind, &str)> {
let mut tokens = Vec::new();
let mut offset = 0;
for Token { kind, len } in tokenize(text) {
let len = len as usize;
if kind != TokenKind::Eof && kind != TokenKind::Whitespace {
tokens.push((kind, &text[offset..offset + len]));
}
offset += len;
}
tokens
}

fn tokens_equivalent(before: (TokenKind, &str), after: (TokenKind, &str)) -> bool {
let (before_kind, before_text) = before;
let (after_kind, after_text) = after;

if before_kind == after_kind {
return before_text.eq_ignore_ascii_case(after_text);
}

// The formatter removes unnecessary identifier quotes, so compare quoted
// and unquoted identifier tokens by their contents.
fn unquote<'a>(kind: &TokenKind, text: &'a str) -> Option<&'a str> {
match kind {
TokenKind::QuotedIdent { .. } => text
.strip_prefix('"')
.and_then(|text| text.strip_suffix('"')),
TokenKind::Ident => Some(text),
_ => None,
}
}

match (
unquote(&before_kind, before_text),
unquote(&after_kind, after_text),
) {
(Some(before), Some(after)) => before.eq_ignore_ascii_case(after),
_ => false,
}
}

pub fn assert_no_dropped_tokens(before: &str, after: &str) {
let before_tokens = meaningful_tokens(before);
let after_tokens = meaningful_tokens(after);

let before_len = before_tokens.len();
let after_len = after_tokens.len();

for (index, (&before, &after)) in before_tokens.iter().zip(&after_tokens).enumerate() {
assert!(
tokens_equivalent(before, after),
"token mismatch at position {index}:\n before: {:?} {:?}\n after: {:?} {:?}",
before.0,
before.1,
after.0,
after.1
);
}

assert!(
before_len == after_len,
"token count mismatch: before has {before_len} tokens, after has {after_len} tokens\n {}",
if before_len > after_len {
let dropped = &before_tokens[after_len..];
format!(
"dropped {} token(s): {}",
dropped.len(),
dropped
.iter()
.map(|(kind, text)| format!("{kind:?} {text:?}"))
.collect::<Vec<_>>()
.join(", ")
)
} else {
let extra = &after_tokens[before_len..];
format!(
"extra {} token(s): {}",
extra.len(),
extra
.iter()
.map(|(kind, text)| format!("{kind:?} {text:?}"))
.collect::<Vec<_>>()
.join(", ")
)
}
);
}
2 changes: 2 additions & 0 deletions crates/squawk_fmt/tests/after/copy.snap
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ copy foo (id, name)
to stdout
with (format csv, header true, delimiter ',', null '', encoding 'UTF8');

copy foo to stdout (select 'x');

copy (
select id, a_very_long_column_name, another_very_long_column_name
from a_very_long_schema_name.a_very_long_table_name
Expand Down
2 changes: 2 additions & 0 deletions crates/squawk_fmt/tests/after/create_table_options.snap
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ create temporary table if not exists t (id int)
on commit delete rows
tablespace fast;

create table label_options (id int) with (select = 1, from.where = 2);

create global temporary table global_temp (id int)
on commit preserve rows;

Expand Down
6 changes: 3 additions & 3 deletions crates/squawk_fmt/tests/after/create_text_search_parser.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ input_file: crates/squawk_fmt/tests/before/create_text_search_parser.sql
create text search parser public.default_parser (
start = public.parser_start,
gettoken = public.parser_gettoken,
"end" = public.parser_end,
end = public.parser_end,
lextypes = public.parser_lextypes
);

create text search parser extraordinarily_long_schema_name.extraordinarily_long_parser_name (
start = public.extraordinarily_long_parser_start_function_name,
gettoken = public.extraordinarily_long_parser_gettoken_function_name,
"end" = public.extraordinarily_long_parser_end_function_name,
end = public.extraordinarily_long_parser_end_function_name,
lextypes = public.extraordinarily_long_parser_lextypes_function_name,
headline = public.extraordinarily_long_parser_headline_function_name
);

create /* text keyword */ text /* search keyword */ search /* parser keyword */ parser /* name */ public /* dot */./* name segment */ commented_parser /* left parenthesis */ (
/* first option */ start /* equals */ = /* value */ public.parser_start /* comma */,
/* second option */ gettoken = /* second value */ public.parser_gettoken /* comma two */,
/* third option */ "end" = public.parser_end,
/* third option */ end = public.parser_end,
lextypes = public.parser_lextypes /* right parenthesis */
)/* semicolon */;
8 changes: 8 additions & 0 deletions crates/squawk_fmt/tests/after/select_expr.snap
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,14 @@ select a_very_long_function_name(
third_very_long_argument_name
);

select
foo.select,
foo.left,
foo.array,
foo.filter,
foo."Mixed",
foo."has space";

select json_object(
'a': 1,
'b' value 2 format json
Expand Down
2 changes: 1 addition & 1 deletion crates/squawk_fmt/tests/after/types.snap
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ select
numeric(10, 2) '10',
foo(1) '100',
foo.bar(10, 2) '100',
foo."select" 'x';
foo.select 'x';

select
varchar e'a',
Expand Down
2 changes: 1 addition & 1 deletion crates/squawk_fmt/tests/after/update.snap
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ update foo
set
(a, b) = row(1, default),
(c, d) = (select x, y from bar),
payload.field[1][2:3] = 4;
payload.select[1][2:3] = 4;

update a_very_long_schema_name.a_very_long_table_name
set
Expand Down
8 changes: 4 additions & 4 deletions crates/squawk_fmt/tests/after/xml_functions.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ source: crates/squawk_fmt/tests/tests.rs
input_file: crates/squawk_fmt/tests/before/xml_functions.sql
---
select
xmlelement(name foo),
xmlelement(name select),
xmlelement(name foo, 1, 2),
xmlelement(name foo, xmlattributes(a, b as c)),
xmlelement(name foo, xmlattributes(a as from, b as c)),
xmlelement(name foo, xmlattributes(a as attr), x, y),
xmlelement(
name a_very_long_element_name,
Expand Down Expand Up @@ -42,7 +42,7 @@ select
/* before document */ doc
/* before second by */ by /* before value */ value /* before closing paren */
) /* after exists */,
xmlforest(a, b as foo),
xmlforest(a, b as where),
xmlforest(
first_very_long_expression as first_very_long_element_name,
second_very_long_expression as second_very_long_element_name
Expand All @@ -63,7 +63,7 @@ select
/* before expression */ value
/* before preserve */ preserve /* before whitespace */ whitespace /* before closing paren */
) /* after parse */,
xmlpi(name php),
xmlpi(name select),
xmlpi(name php, 'echo'),
xmlpi(
name a_very_long_xml_processing_instruction_target,
Expand Down
2 changes: 2 additions & 0 deletions crates/squawk_fmt/tests/before/copy.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ COPY foo FROM '/tmp/foo.csv';

copy foo (id, name) to stdout with (format csv, header true, delimiter ',', null '', encoding 'UTF8');

copy foo to stdout ("select" 'x');

copy (select id, a_very_long_column_name, another_very_long_column_name from a_very_long_schema_name.a_very_long_table_name) to program 'gzip > /tmp/a_very_long_output_file_name.csv' with (format csv, header on);

copy binary foo from stdin binary freeze csv header json delimiter as ',' null as '' quote as '"' escape as '\\' encoding 'UTF8' force not null id,name force quote * force null description where id > 0;
Expand Down
2 changes: 2 additions & 0 deletions crates/squawk_fmt/tests/before/create_table_options.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
create temporary table if not exists t (id int) inherits (parent, archive.parent) partition by hash (id) using heap with (fillfactor=70) on commit delete rows tablespace fast;

create table label_options (id int) with ("select" = 1, "from"."where" = 2);

create global temporary table global_temp (id int) on commit preserve rows;

create local temp table local_temp (id int) on commit drop;
Expand Down
2 changes: 2 additions & 0 deletions crates/squawk_fmt/tests/before/select_expr.sql
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,8 @@ select

select a_very_long_function_name(first_very_long_argument_name, second_very_long_argument_name, third_very_long_argument_name);

select foo."select", foo."left", foo."array", foo."filter", foo."Mixed", foo."has space";

select
json_object(
'a': 1, 'b' value 2 format json
Expand Down
Loading
Loading