mirror of
https://github.com/roc-lang/roc.git
synced 2025-09-27 13:59:08 +00:00
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.
This commit is contained in:
parent
faaa466c70
commit
a046428ce6
57 changed files with 1284 additions and 471 deletions
|
@ -9,15 +9,11 @@ pub mod expr;
|
|||
pub mod module;
|
||||
pub mod pattern;
|
||||
pub mod spaces;
|
||||
pub mod test_helpers;
|
||||
|
||||
use bumpalo::{collections::String, Bump};
|
||||
use roc_parse::ast::Module;
|
||||
|
||||
#[cfg(windows)]
|
||||
const NEWLINE: &str = "\r\n";
|
||||
#[cfg(not(windows))]
|
||||
const NEWLINE: &str = "\n";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Ast<'a> {
|
||||
pub module: Module<'a>,
|
||||
|
@ -28,6 +24,7 @@ pub struct Ast<'a> {
|
|||
pub struct Buf<'a> {
|
||||
text: String<'a>,
|
||||
spaces_to_flush: usize,
|
||||
newlines_to_flush: usize,
|
||||
beginning_of_line: bool,
|
||||
}
|
||||
|
||||
|
@ -36,6 +33,7 @@ impl<'a> Buf<'a> {
|
|||
Buf {
|
||||
text: String::new_in(arena),
|
||||
spaces_to_flush: 0,
|
||||
newlines_to_flush: 0,
|
||||
beginning_of_line: true,
|
||||
}
|
||||
}
|
||||
|
@ -50,9 +48,7 @@ impl<'a> Buf<'a> {
|
|||
|
||||
pub fn indent(&mut self, indent: u16) {
|
||||
if self.beginning_of_line {
|
||||
for _ in 0..indent {
|
||||
self.text.push(' ');
|
||||
}
|
||||
self.spaces_to_flush = indent as usize;
|
||||
}
|
||||
self.beginning_of_line = false;
|
||||
}
|
||||
|
@ -104,48 +100,42 @@ impl<'a> Buf<'a> {
|
|||
|
||||
pub fn newline(&mut self) {
|
||||
self.spaces_to_flush = 0;
|
||||
|
||||
self.text.push_str(NEWLINE);
|
||||
|
||||
self.newlines_to_flush += 1;
|
||||
self.beginning_of_line = true;
|
||||
}
|
||||
|
||||
/// Ensures the current buffer ends in a newline, if it didn't already.
|
||||
/// Doesn't add a newline if the buffer already ends in one.
|
||||
pub fn ensure_ends_with_newline(&mut self) {
|
||||
if self.spaces_to_flush > 0 {
|
||||
self.flush_spaces();
|
||||
self.newline();
|
||||
} else if !self.text.ends_with('\n') && !self.text.is_empty() {
|
||||
if !self.text.is_empty() && self.newlines_to_flush == 0 {
|
||||
self.newline()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensure_ends_with_blank_line(&mut self) {
|
||||
if self.spaces_to_flush > 0 {
|
||||
self.flush_spaces();
|
||||
self.newline();
|
||||
self.newline();
|
||||
} else if !self.text.ends_with('\n') {
|
||||
self.newline();
|
||||
self.newline();
|
||||
} else if !self.text.ends_with("\n\n") {
|
||||
self.newline();
|
||||
if !self.text.is_empty() && self.newlines_to_flush < 2 {
|
||||
self.spaces_to_flush = 0;
|
||||
self.newlines_to_flush = 2;
|
||||
self.beginning_of_line = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_spaces(&mut self) {
|
||||
if self.spaces_to_flush > 0 {
|
||||
for _ in 0..self.spaces_to_flush {
|
||||
self.text.push(' ');
|
||||
}
|
||||
self.spaces_to_flush = 0;
|
||||
for _ in 0..self.newlines_to_flush {
|
||||
self.text.push('\n');
|
||||
}
|
||||
self.newlines_to_flush = 0;
|
||||
|
||||
for _ in 0..self.spaces_to_flush {
|
||||
self.text.push(' ');
|
||||
}
|
||||
self.spaces_to_flush = 0;
|
||||
}
|
||||
|
||||
/// Ensures the text ends in a newline with no whitespace preceding it.
|
||||
pub fn fmt_end_of_file(&mut self) {
|
||||
fmt_text_eof(&mut self.text)
|
||||
self.ensure_ends_with_newline();
|
||||
self.flush_spaces();
|
||||
}
|
||||
|
||||
pub fn ends_with_space(&self) -> bool {
|
||||
|
@ -153,117 +143,10 @@ impl<'a> Buf<'a> {
|
|||
}
|
||||
|
||||
pub fn ends_with_newline(&self) -> bool {
|
||||
self.spaces_to_flush == 0 && self.text.ends_with('\n')
|
||||
self.newlines_to_flush > 0 || self.text.ends_with('\n')
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.spaces_to_flush == 0 && self.text.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensures the text ends in a newline with no whitespace preceding it.
|
||||
fn fmt_text_eof(text: &mut bumpalo::collections::String<'_>) {
|
||||
let mut chars_rev = text.chars().rev();
|
||||
let mut last_whitespace = None;
|
||||
let mut last_whitespace_index = text.len();
|
||||
|
||||
// Keep going until we either run out of characters or encounter one
|
||||
// that isn't whitespace.
|
||||
loop {
|
||||
match chars_rev.next() {
|
||||
Some(ch) if ch.is_whitespace() => {
|
||||
last_whitespace = Some(ch);
|
||||
last_whitespace_index -= 1;
|
||||
}
|
||||
_ => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match last_whitespace {
|
||||
Some('\n') => {
|
||||
// There may have been more whitespace after this newline; remove it!
|
||||
text.truncate(last_whitespace_index + '\n'.len_utf8());
|
||||
}
|
||||
Some(_) => {
|
||||
// There's some whitespace at the end of this file, but the first
|
||||
// whitespace char after the last non-whitespace char isn't a newline.
|
||||
// So replace that whitespace char (and everything after it) with a newline.
|
||||
text.replace_range(last_whitespace_index.., NEWLINE);
|
||||
}
|
||||
None => {
|
||||
debug_assert!(last_whitespace_index == text.len());
|
||||
debug_assert!(!text.ends_with(char::is_whitespace));
|
||||
|
||||
// This doesn't end in whitespace at all, so add a newline.
|
||||
text.push_str(NEWLINE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eof_text_ends_with_newline() {
|
||||
use bumpalo::{collections::String, Bump};
|
||||
|
||||
let arena = Bump::new();
|
||||
let input = "This should be a newline:\n";
|
||||
let mut text = String::from_str_in(input, &arena);
|
||||
|
||||
fmt_text_eof(&mut text);
|
||||
|
||||
// This should be unchanged!
|
||||
assert_eq!(text.as_str(), input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eof_text_ends_with_whitespace() {
|
||||
use bumpalo::{collections::String, Bump};
|
||||
|
||||
let arena = Bump::new();
|
||||
let input = "This should be a newline: \t";
|
||||
let mut text = String::from_str_in(input, &arena);
|
||||
|
||||
fmt_text_eof(&mut text);
|
||||
|
||||
assert_eq!(text.as_str(), "This should be a newline:\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eof_text_ends_with_whitespace_then_newline() {
|
||||
use bumpalo::{collections::String, Bump};
|
||||
|
||||
let arena = Bump::new();
|
||||
let input = "This should be a newline: \n";
|
||||
let mut text = String::from_str_in(input, &arena);
|
||||
|
||||
fmt_text_eof(&mut text);
|
||||
|
||||
assert_eq!(text.as_str(), "This should be a newline:\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eof_text_ends_with_no_whitespace() {
|
||||
use bumpalo::{collections::String, Bump};
|
||||
|
||||
let arena = Bump::new();
|
||||
let input = "This should be a newline:";
|
||||
let mut text = String::from_str_in(input, &arena);
|
||||
|
||||
fmt_text_eof(&mut text);
|
||||
|
||||
assert_eq!(text.as_str(), "This should be a newline:\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eof_text_is_empty() {
|
||||
use bumpalo::{collections::String, Bump};
|
||||
|
||||
let arena = Bump::new();
|
||||
let input = "";
|
||||
let mut text = String::from_str_in(input, &arena);
|
||||
|
||||
fmt_text_eof(&mut text);
|
||||
|
||||
assert_eq!(text.as_str(), "\n");
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue