Skip to content

Implement if statement autobracing #340

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from

Conversation

DavisVaughan
Copy link
Collaborator

@DavisVaughan DavisVaughan commented May 9, 2025

Closes #334
Closes #225

This PR also has the side effect of fixing all idempotence issues on both data.table and R core. Those projects probably won't ever use Air, but they are a very good test case for "are we stable between runs?". We should probably set up some CI for ourselves that runs some kind of idempotence check on a set of large community packages (dplyr, data.table, r-svn) to ensure we don't slip here.


This PR introduces the idea of autobracing. This is the idea of wrapping an if statement body in { }

# Before
if (condition)
  this
else
  that

# After
if (condition) {
  this
} else {
  that
}

This ensures that the if statement is portable. The above original if statement actually won't even parse at top level due to the else on its own line. If you wrap the whole thing in { } (like would be the case if you found it inside a function body) then it parses fine. Portability ensures:

  • That you can copy and paste that if statement out to top level and run it without parse issues
  • That when you are debugging, you can highlight and run that if statement and send it to the console without any parse issues. This is otherwise very confusing!

We still allow short single line if statements when the if statement is in a value position (as opposed to an effect position). The if statement must also meet some other criteria (no existing newlines, no existing braces, etc) to be considered a single line candidate.

# Effect position at top level, will be autobraced
if (a) 1
if (a) 1 else 2

# Effect position in `{`, will be autobraced
fn <- function() {
  if (cond) stop("early check")
  1 + 1
}

# Fine, value position in `{` (the last expression in the `{` expression list)
fn <- function() {
  if (cond) 1 else 2
}

# Fine, value position in `{` (the last expression in the `{` expression list)
map(xs, function(x) {
  if (is.null(x)) this else that
}

# Fine, value position
x <- if (a) 1
x <- if (a) 1 else 2

# Fine, value position
fn(if(a) 1, arg = if(a) 1 else 2)

# Fine, value position
fn <- function(x, ..., arg = if (is.null(x)) 1 else 2) {}

# Fine, value position
x <- x %||% if (a) 1 else 2

# Examples of value position that would still be autobraced
x <- if (a)
  1
x <- if (a) 1 else if (b) 2 else 3
x <- if (a) { 1 } else 2
x <- if (a) 1 else { 2 }

I think the formatter code for this feature is rather elegant and easy enough to follow. By far the most complex part of this PR is if/else comment handling. I've added a huge slew of comment related tests to ensure we are doing it "well enough". Movement of comments with something like this will always be a "best effort" kind of transformation, but I think it is worth a little ambiguity. The main promise we make is that we won't ever drop your comments.


Documentation for if/else autobracing ended up in the new Autobracing section of this PR
#344

And use in if statement handling as a big improvement on context awareness!
Comment on lines +15 to +24
// TODO!: Repurpose this as `FormatBracedBody` for use in:
// - For loops (unconditionally)
// - Repeat loops (unconditionally)
// - While loops (unconditionally)
// - Function definitions
// - Includes anonymous functions
// - Allow 1 liner function definitions
// - Definitely breaks if argument list expands over multiple lines
// - Use `if_group_breaks(&text("{"))` to add missing `{}` if the group breaks,
// like if statements. Probably won't be able to use simple `FormatBracedBody`
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gets removed in the next PR

Comment on lines +136 to +139
# If statements and comments

# fmt: skip
if (TRUE) 1 # hi
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very important test related to comment placement and verbatim formatting

The # hi comment must be trailing on the whole if statement, rather than trailing on the 1 node, for this to work correctly. Otherwise verbatim printing will drop the # hi comment entirely, which would be bad.

When you remove the # fmt: skip, you get this

if (TRUE) {
  1
} # hi

which I can live with for this rare-ish scenario if the alternative is that a comment would be dropped.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an absurd amount of new test cases in here. I tried to be exhaustive in where I thought a comment could be placed, and then also added test cases from real life usage on tidyverse packages.

@DavisVaughan DavisVaughan requested a review from lionel- May 16, 2025 14:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

if/else formatting can lead to unexpected breaks Idempotence issue - if/else with comment
1 participant