mirror of
https://github.com/roc-lang/roc.git
synced 2025-09-03 10:37:20 +00:00
918 lines
28 KiB
Rust
918 lines
28 KiB
Rust
use crate::annotation::{Formattable, Newlines, Parens};
|
|
use crate::expr::{
|
|
expr_is_multiline, expr_lift_spaces_after, fmt_str_literal, format_sq_literal, is_str_multiline,
|
|
};
|
|
use crate::node::{Node, NodeInfo, NodeSequenceBuilder, Prec, Sp};
|
|
use crate::spaces::{fmt_comments_only, fmt_spaces, NewlineAt, INDENT};
|
|
use crate::Buf;
|
|
use bumpalo::Bump;
|
|
use roc_parse::ast::{
|
|
Base, CommentOrNewline, Pattern, PatternAs, Spaceable, Spaces, SpacesAfter, SpacesBefore,
|
|
};
|
|
use roc_parse::expr::merge_spaces;
|
|
use roc_region::all::Loc;
|
|
|
|
pub fn fmt_pattern<'a>(buf: &mut Buf, pattern: &'a Pattern<'a>, indent: u16, parens: Parens) {
|
|
pattern.format_with_options(buf, parens, Newlines::No, indent);
|
|
}
|
|
|
|
impl<'a> Formattable for PatternAs<'a> {
|
|
fn is_multiline(&self) -> bool {
|
|
self.spaces_before.iter().any(|s| s.is_comment())
|
|
}
|
|
|
|
fn format_with_options(
|
|
&self,
|
|
buf: &mut Buf,
|
|
_parens: Parens,
|
|
_newlines: Newlines,
|
|
indent: u16,
|
|
) {
|
|
buf.indent(indent);
|
|
|
|
if !buf.ends_with_space() {
|
|
buf.spaces(1);
|
|
}
|
|
|
|
buf.push_str("as");
|
|
buf.spaces(1);
|
|
|
|
// these spaces "belong" to the identifier, which can never be multiline
|
|
fmt_comments_only(buf, self.spaces_before.iter(), NewlineAt::Bottom, indent);
|
|
|
|
buf.indent(indent);
|
|
buf.push_str(self.identifier.value);
|
|
}
|
|
}
|
|
|
|
impl<'a> Formattable for Pattern<'a> {
|
|
fn is_multiline(&self) -> bool {
|
|
// Theory: a pattern should only be multiline when it contains a comment
|
|
match self {
|
|
Pattern::SpaceBefore(pattern, spaces) | Pattern::SpaceAfter(pattern, spaces) => {
|
|
debug_assert!(
|
|
!spaces.is_empty(),
|
|
"spaces is empty in pattern {:#?}",
|
|
pattern
|
|
);
|
|
|
|
spaces.iter().any(|s| s.is_comment()) || pattern.is_multiline()
|
|
}
|
|
|
|
Pattern::RecordDestructure(fields) => fields.iter().any(|f| f.is_multiline()),
|
|
Pattern::RequiredField(_, subpattern) => subpattern.is_multiline(),
|
|
|
|
Pattern::OptionalField(_, expr) => expr_is_multiline(&expr.value, true),
|
|
|
|
Pattern::As(pattern, pattern_as) => pattern.is_multiline() || pattern_as.is_multiline(),
|
|
Pattern::ListRest(opt_pattern_as) => match opt_pattern_as {
|
|
None => false,
|
|
Some((list_rest_spaces, pattern_as)) => {
|
|
list_rest_spaces.iter().any(|s| s.is_comment()) || pattern_as.is_multiline()
|
|
}
|
|
},
|
|
Pattern::StrLiteral(literal) => is_str_multiline(literal),
|
|
Pattern::Apply(pat, args) => {
|
|
pat.is_multiline() || args.iter().any(|a| a.is_multiline())
|
|
}
|
|
Pattern::PncApply(pat, args) => {
|
|
pat.is_multiline()
|
|
|| args.iter().any(|a| a.is_multiline())
|
|
|| !args.final_comments().is_empty()
|
|
}
|
|
|
|
Pattern::Identifier { .. }
|
|
| Pattern::Tag(_)
|
|
| Pattern::OpaqueRef(_)
|
|
| Pattern::NumLiteral(..)
|
|
| Pattern::NonBase10Literal { .. }
|
|
| Pattern::FloatLiteral(..)
|
|
| Pattern::SingleQuote(_)
|
|
| Pattern::Underscore(_)
|
|
| Pattern::Malformed(_)
|
|
| Pattern::MalformedIdent(_, _)
|
|
| Pattern::MalformedExpr(_)
|
|
| Pattern::QualifiedIdentifier { .. } => false,
|
|
|
|
Pattern::Tuple(patterns) | Pattern::List(patterns) => {
|
|
patterns.iter().any(|p| p.is_multiline())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn format_with_options(&self, buf: &mut Buf, parens: Parens, _newlines: Newlines, indent: u16) {
|
|
fmt_pattern_inner(self, buf, parens, indent, self.is_multiline(), false);
|
|
}
|
|
}
|
|
|
|
fn fmt_pattern_inner(
|
|
pat: &Pattern<'_>,
|
|
buf: &mut Buf,
|
|
parens: Parens,
|
|
indent: u16,
|
|
outer_is_multiline: bool,
|
|
force_newline_at_start: bool,
|
|
) -> bool {
|
|
let me = pattern_lift_spaces(buf.text.bump(), pat);
|
|
|
|
let mut was_multiline = me.item.is_multiline();
|
|
|
|
if !me.before.is_empty() {
|
|
if !outer_is_multiline {
|
|
was_multiline |= me.before.iter().any(|s| s.is_comment());
|
|
fmt_comments_only(buf, me.before.iter(), NewlineAt::Bottom, indent)
|
|
} else {
|
|
was_multiline |= true;
|
|
fmt_spaces(buf, me.before.iter(), indent);
|
|
}
|
|
}
|
|
|
|
if force_newline_at_start {
|
|
buf.ensure_ends_with_newline();
|
|
}
|
|
|
|
let is_multiline = me.item.is_multiline();
|
|
|
|
fmt_pattern_only(&me.item, buf, parens, indent, is_multiline);
|
|
|
|
if !me.after.is_empty() {
|
|
if starts_with_inline_comment(me.after.iter()) {
|
|
buf.spaces(1);
|
|
}
|
|
|
|
if !outer_is_multiline {
|
|
was_multiline |= me.before.iter().any(|s| s.is_comment());
|
|
fmt_comments_only(buf, me.after.iter(), NewlineAt::Bottom, indent)
|
|
} else {
|
|
was_multiline |= true;
|
|
fmt_spaces(buf, me.after.iter(), indent);
|
|
}
|
|
}
|
|
|
|
was_multiline
|
|
}
|
|
|
|
fn fmt_pattern_only(
|
|
me: &Pattern<'_>,
|
|
buf: &mut Buf<'_>,
|
|
parens: Parens,
|
|
indent: u16,
|
|
is_multiline: bool,
|
|
) {
|
|
match me {
|
|
Pattern::Identifier { ident } => {
|
|
buf.indent(indent);
|
|
snakify_camel_ident(buf, ident);
|
|
}
|
|
Pattern::Tag(name) | Pattern::OpaqueRef(name) => {
|
|
buf.indent(indent);
|
|
buf.push_str(name);
|
|
}
|
|
Pattern::PncApply(loc_pattern, loc_arg_patterns) => {
|
|
pattern_fmt_apply(
|
|
buf,
|
|
loc_pattern.value,
|
|
loc_arg_patterns.items,
|
|
Parens::NotNeeded,
|
|
indent,
|
|
is_multiline,
|
|
true,
|
|
Some(loc_arg_patterns.final_comments()),
|
|
);
|
|
}
|
|
Pattern::Apply(loc_pattern, loc_arg_patterns) => {
|
|
pattern_fmt_apply(
|
|
buf,
|
|
loc_pattern.value,
|
|
loc_arg_patterns,
|
|
parens,
|
|
indent,
|
|
is_multiline,
|
|
false,
|
|
None,
|
|
);
|
|
}
|
|
Pattern::RecordDestructure(loc_patterns) => {
|
|
buf.indent(indent);
|
|
buf.push_str("{");
|
|
|
|
if !loc_patterns.is_empty() {
|
|
buf.spaces(1);
|
|
let mut last_was_multiline = false;
|
|
let mut it = loc_patterns.iter().peekable();
|
|
while let Some(loc_pattern) = it.next() {
|
|
let item = pattern_lift_spaces(buf.text.bump(), &loc_pattern.value);
|
|
|
|
if !item.before.is_empty() {
|
|
if !is_multiline {
|
|
fmt_comments_only(buf, item.before.iter(), NewlineAt::Bottom, indent)
|
|
} else {
|
|
fmt_spaces(buf, item.before.iter(), indent);
|
|
}
|
|
}
|
|
|
|
if last_was_multiline {
|
|
buf.ensure_ends_with_newline();
|
|
}
|
|
|
|
fmt_pattern_inner(
|
|
&item.item,
|
|
buf,
|
|
Parens::NotNeeded,
|
|
indent,
|
|
is_multiline,
|
|
false,
|
|
);
|
|
|
|
let is_multiline = item.item.is_multiline();
|
|
last_was_multiline = is_multiline;
|
|
|
|
if it.peek().is_some() {
|
|
buf.push_str(",");
|
|
}
|
|
|
|
if !item.after.is_empty() {
|
|
if starts_with_inline_comment(item.after.iter()) {
|
|
buf.spaces(1);
|
|
}
|
|
|
|
if !is_multiline {
|
|
fmt_comments_only(buf, item.after.iter(), NewlineAt::Bottom, indent)
|
|
} else {
|
|
fmt_spaces(buf, item.after.iter(), indent);
|
|
}
|
|
}
|
|
if it.peek().is_some() {
|
|
buf.ensure_ends_with_whitespace();
|
|
}
|
|
}
|
|
buf.spaces(1);
|
|
}
|
|
|
|
buf.indent(indent);
|
|
buf.push_str("}");
|
|
}
|
|
|
|
Pattern::RequiredField(name, loc_pattern) => {
|
|
buf.indent(indent);
|
|
snakify_camel_ident(buf, name);
|
|
buf.push_str(":");
|
|
buf.spaces(1);
|
|
fmt_pattern_inner(
|
|
&loc_pattern.value,
|
|
buf,
|
|
Parens::NotNeeded,
|
|
indent,
|
|
is_multiline,
|
|
false,
|
|
);
|
|
}
|
|
|
|
Pattern::OptionalField(name, loc_pattern) => {
|
|
buf.indent(indent);
|
|
snakify_camel_ident(buf, name);
|
|
buf.push_str(" ??");
|
|
buf.spaces(1);
|
|
loc_pattern.format(buf, indent);
|
|
}
|
|
|
|
Pattern::NumLiteral(string) => {
|
|
buf.indent(indent);
|
|
let needs_parens = parens == Parens::InClosurePattern
|
|
|| (parens == Parens::InPncApplyFunc && string.starts_with('-'));
|
|
if needs_parens {
|
|
buf.push('(');
|
|
}
|
|
buf.push_str(string);
|
|
if needs_parens {
|
|
buf.push(')');
|
|
}
|
|
}
|
|
Pattern::NonBase10Literal {
|
|
base,
|
|
string,
|
|
is_negative,
|
|
} => {
|
|
buf.indent(indent);
|
|
let needs_parens = parens == Parens::InClosurePattern
|
|
|| (parens == Parens::InPncApplyFunc && *is_negative);
|
|
if needs_parens {
|
|
buf.push('(');
|
|
}
|
|
if *is_negative {
|
|
buf.push('-');
|
|
}
|
|
|
|
match base {
|
|
Base::Hex => buf.push_str("0x"),
|
|
Base::Octal => buf.push_str("0o"),
|
|
Base::Binary => buf.push_str("0b"),
|
|
Base::Decimal => { /* nothing */ }
|
|
}
|
|
|
|
buf.push_str(string);
|
|
|
|
if needs_parens {
|
|
buf.push(')');
|
|
}
|
|
}
|
|
Pattern::FloatLiteral(string) => {
|
|
buf.indent(indent);
|
|
let needs_parens = parens == Parens::InClosurePattern
|
|
|| (parens == Parens::InPncApplyFunc && string.starts_with('-'));
|
|
if needs_parens {
|
|
buf.push('(');
|
|
}
|
|
buf.push_str(string);
|
|
if needs_parens {
|
|
buf.push(')');
|
|
}
|
|
}
|
|
Pattern::StrLiteral(literal) => {
|
|
let needs_parens = parens == Parens::InClosurePattern;
|
|
if needs_parens {
|
|
buf.push('(');
|
|
}
|
|
fmt_str_literal(buf, *literal, indent);
|
|
if needs_parens {
|
|
buf.push(')');
|
|
}
|
|
}
|
|
Pattern::SingleQuote(string) => {
|
|
buf.indent(indent);
|
|
let needs_parens = parens == Parens::InClosurePattern;
|
|
if needs_parens {
|
|
buf.push('(');
|
|
}
|
|
format_sq_literal(buf, string);
|
|
if needs_parens {
|
|
buf.push(')');
|
|
}
|
|
}
|
|
Pattern::Underscore(name) => {
|
|
buf.indent(indent);
|
|
buf.push('_');
|
|
buf.push_str(name);
|
|
}
|
|
Pattern::Tuple(loc_patterns) => {
|
|
buf.indent(indent);
|
|
buf.push_str("(");
|
|
|
|
let mut add_newlines = false;
|
|
|
|
let mut it = loc_patterns.iter().peekable();
|
|
while let Some(loc_pattern) = it.next() {
|
|
add_newlines |= fmt_pattern_inner(
|
|
&loc_pattern.value,
|
|
buf,
|
|
Parens::NotNeeded,
|
|
indent,
|
|
is_multiline,
|
|
add_newlines,
|
|
);
|
|
|
|
if it.peek().is_some() {
|
|
buf.indent(indent);
|
|
buf.push_str(",");
|
|
buf.spaces(1);
|
|
}
|
|
}
|
|
|
|
buf.indent(indent);
|
|
buf.push_str(")");
|
|
}
|
|
Pattern::List(loc_patterns) => {
|
|
buf.indent(indent);
|
|
buf.push_str("[");
|
|
|
|
let mut add_newlines = false;
|
|
|
|
let mut it = loc_patterns.iter().peekable();
|
|
while let Some(loc_pattern) = it.next() {
|
|
add_newlines |= fmt_pattern_inner(
|
|
&loc_pattern.value,
|
|
buf,
|
|
Parens::NotNeeded,
|
|
indent,
|
|
is_multiline,
|
|
add_newlines,
|
|
);
|
|
|
|
if it.peek().is_some() {
|
|
buf.indent(indent);
|
|
buf.push_str(",");
|
|
buf.spaces(1);
|
|
}
|
|
}
|
|
|
|
buf.indent(indent);
|
|
buf.push_str("]");
|
|
}
|
|
Pattern::ListRest(opt_pattern_as) => {
|
|
buf.indent(indent);
|
|
buf.push_str("..");
|
|
|
|
if let Some((list_rest_spaces, pattern_as)) = opt_pattern_as {
|
|
// these spaces "belong" to the `..`, which can never be multiline
|
|
fmt_comments_only(buf, list_rest_spaces.iter(), NewlineAt::Bottom, indent);
|
|
|
|
pattern_as.format(buf, indent + INDENT);
|
|
}
|
|
}
|
|
|
|
Pattern::As(pattern, pattern_as) => {
|
|
let needs_parens = parens == Parens::InAsPattern
|
|
|| parens == Parens::InApply
|
|
|| parens == Parens::InPncApplyFunc
|
|
|| parens == Parens::InClosurePattern;
|
|
|
|
if needs_parens {
|
|
buf.indent(indent);
|
|
buf.push('(');
|
|
}
|
|
|
|
fmt_pattern(buf, &pattern.value, indent, Parens::InAsPattern);
|
|
|
|
pattern_as.format(buf, indent + INDENT);
|
|
|
|
if needs_parens {
|
|
buf.indent(indent);
|
|
buf.push(')');
|
|
}
|
|
}
|
|
|
|
Pattern::SpaceBefore(..) | Pattern::SpaceAfter(..) => {
|
|
unreachable!("handled by lift_spaces")
|
|
}
|
|
|
|
// Malformed
|
|
Pattern::Malformed(string) | Pattern::MalformedIdent(string, _) => {
|
|
buf.indent(indent);
|
|
buf.push_str(string);
|
|
}
|
|
Pattern::MalformedExpr(expr) => {
|
|
buf.indent(indent);
|
|
expr.format(buf, indent);
|
|
}
|
|
Pattern::QualifiedIdentifier { module_name, ident } => {
|
|
buf.indent(indent);
|
|
if !module_name.is_empty() {
|
|
buf.push_str(module_name);
|
|
buf.push('.');
|
|
}
|
|
|
|
snakify_camel_ident(buf, ident);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn pattern_fmt_apply(
|
|
buf: &mut Buf<'_>,
|
|
func: Pattern<'_>,
|
|
args: &[Loc<Pattern<'_>>],
|
|
parens: Parens,
|
|
indent: u16,
|
|
is_multiline: bool,
|
|
is_pnc: bool,
|
|
final_comments: Option<&[CommentOrNewline]>,
|
|
) {
|
|
let use_commas_and_parens = is_pnc || buf.flags().parens_and_commas;
|
|
buf.indent(indent);
|
|
// Sometimes, an Apply pattern needs parens around it.
|
|
// In particular when an Apply's argument is itself an Apply (> 0) arguments
|
|
let parens = !args.is_empty()
|
|
&& (parens == Parens::InApply || parens == Parens::InPncApplyFunc)
|
|
&& !use_commas_and_parens;
|
|
|
|
let indent_more = if is_multiline {
|
|
indent + INDENT
|
|
} else {
|
|
indent
|
|
};
|
|
|
|
if parens {
|
|
buf.push('(');
|
|
}
|
|
|
|
let func = pattern_lift_spaces(buf.text.bump(), &func);
|
|
|
|
if !func.before.is_empty() {
|
|
if !is_multiline {
|
|
fmt_comments_only(buf, func.before.iter(), NewlineAt::Bottom, indent)
|
|
} else {
|
|
fmt_spaces(buf, func.before.iter(), indent);
|
|
}
|
|
}
|
|
|
|
fmt_pattern_only(
|
|
&func.item,
|
|
buf,
|
|
if is_pnc {
|
|
Parens::InPncApplyFunc
|
|
} else {
|
|
Parens::InApply
|
|
},
|
|
indent,
|
|
is_multiline,
|
|
);
|
|
|
|
if use_commas_and_parens {
|
|
buf.push('(');
|
|
}
|
|
|
|
let mut last_after = func.after;
|
|
|
|
let mut add_newlines = is_multiline;
|
|
|
|
for (i, loc_arg) in args.iter().enumerate() {
|
|
let is_last_arg = i == args.len() - 1;
|
|
let is_first_arg = i == 0;
|
|
|
|
if !(is_first_arg && use_commas_and_parens) {
|
|
buf.spaces(1);
|
|
}
|
|
|
|
let parens = if use_commas_and_parens {
|
|
Parens::NotNeeded
|
|
} else {
|
|
Parens::InApply
|
|
};
|
|
let arg = pattern_lift_spaces(buf.text.bump(), &loc_arg.value);
|
|
|
|
let mut was_multiline = arg.item.is_multiline();
|
|
|
|
let mut before = merge_spaces(buf.text.bump(), last_after, arg.before);
|
|
|
|
if !before.is_empty() {
|
|
handle_multiline_str_spaces(&arg.item, &mut before);
|
|
|
|
if !is_multiline {
|
|
was_multiline |= before.iter().any(|s| s.is_comment());
|
|
fmt_comments_only(buf, before.iter(), NewlineAt::Bottom, indent_more)
|
|
} else {
|
|
was_multiline |= true;
|
|
fmt_spaces(buf, before.iter(), indent_more);
|
|
}
|
|
}
|
|
|
|
if add_newlines {
|
|
buf.ensure_ends_with_newline();
|
|
}
|
|
|
|
if matches!(
|
|
arg.item,
|
|
Pattern::Identifier {
|
|
ident: "implements"
|
|
}
|
|
) {
|
|
buf.indent(indent_more);
|
|
buf.push_str("(implements)");
|
|
} else {
|
|
fmt_pattern_only(&arg.item, buf, parens, indent_more, arg.item.is_multiline());
|
|
}
|
|
if use_commas_and_parens && (!is_last_arg || is_multiline) {
|
|
buf.push(',');
|
|
}
|
|
|
|
last_after = arg.after;
|
|
|
|
add_newlines |= was_multiline;
|
|
}
|
|
|
|
if let Some(comments) = final_comments {
|
|
if !is_multiline {
|
|
fmt_comments_only(buf, comments.iter(), NewlineAt::Bottom, indent_more);
|
|
} else {
|
|
fmt_spaces(buf, comments.iter(), indent_more);
|
|
}
|
|
}
|
|
if !last_after.is_empty() {
|
|
if !is_multiline {
|
|
fmt_comments_only(buf, last_after.iter(), NewlineAt::Bottom, indent_more)
|
|
} else {
|
|
fmt_spaces(buf, last_after.iter(), indent_more);
|
|
}
|
|
}
|
|
|
|
if use_commas_and_parens {
|
|
if is_multiline {
|
|
buf.ensure_ends_with_newline();
|
|
buf.indent(indent);
|
|
}
|
|
if buf.ends_with_newline() {
|
|
buf.indent(indent);
|
|
}
|
|
if buf.ends_with_newline() {
|
|
buf.indent(indent);
|
|
}
|
|
buf.push(')');
|
|
}
|
|
|
|
if parens {
|
|
buf.push(')');
|
|
}
|
|
}
|
|
|
|
pub fn pattern_apply_to_node<'b, 'a: 'b>(
|
|
arena: &'b Bump,
|
|
func: Pattern<'a>,
|
|
args: &[Loc<Pattern<'a>>],
|
|
) -> NodeInfo<'b> {
|
|
let func_lifted = pattern_lift_spaces(arena, &func);
|
|
let mut b = NodeSequenceBuilder::new(arena, Node::Pattern(func_lifted.item), args.len(), true);
|
|
|
|
let mut last_after = func_lifted.after;
|
|
|
|
for arg in args {
|
|
let arg_lifted = pattern_lift_spaces(arena, &arg.value);
|
|
b.push(
|
|
Sp::with_space(merge_spaces(arena, last_after, arg_lifted.before)),
|
|
Node::Pattern(arg_lifted.item),
|
|
);
|
|
last_after = arg_lifted.after;
|
|
}
|
|
|
|
NodeInfo {
|
|
before: func_lifted.before,
|
|
node: b.build(),
|
|
after: last_after,
|
|
needs_indent: true,
|
|
prec: if args.is_empty() {
|
|
pattern_prec(func)
|
|
} else {
|
|
Prec::Apply
|
|
},
|
|
}
|
|
}
|
|
|
|
fn pattern_prec(pat: Pattern<'_>) -> Prec {
|
|
match pat {
|
|
Pattern::Identifier { .. }
|
|
| Pattern::QualifiedIdentifier { .. }
|
|
| Pattern::Tag(_)
|
|
| Pattern::OpaqueRef(_)
|
|
| Pattern::RecordDestructure(..)
|
|
| Pattern::RequiredField(_, _)
|
|
| Pattern::OptionalField(_, _)
|
|
| Pattern::NumLiteral(_)
|
|
| Pattern::NonBase10Literal { .. }
|
|
| Pattern::FloatLiteral(_)
|
|
| Pattern::StrLiteral(..)
|
|
| Pattern::Underscore(_)
|
|
| Pattern::SingleQuote(_)
|
|
| Pattern::Tuple(..)
|
|
| Pattern::List(..)
|
|
| Pattern::ListRest(_)
|
|
| Pattern::PncApply(_, _) => Prec::Term,
|
|
Pattern::Apply(_, _) | Pattern::As(_, _) => Prec::Apply,
|
|
Pattern::SpaceBefore(inner, _) | Pattern::SpaceAfter(inner, _) => pattern_prec(*inner),
|
|
Pattern::Malformed(_) | Pattern::MalformedIdent(..) | Pattern::MalformedExpr(_) => {
|
|
Prec::Term
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn starts_with_inline_comment<'a, I: IntoIterator<Item = &'a CommentOrNewline<'a>>>(
|
|
spaces: I,
|
|
) -> bool {
|
|
matches!(
|
|
spaces.into_iter().next(),
|
|
Some(CommentOrNewline::LineComment(_))
|
|
)
|
|
}
|
|
|
|
pub fn pattern_lift_spaces<'a, 'b: 'a>(
|
|
arena: &'a Bump,
|
|
pat: &Pattern<'b>,
|
|
) -> Spaces<'a, Pattern<'a>> {
|
|
match pat {
|
|
Pattern::Apply(func, args) => {
|
|
let func_lifted = pattern_lift_spaces(arena, &func.value);
|
|
|
|
let args = arena.alloc_slice_copy(args);
|
|
let (before, func, after) = if let Some(last) = args.last_mut() {
|
|
let last_lifted = pattern_lift_spaces(arena, &last.value);
|
|
if last_lifted.before.is_empty() {
|
|
*last = Loc::at(last.region, last_lifted.item)
|
|
} else {
|
|
*last = Loc::at(
|
|
last.region,
|
|
Pattern::SpaceBefore(arena.alloc(last_lifted.item), last_lifted.before),
|
|
);
|
|
}
|
|
|
|
let f = if func_lifted.after.is_empty() {
|
|
func_lifted.item
|
|
} else {
|
|
Pattern::SpaceAfter(arena.alloc(func_lifted.item), func_lifted.after)
|
|
};
|
|
|
|
(
|
|
func_lifted.before,
|
|
Loc::at(func.region, f),
|
|
last_lifted.after,
|
|
)
|
|
} else {
|
|
(
|
|
func_lifted.before,
|
|
Loc::at(func.region, func_lifted.item),
|
|
func_lifted.after,
|
|
)
|
|
};
|
|
Spaces {
|
|
before,
|
|
item: Pattern::Apply(arena.alloc(func), args),
|
|
after,
|
|
}
|
|
}
|
|
Pattern::PncApply(func, args) => {
|
|
let func_lifted = pattern_lift_spaces_before(arena, &func.value);
|
|
|
|
Spaces {
|
|
before: func_lifted.before,
|
|
item: Pattern::PncApply(arena.alloc(Loc::at_zero(func_lifted.item)), *args),
|
|
after: &[],
|
|
}
|
|
}
|
|
Pattern::OptionalField(name, expr) => {
|
|
let lifted = expr_lift_spaces_after(Parens::NotNeeded, arena, &expr.value);
|
|
Spaces {
|
|
before: &[],
|
|
item: Pattern::OptionalField(name, arena.alloc(Loc::at(expr.region, lifted.item))),
|
|
after: lifted.after,
|
|
}
|
|
}
|
|
Pattern::RequiredField(name, pat) => {
|
|
let lifted = pattern_lift_spaces_after(arena, &pat.value);
|
|
Spaces {
|
|
before: &[],
|
|
item: Pattern::RequiredField(name, arena.alloc(Loc::at(pat.region, lifted.item))),
|
|
after: lifted.after,
|
|
}
|
|
}
|
|
Pattern::SpaceBefore(expr, spaces) => {
|
|
let mut inner = pattern_lift_spaces(arena, expr);
|
|
inner.before = merge_spaces(arena, spaces, inner.before);
|
|
|
|
handle_multiline_str_spaces(expr, &mut inner.before);
|
|
|
|
inner
|
|
}
|
|
Pattern::SpaceAfter(expr, spaces) => {
|
|
let mut inner = pattern_lift_spaces(arena, expr);
|
|
inner.after = merge_spaces(arena, inner.after, spaces);
|
|
inner
|
|
}
|
|
_ => Spaces {
|
|
before: &[],
|
|
item: *pat,
|
|
after: &[],
|
|
},
|
|
}
|
|
}
|
|
|
|
fn handle_multiline_str_spaces<'a>(pat: &Pattern<'_>, before: &mut &'a [CommentOrNewline<'a>]) {
|
|
if starts_with_block_str(pat) {
|
|
// Ick!
|
|
// The block string will keep "generating" newlines when formatted (it wants to start on its own line),
|
|
// so we strip one out here.
|
|
//
|
|
// Note that this doesn't affect Expr's because those have explicit parens, and we can control
|
|
// whether spaces cross that boundary.
|
|
let chop_off = before
|
|
.iter()
|
|
.rev()
|
|
.take_while(|&&s| matches!(s, CommentOrNewline::Newline))
|
|
.count();
|
|
*before = &before[..before.len() - chop_off];
|
|
}
|
|
}
|
|
|
|
fn starts_with_block_str(item: &Pattern<'_>) -> bool {
|
|
match item {
|
|
Pattern::As(inner, _) | Pattern::Apply(inner, _) | Pattern::PncApply(inner, _) => {
|
|
starts_with_block_str(&inner.value)
|
|
}
|
|
Pattern::SpaceBefore(inner, _) | Pattern::SpaceAfter(inner, _) => {
|
|
starts_with_block_str(inner)
|
|
}
|
|
Pattern::StrLiteral(str_literal) => is_str_multiline(str_literal),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
pub fn pattern_lift_spaces_before<'a, 'b: 'a>(
|
|
arena: &'a Bump,
|
|
pat: &Pattern<'b>,
|
|
) -> SpacesBefore<'a, Pattern<'a>> {
|
|
let lifted = pattern_lift_spaces(arena, pat);
|
|
SpacesBefore {
|
|
before: lifted.before,
|
|
item: lifted.item.maybe_after(arena, lifted.after),
|
|
}
|
|
}
|
|
|
|
pub fn pattern_lift_spaces_after<'a, 'b: 'a>(
|
|
arena: &'a Bump,
|
|
pat: &Pattern<'b>,
|
|
) -> SpacesAfter<'a, Pattern<'a>> {
|
|
let lifted = pattern_lift_spaces(arena, pat);
|
|
SpacesAfter {
|
|
item: lifted.item.maybe_before(arena, lifted.before),
|
|
after: lifted.after,
|
|
}
|
|
}
|
|
|
|
/// Convert camelCase identifier to snake case
|
|
pub fn snakify_camel_ident(buf: &mut Buf, string: &str) {
|
|
let chars: Vec<char> = string.chars().collect();
|
|
if !buf.flags().snakify || (string.contains('_') && !string.ends_with('_')) {
|
|
buf.push_str(string);
|
|
return;
|
|
}
|
|
let mut index = 0;
|
|
let len = chars.len();
|
|
|
|
while index < len {
|
|
let prev = if index == 0 {
|
|
None
|
|
} else {
|
|
Some(chars[index - 1])
|
|
};
|
|
let c = chars[index];
|
|
let next = chars.get(index + 1);
|
|
let boundary = match (prev, c, next) {
|
|
// LUU, LUN, and LUL (simplified to LU_)
|
|
(Some(p), curr, _) if !p.is_ascii_uppercase() && curr.is_ascii_uppercase() => true,
|
|
// UUL
|
|
(Some(p), curr, Some(n))
|
|
if p.is_ascii_uppercase()
|
|
&& curr.is_ascii_uppercase()
|
|
&& n.is_ascii_lowercase() =>
|
|
{
|
|
true
|
|
}
|
|
_ => false,
|
|
};
|
|
// those are boundary transitions - should push _ and curr
|
|
if boundary {
|
|
buf.push('_');
|
|
}
|
|
buf.push(c.to_ascii_lowercase());
|
|
index += 1;
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod snakify_test {
|
|
use bumpalo::Bump;
|
|
|
|
use super::snakify_camel_ident;
|
|
use crate::{Buf, MigrationFlags};
|
|
|
|
fn check_snakify(arena: &Bump, original: &str) -> String {
|
|
let flags = MigrationFlags {
|
|
snakify: true,
|
|
parens_and_commas: false,
|
|
};
|
|
let mut buf = Buf::new_in(arena, flags);
|
|
buf.indent(0);
|
|
snakify_camel_ident(&mut buf, original);
|
|
buf.text.to_string()
|
|
}
|
|
|
|
#[test]
|
|
fn test_snakify_camel_ident() {
|
|
let arena = Bump::new();
|
|
assert_eq!(check_snakify(&arena, "A"), "a");
|
|
assert_eq!(check_snakify(&arena, "Ba"), "ba");
|
|
assert_eq!(check_snakify(&arena, "aB"), "a_b");
|
|
assert_eq!(check_snakify(&arena, "aBa"), "a_ba");
|
|
assert_eq!(check_snakify(&arena, "mBB"), "m_bb");
|
|
assert_eq!(check_snakify(&arena, "NbA"), "nb_a");
|
|
assert_eq!(check_snakify(&arena, "doIT"), "do_it");
|
|
assert_eq!(check_snakify(&arena, "ROC"), "roc");
|
|
assert_eq!(
|
|
check_snakify(&arena, "someHTTPRequest"),
|
|
"some_http_request"
|
|
);
|
|
assert_eq!(check_snakify(&arena, "usingXML"), "using_xml");
|
|
assert_eq!(check_snakify(&arena, "some123"), "some123");
|
|
assert_eq!(
|
|
check_snakify(&arena, "theHTTPStatus404"),
|
|
"the_http_status404"
|
|
);
|
|
assert_eq!(
|
|
check_snakify(&arena, "inThe99thPercentile"),
|
|
"in_the99th_percentile"
|
|
);
|
|
assert_eq!(
|
|
check_snakify(&arena, "all400SeriesErrorCodes"),
|
|
"all400_series_error_codes",
|
|
);
|
|
assert_eq!(check_snakify(&arena, "number4Yellow"), "number4_yellow");
|
|
assert_eq!(check_snakify(&arena, "useCases4Cobol"), "use_cases4_cobol");
|
|
assert_eq!(check_snakify(&arena, "c3PO"), "c3_po")
|
|
}
|
|
}
|