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

Add trailingCommas option #1174

Merged
merged 13 commits into from
May 14, 2018
Merged

Add trailingCommas option #1174

merged 13 commits into from
May 14, 2018

Conversation

gabro
Copy link
Member

@gabro gabro commented May 11, 2018

Closes #1173
Closes #1060

Examples, given this config:

trailingCommas = always
align = none
danglingParentheses = true
BeforeAfter
def method(
    a: String,
    b: Int
)
def method(
    a: String,
    b: Int,
)
// maxColumn          |
def method(a: String, b: Int)
def method(
    a: String,
    b: Int,
)
// maxColumn          |
import x.y.{memberA, memberB}
import x.y.{
  memberA,
  memberB,
}
// maxColumn       |
def method[Key, Value](paramA: Int, paramB: Int)
// maxColumn       |
def method[
    Key,
    Value,
](
    paramA: Int,
    paramB: Int,
)

Before moving forward, I would appreciate if @olafurpg could take a quick look, just to see whether I'm doing this right 😅

@gabro gabro changed the title Add trailingCommas option and implement the "true" case Add trailingCommas option May 11, 2018
@gabro gabro force-pushed the trailingCommas branch 2 times, most recently from 7fe8845 to 5efa3fd Compare May 11, 2018 15:03
@gabro gabro force-pushed the trailingCommas branch 2 times, most recently from 70a1978 to 20ed2fb Compare May 11, 2018 15:48
@@ -7,7 +7,7 @@ foo(
>>>
foo(
1,
2,
2
Copy link
Member Author

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.

Copy link
Member

@olafurpg olafurpg 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 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 &&
Copy link
Member

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

Copy link
Member Author

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

Copy link
Member

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,
)

Copy link
Member Author

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

Copy link
Member

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 &&
Copy link
Member

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

Copy link
Member Author

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] ||
Copy link
Member

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, _)

Copy link
Member

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 👍

@gabro
Copy link
Member Author

gabro commented May 11, 2018

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
@gabro
Copy link
Member Author

gabro commented May 12, 2018

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)
Copy link
Member Author

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)
Copy link
Member Author

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.

@gabro
Copy link
Member Author

gabro commented May 12, 2018

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!

@gabro
Copy link
Member Author

gabro commented May 13, 2018

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)
Copy link
Member Author

@gabro gabro May 13, 2018

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(",")
Copy link
Member

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

Copy link
Member Author

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?

Copy link
Member

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

Copy link
Member Author

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.

@gabro gabro requested a review from olafurpg May 14, 2018 08:32
maxColumn = 30
trailingCommas = always

<<< SKIP should consider comments when adding trailing commas
Copy link
Member Author

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?

Copy link
Member

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.

Copy link
Member

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
Copy link
Member Author

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] ||
Copy link
Member

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,
)

Copy link
Member Author

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 =
Copy link
Member Author

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!

@gabro
Copy link
Member Author

gabro commented May 14, 2018

@olafurpg take a look, I've re-used an existing method in FormatOps, so now the owner check should be fairly comprehensive. I've also added a bunch of new test cases for stuff like

val (
  a,
  b
) = someTuple

Copy link
Member

@olafurpg olafurpg left a 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
Copy link
Member

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

Copy link
Member Author

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,
Copy link
Member

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 👍

Copy link
Member Author

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

@gabro
Copy link
Member Author

gabro commented May 14, 2018

CI is 🍏 merging! Thank you so much @olafurpg for the many and quick reviews!

@gabro gabro merged commit 2a7dc2a into scalameta:master May 14, 2018
@gabro gabro deleted the trailingCommas branch May 14, 2018 14:15
Copy link
Member

@olafurpg olafurpg left a 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

@gabro
Copy link
Member Author

gabro commented May 14, 2018

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 )

@olafurpg
Copy link
Member

I'm fine with prioritizing the new website 👍

olafurpg added a commit to olafurpg/scalafmt that referenced this pull request May 27, 2018
Upgrading scalameta resulted in a "ClassNotFoundException" for a
scalacheck class that was fixed by adding a dependency on scalacheck in
the tests.
olafurpg added a commit that referenced this pull request May 27, 2018
Unskip trailing commas tests, fixes #1174
@olafurpg olafurpg added this to the v1.6.0 milestone May 27, 2018
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.

2 participants