Skip to content
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

Revamp conditional expression indentation. #1626

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

Conversation

munificent
Copy link
Member

@munificent munificent commented Dec 17, 2024

This PR makes two changes to how conditional expressions are indented.

Don't indent subsequent conditional branches if an assignment context:

The formatter has some special handling for infix operators inside assignment-like contexts (after =, :, or =>) to avoid otherwise redundant-seeming indentation. Normally, you'd get:

variable =
    operand +
        another;

The +4 before operand + is from the assignment indenting the RHS. The +8 before another; is the +4 from the assignment plus the +4 indentation from the binary operator. It looks better if the operands line up, so when a binary operator is in an assignment context, we don't indent it. That gives:

variable =
    operand +
    another;

I think that looks nice, and users have asked for it. For no good reason, when writing the new formatter we didn't do that for conditional expressions. Instead, you get:

variable =
    condition
        ? thenBranch
        : elseBranch;

This PR applies the same rule to conditionals that we do for binary operators, giving:

variable =
    condition
    ? thenBranch
    : elseBranch;

Indent then and else branches past their operator:

If there's a split inside a conditional operator's operand, we have to decide how to indent it. Currently, there is no indentation at all. That looks OK if the internal split is from something like a binary operator:

condition
    ? longOperand +
        anotherLongOperand
    : thirdOperand +
        anotherLongOperand;

But it looks less good if the split is something like a function call:

condition
    ? function(
      argument,
      another,
    )
    : function(
      argument,
      another,
    );

It seems weird that the ) are at the same indentation as the conditional operators themselves, like the operands are trying to escape the conditional expression.

This PR fixes #1534 and indents the then and else branches (but not the condition) two spaces past the operands. It does this for all operand subexpressions, giving:

condition
    ? longOperand +
          anotherLongOperand
    : function(
        argument,
        another,
      );

Together:

If you put these changes together, then in an assignment context, the ? and : move fours spaces left and their subexpressions move two spaces left:

// Before:
SomeWidget(
  child:
      condition
          ? AnotherWidget(
            argument,
            argument,
          )
          : ThirdWidget(
            argument,
            argument,
          )
);

// After:
SomeWidget(
  child:
      condition
      ? AnotherWidget(
          argument,
          argument,
        )
      : ThirdWidget(
          argument,
          argument,
        )
);

The overall effect is to move conditionals to the left. That in turn means some code that would wrap no longer has to. This ends up being pretty valuable because conditional expressions are very common in Flutter widget code since we don't allow if inside argument lists.

I've tested this on a big corpus of pub packages and every diff I saw looked better to my eyes. If you look at the test diffs you'll see what I mean.

I'd like to get this change in before 3.7 ships on stable so that users don't have to deal with the churn if possible. At the same time, we've already migrated the Dart SDK, internal code, and much of Flutter, so it would be somewhat disruptive.

If we have to wait until after 3.7 and ship in 3.8, we can do that too.

Thoughts on whether/when we should land this @natebosch, @davidmorgan, @goderbauer, @athomas, @mit-mit?

This PR makes two changes to how conditional expressions are indented:

**Don't indent subsequent conditional branches if an assignment context.**

The formatter has some special handling for infix operators inside assignment-like contexts (after `=`, `:`, or `=>`) to avoid otherwise redundant-seeming indentation. Normally, you'd get:

```dart
variable =
    operand +
        another;
```

The +4 before `operand +` is from the assignment indenting the RHS. And then the +4 before `another;` is that plus the +4 indentation from the binary operator. It looks better if the operands line up, so when a binary operator is in an assignment context, we don't indent it. That gives:

```dart
variable =
    operand +
    another;
```

I think that looks nice, and users have asked for it. For no good reason, when writing the new formatter we didn't do that for conditional expressions. You get:

```dart
variable =
    condition
        ? thenBranch
        : elseBranch;
```

This PR applies the same rule to conditionals that we do for binary operators, giving:

```dart
variable =
    condition
    ? thenBranch
    : elseBranch;
```

**Indent then and else branches past their operator.**

If there's a split *inside* a conditional operator's operand, we have to decide how to indent it. Currently, there is no indentation at all. That looks OK if the internal split is from something like a binary operator:

```dart
condition
    ? longOperand +
        anotherLongOperand
    : thirdOperand +
        anotherLongOperand;
```

But it looks less good if the split is something like a function call:

```dart
condition
    ? function(
      argument,
      another,
    )
    : function(
      argument,
      another,
    );
```

It seems weird that the `)` are at the same indentation as the conditional operators themselves.

This PR fixes #1534 and indents the then and else branches (but not the condition) two spaces past the operands. It does this for all operand subexpressions, giving:

```dart
condition
    ? longOperand +
          anotherLongOperand
    : function(
        argument,
        another,
      );
```

**Together:**

If you put these changes together, then in an assignment context, the `?` and `:` move fours spaces left and their subexpressions move two spaces left:

```dart
// Before:
SomeWidget(
  child:
      condition
          ? AnotherWidget(
            argument,
            argument,
          )
          : ThirdWidget(
            argument,
            argument,
          )
);

// After:
SomeWidget(
  child:
      condition
      ? AnotherWidget(
          argument,
          argument,
        )
      : ThirdWidget(
          argument,
          argument,
        )
);
```

The overall effect is to move conditionals to the left. That in turn means some code that would wrap no longer has to. This ends up being pretty valuable because conditional expressions are very common in Flutter widget code since we don't allow `if` inside argument lists.

I've tested this on a big corpus of pub packages and every diff I saw looked better to my eyes.

I'd like to get this change in before 3.7 ships on stable so that users don't have to deal with the churn if possible. At the same time, we've already migrated the Dart SDK, internal code, and much of Flutter, so it would be somewhat disruptive.

If we have to wait until after 3.7 and ship in 3.8, we can do that too.
@davidmorgan
Copy link

The change itself looks reasonable.

In terms of disruption, I measured for internal code: it affects 11.5% of files. So not as big as the reformat we already did, I think it's landable, but we'd have to plan the work and do it quickly as the new version rolls in to minimize disruption. As before we'd need a few more people signed up to make that happen.

Are there any other changes this could be bundled with? In cases where they touch the same files, that's pretty much free. Happy to measure if you have any WIP changes.

@athomas
Copy link
Member

athomas commented Dec 17, 2024

Either it should land in the SDK this week, over the holidays (if there's someone that can deal with the re-formatting) or after the cut-off on Jan 8th.

@munificent
Copy link
Member Author

munificent commented Dec 17, 2024

The change itself looks reasonable.

👍

I measured for internal code: it affects 11.5% of files.

Wow, more than I expected.

I think it's landable, but we'd have to plan the work and do it quickly as the new version rolls in to minimize disruption. As before we'd need a few more people signed up to make that happen.

Do we need to do a migration? In the past with style changes, we've just landed them and let users reformat as they touch files. I can't recall anyone complaining, though it has been a while and the style changes back then tended to be pretty minor.

Are there any other changes this could be bundled with? In cases where they touch the same files, that's pretty much free. Happy to measure if you have any WIP changes.

Yes, there are a handful of other minor style fixes I've landed on the main branch here that would be rolled in too. They are:

Except for #1602, those are all extremely edge case and shouldn't affect much code.

Either it should land in the SDK this week, over the holidays (if there's someone that can deal with the re-formatting) or after the cut-off on Jan 8th.

Yes, I would aim to roll it into the SDK today or tomorrow. Some unfortunate timing: I will be out for two weeks for the holidays (12/23 - 1/3) and then getting surgery on 1/7 and out some unknown but hopefully small number of days after that.

@davidmorgan
Copy link

There's no way to roll it into google3 on that timeline; it would be necessary to pause rolls.

@davidmorgan
Copy link

We posted simultaneously, let me also reply to your question.

I measured for internal code: it affects 11.5% of files.

Wow, more than I expected.

I think it's landable, but we'd have to plan the work and do it quickly as the new version rolls in to minimize disruption. As before we'd need a few more people signed up to make that happen.

Do we need to do a migration? In the past with style changes, we've just landed them and let users reformat as they touch files. I can't recall anyone complaining, though it has been a while and the style changes back then tended to be pretty minor.

I think we'd definitely get complaints for 11.5% of files; it's enough impact that I guess it needs review+discussion.

@munificent
Copy link
Member Author

it's enough impact that I guess it needs review+discussion.

OK, I'll start an email.

Copy link
Member

@natebosch natebosch left a comment

Choose a reason for hiding this comment

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

This looks like an improvement worth landing whether we can get it in before or after the stable release.

@goderbauer
Copy link

Overall this looks like an improvement. I am happy to coordinate rolling this into flutter/flutter. No preference for weather to land this before or after the cutoff.

@munificent
Copy link
Member Author

We spent some time discussing this and decided that given we're in the holiday season and the 3.7 cutoff is so close, the best thing is to land this change in 3.8. So I'll let this PR sit for a while until we've rolled the last version of dart_style into the SDK and then it will go in after that.

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.

Indent ternary operands beneath the operator
5 participants