Skip to content

Commit

Permalink
Add MaxChainedCallsOnSameLine style rule (#4985)
Browse files Browse the repository at this point in the history
Add a new rule MaxChainedCallsOnSameLine to limit the number of chained calls on placed on a single line. This works well alongside CascadingCallWrapping in #4979 to make long call chains more readable by wrapping them on new lines.
  • Loading branch information
dzirbel committed Jun 28, 2022
1 parent 29158af commit 1e696fd
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 0 deletions.
3 changes: 3 additions & 0 deletions detekt-core/src/main/resources/default-detekt-config.yml
Expand Up @@ -582,6 +582,9 @@ style:
active: false
MandatoryBracesLoops:
active: false
MaxChainedCallsOnSameLine:
active: false
maxChainedCalls: 5
MaxLineLength:
active: true
maxLineLength: 120
Expand Down
@@ -0,0 +1,79 @@
package io.gitlab.arturbosch.detekt.rules.style

import io.gitlab.arturbosch.detekt.api.CodeSmell
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Debt
import io.gitlab.arturbosch.detekt.api.Entity
import io.gitlab.arturbosch.detekt.api.Issue
import io.gitlab.arturbosch.detekt.api.Rule
import io.gitlab.arturbosch.detekt.api.Severity
import io.gitlab.arturbosch.detekt.api.config
import io.gitlab.arturbosch.detekt.api.internal.Configuration
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtQualifiedExpression
import org.jetbrains.kotlin.psi.KtUnaryExpression

/**
* Limits the number of chained calls which can be placed on a single line.
*
* <noncompliant>
* a().b().c().d().e().f()
* </noncompliant>
*
* <compliant>
* a().b().c()
* .d().e().f()
* </compliant>
*/
class MaxChainedCallsOnSameLine(config: Config = Config.empty) : Rule(config) {
override val issue = Issue(
id = javaClass.simpleName,
severity = Severity.Style,
description = "Chained calls beyond the maximum should be wrapped to a new line.",
debt = Debt.FIVE_MINS,
)

@Configuration("maximum chained calls allowed on a single line")
private val maxChainedCalls: Int by config(defaultValue = 5)

override fun visitQualifiedExpression(expression: KtQualifiedExpression) {
super.visitQualifiedExpression(expression)

// skip if the parent is also a call on the same line to avoid duplicated warnings
val parent = expression.parent
if (parent is KtQualifiedExpression && !parent.callOnNewLine()) return

val chainedCalls = expression.countChainedCalls() + 1
if (chainedCalls > maxChainedCalls) {
report(
CodeSmell(
issue = issue,
entity = Entity.from(expression),
message = "$chainedCalls chained calls on a single line; more than $maxChainedCalls calls should " +
"be wrapped to a new line."
)
)
}
}

private fun KtExpression.countChainedCalls(): Int {
return when (this) {
is KtQualifiedExpression ->
if (callOnNewLine()) 0 else receiverExpression.countChainedCalls() + 1
is KtUnaryExpression -> baseExpression?.countChainedCalls() ?: 0
else -> 0
}
}

private fun KtQualifiedExpression.callOnNewLine(): Boolean {
val receiver = receiverExpression
val selector = selectorExpression ?: return false

val receiverEnd = receiver.startOffsetInParent + receiver.textLength
val selectorStart = selector.startOffsetInParent

return text
.subSequence(startIndex = receiverEnd, endIndex = selectorStart)
.contains('\n')
}
}
Expand Up @@ -99,6 +99,7 @@ class StyleGuideProvider : DefaultRuleSetProvider {
UseOrEmpty(config),
UseAnyOrNoneInsteadOfFind(config),
UnnecessaryBackticks(config),
MaxChainedCallsOnSameLine(config),
)
)
}
@@ -0,0 +1,107 @@
package io.gitlab.arturbosch.detekt.rules.style

import io.gitlab.arturbosch.detekt.test.TestConfig
import io.gitlab.arturbosch.detekt.test.assertThat
import io.gitlab.arturbosch.detekt.test.compileAndLint
import org.junit.jupiter.api.Test

class MaxChainedCallsOnSameLineSpec {
@Test
fun `does not report 2 calls on a single line with a max of 3`() {
val rule = MaxChainedCallsOnSameLine(TestConfig(mapOf("maxChainedCalls" to 3)))
val code = "val a = 0.plus(0)"

assertThat(rule.compileAndLint(code)).isEmpty()
}

@Test
fun `does not report 3 calls on a single line with a max of 3`() {
val rule = MaxChainedCallsOnSameLine(TestConfig(mapOf("maxChainedCalls" to 3)))
val code = "val a = 0.plus(0).plus(0)"

assertThat(rule.compileAndLint(code)).isEmpty()
}

@Test
fun `reports 4 calls on a single line with a max of 3`() {
val rule = MaxChainedCallsOnSameLine(TestConfig(mapOf("maxChainedCalls" to 3)))
val code = "val a = 0.plus(0).plus(0).plus(0)"

assertThat(rule.compileAndLint(code)).hasSize(1)
}

@Test
fun `reports 4 safe qualified calls on a single line with a max of 3`() {
val rule = MaxChainedCallsOnSameLine(TestConfig(mapOf("maxChainedCalls" to 3)))
val code = "val a = 0?.plus(0)?.plus(0)?.plus(0)"

assertThat(rule.compileAndLint(code)).hasSize(1)
}

@Test
fun `reports 4 non-null asserted calls on a single line with a max of 3`() {
val rule = MaxChainedCallsOnSameLine(TestConfig(mapOf("maxChainedCalls" to 3)))
val code = "val a = 0!!.plus(0)!!.plus(0)!!.plus(0)"

assertThat(rule.compileAndLint(code)).hasSize(1)
}

@Test
fun `reports once for 7 calls on a single line with a max of 3`() {
val rule = MaxChainedCallsOnSameLine(TestConfig(mapOf("maxChainedCalls" to 3)))
val code = "val a = 0.plus(0).plus(0).plus(0).plus(0).plus(0).plus(0)"

assertThat(rule.compileAndLint(code)).hasSize(1)
}

@Test
fun `does not report 5 calls on separate lines with a max of 3`() {
val rule = MaxChainedCallsOnSameLine(TestConfig(mapOf("maxChainedCalls" to 3)))
val code = """
val a = 0
.plus(0)
.plus(0)
.plus(0)
.plus(0)
"""

assertThat(rule.compileAndLint(code)).isEmpty()
}

@Test
fun `does not report 3 calls on same line with wrapped calls with a max of 3`() {
val rule = MaxChainedCallsOnSameLine(TestConfig(mapOf("maxChainedCalls" to 3)))
val code = """
val a = 0.plus(0).plus(0)
.plus(0).plus(0).plus(0)
.plus(0).plus(0).plus(0)
"""

assertThat(rule.compileAndLint(code)).isEmpty()
}

@Test
fun `reports 4 calls on same line with wrapped calls with a max of 3`() {
val rule = MaxChainedCallsOnSameLine(TestConfig(mapOf("maxChainedCalls" to 3)))
val code = """
val a = 0.plus(0).plus(0).plus(0)
.plus(0)
.plus(0)
"""

assertThat(rule.compileAndLint(code)).hasSize(1)
}

@Test
fun `reports 4 calls on wrapped line with with a max of 3`() {
val rule = MaxChainedCallsOnSameLine(TestConfig(mapOf("maxChainedCalls" to 3)))
val code = """
val a = 0
.plus(0)
.plus(0).plus(0).plus(0).plus(0)
.plus(0)
"""

assertThat(rule.compileAndLint(code)).hasSize(1)
}
}

0 comments on commit 1e696fd

Please sign in to comment.