-
Notifications
You must be signed in to change notification settings - Fork 276
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
Add trailingCommas option #1174
Conversation
7fe8845
to
5efa3fd
Compare
70a1978
to
20ed2fb
Compare
@@ -7,7 +7,7 @@ foo( | |||
>>> | |||
foo( | |||
1, | |||
2, | |||
2 |
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 PR is currently breaking. I have an idea on how to make this non-breaking by default.
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 looks awesome, I'm eager to use this feature myself. The approach looks exactly like what I had in mind 👍
Missing test cases
foo(
a, // comment
)
and if optIn.configStyleArguments = false
// before
foo(
a,
)
// after
foo(a)
@@ -43,6 +43,24 @@ class FormatWriter(formatOps: FormatOps) { | |||
.getOrElse(token.syntax, token.syntax) | |||
sb.append(rewrittenToken) | |||
} | |||
|
|||
if (runner.dialect.allowTrailingCommas && | |||
state.splits.last.modification.isNewline && |
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.
It would be good to have a parallel that removes the trailing comma if !modification.isNewline
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.
Yep, it may be necessary for handling configStyleArguments = false
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.
also necessary for
foo(a,
b,
)
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.
yep, I have a skipped test for it. Thanks for the examples, it's hard to come up with those :D
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.
The comma should btw be removed in that case regardless of the setting since it's a bug to produce foo(a, b,)
@@ -43,6 +43,24 @@ class FormatWriter(formatOps: FormatOps) { | |||
.getOrElse(token.syntax, token.syntax) | |||
sb.append(rewrittenToken) | |||
} | |||
|
|||
if (runner.dialect.allowTrailingCommas && |
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.
Let's move this to a helper method handleTrailingComma
or something like that
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 agree, I was about to do it!
|
||
if (runner.dialect.allowTrailingCommas && | ||
state.splits.last.modification.isNewline && | ||
(formatToken.right.is[Token.RightParen] || |
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.
Conventionally we pattern match against case FormatToken(_: Token.Comma, _: Token.RightParen | _: Token.RightBracket, _)
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.
Actually, pattern matching might not be a good idea since we'll have to handle comments as well. This is fine 👍
Ok, now it handles the removal of trailing commas when going from multi-line to single line, e.g. // before
foo(
a,
)
// after
foo(a) or // before
foo(a,
b,
)
// after
foo(a, b) Comments are next! |
The tests are skipped because of an upstream parser bug in Scalameta. See scalameta/scalameta#1531
I've added support for handling comments after trailing commas when adding/removing: foo(
a,
b, // comment
) Unfortunately I hit scalameta/scalameta#1531 which makes the test fail. I was able to test the addition nonetheless, since the bug affects the output, but I'm not confident the removal works, since the test input doesn't parse. |
case TrailingCommas.always | ||
if left.is[Comment] && !prev(left).is[Comma] && right.is[ClosingBracket] && isNewline => | ||
sb.insert(sb.length - left.syntax.length - 1, ",") | ||
sb.append(whitespace) |
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 had to deal with comments in this way, detecting a comment, looking for a comma next to it, going back in the buffer and removing the comma.
Not sure how safe is this.
I had initially tried to incorporate this into the previous case, but in presence of [ID][COMMENT][NEWLINE][CLOSING_BRACKET]
I couldn't detect the newline after che comment.
case TrailingCommas.never | ||
if left.is[Comment] && prev(left).is[Comma] && right.is[ClosingBracket] && isNewline => | ||
sb.deleteCharAt(sb.length - left.syntax.length - 1) | ||
sb.append(whitespace) |
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 is the case that is currently untested, since the test input fails to parse.
Ok, I've tried using scalameta/scalameta#1532 with a locally published version of Scalameta, and I've found a couple of bugs with the comments. It seems my approach is not idempotent. I'll investigate! |
Done! I've used scalameta/scalameta#1532 to fix the two tests that are skipped in this PR, and they all pass on my machine. |
@@ -43,7 +43,9 @@ class FormatWriter(formatOps: FormatOps) { | |||
.getOrElse(token.syntax, token.syntax) | |||
sb.append(rewrittenToken) | |||
} | |||
sb.append(whitespace) | |||
|
|||
handleTrailingCommasAndWhitespace(formatToken, state, sb, whitespace) |
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 had to incorporate the insertion of the whitespace, since in one case it needs to be skipped, namely when rewriting
foo(a,
b,
)
to
foo(a, b)
Inserting the whitespace would result in
foo(a, b )
instead
case TrailingCommas.always | ||
if !left.is[Comma] && !left.is[Comment] && | ||
right.is[CloseDelim] && isNewline => | ||
sb.append(",") |
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.
Does this force a comma in this case?
// before
lst.map { x =>
x + 1
}
// after
lst.map { x =>
x + 1,
}
I think we may have to limit this behavior to a couple of nodes
- Term.Apply
- Importee/Importer
- Type.Apply
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.
Also tuples, I think. How do I limit to a specific node?
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, tuples as well 👍
FormatOps.owners(Token): Tree
goes to the closest enclosing node of a token
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.
done! The final list includes also Defn.Def
and Decl.Def
. I've added test cases for all of those.
maxColumn = 30 | ||
trailingCommas = always | ||
|
||
<<< SKIP should consider comments when adding trailing commas |
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.
works locally with scalameta master which includes scalameta/scalameta#1532
Should we track to enable this somewhere, @olafurpg?
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.
It may be a couple weeks until the next meta release so I think it would be good to track this in a ticket.
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.
Done #1182
maxColumn = 30 | ||
trailingCommas = never | ||
|
||
<<< SKIP should consider comments when removing trailing commas |
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.
same as above
|
||
val owner = owners(formatToken.right) | ||
val hasValidOwner = | ||
owner.is[Term.Apply] || owner.is[Term.Tuple] || |
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.
Can we introduce a classifier named CommaSeparated
for this group? I think Ctor.{Primary,Secondary}
are missing for
class Foo(
a: Int,
b: String,
)
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.
Nice catch, yes I'll introduce a classifier if you think it's easier to read
@@ -318,6 +319,11 @@ object TreeOps { | |||
def isDefnOrCallSite(tree: Tree): Boolean = |
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 method was already here, I realized!
@olafurpg take a look, I've re-used an existing method in val (
a,
b
) = someTuple |
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.
LGTM 👍 Feel free to merge once ./scalafmt --diff
is happy 😊
|
||
// foo( | ||
// a, | ||
// b, // comment |
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.
👏 Super nice comments, nice break down
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.
Those were mostly for myself 😅 They seemed useful so I've left them
@@ -124,6 +134,7 @@ case class ScalafmtConfig( | |||
assumeStandardLibraryStripMargin: Boolean = false, | |||
danglingParentheses: Boolean = false, | |||
poorMansTrailingCommasInConfigStyle: Boolean = false, | |||
trailingCommas: TrailingCommas = TrailingCommas.preserve, |
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 wonder what is the best default 🤔 Personally, I think always
might be a good choice since I'd prefer scalafmt to be opinionated where possible. However, that will break code on 2.11.
I'll open a discussion to gauge opinions, until then I agree preserve
is the safest choice 👍
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 wonder if the default could depend the dialect of the runner
CI is 🍏 merging! Thank you so much @olafurpg for the many and quick reviews! |
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.
Seems we forgot to document the new setting on the website, I'll write about it in the changelog but I think it would be a good candidate for the "Popular" section
Sort of. I didn't forget, but I didn't want to put energy in writing docs before completing the new website (maybe I'm ambitious :P ) |
I'm fine with prioritizing the new website 👍 |
Upgrading scalameta resulted in a "ClassNotFoundException" for a scalacheck class that was fixed by adding a dependency on scalacheck in the tests.
Unskip trailing commas tests, fixes #1174
Closes #1173
Closes #1060
Examples, given this config:
Before moving forward, I would appreciate if @olafurpg could take a quick look, just to see whether I'm doing this right 😅