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 new experimental rule chain-method-continuation #2088

Merged
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
64fd90b
Add ChainMethodContinuation(chain-method-continuation) rule
atulgpt Jun 24, 2023
bb12ba6
Run the rule in Ktlint repo and update the code
atulgpt Jun 24, 2023
9c0c55c
Update CHANGELOG.md and experimental.md
atulgpt Jun 24, 2023
e287079
Handle comments in between chains
atulgpt Jul 12, 2023
78e4abf
Simplify the code to remove the need to structure changes
atulgpt Jul 25, 2023
baef5ba
Apply ktlint
atulgpt Aug 1, 2023
41c1721
Wrap `map` to separate line
atulgpt Aug 2, 2023
4e0a47b
Run the rule in Ktlint repo and update the code
atulgpt Jun 24, 2023
a27c520
Fix several bugs and functional changes
paul-dingemans Aug 4, 2023
90fa4e6
Revert changes on documentation of currently released version
paul-dingemans Aug 4, 2023
8329e84
Mitigate unwanted side effects found during format of ktlint code
paul-dingemans Aug 8, 2023
2448209
Fix lint violations
paul-dingemans Aug 10, 2023
c92dc9e
Move section to alphabetical position of rule
paul-dingemans Aug 10, 2023
95d4cb5
Rename rule
paul-dingemans Aug 10, 2023
db3f969
Add configuration property for the number of chain operators which fo…
paul-dingemans Aug 11, 2023
e8efc6c
Fix lint violations
paul-dingemans Aug 11, 2023
1d4d3a0
Update public API
paul-dingemans Aug 12, 2023
78b0c7e
Merge remote-tracking branch 'origin/master' into pr/2088_with_master
paul-dingemans Aug 13, 2023
74660d8
Fix problems after merge
paul-dingemans Aug 13, 2023
87eb55f
Fix array access expression
paul-dingemans Aug 15, 2023
f15c0d8
Fix lint violations
paul-dingemans Aug 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).

* Add experimental rule `class-signature`. This rule rewrites the class header to a consistent format. In code style `ktlint_official`, super types are always wrapped to a separate line. In other code styles, super types are only wrapped in classes having multiple super types. Especially for code style `ktlint_official` the class headers are rewritten in a more consistent format. See [examples in documentation](https://pinterest.github.io/ktlint/latest/rules/experimental/#class-signature). `class-signature` [#875](https://github.com/pinterest/ktlint/issues/1349), [#1349](https://github.com/pinterest/ktlint/issues/875)
* Add experimental rule `function-expression-body`. This rule rewrites function bodies only contain a `return` or `throw` expression to an expression body. [#2150](https://github.com/pinterest/ktlint/issues/2150)
* Add experimental rule `chain-method-continuation` to the `ktlint_official` code style, but it can be enabled explicitly for the other code styles as well. This rule requires the operators (`.` or `?.`) for chaining method calls, to be aligned with each other. This rule is enabled by ([#1953](https://github.com/pinterest/ktlint/issues/1953))

### Removed
* As a part of public API stabilization, data classes are no longer used in the public API. As of that, functions like `copy()` or `componentN()` (used for destructuring declarations) are not available anymore. This is a binary incompatible change, breaking backwards compatibility. ([#2133](https://github.com/pinterest/ktlint/issues/2133))
Expand Down
9 changes: 8 additions & 1 deletion build-logic/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ repositories {
}

kotlin {
jvmToolchain(libs.versions.java.compilation.get().toInt())
jvmToolchain(
libs
.versions
.java
.compilation
.get()
.toInt(),
)
}

// TODO: Remove setting `options.release` and `compilerOptions.jvmTarget` after upgrade to Kotlin Gradle Plugin 1.9
Expand Down
7 changes: 6 additions & 1 deletion build-logic/src/main/kotlin/ktlint-kotlin-common.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ tasks.withType<Test>().configureEach {
(Runtime.getRuntime().availableProcessors() / 2).takeIf { it > 0 } ?: 1
}

if (javaLauncher.get().metadata.languageVersion.canCompileOrRun(JavaLanguageVersion.of(11))) {
if (javaLauncher
.get()
.metadata
.languageVersion
.canCompileOrRun(JavaLanguageVersion.of(11))
) {
// workaround for https://github.com/pinterest/ktlint/issues/1618. Java 11 started printing warning logs. Java 16 throws an error
jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED")
}
Expand Down
16 changes: 13 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ plugins {
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.checksum) apply false
alias(libs.plugins.shadow) apply false
alias(libs.plugins.kotlinx.binary.compatibiltiy.validator)
alias(
libs
.plugins
.kotlinx
.binary
.compatibiltiy
.validator,
)
}

val internalNonPublishableProjects by extra(
Expand Down Expand Up @@ -57,6 +64,9 @@ tasks.register<JavaExec>("ktlintFormat") {

tasks.wrapper {
distributionSha256Sum =
URI.create("$distributionUrl.sha256").toURL()
.openStream().use { it.reader().readText().trim() }
URI
.create("$distributionUrl.sha256")
.toURL()
.openStream()
.use { it.reader().readText().trim() }
}
51 changes: 51 additions & 0 deletions documentation/release-latest/docs/rules/experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -930,3 +930,54 @@ A function, class/object body or other block body statement has to be placed on
```

Rule id: `statement-wrapping`

### Chain method continuation

In multi line method chain, this rule requires a chain operators(`.` or `?.`) to be aligned with the next method or in case when previous ending brace is `}` then it should also be aligned with chain operator method call.

=== "[:material-heart:](#) Ktlint"

```kotlin
val foo1 = listOf(1, 2, 3).
filter { it > 2 }!!.
takeIf { it > 2 }.
map {
it * it
}?.
map {
it * it
}
val foo2 = listOf(1, 2, 3)
.filter {
it > 2
}
.map {
2 * it
}
?.map {
2 * it
}
```

=== "[:material-heart-off-outline:](#) Disallowed"

```kotlin
val foo1 = listOf(1, 2, 3)
.filter { it > 2 }!!
.takeIf { it > 2 }
.map {
it * it
}?.map {
it * it
}
val foo2 = listOf(1, 2, 3)
.filter {
it > 2
}.map {
2 * it
}?.map {
2 * it
}
```

Rule id: `chain-method-continuation` (`standard` rule set)
14 changes: 14 additions & 0 deletions documentation/snapshot/docs/rules/configuration-ktlint.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ insert_final_newline = true

This setting only takes effect when rule `final-newline` is enabled.

## Force multiline chained methods based on number of chain operators

Setting `ktlint_chain_method_rule_force_multiline_when_chain_operator_count_greater_or_equal_than` forces a chained method to be wrapped at each chain operator (`.` or `?.`) in case it contains the specified minimum number of chain operators even in case the entire chained method fits on a single line. Use value `unset` (default) to disable this setting.

!!! note
By default, chained methods are wrapped when an expression contains 4 or more chain operators in an expression. Note that if a chained method contains nested expressions the chain operators of the inner expression are not taken into account.

```ini
[*.{kt,kts}]
ktlint_chain_method_rule_force_multiline_when_chain_operator_count_greater_or_equal_than=unset
```

This setting only takes effect when rule `chain-method-continution` is enabled.

## Force multiline function signature based on number of parameters

Setting `ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than` forces a multiline function signature in case the function contains the specified minimum number of parameters even in case the function signature would fit on a single line. Use value `unset` (default) to disable this setting.
Expand Down
83 changes: 83 additions & 0 deletions documentation/snapshot/docs/rules/experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,89 @@ Requires a blank line before any class or function declaration. No blank line is

Rule id: `blank-line-before-declaration` (`standard` rule set)

!!! Note
This rule is only run when `ktlint_code_style` is set to `ktlint_official` or when the rule is enabled explicitly.

### Chain method continuation

In a multiline method chain, the chain operators (`.` or `?.`) have to be aligned with each other.

Multiple chained methods on a single line are allowed as long as the maximum line length, and the maximum number of chain operators are not exceeded. Under certain conditions, it is allowed that the expression before the first and/or the expression after the last chain operator is a multiline expression.

The `.` in `java.class` is ignored when wrapping on chain operators.

This rule can be configured with `.editorconfig` property [`ktlint_chain_method_rule_force_multiline_when_chain_operator_count_greater_or_equal_than`](../configuration-ktlint/#force-multiline-chained-methods-based-on-number-of-chain-operators).

!!! warning
Binary expression for which the left and/or right operand consist of method chain are currently being ignored by this rule. Please reach out, if you can help to determine what the best strategy is to deal with such kind of expressions.

=== "[:material-heart:](#) Ktlint"

```kotlin
val foo1 =
listOf(1, 2, 3)
.filter { it > 2 }!!
.takeIf { it > 2 }
.map {
it * it
}?.map {
it * it
}
val foo2 =
listOf(1, 2, 3)
.filter {
it > 2
}.map {
2 * it
}?.map {
2 * it
}
val foo3 = foo().bar().map {
it.foobar()
}
val foo4 =
"""
Some text
""".trimIndent().foo().bar()
```

=== "[:material-heart-off-outline:](#) Disallowed"

```kotlin
val foo1 =
listOf(1, 2, 3).
filter { it > 2 }!!.
takeIf { it > 2 }.
map {
it * it
}?.
map {
it * it
}
val foo2 =
listOf(1, 2, 3)
.filter {
it > 2
}
.map {
2 * it
}
?.map {
2 * it
}
val foo3 = foo()
.bar().map {
it.foobar()
}
val foo4 =
"""
Some text
""".trimIndent().foo()
.bar()
```

Rule id: `chain-method-continuation` (`standard` rule set)

!!! Note
This rule is only run when `ktlint_code_style` is set to `ktlint_official` or when the rule is enabled explicitly.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,8 +293,7 @@ class KtLintRuleEngineTest {
.properties(
EXPERIMENTAL_RULES_EXECUTION_PROPERTY.toPropertyBuilderWithValue("enabled"),
),
)
.build(),
).build(),
),
fileSystem = ktlintTestFileSystem.fileSystem,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ private class BaselineLoader(
getAttribute("source")
.let { ruleId ->
// Ensure backwards compatibility with baseline files in which the rule set id for standard rules is not saved
RuleId.prefixWithStandardRuleSetIdWhenMissing(ruleId)
RuleId
.prefixWithStandardRuleSetIdWhenMissing(ruleId)
.also { prefixedRuleId ->
if (prefixedRuleId != ruleId) {
ruleReferenceWithoutRuleSetIdPrefix++
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ public class BaselineReporter(
}

private fun String.escapeXMLAttrValue() =
this.replace("&", "&amp;")
this
.replace("&", "&amp;")
.replace("\"", "&quot;")
.replace("'", "&apos;")
.replace("<", "&lt;")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public class CheckStyleReporter(
}

private fun String.escapeXMLAttrValue() =
this.replace("&", "&amp;").replace("\"", "&quot;").replace("'", "&apos;")
.replace("<", "&lt;").replace(">", "&gt;")
this
.replace("&", "&amp;")
.replace("\"", "&quot;")
.replace("'", "&apos;")
.replace("<", "&lt;")
.replace(">", "&gt;")
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ class CheckStyleReporterTest {
</file>
</checkstyle>

""".trimIndent()
.replace("\n", System.lineSeparator()),
""".trimIndent().replace("\n", System.lineSeparator()),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import java.util.jar.Manifest
public fun <T> ktlintVersion(javaClass: Class<T>): String? = javaClass.`package`.implementationVersion ?: getManifestVersion(javaClass)

private fun <T> getManifestVersion(javaClass: Class<T>) =
javaClass.getResourceAsStream("/META-INF/MANIFEST.MF")
?.run {
Manifest(this).mainAttributes.getValue("Implementation-Version")
}
javaClass
.getResourceAsStream("/META-INF/MANIFEST.MF")
?.run { Manifest(this).mainAttributes.getValue("Implementation-Version") }
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ public class HtmlReporter(
}

private fun String.escapeHTMLAttrValue() =
this.replace("&", "&amp;").replace("\"", "&quot;").replace("'", "&apos;")
.replace("<", "&lt;").replace(">", "&gt;")
this
.replace("&", "&amp;")
.replace("\"", "&quot;")
.replace("'", "&apos;")
.replace("<", "&lt;")
.replace(">", "&gt;")
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,7 @@ class JsonReporterTest {
}
]

""".trimIndent()
.replace("\n", System.lineSeparator()),
""".trimIndent().replace("\n", System.lineSeparator()),
)
}

Expand Down Expand Up @@ -104,8 +103,7 @@ class JsonReporterTest {
}
]

""".trimIndent()
.replace("\n", System.lineSeparator()),
""".trimIndent().replace("\n", System.lineSeparator()),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,14 @@ import kotlin.io.path.relativeToOrSelf
private const val SRCROOT = "%SRCROOT%"

internal fun String.sanitize(): String =
this.replace(File.separatorChar, '/')
this
.replace(File.separatorChar, '/')
.let {
if (it.endsWith('/')) it else "$it/"
if (it.endsWith('/')) {
it
} else {
"$it/"
}
}

public class SarifReporter(
Expand Down
10 changes: 9 additions & 1 deletion ktlint-cli/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,15 @@ tasks.withType<Test>().configureEach {

// TODO: Use providers directly after https://github.com/gradle/gradle/issues/12247 is fixed.
val executableFilePath =
providers.provider { shadowJarExecutable.get().outputs.files.first { it.name == "ktlint" }.absolutePath }.get()
providers
.provider {
shadowJarExecutable
.get()
.outputs
.files
.first { it.name == "ktlint" }
.absolutePath
}.get()
val ktlintVersion = providers.provider { version }.get()
doFirst {
systemProperty(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,16 @@ internal fun FileSystem.fileSequence(
): FileVisitResult {
val path =
if (onWindowsOS) {
Paths.get(
filePath
.absolutePathString()
.replace(File.separatorChar, '/'),
).also {
if (it != filePath) {
LOGGER.trace { "On WindowsOS transform '$filePath' to '$it'" }
Paths
.get(
filePath
.absolutePathString()
.replace(File.separatorChar, '/'),
).also {
if (it != filePath) {
LOGGER.trace { "On WindowsOS transform '$filePath' to '$it'" }
}
}
}
} else {
filePath
}
Expand Down
Loading