-
Notifications
You must be signed in to change notification settings - Fork 80
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
Break after annotations iff it is a block-level expression #302
Conversation
@cgrushko has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
if (hasBreak) { | ||
builder.forcedBreak() | ||
} else { | ||
builder.space() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this effectively preserves all whitespace around annotations, right?
feels like a shame, but I guess we don't have any other choice?
(in particular, formatting of annotations is no longer independent of the input)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it preserved all whitespace, just newlines. It normalizes weird combos of newlines into a one-annotation-per-line form, and collapses adjacent spaces into single spaces.
If there's an example you're worried about, let's add it to the tests. I tried to cover all the ones I could think of, but it's a surprisingly tricky problem, since formatting the expression might also insert newlines that change the annotation target.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the space()
means there won't be a line break there. So, if there's a \n, it'll be a forcedBreak, otherwise a non-breaking space. That's what I meant by preserve whitespace.
I don't know enough about annotations in this context to suggest a better solution, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. If there was a line break between any two annotations, or before the expression, then we'll force a break between all annotations and the expression. If there were no line breaks to begin with, all whitespace becomes non-breaking spaces.
@strulovich: "Can we make this so we do the fallback only for complex expressions? |
To what end? Is there some case where this approach gives a bad formatting? Because this part of the formatter has the potential to change the behaviour of code, I'm reluctant to give it a lot of cases. The more paths it has, the more likely it is to behave incorrectly, or surprisingly. At the very least, I would want it to do the "safe" thing by default, with perhaps some exceptions for provably identical cases. |
Our basic premise for this tool is:
This makes Ktfmt less deterministic. If I'm understanding the code right, both of these will remain the same:
This makes the formatter do less of what we need it to, and can result in people having to discuss or manually modify the style of their code more. Sadly, by now the "one rule fits all" issues of the formatter have been solved, so it's more and more likely we need to dig into issues with more branches. Associativity issues should only be arising from binary expressions, so I don't think this is complicated to solve with minimal degradation of our promises. |
Do we know for certain that it really is just binary expressions where behaviour can change? For example, what about infix functions, or |
Nope, I agree it's worth some testing. I think this can be done by annotating a bunch of stuff, loading the AST, and just seeing what annotation is for what. Once we understand that we can probably use the AST information to avoid mistakes while formatting. (I assume the annotations are attached in the AST correctly, otherwise it's a really weird problem) |
…vel expression. For some block-level expressions (e.g. binary operators, infix functions) the line break changes the target of the annotation. When on the previous line, the annotation targets the entire expression. When on the same line, it targets only the left-most sub-expression. In order to always preserve semantics, and guarantee consistent formatting, we always break only for this case.
8118a02
to
60452a3
Compare
How about this version: if the annotated expression is the child of a block, force a break after the annotations. It should hit all our requirements:
Testing against our code base, only 0.2% of files were affected. It also seems to improve formatting of annotations on lambdas, which is a bonus I can't explain. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the delay, was out.
This seems like a good approach.
From looking at all cases, all I could find is KtBinaryExpression/KtBinaryExpressionWithTypeRHS as a way to make this happen.
(Assignments, as
and all the usual +-*/... are all KtBinaryExpression or KtBinaryExpressionWithTypeRHS)
I think we can therefore only use this behavior in a block, and for these expressions.
I'm fine with adding the type checks myself, but let's add a specific test (both options next to each other, and the same thing outside a block being formatted)
Thanks for digging into this nasty bug.
It also definitely happens for infix functions. Are those KtBinaryExpression/KtBinaryExpressionWithTypeRHS? Example https://pl.kotl.in/zyHiVpo_g |
Yes, infix functions are KtBinaryExpression. Here's a test case example I used (with the tree printing to check that):
I was not able to find an example that causes this with |
Ok, at this point, it seems like you know the changes you'd like to make. Do you mind merging them internally? I'm fine as long as the bug is fixed. |
@strulovich has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
These breaks are meaningful, since adding or removing them can change which parts of the expression the annotation applies to.
Fixes #247