/
CascadingCallWrapping.kt
109 lines (95 loc) · 3.71 KB
/
CascadingCallWrapping.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtBinaryExpression
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtQualifiedExpression
import org.jetbrains.kotlin.psi.KtUnaryExpression
/**
* Requires that all chained calls are placed on a new line if a preceding one is.
*
* <noncompliant>
* foo()
* .bar().baz()
* </noncompliant>
*
* <compliant>
* foo().bar().baz()
*
* foo()
* .bar()
* .baz()
* </compliant>
*/
class CascadingCallWrapping(config: Config = Config.empty) : Rule(config) {
override val issue = Issue(
id = javaClass.simpleName,
severity = Severity.Style,
description = "If a chained call is wrapped to a new line, subsequent chained calls should be as well.",
debt = Debt.FIVE_MINS,
)
@Configuration("require trailing elvis expressions to be wrapped on a new line")
private val includeElvis: Boolean by config(true)
override fun visitQualifiedExpression(expression: KtQualifiedExpression) {
super.visitQualifiedExpression(expression)
checkExpression(expression, callExpression = expression.selectorExpression)
}
override fun visitBinaryExpression(expression: KtBinaryExpression) {
super.visitBinaryExpression(expression)
if (includeElvis && expression.operationToken == KtTokens.ELVIS) {
checkExpression(expression, callExpression = expression.right)
}
}
private fun checkExpression(expression: KtExpression, callExpression: KtExpression?) {
if (!expression.containsNewline() && expression.receiverContainsNewline()) {
val callTextOrEmpty = callExpression?.text?.let { " `$it`" }.orEmpty()
report(
CodeSmell(
issue = issue,
entity = Entity.from(expression),
message = "Chained call$callTextOrEmpty should be wrapped to a new line since preceding calls were."
)
)
}
}
@Suppress("ReturnCount")
private fun KtExpression.containsNewline(): Boolean {
val lhs: KtExpression
val rhs: KtExpression
when (this) {
is KtQualifiedExpression -> {
lhs = receiverExpression
rhs = selectorExpression ?: return false
}
is KtBinaryExpression -> {
if (operationToken != KtTokens.ELVIS) return false
lhs = left ?: return false
rhs = right ?: return false
}
else -> return false
}
val receiverEnd = lhs.startOffsetInParent + lhs.textLength
val selectorStart = rhs.startOffsetInParent
return (receiverEnd until selectorStart).any { text[it] == '\n' }
}
private fun KtExpression.receiverContainsNewline(): Boolean {
val lhs = when (this) {
is KtQualifiedExpression -> receiverExpression
is KtBinaryExpression -> left ?: return false
else -> return false
}
return when (lhs) {
is KtQualifiedExpression -> lhs.containsNewline()
is KtUnaryExpression -> (lhs.baseExpression as? KtQualifiedExpression)?.containsNewline() == true
else -> false
}
}
}