Skip to main content

squawk_fmt/
fmt.rs

1use anyhow::{Result, bail};
2use itertools::Itertools;
3use rowan::Direction;
4use squawk_line_index::{LineEnding, UniversalNewlines, find_newline};
5use squawk_syntax::ast::{self, AstNode, LitKind};
6use squawk_syntax::quote::quote_column_alias;
7use squawk_syntax::{SyntaxKind, SyntaxNode, SyntaxToken};
8use tiny_pretty::Doc;
9use tiny_pretty::{LineBreak, PrintOptions, print};
10
11// TODO: anytime we have `syntax().to_string()`, it means we have to do more to
12// actually convert the data into the IR. to_string() is a temp hack
13
14fn build_source_file(source_file: &ast::SourceFile) -> Doc<'_> {
15    let mut doc = Doc::nil();
16    for el in source_file.syntax().children_with_tokens() {
17        match el {
18            rowan::NodeOrToken::Node(node) => {
19                if let Some(stmt) = ast::Stmt::cast(node) {
20                    match stmt {
21                        ast::Stmt::Select(select) => {
22                            doc = doc.append(build_select_doc(&select));
23                        }
24                        ast::Stmt::CreateTable(create_table) => {
25                            doc = doc.append(build_create_table(&create_table));
26                        }
27                        _ => (),
28                    }
29                }
30            }
31            rowan::NodeOrToken::Token(token) => {
32                if token.kind() == SyntaxKind::COMMENT {
33                    doc = doc.append(Doc::text(token.text().to_string()));
34                } else if token.kind() == SyntaxKind::WHITESPACE {
35                    // TODO: I think we can improve this
36                    let lines = token.text().universal_newlines().count();
37                    if lines >= 2 {
38                        doc = doc.append(Doc::empty_line()).append(Doc::empty_line());
39                    } else {
40                        doc = doc.append(Doc::empty_line());
41                    }
42                }
43            }
44        }
45    }
46    doc
47}
48
49fn build_create_table<'a>(create_table: &ast::CreateTable) -> Doc<'a> {
50    let mut doc = Doc::text("create")
51        .append(Doc::space())
52        .append(Doc::text("table"))
53        .append(Doc::space())
54        .append(Doc::text(
55            create_table.table_name().unwrap().syntax().to_string(),
56        ))
57        .append(Doc::text("("))
58        .append(
59            Doc::line_or_nil()
60                .append(Doc::list(
61                    Itertools::intersperse(
62                        create_table
63                            .table_arg_list()
64                            .unwrap()
65                            .args()
66                            .map(build_table_arg),
67                        Doc::text(",").append(Doc::hard_line()),
68                    )
69                    .collect(),
70                ))
71                .nest(2)
72                .append(Doc::line_or_nil())
73                .group(),
74        )
75        .append(Doc::text(")"));
76
77    doc = doc.append(build_semicolon(create_table.semicolon_token()));
78
79    doc
80}
81
82fn build_table_arg<'a>(create_table: ast::TableArg) -> Doc<'a> {
83    match create_table {
84        ast::TableArg::Column(column) => build_column_name(column.name().unwrap())
85            .append(Doc::space())
86            .append(Doc::text(column.ty().unwrap().syntax().to_string())),
87        ast::TableArg::LikeClause(_like_clause) => todo!(),
88        ast::TableArg::TableConstraint(_table_constraint) => todo!(),
89    }
90}
91
92fn build_select_doc<'a>(select: &ast::Select) -> Doc<'a> {
93    let mut doc = Doc::text("select").append(Doc::line_or_space());
94
95    if let Some(select_clause) = select.select_clause() {
96        if let Some(distinct_clause) = select_clause.distinct_clause() {
97            doc = doc.append(leading_comments(distinct_clause.syntax()));
98            doc = doc.append(Doc::text("distinct")).append(Doc::space());
99        }
100        if let Some(all_token) = select_clause.all_token() {
101            doc = doc.append(leading_comments_token(&all_token));
102            doc = doc.append(Doc::text("all")).append(Doc::space());
103        }
104        if let Some(target_list) = select_clause.target_list() {
105            doc = doc.append(leading_comments(target_list.syntax()));
106            doc = doc
107                .append(Doc::list(
108                    Itertools::intersperse(
109                        target_list.targets().flat_map(build_target),
110                        Doc::text(",").append(Doc::line_or_space()),
111                    )
112                    .collect(),
113                ))
114                .nest(2);
115        }
116    }
117
118    if let Some(from) = &select.from_clause() {
119        doc = doc.append(
120            Doc::line_or_space()
121                .append(Doc::text("from"))
122                .append(Doc::space())
123                .append(Doc::text(
124                    from.from_items().next().unwrap().syntax().to_string(),
125                )),
126        );
127    }
128
129    if let Some(group) = &select.group_by_clause() {
130        let mut group_doc = Doc::line_or_space().append(leading_comments(group.syntax()));
131        group_doc = group_doc.append(Doc::text("group")).append(Doc::space());
132        if let Some(by_token) = group.by_token() {
133            group_doc = group_doc.append(leading_comments_token(&by_token));
134        }
135        group_doc = group_doc.append(Doc::text("by")).append(Doc::space());
136        if let Some(list) = group.group_by_list() {
137            group_doc = group_doc.append(build_group_by_list(list));
138        }
139        doc = doc.append(group_doc);
140    }
141
142    doc = doc.append(build_semicolon(select.semicolon_token()));
143
144    doc.group()
145}
146
147fn build_group_by_list<'a>(list: ast::GroupByList) -> Doc<'a> {
148    leading_comments(list.syntax()).append(Doc::text(list.syntax().to_string()))
149}
150
151fn build_semicolon<'a>(semi: Option<SyntaxToken>) -> Doc<'a> {
152    let Some(semi) = semi else {
153        return Doc::nil();
154    };
155    let mut doc = Doc::nil();
156    let mut comments: Vec<SyntaxToken> = vec![];
157    for next in semi.siblings_with_tokens(Direction::Prev).skip(1) {
158        match next {
159            rowan::NodeOrToken::Node(_) => break,
160            rowan::NodeOrToken::Token(token) => {
161                if token.kind() == SyntaxKind::COMMENT {
162                    comments.push(token);
163                } else if token.kind() == SyntaxKind::WHITESPACE {
164                    continue;
165                } else {
166                    break;
167                }
168            }
169        }
170    }
171    for comment in comments.iter().rev() {
172        doc = doc.append(Doc::text(comment.text().to_string()));
173    }
174    doc.append(Doc::text(";"))
175}
176
177fn build_expr<'a>(expr: ast::Expr) -> Doc<'a> {
178    match expr {
179        ast::Expr::ArrayExpr(array_expr) => {
180            let mut doc = Doc::nil();
181
182            // nested parts of array expressions don't require the array token
183            if array_expr.array_token().is_some() {
184                doc = doc.append(Doc::text("array"));
185            };
186
187            if let Some(select) = array_expr.select() {
188                doc = doc
189                    .append(Doc::text("("))
190                    .append(build_select_doc(&select))
191                    .append(Doc::text(")"))
192            } else {
193                doc = doc
194                    .append(Doc::text("["))
195                    .append(Doc::list(
196                        Itertools::intersperse(
197                            array_expr.exprs().map(build_expr),
198                            Doc::text(",").append(Doc::space()),
199                        )
200                        .collect(),
201                    ))
202                    .append(Doc::text("]"));
203            }
204
205            doc
206        }
207        ast::Expr::BetweenExpr(between_expr) => {
208            let mut doc = build_expr(between_expr.target().unwrap());
209            if between_expr.not_token().is_some() {
210                doc = doc.append(Doc::space()).append(Doc::text("not"));
211            }
212            doc = doc.append(Doc::space()).append(Doc::text("between"));
213            if between_expr.asymmetric_token().is_some() {
214                doc = doc.append(Doc::space()).append(Doc::text("asymmetric"));
215            } else if between_expr.symmetric_token().is_some() {
216                doc = doc.append(Doc::space()).append(Doc::text("symmetric"));
217            }
218            doc.append(Doc::space())
219                .append(build_expr(between_expr.start().unwrap()))
220                .append(Doc::space())
221                .append(Doc::text("and"))
222                .append(Doc::space())
223                .append(build_expr(between_expr.end().unwrap()))
224        }
225        ast::Expr::BinExpr(bin_expr) => build_expr(bin_expr.lhs().unwrap())
226            .append(Doc::space())
227            .append(build_op(bin_expr.op().unwrap()))
228            .append(Doc::space())
229            .append(build_expr(bin_expr.rhs().unwrap())),
230        // ast::Expr::CallExpr(call_expr) => todo!(),
231        // ast::Expr::CaseExpr(case_expr) => todo!(),
232        ast::Expr::CastExpr(cast_expr) => {
233            let mut doc = Doc::nil();
234            if cast_expr.colon_colon().is_some() {
235                doc = doc
236                    .append(build_expr(cast_expr.expr().unwrap()))
237                    .append(Doc::text("::"))
238                    .append(build_type(cast_expr.ty().unwrap()))
239            } else if cast_expr.as_token().is_some() {
240                if cast_expr.cast_token().is_some() {
241                    doc = doc.append(Doc::text("cast"))
242                } else if cast_expr.treat_token().is_some() {
243                    doc = doc.append(Doc::text("treat"))
244                }
245                doc = doc
246                    .append(Doc::text("("))
247                    .append(build_expr(cast_expr.expr().unwrap()))
248                    .append(Doc::space())
249                    .append(Doc::text("as"))
250                    .append(Doc::space())
251                    .append(build_type(cast_expr.ty().unwrap()))
252                    .append(Doc::text(")"))
253            } else {
254                doc = doc
255                    .append(build_type(cast_expr.ty().unwrap()))
256                    .append(Doc::space())
257                    .append(build_literal(cast_expr.literal().unwrap()))
258            }
259            doc
260        }
261        ast::Expr::Collate(collate) => build_expr(collate.expr().unwrap())
262            .append(Doc::space())
263            .append(Doc::text("collate"))
264            .append(Doc::space())
265            .append(Doc::text(
266                collate.collation_ref().unwrap().syntax().to_string(),
267            )),
268        // ast::Expr::FieldExpr(field_expr) => todo!(),
269        // ast::Expr::IndexExpr(index_expr) => todo!(),
270        ast::Expr::Literal(literal) => build_literal(literal),
271        // ast::Expr::NameRef(name_ref) => todo!(),
272        // ast::Expr::ParenExpr(paren_expr) => todo!(),
273        ast::Expr::PostfixExpr(postfix_expr) => {
274            let expr = build_expr(postfix_expr.expr().unwrap());
275            let op = match postfix_expr.op().unwrap() {
276                ast::PostfixOp::AtLocal(_) => Doc::text("at local"),
277                ast::PostfixOp::IsNull(_) => Doc::text("isnull"),
278                ast::PostfixOp::NotNull(_) => Doc::text("notnull"),
279                ast::PostfixOp::IsJson(n) => {
280                    let mut doc = Doc::text("is json");
281                    if let Some(clause) = n.json_keys_unique_clause() {
282                        doc = doc
283                            .append(Doc::space())
284                            .append(build_json_keys_unique_clause(clause));
285                    }
286                    doc
287                }
288                ast::PostfixOp::IsJsonArray(n) => {
289                    let mut doc = Doc::text("is json array");
290                    if let Some(clause) = n.json_keys_unique_clause() {
291                        doc = doc
292                            .append(Doc::space())
293                            .append(build_json_keys_unique_clause(clause));
294                    }
295                    doc
296                }
297                ast::PostfixOp::IsJsonObject(n) => {
298                    let mut doc = Doc::text("is json object");
299                    if let Some(clause) = n.json_keys_unique_clause() {
300                        doc = doc
301                            .append(Doc::space())
302                            .append(build_json_keys_unique_clause(clause));
303                    }
304                    doc
305                }
306                ast::PostfixOp::IsJsonScalar(n) => {
307                    let mut doc = Doc::text("is json scalar");
308                    if let Some(clause) = n.json_keys_unique_clause() {
309                        doc = doc
310                            .append(Doc::space())
311                            .append(build_json_keys_unique_clause(clause));
312                    }
313                    doc
314                }
315                ast::PostfixOp::IsJsonValue(n) => {
316                    let mut doc = Doc::text("is json value");
317                    if let Some(clause) = n.json_keys_unique_clause() {
318                        doc = doc
319                            .append(Doc::space())
320                            .append(build_json_keys_unique_clause(clause));
321                    }
322                    doc
323                }
324                ast::PostfixOp::IsNormalized(n) => {
325                    let mut doc = Doc::text("is");
326                    if let Some(form) = n.unicode_normal_form() {
327                        doc = doc
328                            .append(Doc::space())
329                            .append(build_unicode_normal_form(form));
330                    }
331                    doc.append(Doc::space()).append(Doc::text("normalized"))
332                }
333                ast::PostfixOp::IsNotJson(n) => {
334                    let mut doc = Doc::text("is not json");
335                    if let Some(clause) = n.json_keys_unique_clause() {
336                        doc = doc
337                            .append(Doc::space())
338                            .append(build_json_keys_unique_clause(clause));
339                    }
340                    doc
341                }
342                ast::PostfixOp::IsNotJsonArray(n) => {
343                    let mut doc = Doc::text("is not json array");
344                    if let Some(clause) = n.json_keys_unique_clause() {
345                        doc = doc
346                            .append(Doc::space())
347                            .append(build_json_keys_unique_clause(clause));
348                    }
349                    doc
350                }
351                ast::PostfixOp::IsNotJsonObject(n) => {
352                    let mut doc = Doc::text("is not json object");
353                    if let Some(clause) = n.json_keys_unique_clause() {
354                        doc = doc
355                            .append(Doc::space())
356                            .append(build_json_keys_unique_clause(clause));
357                    }
358                    doc
359                }
360                ast::PostfixOp::IsNotJsonScalar(n) => {
361                    let mut doc = Doc::text("is not json scalar");
362                    if let Some(clause) = n.json_keys_unique_clause() {
363                        doc = doc
364                            .append(Doc::space())
365                            .append(build_json_keys_unique_clause(clause));
366                    }
367                    doc
368                }
369                ast::PostfixOp::IsNotJsonValue(n) => {
370                    let mut doc = Doc::text("is not json value");
371                    if let Some(clause) = n.json_keys_unique_clause() {
372                        doc = doc
373                            .append(Doc::space())
374                            .append(build_json_keys_unique_clause(clause));
375                    }
376                    doc
377                }
378                ast::PostfixOp::IsNotNormalized(n) => {
379                    let mut doc = Doc::text("is not");
380                    if let Some(form) = n.unicode_normal_form() {
381                        doc = doc
382                            .append(Doc::space())
383                            .append(build_unicode_normal_form(form));
384                    }
385                    doc.append(Doc::space()).append(Doc::text("normalized"))
386                }
387            };
388            expr.append(Doc::space()).append(op)
389        }
390        // ast::Expr::PrefixExpr(prefix_expr) => todo!(),
391        // ast::Expr::SliceExpr(slice_expr) => todo!(),
392        // ast::Expr::TupleExpr(tuple_expr) => todo!(),
393        _ => Doc::text(expr.syntax().to_string()),
394    }
395}
396
397fn build_json_keys_unique_clause<'a>(clause: ast::JsonKeysUniqueClause) -> Doc<'a> {
398    let prefix = match clause {
399        ast::JsonKeysUniqueClause::JsonWithoutUniqueKeys(_) => "without",
400        ast::JsonKeysUniqueClause::JsonWithUniqueKeys(_) => "with",
401    };
402    Doc::text(prefix)
403        .append(Doc::space())
404        .append(Doc::text("unique"))
405        .append(Doc::space())
406        .append(Doc::text("keys"))
407}
408
409fn build_unicode_normal_form<'a>(form: ast::UnicodeNormalForm) -> Doc<'a> {
410    if form.nfc_token().is_some() {
411        Doc::text("nfc")
412    } else if form.nfd_token().is_some() {
413        Doc::text("nfd")
414    } else if form.nfkc_token().is_some() {
415        Doc::text("nfkc")
416    } else {
417        Doc::text("nfkd")
418    }
419}
420
421fn build_keyword_node<'a>(node: &SyntaxNode) -> Doc<'a> {
422    let mut docs: Vec<Doc<'a>> = vec![];
423    for el in node.children_with_tokens() {
424        match el {
425            rowan::NodeOrToken::Token(token) => match token.kind() {
426                SyntaxKind::WHITESPACE => continue,
427                SyntaxKind::COMMENT => {
428                    if !docs.is_empty() {
429                        docs.push(Doc::space());
430                    }
431                    docs.push(Doc::text(token.text().to_string()));
432                }
433                _ => {
434                    if !docs.is_empty() {
435                        docs.push(Doc::space());
436                    }
437                    docs.push(Doc::text(token.text().to_ascii_lowercase()));
438                }
439            },
440            rowan::NodeOrToken::Node(_) => (),
441        }
442    }
443    Doc::list(docs)
444}
445
446fn build_op<'a>(op: ast::BinOp) -> Doc<'a> {
447    match op {
448        ast::BinOp::And(_) => Doc::text("and"),
449        ast::BinOp::AtTimeZone(n) => build_keyword_node(n.syntax()),
450        ast::BinOp::Caret(_) => Doc::text("^"),
451        ast::BinOp::ColonColon(_) => Doc::text("::"),
452        ast::BinOp::ColonEq(_) => Doc::text(":="),
453        ast::BinOp::CustomOp(custom_op) => Doc::text(custom_op.syntax().to_string()),
454        ast::BinOp::Eq(_) => Doc::text("="),
455        ast::BinOp::Escape(_) => Doc::text("escape"),
456        ast::BinOp::FatArrow(_) => Doc::text("=>"),
457        ast::BinOp::Gteq(_) => Doc::text(">="),
458        ast::BinOp::Ilike(_) => Doc::text("ilike"),
459        ast::BinOp::In(_) => Doc::text("in"),
460        ast::BinOp::Is(_) => Doc::text("is"),
461        ast::BinOp::IsDistinctFrom(n) => build_keyword_node(n.syntax()),
462        ast::BinOp::IsNot(n) => build_keyword_node(n.syntax()),
463        ast::BinOp::IsNotDistinctFrom(n) => build_keyword_node(n.syntax()),
464        ast::BinOp::LAngle(_) => Doc::text("<"),
465        ast::BinOp::Like(_) => Doc::text("like"),
466        ast::BinOp::Lteq(_) => Doc::text("<="),
467        ast::BinOp::Minus(_) => Doc::text("-"),
468        ast::BinOp::Neq(_) => Doc::text("!="),
469        ast::BinOp::Neqb(_) => Doc::text("<>"),
470        ast::BinOp::NotIlike(n) => build_keyword_node(n.syntax()),
471        ast::BinOp::NotIn(n) => build_keyword_node(n.syntax()),
472        ast::BinOp::NotLike(n) => build_keyword_node(n.syntax()),
473        ast::BinOp::NotSimilarTo(n) => build_keyword_node(n.syntax()),
474        ast::BinOp::OperatorCall(op) => Doc::text(op.syntax().to_string()),
475        ast::BinOp::Or(_) => Doc::text("or"),
476        ast::BinOp::Overlaps(_) => Doc::text("overlaps"),
477        ast::BinOp::Percent(_) => Doc::text("%"),
478        ast::BinOp::Plus(_) => Doc::text("+"),
479        ast::BinOp::RAngle(_) => Doc::text(">"),
480        ast::BinOp::SimilarTo(n) => build_keyword_node(n.syntax()),
481        ast::BinOp::Slash(_) => Doc::text("/"),
482        ast::BinOp::Star(_) => Doc::text("*"),
483    }
484}
485
486fn build_literal<'a>(lit: ast::Literal) -> Doc<'a> {
487    let Some(kind) = lit.kind() else {
488        return Doc::nil();
489    };
490    match kind {
491        LitKind::Default(_) => Doc::text("default"),
492        LitKind::False(_) => Doc::text("false"),
493        LitKind::IntNumber(t) => Doc::text(t.text().to_string()),
494        LitKind::Null(_) => Doc::text("null"),
495        LitKind::NumericNumber(t) => Doc::text(t.text().to_string()),
496        LitKind::PositionalParam(t) => Doc::text(t.text().to_string()),
497        LitKind::True(_) => Doc::text("true"),
498        LitKind::BitString(_)
499        | LitKind::ByteString(_)
500        | LitKind::DollarQuotedString(_)
501        | LitKind::EscString(_)
502        | LitKind::NationalString(_)
503        | LitKind::String(_)
504        | LitKind::UnicodeEscString(_) => build_string_literal(&lit),
505    }
506}
507
508fn build_string_literal<'a>(lit: &ast::Literal) -> Doc<'a> {
509    let parts: Vec<Doc<'a>> = lit
510        .syntax()
511        .children_with_tokens()
512        .filter_map(|el| match el {
513            rowan::NodeOrToken::Token(t) if t.kind() != SyntaxKind::WHITESPACE => {
514                Some(Doc::text(format_string_token(&t)))
515            }
516            _ => None,
517        })
518        .collect();
519    Doc::list(Itertools::intersperse(parts.into_iter(), Doc::hard_line()).collect())
520}
521
522fn format_string_token(t: &SyntaxToken) -> String {
523    let text = t.text();
524    if matches!(
525        t.kind(),
526        SyntaxKind::STRING | SyntaxKind::DOLLAR_QUOTED_STRING
527    ) {
528        return text.to_string();
529    }
530    match text.find('\'') {
531        Some(idx) => {
532            let (prefix, rest) = text.split_at(idx);
533            let mut s = String::with_capacity(text.len());
534            s.push_str(&prefix.to_ascii_lowercase());
535            s.push_str(rest);
536            s
537        }
538        None => text.to_string(),
539    }
540}
541
542fn build_column_name<'a>(name: ast::ColumnName) -> Doc<'a> {
543    Doc::text(quote_column_alias(&name.text()))
544}
545
546fn build_type<'a>(ty: ast::Type) -> Doc<'a> {
547    Doc::text(ty.syntax().to_string())
548}
549
550fn leading_comments_token<'a>(node: &SyntaxToken) -> Doc<'a> {
551    let mut doc = Doc::nil();
552    for next in node.siblings_with_tokens(Direction::Prev).skip(1) {
553        match next {
554            rowan::NodeOrToken::Node(_node) => {
555                break;
556            }
557            rowan::NodeOrToken::Token(token) => {
558                if token.kind() == SyntaxKind::COMMENT {
559                    doc = doc
560                        .append(Doc::text(token.text().to_string()))
561                        .append(Doc::space());
562                } else if token.kind() == SyntaxKind::WHITESPACE {
563                    continue;
564                } else {
565                    break;
566                }
567            }
568        }
569    }
570    doc
571}
572
573fn leading_comments<'a>(node: &SyntaxNode) -> Doc<'a> {
574    let mut doc = Doc::nil();
575    for next in node.siblings_with_tokens(Direction::Prev).skip(1) {
576        match next {
577            rowan::NodeOrToken::Node(_node) => {
578                break;
579            }
580            rowan::NodeOrToken::Token(token) => {
581                if token.kind() == SyntaxKind::COMMENT {
582                    let is_block = token.text().starts_with("--");
583                    doc = doc
584                        .append(Doc::text(token.text().to_string()))
585                        .append(if is_block {
586                            Doc::hard_line()
587                        } else {
588                            Doc::space()
589                        });
590                } else if token.kind() == SyntaxKind::WHITESPACE {
591                    continue;
592                } else {
593                    break;
594                }
595            }
596        }
597    }
598    doc
599}
600
601fn trailing_comments<'a>(node: &SyntaxNode) -> Doc<'a> {
602    let mut doc = Doc::nil();
603    for next in node.siblings_with_tokens(Direction::Next).skip(1) {
604        match next {
605            rowan::NodeOrToken::Node(_node) => {
606                break;
607            }
608            rowan::NodeOrToken::Token(token) => {
609                if token.kind() == SyntaxKind::COMMENT {
610                    doc = doc
611                        .append(Doc::space())
612                        .append(Doc::text(token.text().to_string()));
613                } else if token.kind() == SyntaxKind::WHITESPACE {
614                    continue;
615                } else {
616                    break;
617                }
618            }
619        }
620    }
621    doc
622}
623
624fn build_target<'a>(target: ast::Target) -> Option<Doc<'a>> {
625    let mut doc = leading_comments(target.syntax());
626
627    if target.star_token().is_some() {
628        return Some(doc.append(Doc::text("*")));
629    }
630    let expr = target.expr()?;
631    doc = doc.append(build_expr(expr));
632
633    if let Some(as_name) = target.as_name() {
634        if as_name.as_token().is_some() {
635            doc = doc.append(Doc::space()).append(Doc::text("as"))
636        }
637
638        if let Some(column_name) = as_name.name() {
639            doc = doc
640                .append(Doc::space())
641                .append(build_column_name(column_name));
642        }
643    }
644
645    doc = doc.append(trailing_comments(target.syntax()));
646
647    Some(doc)
648}
649
650pub fn fmt(text: &str) -> Result<String> {
651    let line_ending = find_newline(text)
652        .map(|(_, ending)| ending)
653        .unwrap_or_default();
654
655    let line_break = match line_ending {
656        // see: https://github.com/g-plane/tiny_pretty/issues/3
657        LineEnding::Cr => bail!("CR line endings aren't supported"),
658        LineEnding::CrLf => LineBreak::Crlf,
659        LineEnding::Lf => LineBreak::Lf,
660    };
661
662    let parse = ast::SourceFile::parse(text);
663    let file = parse.tree();
664    debug_assert_eq!(
665        parse.errors(),
666        vec![],
667        "should bail out when there's parse errors"
668    );
669    let doc = build_source_file(&file);
670
671    Ok(print(
672        &doc,
673        &PrintOptions {
674            line_break,
675            ..Default::default()
676        },
677    ))
678}