Skip to content

Autobrace for loops, while loops, repeat loops, and function definitions #344

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 14 commits into
base: feature/if-else-bracing
Choose a base branch
from

Conversation

DavisVaughan
Copy link
Collaborator

@DavisVaughan DavisVaughan commented May 14, 2025

Branched from #340

Added autobracing to:

  • for loops
  • while loops
  • repeat loops
  • function definitions

These are significantly less complicated than if/else handling because we don't have to worry about nesting (like if (cond) 1 else if (cond) 2 else 3).

We unconditionally autobrace the body of all loop types. As discussed, we don't see a use case for 1 line loops that would be more clear without braces.

Function definitions are allowed on one line. Braces will be added if any part of the function definition spans multiple lines (the parameters or the body).


The comment handling of all of these types needed a bit of an overhaul, so there's a decent amount of churn there. We more comprehensively handle all possible comment positions now, and I've added a slew of new comment related tests to ensure we don't break anything in the future.


I've added the CHANGELOG bullet for this feature here, along with a new Autobracing section in the documentation.

Addition of "most flat" variant

The most subtle but also most impactful change here is that I've had to touch call_arguments.rs.

Consider the following two map() calls:

map(xs, function(x) x + 1)

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

Previously, we would only consider a function definition eligible for the "best fitting" algorithm if its body already had {. So only the second would be considered as a candidate for "best fitting". The first would have chosen between a call with no breaks at all, or a call that is completely expanded.

Best fitting in theory chooses between 3 layouts:

  • Most flat variant
  • Middle variant (the special form you see above)
  • Most expanded variant (fully expanded function call)

Because we required {, this actually immediately ruled out the most flat variant. I've always had the intention of adding it back in when we needed it, but up until now it was in a branch that was unreachable!().

We now need to consider the most flat variant, because we've changed the function definition rule from "only do best fitting if it has { in the body" to "always try best fitting". That means for something like this:

map(xs, function(x) x + something_really_really_long_here_exceeding_line_length)

we now decide between:

# most flat
map(xs, function(x) x + something_really_really_long_here_exceeding_line_length)

# middle variant
map(xs, function(x) {
  x + something_really_really_long_here_exceeding_line_length
})

# most expanded (may or may not autobrace depending on line length)
map(
  xs,
  function(x) {
    x + something_really_really_long_here_exceeding_line_length
  }
)

this also allows the cool "code reflow" move of putting a line break here:

map(xs, function(x) <here>x + 1)

to get

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

Even before autobracing, I think it was actually a mistake that we required { as a prerequisite for trying the best fitting algorithm. Like, this doesn't have { and probably should have gone through the best fitting algorithm so it could select the middle variant (which is the form you see here):

map(xs, function(x) 
  x + y + z + this_other_long_thing
)

This will now get autobraced to

map(xs, function(x) {
  x + y + z + this_other_long_thing
})

@DavisVaughan DavisVaughan force-pushed the feature/remaining-autobracing branch from 3deaee5 to 54ed8f7 Compare May 16, 2025 13:31
Comment on lines +393 to +427
## With persistent line breaks

Autobracing is particularly useful as a code rewriting tool when combined with persistent line breaks.
Consider:

``` r
result <- map_lgl(xs, function(x) is.logical(x) && length(x) == 1L && !is.na(x))
```

This may be easier to read if it spanned across multiple lines.
You could manually rework this, or you could let Air help you!
There are two places you could put a persistent line break depending on what your desired final result is:

``` r
# Adding a line break before `xs` expands the call
result <- map_lgl(
xs, function(x) is.logical(x) && length(x) == 1L && !is.na(x))

# Becomes:
result <- map_lgl(
xs,
function(x) is.logical(x) && length(x) == 1L && !is.na(x)
)
```

``` r
# Adding a line break before `is.logical(x)` forces autobracing
result <- map_lgl(xs, function(x)
is.logical(x) && length(x) == 1L && !is.na(x))

# Becomes:
result <- map_lgl(xs, function(x) {
is.logical(x) && length(x) == 1L && !is.na(x)
})
```
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I thought this was a cool section

@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.

1 participant