Skip to content

Commit c0e6243

Browse files
davidtorosyanfacebook-github-bot
authored andcommitted
Add line breaks to lambdas after broken function arguments
Summary: We've had some requests to format this: ``` foo.bar( trailingComma, ) { x = 0 } ``` As this: ``` foo.bar( trailingComma, ) { x = 0 } ``` Here I do that by keeping track of when we enter lambda arguments in call elements and noting if there's a trailing comma. ## Caveat **Google-style requires a trailing comma** The Google-style formatting puts the closing paren on the next line, even when there's no trailing comma. Due to this we can't detect that we need to put a break in the lambda. This could be fixed by detecting if the params broke (instead of looking for a trailing comma) but I'm not sure how to do that. Reviewed By: strulovich Differential Revision: D36649191 fbshipit-source-id: 2c8d316f068c1030437af59b5114ffae9d5d2aa8
1 parent bd2e5c7 commit c0e6243

File tree

3 files changed

+194
-15
lines changed

3 files changed

+194
-15
lines changed

Diff for: core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt

+78-15
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,7 @@ class KotlinInputAstVisitor(
522522
}
523523
val argsIndentElse = if (index == parts.size - 1) ZERO else expressionBreakIndent
524524
val lambdaIndentElse = if (isTrailingLambda) expressionBreakNegativeIndent else ZERO
525+
525526
// emit `(1, 2) { it }` from `doIt(1, 2) { it }`
526527
visitCallElement(
527528
null,
@@ -751,9 +752,14 @@ class KotlinInputAstVisitor(
751752
builder.token(")")
752753
}
753754
}
755+
val hasTrailingComma = argumentList?.trailingComma != null
754756
if (lambdaArguments.isNotEmpty()) {
755757
builder.space()
756-
builder.block(lambdaIndent) { lambdaArguments.forEach { visit(it) } }
758+
builder.block(lambdaIndent) {
759+
lambdaArguments.forEach {
760+
visitArgumentInternal(it, forceBreakLambdaBody = hasTrailingComma)
761+
}
762+
}
757763
}
758764
}
759765
}
@@ -783,12 +789,38 @@ class KotlinInputAstVisitor(
783789

784790
/** Example `{ 1 + 1 }` (as lambda) or `{ (x, y) -> x + y }` */
785791
override fun visitLambdaExpression(lambdaExpression: KtLambdaExpression) {
786-
visitLambdaExpression(lambdaExpression, null as BreakTag?)
792+
visitLambdaExpressionInternal(lambdaExpression, brokeBeforeBrace = null, forceBreakBody = false)
787793
}
788794

789-
private fun visitLambdaExpression(
795+
/**
796+
* The internal version of [visitLambdaExpression].
797+
*
798+
* @param brokeBeforeBrace used for tracking if a break was taken right before the lambda
799+
* expression. Useful for scoping functions where we want good looking indentation. For example,
800+
* here we have correct indentation before `bar()` and `car()` because we can detect the break
801+
* after the equals:
802+
* ```
803+
* fun foo() =
804+
* coroutineScope { x ->
805+
* bar()
806+
* car()
807+
* }
808+
* ```
809+
* @param forceBreakBody if true, forces the lambda to be multi-line. Useful for call expressions
810+
* where it would look weird for the lambda to be on one-line. For example, here we avoid
811+
* one-lining `{ x = 0 }` since the parameters have a trailing comma:
812+
* ```
813+
* foo.bar(
814+
* trailingComma,
815+
* ) {
816+
* x = 0
817+
* }
818+
* ```
819+
*/
820+
private fun visitLambdaExpressionInternal(
790821
lambdaExpression: KtLambdaExpression,
791822
brokeBeforeBrace: BreakTag?,
823+
forceBreakBody: Boolean,
792824
) {
793825
builder.sync(lambdaExpression)
794826

@@ -838,6 +870,10 @@ class KotlinInputAstVisitor(
838870
builder.breakOp(Doc.FillMode.UNIFIED, "", bracePlusZeroIndent)
839871
}
840872

873+
if (forceBreakBody) {
874+
builder.forcedBreak()
875+
}
876+
841877
if (hasStatements) {
842878
builder.breakOp(Doc.FillMode.UNIFIED, " ", bracePlusBlockIndent)
843879
builder.block(bracePlusBlockIndent) {
@@ -985,6 +1021,20 @@ class KotlinInputAstVisitor(
9851021

9861022
/** Example `a` in `foo(a)`, or `*a`, or `limit = 50` */
9871023
override fun visitArgument(argument: KtValueArgument) {
1024+
visitArgumentInternal(argument, forceBreakLambdaBody = false)
1025+
}
1026+
1027+
/**
1028+
* The internal version of [visitArgument].
1029+
*
1030+
* @param forceBreakLambdaBody if true (and [argument] is of type [KtLambdaExpression]), forces
1031+
* the lambda to be multi-line. See documentation of [visitLambdaExpressionInternal] for an
1032+
* example.
1033+
*/
1034+
private fun visitArgumentInternal(
1035+
argument: KtValueArgument,
1036+
forceBreakLambdaBody: Boolean,
1037+
) {
9881038
builder.sync(argument)
9891039
val hasArgName = argument.getArgumentName() != null
9901040
val isLambda = argument.getArgumentExpression() is KtLambdaExpression
@@ -1004,7 +1054,15 @@ class KotlinInputAstVisitor(
10041054
if (argument.isSpread) {
10051055
builder.token("*")
10061056
}
1007-
visit(argument.getArgumentExpression())
1057+
if (isLambda) {
1058+
visitLambdaExpressionInternal(
1059+
argument.getArgumentExpression() as KtLambdaExpression,
1060+
brokeBeforeBrace = null,
1061+
forceBreakBody = forceBreakLambdaBody,
1062+
)
1063+
} else {
1064+
visit(argument.getArgumentExpression())
1065+
}
10081066
}
10091067
}
10101068
}
@@ -1274,17 +1332,22 @@ class KotlinInputAstVisitor(
12741332
val breakToExpr = genSym()
12751333
builder.breakOp(Doc.FillMode.INDEPENDENT, " ", expressionBreakIndent, Optional.of(breakToExpr))
12761334

1277-
when (expr) {
1278-
is KtLambdaExpression -> {
1279-
visitLambdaExpression(expr, breakToExpr)
1280-
}
1281-
is KtCallExpression -> {
1282-
visit(expr.calleeExpression)
1283-
builder.space()
1284-
visitLambdaExpression(expr.lambdaArguments[0].getLambdaExpression() ?: fail(), breakToExpr)
1285-
}
1286-
else -> throw AssertionError(expr)
1287-
}
1335+
val lambdaExpression =
1336+
when (expr) {
1337+
is KtLambdaExpression -> expr
1338+
is KtCallExpression -> {
1339+
visit(expr.calleeExpression)
1340+
builder.space()
1341+
expr.lambdaArguments[0].getLambdaExpression() ?: fail()
1342+
}
1343+
else -> throw AssertionError(expr)
1344+
}
1345+
1346+
visitLambdaExpressionInternal(
1347+
lambdaExpression,
1348+
brokeBeforeBrace = breakToExpr,
1349+
forceBreakBody = false,
1350+
)
12881351
}
12891352

12901353
override fun visitClassOrObject(classOrObject: KtClassOrObject) {

Diff for: core/src/test/java/com/facebook/ktfmt/format/FormatterTest.kt

+58
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,64 @@ class FormatterTest {
717717
|}
718718
|""".trimMargin())
719719

720+
@Test
721+
fun `don't one-line lambdas following parameter breaks`() =
722+
assertFormatted(
723+
"""
724+
|------------------------------------------------------------------------
725+
|class Foo : Bar() {
726+
| fun doIt() {
727+
| // don't break in lambda, no parameter breaks found
728+
| fruit.forEach { eat(it) }
729+
|
730+
| // break in the lambda because the closing paren gets attached
731+
| // to the last argument
732+
| fruit.forEach(
733+
| someVeryLongParameterNameThatWillCauseABreak,
734+
| evenWithoutATrailingCommaOnTheParameterListSoLetsSeeIt) {
735+
| eat(it)
736+
| }
737+
|
738+
| // break in the lambda
739+
| fruit.forEach(
740+
| fromTheVine = true,
741+
| ) {
742+
| eat(it)
743+
| }
744+
|
745+
| // don't break in the inner lambda, as nesting doesn't respect outer levels
746+
| fruit.forEach(
747+
| fromTheVine = true,
748+
| ) {
749+
| fruit.forEach { eat(it) }
750+
| }
751+
|
752+
| // don't break in the lambda, as breaks don't propagate
753+
| fruit
754+
| .onlyBananas(
755+
| fromTheVine = true,
756+
| )
757+
| .forEach { eat(it) }
758+
|
759+
| // don't break in the inner lambda, as breaks don't propagate to parameters
760+
| fruit.onlyBananas(
761+
| fromTheVine = true,
762+
| processThem = { eat(it) },
763+
| ) {
764+
| eat(it)
765+
| }
766+
|
767+
| // don't break in the inner lambda, as breaks don't propagate to the body
768+
| fruit.onlyBananas(
769+
| fromTheVine = true,
770+
| ) {
771+
| val anon = { eat(it) }
772+
| }
773+
| }
774+
|}
775+
|""".trimMargin(),
776+
deduceMaxWidth = true)
777+
720778
@Test
721779
fun `indent parameters after a break when there's a lambda afterwards`() =
722780
assertFormatted(

Diff for: core/src/test/java/com/facebook/ktfmt/format/GoogleStyleFormatterKtTest.kt

+58
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,64 @@ class GoogleStyleFormatterKtTest {
237237
formattingOptions = Formatter.GOOGLE_FORMAT,
238238
deduceMaxWidth = true)
239239

240+
@Test
241+
fun `don't one-line lambdas following parameter breaks`() =
242+
assertFormatted(
243+
"""
244+
|------------------------------------------------------------------------
245+
|class Foo : Bar() {
246+
| fun doIt() {
247+
| // don't break in lambda, no parameter breaks found
248+
| fruit.forEach { eat(it) }
249+
|
250+
| // don't break in lambda, because we only detect parameter breaks
251+
| // with trailing commas
252+
| fruit.forEach(
253+
| someVeryLongParameterNameThatWillCauseABreak,
254+
| evenWithoutATrailingCommaOnTheParameterListSoLetsSeeIt
255+
| ) { eat(it) }
256+
|
257+
| // break in the lambda
258+
| fruit.forEach(
259+
| fromTheVine = true,
260+
| ) {
261+
| eat(it)
262+
| }
263+
|
264+
| // don't break in the inner lambda, as nesting doesn't respect outer levels
265+
| fruit.forEach(
266+
| fromTheVine = true,
267+
| ) {
268+
| fruit.forEach { eat(it) }
269+
| }
270+
|
271+
| // don't break in the lambda, as breaks don't propagate
272+
| fruit
273+
| .onlyBananas(
274+
| fromTheVine = true,
275+
| )
276+
| .forEach { eat(it) }
277+
|
278+
| // don't break in the inner lambda, as breaks don't propagate to parameters
279+
| fruit.onlyBananas(
280+
| fromTheVine = true,
281+
| processThem = { eat(it) },
282+
| ) {
283+
| eat(it)
284+
| }
285+
|
286+
| // don't break in the inner lambda, as breaks don't propagate to the body
287+
| fruit.onlyBananas(
288+
| fromTheVine = true,
289+
| ) {
290+
| val anon = { eat(it) }
291+
| }
292+
| }
293+
|}
294+
|""".trimMargin(),
295+
formattingOptions = Formatter.GOOGLE_FORMAT,
296+
deduceMaxWidth = true)
297+
240298
@Test
241299
fun `function calls with multiple arguments`() =
242300
assertFormatted(

0 commit comments

Comments
 (0)