Skip to content

Commit

Permalink
Wrap the parameters of a function literal containing a multiline para…
Browse files Browse the repository at this point in the history
…meter list (only in `ktlint_official` code style).

Closes pinterest#1681

The parameters of a function literal containing a multiline parameter list are aligned with first parameter whenever the first parameter is on the same line as the start of that function literal (not allowed in `ktlint_official` code style)

Closes pinterest#1756
  • Loading branch information
paul-dingemans committed Jan 16, 2023
1 parent db02b18 commit 7c7ecb3
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 18 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,11 @@ Code style `android` has been renamed to `android_studio`. Code formatted with t
* An enumeration class having a primary constructor and in which the list of enum entries is followed by a semicolon then do not remove the semicolon in case it is followed by code element `no-semi` ([#1733](https://github.com/pinterest/ktlint/issues/1733))
* Do not add the (first line of the) body expression on the same line as the function signature in case the max line length would be exceeded. `function-signature`.
* Do not add the first line of a multiline body expression on the same line as the function signature in case function body expression wrapping property is set to `multiline`. `function-signature`.
* Disable the `standard:filename` rule whenever Ktlint CLI is run with option `--stdin` ([#1742](https://github.com/pinterest/ktlint/issues/1742))
* The parameters of a function literal containing a multiline parameter list are aligned with first parameter whenever the first parameter is on the same line as the start of that function literal (not allowed in `ktlint_official` code style) `indent` ([#1756](https://github.com/pinterest/ktlint/issues/1756)).

### Changed
* Wrap the parameters of a function literal containing a multiline parameter list (only in `ktlint_official` code style) `parameter-list-wrapping` ([#1681](https://github.com/pinterest/ktlint/issues/1681)).

## [0.48.2] - Unreleased

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import com.pinterest.ktlint.core.IndentConfig.IndentStyle.TAB
import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.api.EditorConfigProperties
import com.pinterest.ktlint.core.api.UsesEditorConfigProperties
import com.pinterest.ktlint.core.api.editorconfig.CODE_STYLE_PROPERTY
import com.pinterest.ktlint.core.api.editorconfig.CodeStyleValue.ktlint_official
import com.pinterest.ktlint.core.api.editorconfig.EditorConfigProperty
import com.pinterest.ktlint.core.api.editorconfig.INDENT_SIZE_PROPERTY
import com.pinterest.ktlint.core.api.editorconfig.INDENT_STYLE_PROPERTY
Expand Down Expand Up @@ -56,6 +58,7 @@ import com.pinterest.ktlint.core.ast.ElementType.PROPERTY
import com.pinterest.ktlint.core.ast.ElementType.PROPERTY_ACCESSOR
import com.pinterest.ktlint.core.ast.ElementType.RBRACE
import com.pinterest.ktlint.core.ast.ElementType.RBRACKET
import com.pinterest.ktlint.core.ast.ElementType.REFERENCE_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.REGULAR_STRING_PART
import com.pinterest.ktlint.core.ast.ElementType.RPAR
import com.pinterest.ktlint.core.ast.ElementType.SAFE_ACCESS_EXPRESSION
Expand Down Expand Up @@ -134,9 +137,12 @@ public class IndentationRule :
UsesEditorConfigProperties {
override val editorConfigProperties: List<EditorConfigProperty<*>> =
listOf(
CODE_STYLE_PROPERTY,
INDENT_SIZE_PROPERTY,
INDENT_STYLE_PROPERTY,
)

private var codeStyle = CODE_STYLE_PROPERTY.defaultValue
private var indentConfig = IndentConfig.DEFAULT_INDENT_CONFIG

private var line = 1
Expand All @@ -146,6 +152,7 @@ public class IndentationRule :
private lateinit var stringTemplateIndenter: StringTemplateIndenter

override fun beforeFirstNode(editorConfigProperties: EditorConfigProperties) {
codeStyle = editorConfigProperties.getEditorConfigValue(CODE_STYLE_PROPERTY)
indentConfig = IndentConfig(
indentStyle = editorConfigProperties.getEditorConfigValue(INDENT_STYLE_PROPERTY),
tabWidth = editorConfigProperties.getEditorConfigValue(INDENT_SIZE_PROPERTY),
Expand Down Expand Up @@ -384,11 +391,44 @@ public class IndentationRule :
startIndentContext(
fromAstNode = node,
toAstNode = arrow.prevCodeLeaf()!!,
childIndent = indentConfig.indent.repeat(2),
childIndent = arrow.calculateIndentOfFunctionLiteralParameters(),
)
}
}

private fun ASTNode.calculateIndentOfFunctionLiteralParameters() =
if (codeStyle == ktlint_official || isFirstParameterOfFunctionLiteralPrecededByNewLine()) {
// val fieldExample =
// LongNameClass {
// paramA,
// paramB,
// paramC ->
// ClassB(paramA, paramB, paramC)
// }
indentConfig.indent.repeat(2)
} else {
// Allow default IntelliJ IDEA formatting:
// val fieldExample =
// LongNameClass { paramA,
// paramB,
// paramC ->
// ClassB(paramA, paramB, paramC)
// }
parent(CALL_EXPRESSION)
?.let { callExpression ->
val textBeforeFirstParameter =
callExpression.findChildByType(REFERENCE_EXPRESSION)?.text +
" { "
" ".repeat(textBeforeFirstParameter.length)
}
?: indentConfig.indent.repeat(2)
}

private fun ASTNode.isFirstParameterOfFunctionLiteralPrecededByNewLine() =
parent(FUNCTION_LITERAL)
?.findChildByType(VALUE_PARAMETER_LIST)
?.prevSibling { it.textContains('\n') } != null

private fun visitLparBeforeCondition(node: ASTNode) {
startIndentContext(
fromAstNode = requireNotNull(node.nextLeaf()), // Allow to pickup whitespace before condition
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import com.pinterest.ktlint.core.IndentConfig
import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.api.EditorConfigProperties
import com.pinterest.ktlint.core.api.UsesEditorConfigProperties
import com.pinterest.ktlint.core.api.editorconfig.CODE_STYLE_PROPERTY
import com.pinterest.ktlint.core.api.editorconfig.CodeStyleValue.ktlint_official
import com.pinterest.ktlint.core.api.editorconfig.EditorConfigProperty
import com.pinterest.ktlint.core.api.editorconfig.INDENT_SIZE_PROPERTY
import com.pinterest.ktlint.core.api.editorconfig.INDENT_STYLE_PROPERTY
Expand Down Expand Up @@ -37,15 +39,18 @@ public class ParameterListWrappingRule :
UsesEditorConfigProperties {
override val editorConfigProperties: List<EditorConfigProperty<*>> =
listOf(
CODE_STYLE_PROPERTY,
INDENT_SIZE_PROPERTY,
INDENT_STYLE_PROPERTY,
MAX_LINE_LENGTH_PROPERTY,
)

private var codeStyle = CODE_STYLE_PROPERTY.defaultValue
private var indentConfig = IndentConfig.DEFAULT_INDENT_CONFIG
private var maxLineLength = -1

override fun beforeFirstNode(editorConfigProperties: EditorConfigProperties) {
codeStyle = editorConfigProperties.getEditorConfigValue(CODE_STYLE_PROPERTY)
maxLineLength = editorConfigProperties.getEditorConfigValue(MAX_LINE_LENGTH_PROPERTY)
indentConfig = IndentConfig(
indentStyle = editorConfigProperties.getEditorConfigValue(INDENT_STYLE_PROPERTY),
Expand Down Expand Up @@ -112,23 +117,34 @@ public class ParameterListWrappingRule :
}

private fun ASTNode.needToWrapParameterList() =
if ( // skip when there are no parameters
firstChildNode?.treeNext?.elementType != RPAR &&
// skip lambda parameters
treeParent?.elementType != FUNCTION_LITERAL &&
// skip when function type is wrapped in a nullable type [which was already when processing the nullable
// type node itself.
!(treeParent.elementType == FUNCTION_TYPE && treeParent?.treeParent?.elementType == NULLABLE_TYPE)
if (hasNoParameters() ||
isPartOfFunctionLiteralInNonKtlintOfficialCodeStyle() ||
isFunctionTypeWrappedInNullableType()
) {
false
} else {
// each parameter should be on a separate line if
// - at least one of the parameters is
// - maxLineLength exceeded (and separating parameters with \n would actually help)
// in addition, "(" and ")" must be on separates line if any of the parameters are (otherwise on the same)
textContains('\n') || this.exceedsMaxLineLength()
} else {
false
}

private fun ASTNode.hasNoParameters(): Boolean {
require(elementType == VALUE_PARAMETER_LIST)
return firstChildNode?.treeNext?.elementType == RPAR
}

private fun ASTNode.isPartOfFunctionLiteralInNonKtlintOfficialCodeStyle(): Boolean {
require(elementType == VALUE_PARAMETER_LIST)
return codeStyle != ktlint_official && treeParent?.elementType == FUNCTION_LITERAL
}

private fun ASTNode.isFunctionTypeWrappedInNullableType(): Boolean {
require(elementType == VALUE_PARAMETER_LIST)
return treeParent.elementType == FUNCTION_TYPE && treeParent?.treeParent?.elementType == NULLABLE_TYPE
}

private fun wrapParameterList(
node: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.pinterest.ktlint.ruleset.standard.rules

import com.pinterest.ktlint.core.api.editorconfig.CODE_STYLE_PROPERTY
import com.pinterest.ktlint.core.api.editorconfig.CodeStyleValue.ktlint_official
import com.pinterest.ktlint.core.api.editorconfig.INDENT_SIZE_PROPERTY
import com.pinterest.ktlint.core.api.editorconfig.INDENT_STYLE_PROPERTY
import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule
Expand Down Expand Up @@ -4757,6 +4759,44 @@ internal class IndentationRuleTest {
}
}

@Nested
inner class `Given the ktlint_official code style is enabled` {
@Test
fun `Issue 1681 - Given a lambda having multiple arguments on different lines`() {
val code =
"""
val bar =
BarBarBarBar { paramA,
paramB,
paramC ->
Bar(paramA, paramB, paramC)
}
""".trimIndent()
val formattedCode =
"""
val bar =
BarBarBarBar {
paramA,
paramB,
paramC ->
Bar(paramA, paramB, paramC)
}
""".trimIndent()
indentationRuleAssertThat(code)
.addAdditionalRuleProvider { ParameterListWrappingRule() }
.withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official)
.hasLintViolationForAdditionalRule(
2,
20,
"Parameter should be on a separate line (unless all parameters can fit a single line)",
)
.hasLintViolations(
LintViolation(3, 1, "Unexpected indentation (19) (should be 12)"),
LintViolation(4, 1, "Unexpected indentation (19) (should be 12)"),
).isFormattedAs(formattedCode)
}
}

private companion object {
val INDENT_STYLE_TAB =
INDENT_STYLE_PROPERTY to PropertyType.IndentStyleValue.tab
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package com.pinterest.ktlint.ruleset.standard.rules

import com.pinterest.ktlint.core.RuleProvider
import com.pinterest.ktlint.core.api.editorconfig.CODE_STYLE_PROPERTY
import com.pinterest.ktlint.core.api.editorconfig.CodeStyleValue
import com.pinterest.ktlint.core.api.editorconfig.CodeStyleValue.ktlint_official
import com.pinterest.ktlint.core.api.editorconfig.MAX_LINE_LENGTH_PROPERTY
import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule
import com.pinterest.ktlint.test.LintViolation
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.EnumSource

class ParameterListWrappingRuleTest {
private val parameterListWrappingRuleAssertThat =
Expand Down Expand Up @@ -171,9 +177,9 @@ class ParameterListWrappingRuleTest {
).isFormattedAs(formattedCode)
}

@Test
fun `Given a function with lambda parameters then do not reformat`() {
val code =
@Nested
inner class `Given a function literal having a multiline parameter list` {
private val code =
"""
val fieldExample =
LongNameClass { paramA,
Expand All @@ -182,11 +188,42 @@ class ParameterListWrappingRuleTest {
ClassB(paramA, paramB, paramC)
}
""".trimIndent()
val parameterListWrappingRuleWithoutIndentationRule = assertThatRule { ParameterListWrappingRule() }
parameterListWrappingRuleWithoutIndentationRule(code).hasNoLintViolations()
// IndentationRule does alter the code while the code is accepted by the Default IDEA formatter. So statement
// below would fail!
// parameterListWrappingRuleAssertThat(code).hasNoLintViolations()

@Test
fun `Given ktlint_official code style then reformat`() {
val formattedCode =
"""
val fieldExample =
LongNameClass {
paramA,
paramB,
paramC ->
ClassB(paramA, paramB, paramC)
}
""".trimIndent()
parameterListWrappingRuleAssertThat(code)
.withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official)
.hasLintViolationsForAdditionalRule(
LintViolation(3, 1, "Unexpected indentation (20) (should be 12)"),
LintViolation(4, 1, "Unexpected indentation (20) (should be 12)"),
).isFormattedAs(formattedCode)
}

@ParameterizedTest(name = "Code style = {0}")
@EnumSource(
value = CodeStyleValue::class,
mode = EnumSource.Mode.EXCLUDE,
names = ["ktlint_official"],
)
fun `Given another than ktlint_official code style then do not reformat`(codeStyleValue: CodeStyleValue) {
parameterListWrappingRuleAssertThat(code)
.withEditorConfigOverride(CODE_STYLE_PROPERTY to codeStyleValue)
.hasNoLintViolations()
// .hasLintViolationsForAdditionalRule(
// LintViolation(3, 1, "Unexpected indentation (20) (should be 12)"),
// LintViolation(4, 1, "Unexpected indentation (20) (should be 12)"),
// )
}
}

@Test
Expand Down

0 comments on commit 7c7ecb3

Please sign in to comment.