Skip to content

Implement if statement autobracing#340

Merged
DavisVaughan merged 21 commits into
mainfrom
feature/if-else-bracing
May 29, 2025
Merged

Implement if statement autobracing#340
DavisVaughan merged 21 commits into
mainfrom
feature/if-else-bracing

Conversation

@DavisVaughan

@DavisVaughan DavisVaughan commented May 9, 2025

Copy link
Copy Markdown
Collaborator

Closes #334
Closes #225
Closes #348
Closes #321
Closes #22
Closes #9

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 thread crates/air_r_formatter/src/statement_body.rs Outdated
Comment on lines +136 to +139
# If statements and comments

# fmt: skip
if (TRUE) 1 # hi

Copy link
Copy Markdown
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.

Comment thread crates/air_r_formatter/tests/specs/r/if_statement.R

@lionel- lionel- left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks great!

I'm very happy with the final set of rules we came up with.

…ons (#344)

* Add autobracing for `for_statement`

* Add autobracing for `repeat_statement`

* Add autobracing for `while_statement`

* Add autobracing for `function_definition`

* In `for_statement`, push onto the `body` again

* In `while_statement`, push onto the `body` again

* In `repeat_statement`, push onto the `body` again

* Add documentation on autobracing

* CHANGELOG bullet

* Mention synergy with persistent line breaks

* Declare that any `RFunctionDefinition` can benefit from argument grouping

Allowing you to put a line break here `map(xs, function(x)<here>x + 1)`, which then causes autobracing to kick in, and now gives you the "middle variant" that you probably want of

```
map(xs, function(x) {
  x + 1
})
```

* Rework if statement section with value vs effect position

* Tweak CHANGELOG

* Portability is for multiline if statements

* Consistency with if statements

* Make effect/value comparison clearer
@DavisVaughan DavisVaughan merged commit 2ef782d into main May 29, 2025
4 checks passed
@DavisVaughan DavisVaughan deleted the feature/if-else-bracing branch May 29, 2025 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants