roc/crates/compiler/fmt/tests/test_fmt.rs
Joshua Warner a046428ce6
Add fuzzing for the formatter and fix bugs
This commit adds fuzzing for the (expr) formatter, with the same invariants that we use for fmt tests:
  * We start with text, which we parse
  * We format the AST, which must succeed
  * We parse back the AST and make sure it's identical igoring whitespace+comments
  * We format the new AST and assert it's equal to the first formatted version ("idempotency")

Interestingly, while a lot of bugs this found were in the formatter, it also found some parsing bugs.

It then fixes a bunch of bugs that fell out:
* Some small oversights in RemoveSpaces
* Make sure `_a` doesn't parse as an inferred type (`_`) followed by an identifier (parsing bug!)
* Call `extract_spaces` on a parsed expr before matching on it, lest it be Expr::SpaceBefore - when parsing aliases
* A few cases where the formatter generated invalid/different code
* Numerous formatting bugs that caused the formatting to not be idempotent

The last point there is worth talking further about. There were several cases where the old code was trying to enforce strong
opinions about how to insert newlines in function types and defs. In both of those cases, it looked like the goals of
(1) idempotency, (2) giving the user some say in the output, and (3) these strong opinions - were often in conflict.

For these cases, I erred on the side of following the user's existing choices about where to put newlines.

We can go back and re-add this strong opinionation later - but this seemed the right approach for now.
2022-12-17 09:52:09 -08:00

5902 lines
119 KiB
Rust

#[macro_use]
extern crate indoc;
extern crate bumpalo;
extern crate roc_fmt;
#[cfg(test)]
mod test_fmt {
use std::path::PathBuf;
use bumpalo::Bump;
use roc_fmt::def::fmt_defs;
use roc_fmt::module::fmt_module;
use roc_fmt::test_helpers::expr_formats;
use roc_fmt::Buf;
use roc_parse::ast::Module;
use roc_parse::module::{self, module_defs};
use roc_parse::parser::Parser;
use roc_parse::state::State;
use roc_test_utils::{assert_multiline_str_eq, workspace_root};
fn check_formatting(expected: &'_ str) -> impl Fn(&str) + '_ {
let expected = expected.trim();
move |output| {
assert_multiline_str_eq!(expected, output);
}
}
fn expr_formats_to(input: &str, expected: &str) {
expr_formats(input, check_formatting(expected), true);
}
fn expr_formats_same(input: &str) {
expr_formats(input, check_formatting(input), true);
}
fn fmt_module_and_defs<'a>(
arena: &Bump,
src: &str,
module: &Module<'a>,
state: State<'a>,
buf: &mut Buf<'_>,
) {
fmt_module(buf, module);
match module_defs().parse(arena, state, 0) {
Ok((_, loc_defs, _)) => {
fmt_defs(buf, &loc_defs, 0);
}
Err(error) => panic!(
r"Unexpected parse failure when parsing this for defs formatting:\n\n{:?}\n\nParse error was:\n\n{:?}\n\n",
src, error
),
}
}
// Not intended to be used directly in tests; please use module_formats_to or module_formats_same
fn expect_format_module_helper(src: &str, expected: &str) {
let arena = Bump::new();
let src = src.trim();
let expected = expected.trim();
match module::parse_header(&arena, State::new(src.as_bytes())) {
Ok((actual, state)) => {
use roc_fmt::spaces::RemoveSpaces;
let mut buf = Buf::new_in(&arena);
fmt_module_and_defs(&arena, src, &actual, state, &mut buf);
let output = buf.as_str().trim();
let (reparsed_ast, state) = module::parse_header(&arena, State::new(output.as_bytes())).unwrap_or_else(|err| {
panic!(
"After formatting, the source code no longer parsed!\n\nParse error was: {:?}\n\nThe code that failed to parse:\n\n{}\n\n",
err, output
);
});
let ast_normalized = actual.remove_spaces(&arena);
let reparsed_ast_normalized = reparsed_ast.remove_spaces(&arena);
// HACK!
// We compare the debug format strings of the ASTs, because I'm finding in practice that _somewhere_ deep inside the ast,
// the PartialEq implementation is returning `false` even when the Debug-formatted impl is exactly the same.
// I don't have the patience to debug this right now, so let's leave it for another day...
// TODO: fix PartialEq impl on ast types
if format!("{:?}", ast_normalized) != format!("{:?}", reparsed_ast_normalized) {
panic!(
"Formatting bug; formatting didn't reparse to the same AST (after removing spaces)\n\n\
* * * Source code before formatting:\n{}\n\n\
* * * Source code after formatting:\n{}\n\n",
src,
output
);
}
// Now verify that the resultant formatting is _stable_ - i.e. that it doesn't change again if re-formatted
let mut reformatted_buf = Buf::new_in(&arena);
fmt_module_and_defs(&arena, output, &reparsed_ast, state, &mut reformatted_buf);
let reformatted = reformatted_buf.as_str().trim();
if output != reformatted {
eprintln!("Formatting bug; formatting is not stable. Reformatting the formatted code changed it again, as follows:\n\n");
assert_multiline_str_eq!(output, reformatted);
}
// If everything was idempotent re-parsing worked, finally assert
// that the formatted code was what we expected it to be.
//
// Do this last because if there were any serious problems with the
// formatter (e.g. it wasn't idempotent), we want to know about
// those more than we want to know that the expectation failed!
assert_multiline_str_eq!(expected, output);
}
Err(error) => panic!("Unexpected parse failure when parsing this for module header formatting:\n\n{:?}\n\nParse error was:\n\n{:?}\n\n", src, error)
};
}
fn module_formats_to(input: &str, expected: &str) {
// First check that input formats to the expected version
expect_format_module_helper(input, expected);
// Parse the expected result format it, asserting that it doesn't change
// It's important that formatting be stable / idempotent
expect_format_module_helper(expected, expected);
}
fn module_formats_same(input: &str) {
module_formats_to(input, input);
}
// STRING LITERALS
#[test]
fn empty_string() {
expr_formats_same(indoc!(
r#"
""
"#
));
}
#[test]
fn def_with_comment() {
expr_formats_same(indoc!(
r#"
# This variable is for greeting
a = "Hello"
a
"#
));
}
#[test]
fn comment_with_trailing_space() {
expr_formats_to(
&format!(
indoc!(
r#"
# first comment{space}
x = 0 # second comment{space}
x
"#
),
space = " ",
),
indoc!(
r#"
# first comment
x = 0 # second comment
x
"#
),
);
}
#[test]
fn def_with_inline_comment() {
expr_formats_same(indoc!(
r#"
x = 0 # comment
x
"#
));
expr_formats_to(
indoc!(
r#"
x = 0# comment
x
"#
),
indoc!(
r#"
x = 0 # comment
x
"#
),
);
expr_formats_to(
indoc!(
r#"
x = 0# comment
x
"#
),
indoc!(
r#"
x = 0 # comment
x
"#
),
);
expr_formats_to(
indoc!(
r#"
x = 0 # comment
x
"#
),
indoc!(
r#"
x = 0 # comment
x
"#
),
);
}
#[test]
fn def_with_comment_and_extra_space() {
expr_formats_to(
indoc!(
r#"
# This variable is for greeting
a = "Hello"
a
"#
),
indoc!(
r#"
# This variable is for greeting
a = "Hello"
a
"#
),
);
}
#[test]
fn type_annotation_allow_blank_line_before_and_after_comment() {
expr_formats_same(indoc!(
r#"
person : {
firstName : Str,
# comment
lastName : Str,
}
person
"#
));
expr_formats_same(indoc!(
r#"
person : {
firstName : Str,
# comment
lastName : Str,
}
person
"#
));
expr_formats_same(indoc!(
r#"
person : {
firstName : Str,
# comment
lastName : Str,
}
person
"#
));
expr_formats_same(indoc!(
r#"
person : {
firstName : Str,
# comment
lastName : Str,
}
person
"#
));
expr_formats_same(indoc!(
r#"
person : {
firstName : Str,
# comment 1
lastName : Str,
# comment 2
# comment 3
}
person
"#
));
expr_formats_same(indoc!(
r#"
person : {
firstName : Str,
# comment 1
lastName : Str,
# comment 2
# comment 3
}
person
"#
));
expr_formats_to(
indoc!(
r#"
person : {
# comment
firstName : Str,
lastName : Str,
}
person
"#
),
indoc!(
r#"
person : {
# comment
firstName : Str,
lastName : Str,
}
person
"#
),
);
expr_formats_to(
indoc!(
r#"
person : {
firstName : Str,
lastName : Str,
# comment
}
person
"#
),
indoc!(
r#"
person : {
firstName : Str,
lastName : Str,
# comment
}
person
"#
),
);
expr_formats_to(
indoc!(
r#"
person : {
firstName : Str,
# comment
lastName : Str,
}
person
"#
),
indoc!(
r#"
person : {
firstName : Str,
# comment
lastName : Str,
}
person
"#
),
);
expr_formats_to(
indoc!(
r#"
person : {
firstName : Str,
# comment
lastName : Str,
}
person
"#
),
indoc!(
r#"
person : {
firstName : Str,
# comment
lastName : Str,
}
person
"#
),
);
expr_formats_to(
indoc!(
r#"
person : {
firstName : Str,
# comment
lastName : Str,
}
person
"#
),
indoc!(
r#"
person : {
firstName : Str,
# comment
lastName : Str,
}
person
"#
),
);
}
#[test]
fn record_allow_blank_line_before_and_after_comment() {
expr_formats_same(indoc!(
r#"
person = {
firstName: "first",
# comment 1
lastName: "last",
}
person
"#
));
expr_formats_same(indoc!(
r#"
person = {
firstName: "first",
# comment 1
lastName: "last",
}
person
"#
));
expr_formats_same(indoc!(
r#"
person = {
firstName: "first",
# comment 1
lastName: "last",
}
person
"#
));
expr_formats_same(indoc!(
r#"
person = {
firstName: "first",
# comment 1
lastName: "last",
}
person
"#
));
expr_formats_same(indoc!(
r#"
person = {
firstName: "first",
# comment 1
lastName: "last",
# comment 2
# comment 3
}
person
"#
));
expr_formats_same(indoc!(
r#"
person = {
firstName: "first",
# comment 1
lastName: "last",
# comment 2
# comment 3
}
person
"#
));
expr_formats_to(
indoc!(
r#"
person = {
# comment
firstName: "first",
lastName: "last",
}
person
"#
),
indoc!(
r#"
person = {
# comment
firstName: "first",
lastName: "last",
}
person
"#
),
);
expr_formats_to(
indoc!(
r#"
person = {
firstName: "first",
lastName: "last",
# comment
}
person
"#
),
indoc!(
r#"
person = {
firstName: "first",
lastName: "last",
# comment
}
person
"#
),
);
expr_formats_to(
indoc!(
r#"
person = {
firstName: "first",
# comment 1
lastName: "last",
}
person
"#
),
indoc!(
r#"
person = {
firstName: "first",
# comment 1
lastName: "last",
}
person
"#
),
);
expr_formats_to(
indoc!(
r#"
person = {
firstName: "first",
# comment 1
lastName: "last",
}
person
"#
),
indoc!(
r#"
person = {
firstName: "first",
# comment 1
lastName: "last",
}
person
"#
),
);
expr_formats_to(
indoc!(
r#"
person = {
firstName: "first",
# comment 1
lastName: "last",
}
person
"#
),
indoc!(
r#"
person = {
firstName: "first",
# comment 1
lastName: "last",
}
person
"#
),
);
}
#[test]
fn list_allow_blank_line_before_and_after_comment() {
expr_formats_same(indoc!(
r#"
list = [
0,
# comment
1,
]
list
"#
));
expr_formats_same(indoc!(
r#"
list = [
0,
# comment
1,
]
list
"#
));
expr_formats_same(indoc!(
r#"
list = [
0,
# comment
1,
]
list
"#
));
expr_formats_same(indoc!(
r#"
list = [
0,
# comment
1,
]
list
"#
));
expr_formats_same(indoc!(
r#"
list = [
0,
# comment 1
1,
# comment 2
# comment 3
]
list
"#
));
expr_formats_same(indoc!(
r#"
list = [
0,
# comment 1
1,
# comment 2
# comment 3
]
list
"#
));
expr_formats_to(
indoc!(
r#"
list = [
# comment
0,
1,
]
list
"#
),
indoc!(
r#"
list = [
# comment
0,
1,
]
list
"#
),
);
expr_formats_to(
indoc!(
r#"
list = [
0,
1,
# comment
]
list
"#
),
indoc!(
r#"
list = [
0,
1,
# comment
]
list
"#
),
);
expr_formats_to(
indoc!(
r#"
list = [
0,
# comment
1,
]
list
"#
),
indoc!(
r#"
list = [
0,
# comment
1,
]
list
"#
),
);
expr_formats_to(
indoc!(
r#"
list = [
0,
# comment
1,
]
list
"#
),
indoc!(
r#"
list = [
0,
# comment
1,
]
list
"#
),
);
expr_formats_to(
indoc!(
r#"
list = [
0,
# comment
1,
]
list
"#
),
indoc!(
r#"
list = [
0,
# comment
1,
]
list
"#
),
);
}
#[test]
fn force_space_at_beginning_of_comment() {
expr_formats_to(
indoc!(
r#"
#comment
f
"#
),
indoc!(
r#"
# comment
f
"#
),
);
}
#[test]
fn func_def() {
expr_formats_same(indoc!(
r#"
f = \x, y ->
x
f 4
"#
));
}
#[test]
fn new_line_above_return() {
expr_formats_to(
indoc!(
r#"
f = \x, y ->
y = 4
z = 8
x
"string"
"#
),
indoc!(
r#"
f = \x, y ->
y = 4
z = 8
x
"string"
"#
),
);
expr_formats_same(indoc!(
r#"
f = \x, y ->
a = 3
b = 6
c
"string"
"#
));
}
#[test]
fn basic_string() {
expr_formats_same(indoc!(
r#"
"blah"
"#
));
}
#[test]
fn escaped_unicode_string() {
expr_formats_same(indoc!(
r#"
"unicode: \u(A00A)!"
"#
));
}
#[test]
fn escaped_quote_string() {
expr_formats_same(indoc!(
r#"
"\""
"#
));
}
#[test]
fn empty_block_string() {
expr_formats_same(indoc!(
r#"
"""
"""
"#
));
}
#[test]
fn oneline_empty_block_string() {
expr_formats_to(
indoc!(
r#"
""""""
"#
),
indoc!(
r#"
"""
"""
"#
),
);
}
#[test]
fn basic_block_string() {
expr_formats_to(
indoc!(
r#"
"""griffin"""
"#
),
indoc!(
r#"
"griffin"
"#
),
);
}
#[test]
fn multiline_basic_block_string() {
expr_formats_to(
indoc!(
r#"
"""griffin
harpy"""
"#
),
indoc!(
r#"
"""
griffin
harpy
"""
"#
),
);
}
#[test]
fn newlines_block_string() {
expr_formats_to(
indoc!(
r#"
"""griffin
harpy
phoenix"""
"#
),
indoc!(
r#"
"""
griffin
harpy
phoenix
"""
"#
),
);
}
#[test]
fn quotes_block_string_single_segment() {
expr_formats_same(indoc!(
r#"
"""
"griffin"
"""
"#
));
}
#[test]
fn quotes_block_string() {
expr_formats_same(indoc!(
r#"
"""
"" \""" ""\"
"""
"#
));
}
#[test]
fn zero() {
expr_formats_same(indoc!(
r#"
0
"#
));
}
#[test]
fn zero_point_zero() {
expr_formats_same(indoc!(
r#"
0.0
"#
));
}
#[test]
fn int_with_underscores() {
expr_formats_same(indoc!(
r#"
1_23_456
"#
));
}
#[test]
fn float_with_underscores() {
expr_formats_same(indoc!(
r#"
1_23_456.7_89_10
"#
));
}
#[test]
fn multi_arg_closure() {
expr_formats_same(indoc!(
r#"
\a, b, c -> a b c
"#
));
}
#[test]
fn destructure_tag_closure() {
expr_formats_same(indoc!(
r#"
\Foo a -> Foo a
"#
));
}
#[test]
fn destructure_nested_tag_closure() {
expr_formats_same(indoc!(
r#"
\Foo (Bar a) -> Foo (Bar a)
"#
));
}
// DEFS
#[test]
fn single_def() {
expr_formats_same(indoc!(
r#"
x = 5
42
"#
));
}
#[test]
fn two_defs() {
expr_formats_same(indoc!(
r#"
x = 5
y = 10
42
"#
));
expr_formats_to(
indoc!(
r#"
x = 5
y = 10
42
"#
),
indoc!(
r#"
x = 5
y = 10
42
"#
),
);
}
#[test]
fn excess_parens() {
expr_formats_to(
indoc!(
r#"
x = (5)
y = ((10))
42
"#
),
indoc!(
r#"
x = 5
y = 10
42
"#
),
);
}
#[test]
fn defs_with_defs() {
expr_formats_same(indoc!(
r#"
x =
y = 4
z = 8
w
x
"#
));
}
#[test]
fn comment_between_two_defs() {
expr_formats_same(indoc!(
r#"
x = 5
# Hello
y = 10
42
"#
));
expr_formats_same(indoc!(
r#"
x = 5
# Hello
# two comments
y = 10
42
"#
));
expr_formats_same(indoc!(
r#"
x = 5
# Hello
# two comments
y = 10
# v-- This is the return value
42
"#
));
}
#[test]
fn space_between_comments() {
expr_formats_to(
indoc!(
r#"
# 9
# A
# B
# C
9
"#
),
indoc!(
r#"
# 9
# A
# B
# C
9
"#
),
);
}
#[test]
fn reduce_space_between_comments() {
expr_formats_to(
indoc!(
r#"
# First
# Second
x
"#
),
indoc!(
r#"
# First
# Second
x
"#
),
);
expr_formats_to(
indoc!(
r#"
f = \x ->
# 1st
# 2nd
x
f 4
"#
),
indoc!(
r#"
f = \x ->
# 1st
# 2nd
x
f 4
"#
),
);
}
#[test]
fn doesnt_detect_comment_in_comment() {
expr_formats_same(indoc!(
r#"
# One Comment # Still one Comment
9
"#
));
}
#[test]
fn parenthetical_def() {
expr_formats_same(indoc!(
r#"
(UserId userId) = 5
y = 10
42
"#
));
expr_formats_same(indoc!(
r#"
# A
(UserId userId) = 5
# B
y = 10
42
"#
));
}
#[test]
fn record_destructuring() {
expr_formats_same(indoc!(
r#"
{ x, y } = 5
{ x: 5 } = { x: 5 }
42
"#
));
}
#[test]
fn record_field_destructuring() {
expr_formats_same(indoc!(
r#"
when foo is
{ x: 5 } ->
42
"#
));
}
#[test]
fn lambda_returns_record() {
expr_formats_same(indoc!(
r#"
toRecord = \_ -> {
x: 1,
y: 2,
z: 3,
}
toRecord
"#
));
expr_formats_same(indoc!(
r#"
func = \_ ->
{ x: 1, y: 2, z: 3 }
func
"#
));
expr_formats_same(indoc!(
r#"
toRecord = \_ ->
val = 0
{
x: 1,
y: 2,
z: 3,
}
toRecord
"#
));
expr_formats_to(
indoc!(
r#"
toRecord = \_ ->
{
x: 1,
y: 2,
z: 3,
}
toRecord
"#
),
indoc!(
r#"
toRecord = \_ -> {
x: 1,
y: 2,
z: 3,
}
toRecord
"#
),
);
}
#[test]
fn lambda_returns_list() {
expr_formats_same(indoc!(
r#"
toList = \_ -> [
1,
2,
3,
]
toList
"#
));
expr_formats_same(indoc!(
r#"
func = \_ ->
[1, 2, 3]
func
"#
));
expr_formats_same(indoc!(
r#"
toList = \_ ->
val = 0
[
1,
2,
3,
]
toList
"#
));
expr_formats_to(
indoc!(
r#"
toList = \_ ->
[
1,
2,
3,
]
toList
"#
),
indoc!(
r#"
toList = \_ -> [
1,
2,
3,
]
toList
"#
),
);
}
#[test]
fn multiline_list_func_arg() {
expr_formats_same(indoc!(
r#"
result = func arg [
1,
2,
3,
]
result
"#
));
expr_formats_to(
indoc!(
r#"
result = func arg
[ 1, 2, 3 ]
result
"#
),
indoc!(
r#"
result = func
arg
[1, 2, 3]
result
"#
),
);
expr_formats_to(
indoc!(
r#"
result = func arg [
1,
2,
3,
]
result
"#
),
indoc!(
r#"
result = func arg [
1,
2,
3,
]
result
"#
),
);
expr_formats_to(
indoc!(
r#"
result = func [
1,
2,
3,
]
arg
result
"#
),
indoc!(
r#"
result = func
[
1,
2,
3,
]
arg
result
"#
),
);
expr_formats_to(
indoc!(
r#"
result = func arg
[
1,
2,
3,
]
result
"#
),
indoc!(
r#"
result = func arg [
1,
2,
3,
]
result
"#
),
);
expr_formats_same(indoc!(
r#"
result = func
arg
[
1,
2,
3,
]
result
"#
));
}
#[test]
fn multiline_record_func_arg() {
expr_formats_same(indoc!(
r#"
result = func arg {
x: 1,
y: 2,
z: 3,
}
result
"#
));
expr_formats_to(
indoc!(
r#"
result = func arg
{ x: 1, y: 2, z: 3 }
result
"#
),
indoc!(
r#"
result = func
arg
{ x: 1, y: 2, z: 3 }
result
"#
),
);
expr_formats_to(
indoc!(
r#"
result = func arg {
x: 1,
y: 2,
z: 3,
}
result
"#
),
indoc!(
r#"
result = func arg {
x: 1,
y: 2,
z: 3,
}
result
"#
),
);
expr_formats_to(
indoc!(
r#"
result = func {
x: 1,
y: 2,
z: 3,
}
arg
result
"#
),
indoc!(
r#"
result = func
{
x: 1,
y: 2,
z: 3,
}
arg
result
"#
),
);
expr_formats_to(
indoc!(
r#"
result = func arg
{
x: 1,
y: 2,
z: 3,
}
result
"#
),
indoc!(
r#"
result = func arg {
x: 1,
y: 2,
z: 3,
}
result
"#
),
);
expr_formats_same(indoc!(
r#"
result = func
arg
{
x: 1,
y: 2,
z: 3,
}
result
"#
));
}
#[test]
fn record_updating() {
expr_formats_same(indoc!(
r#"
{ shoes & leftShoe: nothing }
"#
));
expr_formats_to(
indoc!(
r#"
{ shoes & rightShoe : nothing }
"#
),
indoc!(
r#"
{ shoes & rightShoe: nothing }
"#
),
);
expr_formats_to(
indoc!(
r#"
{ shoes & rightShoe : nothing }
"#
),
indoc!(
r#"
{ shoes & rightShoe: nothing }
"#
),
);
expr_formats_same(indoc!(
r#"
{ shoes &
rightShoe: newRightShoe,
leftShoe: newLeftShoe,
}
"#
));
expr_formats_to(
indoc!(
r#"
{ shoes
& rightShoe: bareFoot
, leftShoe: bareFoot }
"#
),
indoc!(
r#"
{ shoes &
rightShoe: bareFoot,
leftShoe: bareFoot,
}
"#
),
);
}
#[test]
fn final_comments_in_records() {
expr_formats_same(indoc!(
r#"
{
x: 42,
# comment
}"#
));
expr_formats_same(indoc!(
r#"
{
x: 42,
# comment
# other comment
}"#
));
}
#[test]
fn final_comments_without_comma_in_records() {
expr_formats_to(
indoc!(
r#"
{
y: 41,
# comment 1
x: 42 # comment 2
}"#
),
indoc!(
r#"
{
y: 41,
# comment 1
x: 42,
# comment 2
}"#
),
);
}
#[test]
fn multiple_final_comments_without_comma_in_records() {
expr_formats_to(
indoc!(
r#"
{
y: 41,
x: 42 # comment 1
# comment 2
}"#
),
indoc!(
r#"
{
y: 41,
x: 42,
# comment 1
# comment 2
}"#
),
);
}
#[test]
fn comments_with_newlines_in_records() {
expr_formats_to(
indoc!(
r#"
{
z: 44 #comment 0
,
y: 41, # comment 1
# comment 2
x: 42
# comment 3
# comment 4
}"#
),
indoc!(
r#"
{
z: 44,
# comment 0
y: 41,
# comment 1
# comment 2
x: 42,
# comment 3
# comment 4
}"#
),
);
}
#[test]
fn multiple_final_comments_with_comma_in_records() {
expr_formats_to(
indoc!(
r#"
{
y: 41,
x: 42, # comment 1
# comment 2
}"#
),
indoc!(
r#"
{
y: 41,
x: 42,
# comment 1
# comment 2
}"#
),
);
}
#[test]
fn trailing_comma_in_record_annotation() {
expr_formats_to(
indoc!(
r#"
f: { y : Int *,
x : Int * ,
}
f"#
),
indoc!(
r#"
f : {
y : Int *,
x : Int *,
}
f"#
),
);
}
#[test]
fn trailing_comma_in_record_annotation_same() {
expr_formats_same(indoc!(
r#"
f : {
y : Int *,
x : Int *,
}
f
"#
));
expr_formats_to(
indoc!(
r#"
f :
{
y : Int *,
x : Int *,
}
f
"#
),
indoc!(
r#"
f : {
y : Int *,
x : Int *,
}
f
"#
),
);
}
#[test]
fn multiline_type_definition() {
expr_formats_same(indoc!(
r#"
f :
Int *
f"#
));
}
#[test]
fn multiline_empty_record_type_definition() {
expr_formats_same(indoc!(
r#"
f :
{}
f
"#
));
expr_formats_same(indoc!(
r#"
f :
{
}
f
"#
));
}
#[test]
fn type_definition_comment_after_colon() {
expr_formats_to(
indoc!(
r#"
f : # comment
{}
f"#
),
indoc!(
r#"
f :
# comment
{}
f"#
),
);
}
#[test]
#[ignore]
fn final_comment_in_empty_record_type_definition() {
expr_formats_to(
indoc!(
r#"
f :
{ # comment
}
f"#
),
indoc!(
r#"
f : {
# comment
}
f"#
),
);
}
#[test]
fn multiline_curly_brace_type() {
expr_formats_same(indoc!(
r#"
x : {
a : Int,
}
x
"#
));
expr_formats_same(indoc!(
r#"
x :
{ a : Int }
x
"#
));
}
#[test]
fn multiline_brace_type() {
expr_formats_same(indoc!(
r#"
x : [
Int,
]
x
"#
));
expr_formats_same(indoc!(
r#"
x :
[Int]
x
"#
));
}
#[test]
fn multiline_fn_signature() {
expr_formats_same(indoc!(
r#"
foo :
Str,
Nat
-> Bool
foo
"#
));
expr_formats_same(indoc!(
r#"
foo :
Str, Int, Nat -> Bool
foo
"#
));
expr_formats_to(
indoc!(
r#"
foo :
Str,
Nat -> Bool
foo
"#
),
indoc!(
r#"
foo :
Str,
Nat
-> Bool
foo
"#
),
);
expr_formats_to(
indoc!(
r#"
foo :
Str,
Nat
-> Bool
foo
"#
),
indoc!(
r#"
foo :
Str,
Nat
-> Bool
foo
"#
),
);
expr_formats_to(
indoc!(
r#"
foo :
Str, Nat -> Bool
foo
"#
),
indoc!(
r#"
foo :
Str, Nat -> Bool
foo
"#
),
);
}
#[test]
fn final_comment_record_annotation() {
expr_formats_to(
indoc!(
r#"
f :
{
x: Int * # comment 1
,
# comment 2
}
f"#
),
indoc!(
r#"
f : {
x : Int *, # comment 1
# comment 2
}
f"#
),
);
}
#[test]
fn def_closure() {
expr_formats_same(indoc!(
r#"
identity = \a -> a
identity 42
"#
));
expr_formats_same(indoc!(
r#"
identity = \a ->
a
identity 44
"#
));
expr_formats_same(indoc!(
r#"
identity = \a -> a
# Hello
identity 40
"#
));
expr_formats_to(
indoc!(
r#"
identity = \a
-> a
identity 41
"#
),
indoc!(
r#"
identity = \a -> a
identity 41
"#
),
);
expr_formats_to(
indoc!(
r#"
identity = \a
->
a + b
identity 4010
"#
),
indoc!(
r#"
identity = \a ->
a + b
identity 4010
"#
),
);
expr_formats_same(indoc!(
r#"
identity = \a, b -> a
identity 43
"#
));
// expr_formats_same(indoc!(
// r#"
// identity =
// \{
// x,
// y
// }
// -> a
//
// identity 43
// "#
// ));
expr_formats_same(indoc!(
r#"
identity = \a,
b,
# it's c!!
c
-> a
identity 43
"#
));
}
#[test]
fn closure_multiline_pattern() {
expr_formats_same(indoc!(
r#"
identity = \a,
b,
# it's c!!
c
-> a
identity 43
"#
));
}
// LIST
#[test]
fn empty_list() {
expr_formats_same("[]");
expr_formats_to("[ ]", "[]");
}
#[test]
fn one_item_list() {
expr_formats_same(indoc!("[4]"));
expr_formats_to(indoc!("[ 4 ]"), indoc!("[4]"));
}
#[test]
fn two_item_list() {
expr_formats_same(indoc!("[7, 8]"));
expr_formats_to(indoc!("[ 7 , 8 ]"), indoc!("[7, 8]"));
}
#[test]
fn multi_line_list() {
expr_formats_same(indoc!(
r#"
[
7,
8,
9,
]
"#
));
expr_formats_to(
indoc!(
r#"
[ 17
, 18
, 19
]
"#
),
indoc!(
r#"
[
17,
18,
19,
]
"#
),
);
expr_formats_to(
indoc!(
r#"
[ 27
, 28
, 29
]
"#
),
indoc!(
r#"
[
27,
28,
29,
]
"#
),
);
expr_formats_to(
indoc!(
r#"
[
157, 158,
159
]
"#
),
indoc!(
r#"
[
157,
158,
159,
]
"#
),
);
expr_formats_to(
indoc!(
r#"
[
557, 648,
759, 837
]
"#
),
indoc!(
r#"
[
557,
648,
759,
837,
]
"#
),
);
expr_formats_to(
indoc!(
r#"
[
257, 358,
# Hey!
459
]
"#
),
indoc!(
r#"
[
257,
358,
# Hey!
459,
]
"#
),
);
expr_formats_to(
indoc!(
r#"
[
# Thirty Seven
37
# Thirty Eight
, 38
, 39
]
"#
),
indoc!(
r#"
[
# Thirty Seven
37,
# Thirty Eight
38,
39,
]
"#
),
);
expr_formats_to(
indoc!(
r#"
[ # 47!
# Top 47
47
# Bottom 47
# Top 48
, 48
# Bottom 48
# Top 49
, 49
# Bottom 49
# 49!
]
"#
),
indoc!(
r#"
[
# 47!
# Top 47
47,
# Bottom 47
# Top 48
48,
# Bottom 48
# Top 49
49,
# Bottom 49
# 49!
]
"#
),
);
}
#[test]
fn ending_comments_in_list() {
expr_formats_to(
indoc!(
r#"
[ # Top 49
49
# Bottom 49
,
# 49!
]
"#
),
indoc!(
r#"
[
# Top 49
49,
# Bottom 49
# 49!
]
"#
),
);
}
#[test]
fn multi_line_list_def() {
expr_formats_same(indoc!(
r#"
l = [
1,
2,
]
l
"#
));
expr_formats_same(indoc!(
r#"
l =
[1, 2]
l
"#
));
expr_formats_to(
indoc!(
r#"
l =
[
1,
2,
]
l
"#
),
indoc!(
r#"
l = [
1,
2,
]
l
"#
),
);
expr_formats_to(
indoc!(
r#"
results = [
Ok 4,
Ok 5
]
allOks results
"#
),
indoc!(
r#"
results = [
Ok 4,
Ok 5,
]
allOks results
"#
),
);
expr_formats_to(
indoc!(
r#"
results =
# Let's count past 6
[
Ok 6,
Err CountError
]
allOks results
"#
),
indoc!(
r#"
results =
# Let's count past 6
[
Ok 6,
Err CountError,
]
allOks results
"#
),
);
}
// RECORD LITERALS
#[test]
fn empty_record() {
expr_formats_same("{}");
expr_formats_to("{ }", "{}");
}
#[test]
fn empty_record_patterns() {
expr_formats_to(
indoc!(
r#"
f = \{ } -> "Hello World"
f
"#
),
indoc!(
r#"
f = \{} -> "Hello World"
f
"#
),
);
expr_formats_to(
indoc!(
r#"
f = \a, b -> { }
f
"#
),
indoc!(
r#"
f = \a, b -> {}
f
"#
),
);
expr_formats_to(
indoc!(
r#"
{ } <- f a b
{}
"#
),
indoc!(
r#"
{} <- f a b
{}
"#
),
);
}
#[test]
#[ignore]
fn empty_record_with_comment() {
expr_formats_same(indoc!(
r#"
{
# comment
}"#
));
}
#[test]
#[ignore]
fn empty_record_with_newline() {
expr_formats_to(
indoc!(
r#"
{
}"#
),
"{}",
);
}
#[test]
fn one_field() {
expr_formats_same("{ x: 4 }");
}
#[test]
fn two_fields() {
expr_formats_same("{ x: 4, y: 42 }");
}
#[test]
fn two_fields_newline() {
expr_formats_same(indoc!(
r#"
{
x: 4,
y: 42,
}
"#
));
}
#[test]
fn multi_line_record_def() {
expr_formats_same(indoc!(
r#"
pos = {
x: 4,
y: 11,
z: 16,
}
pos
"#
));
expr_formats_same(indoc!(
r#"
pos =
{ x: 4, y: 11, z: 16 }
pos
"#
));
expr_formats_same(indoc!(
r#"
myDef =
list = [
a,
b,
]
{
c,
d,
}
myDef
"#
));
expr_formats_to(
indoc!(
r#"
pos =
{
x: 4,
y: 11,
z: 16,
}
pos
"#
),
indoc!(
r#"
pos = {
x: 4,
y: 11,
z: 16,
}
pos
"#
),
);
expr_formats_to(
indoc!(
r#"
pos = {
x: 5,
y: 10,
}
pos
"#
),
indoc!(
r#"
pos = {
x: 5,
y: 10,
}
pos
"#
),
);
}
#[test]
fn two_fields_center_newline() {
expr_formats_to(
indoc!(
r#"
{ x: 4,
y: 42
}
"#
),
indoc!(
r#"
{
x: 4,
y: 42,
}
"#
),
);
}
#[test]
fn one_unnamed_field() {
expr_formats_same(indoc!(
r#"
foo = 4
{ foo }
"#
));
}
// IF
#[test]
fn single_line_if() {
expr_formats_same(indoc!(
r#"
if foo bar then a b c else d e f
"#
));
expr_formats_same(indoc!(
r#"
if foo (a b c) then a b c else d e f
"#
));
}
#[test]
fn multi_line_if_condition() {
expr_formats_same(indoc!(
r#"
if
waterWillBoil pressure temperature
then
turnOnAc
else
identity
"#
));
}
#[test]
fn multi_line_if_condition_with_spaces() {
expr_formats_to(
indoc!(
r#"
if
willBoil home water
then
\_ -> leave
else
identity
"#
),
indoc!(
r#"
if
willBoil home water
then
\_ -> leave
else
identity
"#
),
);
}
#[test]
fn multi_line_if_condition_with_multi_line_expr_1() {
expr_formats_same(indoc!(
r#"
if
snowWillFall
pressure
temperature
then
bundleUp
else
identity
"#
));
}
#[test]
fn multi_line_if_condition_with_multi_line_expr_2() {
expr_formats_same(indoc!(
r#"
if
1
== 2
then
"yes"
else
"no"
"#
));
}
#[test]
fn if_removes_newlines_from_else() {
expr_formats_to(
indoc!(
r#"
if
isPrime 8
then
nothing
else
# C
# D
# E
# F
just (div 1 8)
"#
),
indoc!(
r#"
if
isPrime 8
then
nothing
else
# C
# D
# E
# F
just (div 1 8)
"#
),
);
}
#[test]
fn if_removes_newlines_from_then() {
expr_formats_to(
indoc!(
r#"
if
isPrime 9
then
# EE
# FF
nothing
# GG
else
just (div 1 9)
"#
),
indoc!(
r#"
if
isPrime 9
then
# EE
# FF
nothing
# GG
else
just (div 1 9)
"#
),
);
}
#[test]
fn if_removes_newlines_from_condition() {
expr_formats_to(
indoc!(
r#"
if
# Is
# It
isPrime 10
# Prime?
then
nothing
else
just (div 1 10)
"#
),
indoc!(
r#"
if
# Is
# It
isPrime 10
# Prime?
then
nothing
else
just (div 1 10)
"#
),
);
}
#[test]
fn multi_line_if() {
expr_formats_to(
indoc!(
r#"
if lessThan four five then
four
else
five
"#
),
indoc!(
r#"
if lessThan four five then
four
else
five
"#
),
);
expr_formats_to(
indoc!(
r#"
if lessThan three four then
three
else
four
"#
),
indoc!(
r#"
if lessThan three four then
three
else
four
"#
),
);
expr_formats_same(indoc!(
r#"
if foo bar then
a b c
else
d e f
"#
));
}
#[test]
fn multi_line_application() {
expr_formats_same(indoc!(
r#"
combine
peanutButter
chocolate
"#
));
}
#[test]
fn partial_multi_line_application() {
expr_formats_to(
indoc!(
r#"
mix vodka
tonic
"#
),
indoc!(
r#"
mix
vodka
tonic
"#
),
);
expr_formats_to(
indoc!(
r#"
f
a b c
"#
),
indoc!(
r#"
f
a
b
c
"#
),
);
}
// WHEN
#[test]
fn integer_when() {
expr_formats_same(indoc!(
r#"
when b is
1 ->
1
_ ->
2
"#
));
}
#[test]
fn integer_when_with_space() {
expr_formats_to(
indoc!(
r#"
when year is
1999 ->
1
_ ->
0
"#
),
indoc!(
r#"
when year is
1999 ->
1
_ ->
0
"#
),
);
}
#[test]
fn when_with_comments() {
expr_formats_same(indoc!(
r#"
when b is
# look at cases
1 -> # when 1
1
# important
# fall through
_ ->
# case 2
# more comment
2
"#
));
}
#[test]
fn when_with_integer_comments() {
expr_formats_same(indoc!(
r#"
when 0 is
1 # comment
| 2 -> "a"
_ -> "b"
"#
));
}
#[test]
fn nested_when() {
expr_formats_same(indoc!(
r#"
when b is
_ ->
when c is
_ ->
1
"#
));
}
#[test]
fn def_when() {
expr_formats_same(indoc!(
r#"
myLongFunctionName = \x ->
when b is
1 | 2 ->
when c is
6 | 7 ->
8
3 | 4 ->
5
123
"#
));
}
#[test]
#[ignore] // TODO: reformat when-in-function-body with extra newline
fn def_when_with_python_indentation() {
expr_formats_to(
// vvv Currently this input formats to _itself_ :( vvv
// Instead, if the body of the `when` is multiline (the overwhelmingly common case)
// we want to make sure the `when` is at the beginning of the line, inserting
// a newline if necessary.
indoc!(
r#"
myLongFunctionName = \x -> when b is
1 | 2 ->
when c is
6 | 7 ->
8
3 | 4 ->
5
123
"#
),
indoc!(
r#"
myLongFunctionName = \x ->
when b is
1 | 2 ->
when c is
6 | 7 ->
8
3 | 4 ->
5
123
"#
),
);
}
#[test]
fn when_with_alternatives_1() {
expr_formats_same(indoc!(
r#"
when b is
1 | 2 ->
when c is
6 | 7 ->
8
3 | 4 ->
5
"#
));
}
#[test]
fn when_with_alternatives_2() {
expr_formats_same(indoc!(
r#"
when b is
# a comment here
1 | 2 ->
# a comment there
1
"#
));
}
#[test]
fn when_with_alternatives_3() {
expr_formats_to(
indoc!(
r#"
when b is
1 | 2 |3 ->
1
"#
),
indoc!(
r#"
when b is
1 | 2 | 3 ->
1
"#
),
);
}
#[test]
fn when_with_alternatives_4() {
expr_formats_to(
indoc!(
r#"
when b is
1 | 2 |
3
->
4
5 | 6 | 7 ->
8
9
| 10 -> 11
12 | 13 ->
when c is
14 | 15 -> 16
17
| 18 -> 19
20 -> 21
"#
),
indoc!(
r#"
when b is
1
| 2
| 3 ->
4
5 | 6 | 7 ->
8
9
| 10 -> 11
12 | 13 ->
when c is
14 | 15 -> 16
17
| 18 -> 19
20 -> 21
"#
),
);
}
#[test]
fn with_multiline_pattern_indentation() {
expr_formats_to(
indoc!(
r#"
when b is 3->4
9
|8->9
"#
),
indoc!(
r#"
when b is
3 -> 4
9
| 8 -> 9
"#
),
);
}
#[test]
fn multi_line_when_condition_1() {
expr_formats_same(indoc!(
r#"
when
complexFunction a b c
is
1 ->
Nothing
_ ->
Just True
"#
));
}
#[test]
fn multi_line_when_condition_2() {
expr_formats_same(indoc!(
r#"
when
# this is quite complicated
complexFunction a b c
# Watch out
is
Complex x y ->
simplify x y
Simple z ->
z
"#
));
}
#[test]
fn multi_line_when_condition_3() {
expr_formats_to(
indoc!(
r#"
x = 2
y = 3
when 1
+ 1 is
2 ->
x
_ ->
y
"#
),
indoc!(
r#"
x = 2
y = 3
when
1
+ 1
is
2 ->
x
_ ->
y
"#
),
);
}
#[test]
fn multi_line_when_condition_4() {
expr_formats_to(
indoc!(
r#"
x = 2
y = 3
when 2
+ 2
is
4 ->
x
_ ->
y
"#
),
indoc!(
r#"
x = 2
y = 3
when
2
+ 2
is
4 ->
x
_ ->
y
"#
),
);
}
#[test]
fn multi_line_when_branch() {
expr_formats_to(
indoc!(
r#"
when x is
Foo -> bar
"arg1" "arg2"
Bar -> 2
"#
),
indoc!(
r#"
when x is
Foo ->
bar
"arg1"
"arg2"
Bar -> 2
"#
),
);
}
#[test]
fn single_line_when_patterns() {
expr_formats_same(indoc!(
r#"
when x is
Foo -> 1
Bar -> 2
"#
));
expr_formats_same(indoc!(
r#"
when x is
Foo -> 1
Bar ->
2
"#
));
expr_formats_to(
indoc!(
r#"
when x is
Foo -> 1
Bar ->
2
"#
),
indoc!(
r#"
when x is
Foo -> 1
Bar ->
2
"#
),
);
expr_formats_to(
indoc!(
r#"
when x is
Foo -> 1
Bar -> 2
"#
),
indoc!(
r#"
when x is
Foo -> 1
Bar -> 2
"#
),
);
}
#[test]
fn when_with_single_quote_char() {
expr_formats_same(indoc!(
r#"
when x is
'0' -> 0
'1' -> 1
"#
));
}
// NEWLINES
#[test]
fn multiple_blank_lines_collapse_to_one() {
expr_formats_to(
indoc!(
r#"
x = 5
y = 10
42
"#
),
indoc!(
r#"
x = 5
y = 10
42
"#
),
);
}
#[test]
fn def_returning_closure() {
expr_formats_same(indoc!(
r#"
f = \x -> x
g = \x -> x
\x ->
a = f x
b = f x
x
"#
));
}
#[test]
fn inner_def_with_triple_newline_before() {
// The triple newline used to cause the code in add_spaces to not indent the next line,
// which of course is not the same tree (and nor does it parse)
expr_formats_to(
indoc!(
r#"
\x ->
m = 2
m1 = insert m n powerOf10
42
"#
),
indoc!(
r#"
\x ->
m = 2
m1 = insert m n powerOf10
42
"#
),
);
}
#[test]
fn when_guard() {
expr_formats_same(indoc!(
r#"
when maybeScore is
Just score if score > 21 ->
win
_ ->
nextRound
"#
));
}
#[test]
fn when_guard_using_function() {
expr_formats_same(indoc!(
r#"
when authenticationResponse is
Ok user if hasPermission user ->
loadPage route user
Ok user ->
PageNotFound
Err _ ->
ErrorPage
"#
));
}
// ACCESSOR
#[test]
fn accessor() {
expr_formats_same(indoc!(
r#"
.id
"#
));
expr_formats_same(indoc!(
r#"
user.name
"#
));
expr_formats_same(indoc!(
r#"
(getUser userId users).name
"#
));
}
// PRECEDENCE CONFLICT
#[test]
fn precedence_conflict() {
expr_formats_same(indoc!(
r#"
if True == False == True then
False
else
True
"#
));
}
#[test]
fn multi_line_precedence_conflict_1() {
expr_formats_to(
indoc!(
r#"
if True
== False == True
then
False
else
True
"#
),
indoc!(
r#"
if
True
== False
== True
then
False
else
True
"#
),
);
}
#[test]
fn multi_line_precedence_conflict_2() {
expr_formats_to(
indoc!(
r#"
if False
== False == False then
"true"
else
"false"
"#
),
indoc!(
r#"
if
False
== False
== False
then
"true"
else
"false"
"#
),
);
}
#[test]
fn precedence_conflict_functions() {
expr_formats_same(indoc!(
r#"
when f x == g y == h z is
True ->
Ok 1
False ->
Err 2
"#
));
}
#[test]
fn binop_parens() {
expr_formats_same(indoc!(
r#"
if 4 == (6 ^ 6 ^ 7 ^ 8) then
"Hard to believe"
else
"Naturally"
"#
));
expr_formats_same(indoc!(
r#"
if 5 == 1 ^ 1 ^ 1 ^ 1 then
"Not buying it"
else
"True"
"#
));
expr_formats_to(
indoc!(
r#"
if (1 == 1)
&& (2 == 1) && (3 == 2) then
"true"
else
"false"
"#
),
indoc!(
r#"
if
(1 == 1)
&& (2 == 1)
&& (3 == 2)
then
"true"
else
"false"
"#
),
);
}
#[test]
fn multiline_binop_with_comments() {
expr_formats_to(
indoc!(
r#"
x = 1
+ 1 # comment 1
- 1 # comment 2
* 1 # comment 3
x
"#
),
indoc!(
r#"
x =
1
+ 1 # comment 1
- 1 # comment 2
* 1 # comment 3
x
"#
),
);
expr_formats_to(
indoc!(
r#"
x = 1
+ 1 # comment 1
* 1 # comment 2
x
"#
),
indoc!(
r#"
x =
1
+ 1 # comment 1
* 1 # comment 2
x
"#
),
);
expr_formats_to(
indoc!(
r#"
x = 1
+ 1 # comment
x
"#
),
indoc!(
r#"
x =
1
+ 1 # comment
x
"#
),
);
expr_formats_to(
indoc!(
r#"
x = 1
* 1
+ 1 # comment
x
"#
),
indoc!(
r#"
x =
1
* 1
+ 1 # comment
x
"#
),
);
expr_formats_to(
indoc!(
r#"
x = 1
- 1
* 1
+ 1
x
"#
),
indoc!(
r#"
x =
1
- 1
* 1
+ 1
x
"#
),
);
}
#[test]
fn multiline_binop_if_with_comments() {
expr_formats_same(indoc!(
r#"
if
x
+ 1 # comment 1
> 0 # comment 2
then
y
* 2 # comment 3
< 1 # comment 4
else
42
"#
));
}
#[test]
fn multiline_binop_when_with_comments() {
expr_formats_to(
indoc!(
r#"
when
x
+ 1 # comment 1
> 0 # comment 2
is
y ->
3
* 2 # comment 3
< 1 # comment 4
z ->
4
/ 5 # comment 5
< 1 # comment 6
46 # first pattern comment
| 95 # alternative comment 1
| 126 # alternative comment 2
| 150 -> # This comment came after the ->
# This comment is for the expr
foo bar
|> Result.withDefault "" # one last comment
_ ->
42
"#
),
indoc!(
r#"
when
x
+ 1 # comment 1
> 0 # comment 2
is
y ->
3
* 2 # comment 3
< 1 # comment 4
z ->
4
/ 5 # comment 5
< 1 # comment 6
46 # first pattern comment
| 95 # alternative comment 1
| 126 # alternative comment 2
| 150 -> # This comment came after the ->
# This comment is for the expr
foo bar
|> Result.withDefault "" # one last comment
_ ->
42
"#
),
);
}
#[test]
fn precedence_conflict_greater_than() {
expr_formats_same(indoc!(
r#"
3 > 4 > 10
"#
));
}
#[test]
fn precedence_conflict_greater_than_and_less_than() {
expr_formats_same(indoc!(
r#"
1 < 4 > 1
"#
));
}
#[test]
fn binop_if() {
expr_formats_same(indoc!(
r#"
5 * (if x > 0 then 1 else 2)
"#
));
}
// UNARY OP
#[test]
fn unary_op() {
expr_formats_same(indoc!(
r#"
y = -4
!x
"#
));
}
#[test]
fn unary_call_parens() {
expr_formats_same(indoc!(
r#"
!(f 1)
"#
));
}
#[test]
fn unary_call_no_parens() {
// TIL: Negating a function "does what you might expect"... which is cool!
expr_formats_same(indoc!(
r#"
!f 1
"#
));
}
// BINARY OP
#[test]
fn binary_op() {
expr_formats_same(indoc!(
r#"
1 == 1
"#
));
}
#[test]
fn binary_op_with_spaces() {
expr_formats_to(
indoc!(
r#"
2 != 3
"#
),
indoc!(
r#"
2 != 3
"#
),
);
}
#[test]
fn multi_line_binary_op_1() {
expr_formats_same(indoc!(
r#"
isLast
&& isEmpty
&& isLoaded
"#
));
}
#[test]
fn multi_line_binary_op_2() {
expr_formats_to(
indoc!(
r#"
x = 1
< 2
f x
"#
),
indoc!(
r#"
x =
1
< 2
f x
"#
),
);
}
#[test]
fn multi_line_binary_op_with_comments() {
expr_formats_to(
indoc!(
r#"
1
* 2
/ 3
// 4
"#
),
indoc!(
r#"
1
* 2
/ 3
// 4
"#
),
);
}
#[test]
fn partial_multi_line_binary_op_1() {
expr_formats_to(
indoc!(
r#"
2 % 3
// 5
+ 7
"#
),
indoc!(
r#"
2
% 3
// 5
+ 7
"#
),
);
}
#[test]
fn partial_multi_line_binary_op_2() {
expr_formats_to(
indoc!(
r#"
isGreenLight
&& isRedLight && isYellowLight
"#
),
indoc!(
r#"
isGreenLight
&& isRedLight
&& isYellowLight
"#
),
);
}
#[test]
fn pipline_op_with_apply() {
expr_formats_same(indoc!(
r#"
output
|> List.set (offset + 0) b
|> List.set (offset + 1) a
"#
));
}
#[test]
fn apply_lambda() {
expr_formats_same(indoc!(
r#"
List.map
xs
(\i ->
i + length)
"#
));
}
#[test]
fn pipline_apply_lambda_1() {
expr_formats_same(indoc!(
r#"
shout
|> List.map
xs
(\i -> i)
"#
));
}
#[test]
fn pipline_apply_lambda_2() {
expr_formats_same(indoc!(
r#"
shout
|> List.map
xs
(\i -> i)
|> List.join
"#
));
}
#[test]
fn comment_between_multiline_ann_args() {
expr_formats_same(indoc!(
r#"
blah :
Str,
# comment
(Str -> Str)
-> Str
42
"#
))
}
#[test]
fn pipeline_apply_lambda_multiline() {
expr_formats_same(indoc!(
r#"
example = \model ->
model
|> withModel
(\result ->
when result is
Err _ ->
Err {}
Ok val ->
Ok {}
)
example
"#
));
expr_formats_to(
indoc!(
r#"
example = \model ->
model
|> withModel
(\result ->
when result is
Err _ ->
Err {}
Ok val ->
Ok {}
)
example
"#
),
indoc!(
r#"
example = \model ->
model
|> withModel
(\result ->
when result is
Err _ ->
Err {}
Ok val ->
Ok {}
)
example
"#
),
);
}
#[test]
fn func_call_trailing_multiline_lambda() {
expr_formats_same(indoc!(
r#"
list = List.map [1, 2, 3] \x ->
x + 1
list
"#
));
}
// MODULES
#[test]
fn single_line_interface() {
module_formats_same(indoc!(
r#"
interface Foo exposes [] imports []"#
));
}
#[test]
fn defs_with_trailing_comment() {
// TODO: make the formatter add a space between '42' and # below:
module_formats_to(
indoc!(
r#"
interface Foo exposes [] imports []
a = 42 # Yay greetings"#
),
indoc!(
r#"
interface Foo exposes [] imports []
a = 42 # Yay greetings
"#
),
);
}
#[test]
fn multiline_interface() {
module_formats_same(indoc!(
r#"
interface Foo
exposes []
imports []"#
));
}
#[test]
fn interface_exposing() {
module_formats_same(indoc!(
r#"
interface Foo
exposes [Bar, Baz, a, b]
imports []"#
));
}
#[test]
fn interface_importing() {
module_formats_same(indoc!(
r#"
interface Foo
exposes [Bar, Baz, a, b]
imports [Blah, Thing.{ foo, bar }, Stuff]"#
));
}
#[test]
fn multi_line_interface() {
module_formats_same(indoc!(
r#"
interface Foo
exposes [
Stuff,
Things,
somethingElse,
]
imports [
Blah,
Baz.{ stuff, things },
]"#
));
}
#[test]
fn single_line_app() {
module_formats_same(indoc!(
r#"
app "Foo" packages { pf: "platform/main.roc" } imports [] provides [main] to pf"#
));
}
#[test]
fn single_line_platform() {
module_formats_same(
"platform \"folkertdev/foo\" \
requires { Model, Msg } { main : Effect {} } \
exposes [] \
packages {} \
imports [Task.{ Task }] \
provides [mainForHost]",
);
}
#[test]
fn module_defs_with_comments() {
module_formats_to(
&format!(
indoc!(
r#"
interface Foo
exposes []
imports []
# comment 1{space}
def = "" # comment 2{space}
# comment 3{space}
"#
),
space = " "
),
indoc!(
r#"
interface Foo
exposes []
imports []
# comment 1
def = "" # comment 2
# comment 3
"#
),
);
}
#[test]
fn format_tui_package_config() {
// At one point this failed to reformat.
module_formats_to(
indoc!(
r#"
platform "tui"
requires { Model } { main : { init : ({} -> Model), update : (Model, Str -> Model), view : (Model -> Str) } }
exposes []
packages {}
imports []
provides [ mainForHost ]
mainForHost : { init : ({} -> Model) as Init, update : (Model, Str -> Model) as Update, view : (Model -> Str) as View }
mainForHost = main
"#
),
indoc!(
r#"
platform "tui"
requires { Model } { main : { init : {} -> Model, update : Model, Str -> Model, view : Model -> Str } }
exposes []
packages {}
imports []
provides [mainForHost]
mainForHost : { init : ({} -> Model) as Init, update : (Model, Str -> Model) as Update, view : (Model -> Str) as View }
mainForHost = main
"#
),
);
}
#[test]
fn single_line_hosted() {
module_formats_same(indoc!(
r#"
hosted Foo exposes [] imports [] generates Bar with []"#
));
}
#[test]
fn multi_line_hosted() {
module_formats_same(indoc!(
r#"
hosted Foo
exposes [
Stuff,
Things,
somethingElse,
]
imports [
Blah,
Baz.{ stuff, things },
]
generates Bar with [
map,
after,
loop,
]"#
));
}
/// Annotations and aliases
#[test]
fn list_alias() {
expr_formats_same(indoc!(
r#"
ConsList a : [Cons a (ConsList a), Nil]
f : ConsList a -> ConsList a
f = \_ -> Nil
f
"#
));
}
#[test]
fn wildcard() {
expr_formats_same(indoc!(
r#"
f : List *
f = []
a
"#
));
}
#[test]
fn identity() {
expr_formats_same(indoc!(
r#"
f : a -> a
f = []
a
"#
));
}
#[test]
fn weird_triple_string() {
expr_formats_to(
indoc!(
r#"
_""""w"""
"#
),
indoc!(
r#"
_
"""
"w
"""
"#
),
);
}
#[test]
fn multiline_tag_union_annotation_no_comments() {
expr_formats_same(indoc!(
r#"
b : [
True,
False,
]
b
"#
));
expr_formats_same(indoc!(
r#"
b :
[True, False]
b
"#
));
expr_formats_to(
indoc!(
r#"
b :
[
True,
False,
]
b
"#
),
indoc!(
r#"
b : [
True,
False,
]
b
"#
),
);
expr_formats_to(
indoc!(
r#"
b : [
True,
False,
]
b
"#
),
indoc!(
r#"
b : [
True,
False,
]
b
"#
),
);
}
#[test]
fn multiline_tag_union_annotation_beginning_on_same_line() {
expr_formats_same(indoc!(
r#"
Expr : [
Add Expr Expr,
Mul Expr Expr,
Val I64,
Var I64,
]
Expr"#
));
}
#[test]
fn multiline_tag_union_annotation_with_final_comment() {
expr_formats_to(
indoc!(
r#"
b :
[
True,
# comment 1
False # comment 2
,
# comment 3
]
b
"#
),
indoc!(
r#"
b : [
True,
# comment 1
False, # comment 2
# comment 3
]
b
"#
),
);
}
#[test]
fn tag_union() {
expr_formats_same(indoc!(
r#"
f : [True, False] -> [True, False]
f = \x -> x
a
"#
));
}
// TODO: the current formatting seems a bit odd for multiline function annotations
// (beside weird indentation, note the trailing space after the "->")
// #[test]
// fn multiline_tag_union_function_annotation() {
// expr_formats_same(indoc!(
// r#"
// f :
// [
// True,
// False,
// ] ->
// [
// True,
// False,
// ]
// f = \x -> x
// a
// "#
// ));
// }
#[test]
fn recursive_tag_union() {
expr_formats_same(indoc!(
r#"
f : [Cons a (ConsList a), Nil] as ConsList a -> [Just a, Nothing]
f = \list ->
when list is
Nil ->
Nothing
Cons first _ ->
Just first
f
"#
));
}
#[test]
fn function_application_package_type() {
expr_formats_same(indoc!(
r#"
main : Task.Task {} []
main = 42
main
"#
));
}
#[test]
fn record_type() {
expr_formats_same(indoc!(
r#"
f : { foo : Int * }
f = { foo: 1000 }
a
"#
));
}
#[test]
fn record_pattern_with_apply_guard() {
expr_formats_same(indoc!(
r#"
when { x: 1 } is
{ x: Just 4 } ->
4
"#
));
}
#[test]
fn record_pattern_with_record_guard() {
expr_formats_same(indoc!(
r#"
when { x: 1 } is
{ x: { x: True } } ->
4
"#
));
}
#[test]
fn body_starts_with_spaces_multiline() {
expr_formats_same(indoc!(
r#"
y =
Foo
1
2
y
"#
));
}
#[test]
fn backpassing_simple() {
expr_formats_same(indoc!(
r#"
getChar = \ctx ->
x <- Task.await (getCharScope scope)
42
42
"#
));
}
#[test]
fn backpassing_apply_tag() {
expr_formats_same(indoc!(
r#"
getChar = \ctx ->
(T val newScope) <- Task.await (getCharScope scope)
42
42
"#
));
}
#[test]
fn backpassing_parens_body() {
expr_formats_same(indoc!(
r#"
Task.fromResult
(
b <- binaryOp ctx
if a == b then
-1
else
0
)
"#
));
expr_formats_to(
indoc!(
r#"
Task.fromResult
(b <- binaryOp ctx
if a == b then
-1
else
0
)
"#
),
indoc!(
r#"
Task.fromResult
(
b <- binaryOp ctx
if a == b then
-1
else
0
)
"#
),
);
expr_formats_to(
indoc!(
r#"
Task.fromResult
(b <- binaryOp ctx
if a == b then
-1
else
0)
"#
),
indoc!(
r#"
Task.fromResult
(
b <- binaryOp ctx
if a == b then
-1
else
0
)
"#
),
);
}
#[test]
fn backpassing_body_on_newline() {
expr_formats_same(indoc!(
r#"
getChar = \ctx ->
x <-
Task.await (getCharScope scope)
42
42
"#
));
}
#[test]
fn multiline_higher_order_function() {
expr_formats_same(indoc!(
r#"
foo :
(Str -> Bool)
-> Bool
42
"#
));
expr_formats_same(indoc!(
r#"
foo :
(Str -> Bool),
Str
-> Bool
foo = \bar, baz ->
42
42
"#
));
expr_formats_same(indoc!(
r#"
foo :
(Str -> Bool) -> Bool
42
"#
));
expr_formats_same(indoc!(
r#"
foo :
(Str -> Bool), Str -> Bool
foo = \bar, baz ->
42
42
"#
));
expr_formats_same(indoc!(
r#"
foo :
(Str -> Bool), Str -> Bool # comment
foo = \bar, baz ->
42
42
"#
));
}
#[test]
fn multiline_opaque_tag_union() {
expr_formats_same(indoc!(
r#"
A := [
B,
C,
]
0
"#
));
}
#[test]
fn opaque_has_clause() {
expr_formats_same(indoc!(
r#"
A := U8 has [Eq, Hash]
0
"#
));
expr_formats_to(
indoc!(
r#"
A :=
U8
has [Eq, Hash]
0
"#
),
indoc!(
r#"
A := U8
has [Eq, Hash]
0
"#
),
);
expr_formats_to(
indoc!(
r#"
A := a | a has Hash has [ Eq, Hash ]
0
"#
),
indoc!(
r#"
A := a | a has Hash
has [Eq, Hash]
0
"#
),
);
expr_formats_to(
indoc!(
r#"
A := U8 has []
0
"#
),
indoc!(
r#"
A := U8 has []
0
"#
),
);
}
#[test]
fn comma_prefixed_indented_record() {
expr_formats_to(
indoc!(
r#"
Model position :
{ evaluated : Set position
, openSet : Set position
, costs : Dict.Dict position F64
, cameFrom : Dict.Dict position position
}
a
"#,
),
indoc!(
r#"
Model position : {
evaluated : Set position,
openSet : Set position,
costs : Dict.Dict position F64,
cameFrom : Dict.Dict position position,
}
a
"#,
),
);
}
#[test]
fn opaque_has_with_impls() {
expr_formats_same(indoc!(
r#"
A := U8 has [Eq { eq }, Hash { hash }]
0
"#
));
expr_formats_same(indoc!(
r#"
A := U8 has [Eq { eq, eq1 }]
0
"#
));
expr_formats_to(
indoc!(
r#"
A := U8 has [Eq { eq, eq1 }]
A := U8 has [Eq {
eq,
eq1
}]
0
"#
),
indoc!(
r#"
A := U8 has [Eq { eq, eq1 }]
A := U8 has [
Eq {
eq,
eq1,
},
]
0
"#
),
);
expr_formats_same(indoc!(
r#"
A := a | a has Other
has [Eq { eq }, Hash { hash }]
0
"#
));
expr_formats_same(indoc!(
r#"
A := U8 has [Eq {}]
0
"#
));
}
#[test]
fn comments_in_multiline_tag_union_annotation() {
expr_formats_to(
indoc!(
r#"
UnionAnn : [
Foo, # comment 1
Bar, # comment 2
Baz, # comment 3
# comment 4 line 1
# comment 4 line 2
]
0
"#
),
indoc!(
r#"
UnionAnn : [
Foo, # comment 1
Bar, # comment 2
Baz, # comment 3
# comment 4 line 1
# comment 4 line 2
]
0
"#
),
);
}
#[test]
fn test_where_after() {
expr_formats_same(indoc!(
r#"
Dict k v := {
metadata : List I8,
dataIndices : List Nat,
data : List (T k v),
size : Nat,
} | k has Hash & Eq
a
"#
));
}
#[test]
/// Test that everything under examples/ is formatted correctly
/// If this test fails on your diff, it probably means you need to re-format the examples.
/// Try this:
/// `cargo run -- format $(find examples -name \*.roc)`
fn test_fmt_examples() {
let mut count = 0;
let mut root = workspace_root();
root.push("examples");
for entry in walkdir::WalkDir::new(&root) {
let entry = entry.unwrap();
let path = entry.path();
if path.extension() == Some(std::ffi::OsStr::new("roc")) {
count += 1;
let src = std::fs::read_to_string(path).unwrap();
println!("Now trying to format {}", path.display());
module_formats_same(&src);
}
}
assert!(
count > 0,
"Expecting to find at least 1 .roc file to format under {}",
root.display()
);
}
#[test]
/// Test that builtins are formatted correctly
/// If this test fails on your diff, it probably means you need to re-format a builtin.
/// Try this:
/// `cargo run -- format $(find crates/compiler/builtins/roc -name \*.roc)`
fn test_fmt_builtins() {
let mut count = 0;
let builtins_path = workspace_root()
.join("crates")
.join("compiler")
.join("builtins")
.join("roc");
for entry in walkdir::WalkDir::new(&builtins_path) {
let entry = entry.unwrap();
let path = entry.path();
if path.extension() == Some(std::ffi::OsStr::new("roc")) {
count += 1;
let src = std::fs::read_to_string(path).unwrap();
println!("Now trying to format {}", path.display());
module_formats_same(&src);
}
}
assert!(
count > 0,
"Expecting to find at least 1 .roc file to format under {}",
builtins_path.display()
);
}
#[test]
fn expect_single_line() {
expr_formats_same(indoc!(
r#"
x = 5
expect x == y
expect y == z
42
"#
));
module_formats_same(indoc!(
r#"
interface Foo exposes [] imports []
expect x == y
expect y == z
foo = bar
"#
));
}
#[test]
fn expect_multiline() {
expr_formats_same(indoc!(
r#"
x = 5
expect
foo bar
|> baz
42
"#
));
module_formats_same(indoc!(
r#"
interface Foo exposes [] imports []
expect
foo bar
|> baz
expect
blah
etc
foo = bar
"#
));
}
#[test]
fn single_line_string_literal_in_pattern() {
expr_formats_same(indoc!(
r#"
when foo is
"abc" -> ""
"#
));
}
#[test]
fn multi_line_string_literal_in_pattern() {
expr_formats_same(indoc!(
r#"
when foo is
"""
abc
def
""" -> ""
"#
));
}
#[test]
fn multi_line_string_literal_that_can_be_single_line_in_pattern() {
expr_formats_to(
indoc!(
r#"
when foo is
"""
abc
""" -> ""
"#
),
indoc!(
r#"
when foo is
"abc" -> ""
"#
),
);
}
#[test]
fn format_chars() {
expr_formats_same(indoc!(
r#"
' '
"#
));
expr_formats_same(indoc!(
r#"
'\n'
"#
));
}
#[test]
fn format_char_pattern() {
expr_formats_same(indoc!(
r#"
when x is
' ' -> x
'\n' -> x
'\t' -> x
"#
));
}
#[test]
fn format_nested_pipeline() {
expr_formats_same(indoc!(
r#"
(a |> b) |> c
"#
));
expr_formats_same(indoc!(
r#"
a |> b |> c
"#
));
}
#[test]
fn ability_member_doc_comments() {
module_formats_same(indoc!(
r#"
interface Foo exposes [] imports []
A has
## This is member ab
ab : a -> a | a has A
## This is member de
de : a -> a | a has A
f = g
"#
));
}
#[test]
fn leading_comments_preserved() {
module_formats_same(indoc!(
r#"
# hello world
interface Foo
exposes []
imports []
"#
));
module_formats_same(indoc!(
r#"
# hello world
app "test" packages {} imports [] provides [] to "./platform"
"#
));
module_formats_same(indoc!(
r#"
# hello world
platform "hello-world"
requires {} { main : Str }
exposes []
packages {}
imports []
provides [mainForHost]
"#
));
}
#[test]
fn clauses_with_multiple_abilities() {
expr_formats_same(indoc!(
r#"
f : {} -> a | a has Eq & Hash & Decode
f
"#
));
expr_formats_to(
indoc!(
r#"
f : {} -> a | a has Eq & Hash & Decode,
b has Eq & Hash
f
"#
),
indoc!(
// TODO: ideally, this would look a bit nicer - consider
// f : {} -> a
// | a has Eq & Hash & Decode,
// b has Eq & Hash
r#"
f : {} -> a | a has Eq & Hash & Decode, b has Eq & Hash
f
"#
),
);
}
#[test]
fn format_list_patterns() {
expr_formats_same(indoc!(
r#"
when [] is
[] -> []
"#
));
expr_formats_to(
indoc!(
r#"
when [] is
[ ] -> []
"#
),
indoc!(
r#"
when [] is
[] -> []
"#
),
);
expr_formats_to(
indoc!(
r#"
when [] is
[ x, .. , A 5 6, .. ] -> []
"#
),
indoc!(
r#"
when [] is
[x, .., A 5 6, ..] -> []
"#
),
);
expr_formats_to(
indoc!(
r#"
when [] is
[ x, 4, 5 ] -> []
[ .., 5 ] -> []
[ x, .. ] -> []
"#
),
indoc!(
r#"
when [] is
[x, 4, 5] -> []
[.., 5] -> []
[x, ..] -> []
"#
),
);
}
#[test]
fn format_crash() {
expr_formats_same(indoc!(
r#"
_ = crash
_ = crash ""
crash "" ""
"#
));
expr_formats_to(
indoc!(
r#"
_ = crash
_ = crash ""
_ = crash "" ""
try
foo
(\_ -> crash "")
"#
),
indoc!(
r#"
_ = crash
_ = crash ""
_ = crash "" ""
try
foo
(\_ -> crash "")
"#
),
);
}
// this is a parse error atm
// #[test]
// fn multiline_apply() {
// expr_formats_same(indoc!(
// r#"
// f :
// Result a
// { x : Int *
// , y : Float
// }
// c
// -> Int *
// f =
// \_ -> 4
// "#
// ));
// }
#[test]
fn parse_test_snapshots_format_without_error() {
fn list(dir: &std::path::Path) -> std::vec::Vec<String> {
std::fs::read_dir(dir)
.unwrap()
.map(|f| f.unwrap().file_name().to_str().unwrap().to_string())
.collect::<std::vec::Vec<_>>()
}
fn check_saved_formatting(original: &'_ str, result_path: PathBuf) -> impl Fn(&str) + '_ {
move |actual_result: &str| {
if std::env::var("ROC_SNAPSHOT_TEST_OVERWRITE").is_ok() {
if original == actual_result {
std::fs::remove_file(&result_path)
.or_else(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
Ok(())
} else {
Err(e)
}
})
.unwrap();
} else {
std::fs::write(&result_path, actual_result).unwrap();
}
} else if original == actual_result {
// We represent this expectation on the filesystem as the _absence_ of the .formatted.expr.roc file.
// This makes the directory a bit cleaner.
assert!(!result_path.exists(),
"Expected no file at {}\n\
This how we represent the expectation that the formatting of the input file should not change.\n\
consider running the tests with:\n\
`env ROC_SNAPSHOT_TEST_OVERWRITE=1 cargo test ...` (which will delete the file for you),\n\
and commiting the delete.",
result_path.display());
} else {
let expected_result =
std::fs::read_to_string(&result_path).unwrap_or_else(|e| {
panic!(
"Error opening test output file {}:\n\
{:?}
Supposing the file is missing, consider running the tests with:\n\
`env ROC_SNAPSHOT_TEST_OVERWRITE=1 cargo test ...`\n\
and committing the file that creates.",
result_path.display(),
e
);
});
assert_multiline_str_eq!(expected_result.as_str(), actual_result);
}
}
}
let base = std::path::PathBuf::from("../parse/tests/snapshots/pass");
for file in list(&base) {
if let Some(prefix) = file.strip_suffix(".expr.roc") {
println!("formatting {}", file);
let contents = std::fs::read_to_string(base.join(&file)).unwrap();
let formatted_path = base.join(format!("{}.expr.formatted.roc", prefix));
expr_formats(
&contents,
check_saved_formatting(&contents, formatted_path),
true,
);
} else if file.ends_with(".module.roc") {
// TODO: re-format module defs and ensure they're correct.
// Note that these tests don't have an actual module header,
// so we'll have to pre-pend that for this test.
}
}
}
}