Merge pull request #4519 from joshuawarner32/fix-fmt

Add fmt test to make sure formatting works for all parser test cases
This commit is contained in:
Joshua Warner 2022-11-15 19:39:18 -08:00 committed by GitHub
commit be42668202
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
112 changed files with 641 additions and 25 deletions

View file

@ -8,6 +8,7 @@ use crate::{
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum Braces {
Round,
Square,
Curly,
}
@ -22,11 +23,13 @@ pub fn fmt_collection<'a, 'buf, T: ExtractSpaces<'a> + Formattable>(
<T as ExtractSpaces<'a>>::Item: Formattable,
{
let start = match braces {
Braces::Round => '(',
Braces::Curly => '{',
Braces::Square => '[',
};
let end = match braces {
Braces::Round => ')',
Braces::Curly => '}',
Braces::Square => ']',
};

View file

@ -134,7 +134,7 @@ impl<'a> Formattable for TypeDef<'a> {
if !self.is_multiline() {
debug_assert_eq!(members.len(), 1);
buf.push_str(" ");
buf.spaces(1);
members[0].format_with_options(
buf,
Parens::NotNeeded,

View file

@ -326,9 +326,6 @@ impl<'a> Formattable for Expr<'a> {
Record(fields) => {
fmt_record(buf, None, *fields, indent);
}
Tuple(_fields) => {
todo!("format tuple");
}
RecordUpdate { update, fields } => {
fmt_record(buf, Some(*update), *fields, indent);
}
@ -386,6 +383,7 @@ impl<'a> Formattable for Expr<'a> {
fmt_if(buf, branches, final_else, self.is_multiline(), indent);
}
When(loc_condition, branches) => fmt_when(buf, loc_condition, branches, indent),
Tuple(items) => fmt_collection(buf, indent, Braces::Round, *items, Newlines::No),
List(items) => fmt_collection(buf, indent, Braces::Square, *items, Newlines::No),
BinOps(lefts, right) => fmt_binops(buf, lefts, right, false, parens, indent),
UnaryOp(sub_expr, unary_op) => {
@ -421,7 +419,10 @@ impl<'a> Formattable for Expr<'a> {
buf.push('.');
buf.push_str(key);
}
MalformedIdent(_, _) => {}
MalformedIdent(str, _) => {
buf.indent(indent);
buf.push_str(str)
}
MalformedClosure => {}
PrecedenceConflict { .. } => {}
}
@ -505,10 +506,10 @@ fn push_op(buf: &mut Buf, op: BinOp) {
pub fn fmt_str_literal<'buf>(buf: &mut Buf<'buf>, literal: StrLiteral, indent: u16) {
use roc_parse::ast::StrLiteral::*;
buf.indent(indent);
buf.push('"');
match literal {
PlainLine(string) => {
buf.indent(indent);
buf.push('"');
// When a PlainLine contains '\n' or '"', format as a block string
if string.contains('"') || string.contains('\n') {
buf.push_str("\"\"");
@ -523,15 +524,21 @@ pub fn fmt_str_literal<'buf>(buf: &mut Buf<'buf>, literal: StrLiteral, indent: u
} else {
buf.push_str_allow_spaces(string);
};
buf.push('"');
}
Line(segments) => {
buf.indent(indent);
buf.push('"');
for seg in segments.iter() {
format_str_segment(seg, buf, 0)
}
buf.push('"');
}
Block(lines) => {
// Block strings will always be formatted with """ on new lines
buf.push_str("\"\"");
buf.ensure_ends_with_newline();
buf.indent(indent);
buf.push_str("\"\"\"");
buf.newline();
for segments in lines.iter() {
@ -543,10 +550,9 @@ pub fn fmt_str_literal<'buf>(buf: &mut Buf<'buf>, literal: StrLiteral, indent: u
buf.newline();
}
buf.indent(indent);
buf.push_str("\"\"");
buf.push_str("\"\"\"");
}
}
buf.push('"');
}
fn fmt_binops<'a, 'buf>(

View file

@ -109,7 +109,7 @@ impl<'a> Buf<'a> {
if self.spaces_to_flush > 0 {
self.flush_spaces();
self.newline();
} else if !self.text.ends_with('\n') {
} else if !self.text.ends_with('\n') && !self.text.is_empty() {
self.newline()
}
}

View file

@ -5,6 +5,8 @@ extern crate roc_fmt;
#[cfg(test)]
mod test_fmt {
use std::path::PathBuf;
use bumpalo::Bump;
use roc_fmt::annotation::{Formattable, Newlines, Parens};
use roc_fmt::def::fmt_defs;
@ -16,11 +18,14 @@ mod test_fmt {
use roc_parse::state::State;
use roc_test_utils::{assert_multiline_str_eq, workspace_root};
// Not intended to be used directly in tests; please use expr_formats_to or expr_formats_same
fn expr_formats_to(input: &str, expected: &str) {
/// Check that the given input is formatted to the given output.
/// If the expected output is `None`, then this only checks that the input
/// parses without error, formats without error, and
/// (optionally, based on the value of `check_stability`) re-parses to
/// the same AST as the original.
fn expr_formats(input: &str, check_formatting: impl Fn(&str), check_stability: bool) {
let arena = Bump::new();
let input = input.trim();
let expected = expected.trim();
match roc_parse::test_helpers::parse_expr_with(&arena, input) {
Ok(actual) => {
@ -32,12 +37,15 @@ mod test_fmt {
let output = buf.as_str();
assert_multiline_str_eq!(expected, output);
check_formatting(output);
let reparsed_ast = roc_parse::test_helpers::parse_expr_with(&arena, output).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
"After formatting, the source code no longer parsed!\n\n\
Parse error was: {:?}\n\n\
The code that failed to parse:\n\n{}\n\n\
The original ast was:\n\n{:#?}\n\n",
err, output, actual
);
});
@ -60,21 +68,34 @@ mod test_fmt {
}
// 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);
reparsed_ast.format_with_options(&mut reformatted_buf, Parens::NotNeeded, Newlines::Yes, 0);
if check_stability {
let mut reformatted_buf = Buf::new_in(&arena);
reparsed_ast.format_with_options(&mut reformatted_buf, Parens::NotNeeded, Newlines::Yes, 0);
if output != reformatted_buf.as_str() {
eprintln!("Formatting bug; formatting is not stable. Reformatting the formatted code changed it again, as follows:\n\n");
if output != reformatted_buf.as_str() {
eprintln!("Formatting bug; formatting is not stable. Reformatting the formatted code changed it again, as follows:\n\n");
assert_multiline_str_eq!(output, reformatted_buf.as_str());
assert_multiline_str_eq!(output, reformatted_buf.as_str());
}
}
}
Err(error) => panic!("Unexpected parse failure when parsing this for formatting:\n\n{}\n\nParse error was:\n\n{:?}\n\n", input, error)
};
}
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_to(input, input);
expr_formats(input, check_formatting(input), true);
}
fn fmt_module_and_defs<'a>(
@ -5825,4 +5846,78 @@ mod test_fmt {
// "#
// ));
// }
#[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),
false,
);
} 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.
}
}
}
}

View file

@ -0,0 +1,4 @@
Hash has
hash : a -> U64
1

View file

@ -0,0 +1,5 @@
Hash has
hash : a -> U64
hash2 : a -> U64
1

View file

@ -0,0 +1,3 @@
Hash has hash : a -> U64 | a has Hash
1

View file

@ -0,0 +1,5 @@
Ab1 has ab1 : a -> {} | a has Ab1
Ab2 has ab2 : a -> {} | a has Ab2
1

View file

@ -0,0 +1,4 @@
{ x, y } : Foo
{ x, y } = { x: "foo", y: 3.14 }
x

View file

@ -0,0 +1,4 @@
UserId x : [UserId I64]
(UserId x) = UserId 42
x

View file

@ -0,0 +1,4 @@
(x, y) : Foo
(x, y) = ("foo", 3.14)
x

View file

@ -0,0 +1 @@
whee 12 34

View file

@ -0,0 +1 @@
!whee 12 foo

View file

@ -0,0 +1,9 @@
## first line of docs
## second line
## third line
## fourth line
##
## sixth line after doc new line
x = 5
42

View file

@ -0,0 +1,3 @@
12
* # test!
92

View file

@ -0,0 +1,2 @@
3 # test!
+ 4

View file

@ -0,0 +1,2 @@
3 # 2 × 2
+ 4

View file

@ -0,0 +1,3 @@
(Email str) = Email "blah@example.com"
str

View file

@ -0,0 +1 @@
x == y

View file

@ -0,0 +1,4 @@
expect
1 == 1
4

View file

@ -0,0 +1,3 @@
iffy = 5
42

View file

@ -0,0 +1,2 @@
\x ->
1

View file

@ -0,0 +1,10 @@
my_list = [
0,
[
a,
b,
],
1,
]
42

View file

@ -0,0 +1,9 @@
when [] is
[] -> {}
[..] -> {}
[_, .., _, ..] -> {}
[a, b, c, d] -> {}
[a, b, ..] -> {}
[.., c, d] -> {}
[[A], [..], [a]] -> {}
[[[], []], [[], x]] -> {}

View file

@ -0,0 +1,3 @@
when x is
bar.and -> 1
_ -> 4

View file

@ -0,0 +1,3 @@
when x is
Foo.and -> 1
_ -> 4

View file

@ -0,0 +1,7 @@
# ## not docs!
## docs, but with a problem
## (namely that this is a mix of docs and regular comments)
# not docs
x = 5
42

View file

@ -0,0 +1,3 @@
x, y <- List.map2 [] []
x + y

View file

@ -0,0 +1,13 @@
a = "Hello,\n\nWorld!"
b =
"""
Hello,\n\nWorld!
"""
c =
"""
Hello,
World!
"""
42

View file

@ -0,0 +1,11 @@
(
# before 1
1,
# after 1
# before 2
2,
# after 2
# before 3
3,
# after 3
)

View file

@ -0,0 +1,61 @@
Tuple(
[
@20-21 SpaceBefore(
SpaceAfter(
Num(
"1",
),
[
Newline,
LineComment(
"after 1",
),
],
),
[
Newline,
LineComment(
"before 1",
),
],
),
@59-60 SpaceBefore(
SpaceAfter(
Num(
"2",
),
[
Newline,
LineComment(
"after 2",
),
],
),
[
Newline,
LineComment(
"before 2",
),
],
),
@98-99 SpaceBefore(
SpaceAfter(
Num(
"3",
),
[
Newline,
LineComment(
" after 3",
),
],
),
[
Newline,
LineComment(
"before 3",
),
],
),
],
)

View file

@ -0,0 +1,13 @@
(
#before 1
1
#after 1
,
#before 2
2
#after 2
,
#before 3
3
# after 3
)

View file

@ -0,0 +1 @@
31 * 42 + 534

View file

@ -0,0 +1,6 @@
if t1 then
1
else if t2 then
2
else
3

View file

@ -0,0 +1,7 @@
# ######
# ## not docs!
# #still not docs
# #####
x = 5
42

View file

@ -0,0 +1,37 @@
{
u8: 123u8,
u16: 123u16,
u32: 123u32,
u64: 123u64,
u128: 123u128,
i8: 123i8,
i16: 123i16,
i32: 123i32,
i64: 123i64,
i128: 123i128,
nat: 123nat,
dec: 123dec,
u8Neg: -123u8,
u16Neg: -123u16,
u32Neg: -123u32,
u64Neg: -123u64,
u128Neg: -123u128,
i8Neg: -123i8,
i16Neg: -123i16,
i32Neg: -123i32,
i64Neg: -123i64,
i128Neg: -123i128,
natNeg: -123nat,
decNeg: -123dec,
u8Bin: 0b101u8,
u16Bin: 0b101u16,
u32Bin: 0b101u32,
u64Bin: 0b101u64,
u128Bin: 0b101u128,
i8Bin: 0b101i8,
i16Bin: 0b101i16,
i32Bin: 0b101i32,
i64Bin: 0b101i64,
i128Bin: 0b101i128,
natBin: 0b101nat,
}

View file

@ -0,0 +1,4 @@
# leading comment
x <- \y -> y
x

View file

@ -0,0 +1,4 @@
# leading comment
x = 5
42

View file

@ -0,0 +1,4 @@
# leading comment
x = 5
42

View file

@ -0,0 +1,3 @@
(@Thunk it) = id (@A {})
it {}

View file

@ -0,0 +1,24 @@
A := U8 has [Eq, Hash]
A := a | a has Other
has [Eq, Hash]
A := a | a has Other
has [Eq, Hash]
A := U8 has [Eq { eq }, Hash { hash }]
A := U8 has [Eq { eq, eq1 }]
A := U8 has [Eq { eq, eq1 }, Hash]
A := U8 has [Hash, Eq { eq, eq1 }]
A := U8 has []
A := a | a has Other
has [Eq { eq }, Hash { hash }]
A := U8 has [Eq {}]
0

View file

@ -0,0 +1,2 @@
when n is
@Age -> 1

View file

@ -0,0 +1,2 @@
when n is
@Add n m -> n + m

View file

@ -0,0 +1,8 @@
x = foo
(
baz {
bar: blah,
}
)
x

View file

@ -0,0 +1,7 @@
a = [
1,
2,
3,
]
a

View file

@ -0,0 +1,5 @@
x = foo {
bar: blah,
}
x

View file

@ -0,0 +1,3 @@
Blah a b : Foo.Bar.Baz x y
42

View file

@ -0,0 +1,3 @@
foo : Foo.Bar.Baz x y as Blah a b
42

View file

@ -0,0 +1,2 @@
when Delmin (Del rx) 0 is
Delmin (Del ry) _ -> Node Black 0 Bool.false ry

View file

@ -0,0 +1 @@
1 * if Bool.true then 1 else 1

View file

@ -0,0 +1,5 @@
1
+
when Foo is
Foo -> 2
Bar -> 3

View file

@ -0,0 +1,5 @@
# leading comment
{ x, y } = 5
y = 6
42

View file

@ -0,0 +1,8 @@
f : {
getLine : Effect Str,
putLine : Str -> Effect Int,
text : Str,
value : Int *,
}
42

View file

@ -0,0 +1,3 @@
x : { init : {} -> Model, update : Model, Str -> Model, view : Model -> Str }
42

View file

@ -0,0 +1 @@
{ x: if Bool.true then 1 else 2, y: 3 }

View file

@ -0,0 +1,5 @@
# leading comment
x <- \y -> y
z <- {}
x

View file

@ -0,0 +1,3 @@
when x is
"" -> 1
"mise" -> 2

View file

@ -0,0 +1,5 @@
# leading comment
x = 5
y = 6
42

View file

@ -0,0 +1,3 @@
doStuff : UserId -> Task Str _
42

View file

@ -0,0 +1,4 @@
# leading comment
_ <- \y -> y
4

View file

@ -0,0 +1,7 @@
(Pair x _) = Pair 0 1
(Pair _ y) = Pair 0 1
(Pair _ _) = Pair 0 1
_ = Pair 0 1
(Pair (Pair x _) (Pair _ y)) = Pair (Pair 0 1) (Pair 2 3)
0

View file

@ -0,0 +1,9 @@
when x is
_ ->
1
_ ->
2
Ok ->
3

View file

@ -0,0 +1,4 @@
x = when n is
0 -> 0
42

View file

@ -0,0 +1,4 @@
func = \x -> when n is
0 -> 0
42

View file

@ -0,0 +1,4 @@
func = \x -> when n is
0 -> 0
42

View file

@ -0,0 +1,3 @@
when x is
Ok ->
3

View file

@ -0,0 +1,2 @@
when x is
Ok -> 3

View file

@ -0,0 +1,7 @@
when x is
"blah" | "blop" -> 1
"foo"
| "bar"
| "baz" -> 2
"stuff" -> 4

View file

@ -0,0 +1,6 @@
when x is
1 ->
Num.neg
2
_ -> 4

Some files were not shown because too many files have changed in this diff Show more