Add MaxChainedCallsOnSameLine style rule (#4985)

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.
This commit is contained in:
Dominic Zirbel
2022-06-28 06:28:01 -07:00
committed by GitHub
parent 29158afff7
commit 1e696fd9fc
4 changed files with 190 additions and 0 deletions

View File

@@ -582,6 +582,9 @@ style:
active: false
MandatoryBracesLoops:
active: false
MaxChainedCallsOnSameLine:
active: false
maxChainedCalls: 5
MaxLineLength:
active: true
maxLineLength: 120

View File

@@ -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')
}
}

View File

@@ -99,6 +99,7 @@ class StyleGuideProvider : DefaultRuleSetProvider {
UseOrEmpty(config),
UseAnyOrNoneInsteadOfFind(config),
UnnecessaryBackticks(config),
MaxChainedCallsOnSameLine(config),
)
)
}

View File

@@ -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)
}
}