Skip to content

Commit ef02081

Browse files
committed
Implement autocorrect on KtFile.modifiedText
We use string/text replacement instead of PSI tree or ASTNode manipulation to avoid runtime exceptions in the K2 analyzer API due to modified PSI trees.
1 parent 580ea0c commit ef02081

20 files changed

Lines changed: 232 additions & 242 deletions

src/main/kotlin/com/faire/detekt/rules/AlwaysUseIsTrueOrIsFalse.kt

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
package com.faire.detekt.rules
22

3+
import com.faire.detekt.utils.AutoCorrectRule
34
import com.faire.detekt.utils.isAssertThat
45
import dev.detekt.api.Config
56
import dev.detekt.api.Entity
67
import dev.detekt.api.Finding
7-
import dev.detekt.api.Rule
88
import org.jetbrains.kotlin.psi.KtCallExpression
99
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
10-
import org.jetbrains.kotlin.psi.KtPsiFactory
11-
import org.jetbrains.kotlin.psi.psiUtil.astReplace
1210
import org.jetbrains.kotlin.psi.psiUtil.referenceExpression
1311

1412
/**
@@ -35,7 +33,8 @@ import org.jetbrains.kotlin.psi.psiUtil.referenceExpression
3533
* ```
3634
*/
3735
internal class AlwaysUseIsTrueOrIsFalse(config: Config = Config.empty) :
38-
Rule(config, "Do not use isEqualTo(true) or isEqualTo(false), use isTrue() or isFalse()") {
36+
AutoCorrectRule(config, "Do not use isEqualTo(true) or isEqualTo(false), use isTrue() or isFalse()") {
37+
3938
override fun visitDotQualifiedExpression(expression: KtDotQualifiedExpression) {
4039
super.visitDotQualifiedExpression(expression)
4140

@@ -57,13 +56,8 @@ internal class AlwaysUseIsTrueOrIsFalse(config: Config = Config.empty) :
5756
)
5857

5958
if (autoCorrect) {
60-
if (isEqualToExpression.isComparingToTrue()) {
61-
val isTrueExpression = KtPsiFactory(isEqualToExpression.project).createExpression("isTrue()")
62-
isEqualToExpression.astReplace(isTrueExpression)
63-
} else {
64-
val isFalseExpression = KtPsiFactory(isEqualToExpression.project).createExpression("isFalse()")
65-
isEqualToExpression.astReplace(isFalseExpression)
66-
}
59+
val replacement = if (isEqualToExpression.isComparingToTrue()) "isTrue()" else "isFalse()"
60+
pending.add(isEqualToExpression.text to replacement)
6761
}
6862
}
6963
}

src/main/kotlin/com/faire/detekt/rules/DoNotAssertIsEqualOnTheResultOfSingle.kt

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
package com.faire.detekt.rules
22

3+
import com.faire.detekt.utils.AutoCorrectRule
34
import com.faire.detekt.utils.isAssertThat
45
import dev.detekt.api.Config
56
import dev.detekt.api.Entity
67
import dev.detekt.api.Finding
7-
import dev.detekt.api.Rule
88
import org.jetbrains.kotlin.psi.KtCallExpression
99
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
1010
import org.jetbrains.kotlin.psi.KtExpression
11-
import org.jetbrains.kotlin.psi.KtPsiFactory
12-
import org.jetbrains.kotlin.psi.psiUtil.astReplace
1311
import org.jetbrains.kotlin.psi.psiUtil.referenceExpression
1412

1513
/**
@@ -27,7 +25,8 @@ import org.jetbrains.kotlin.psi.psiUtil.referenceExpression
2725
* ```
2826
*/
2927
internal class DoNotAssertIsEqualOnTheResultOfSingle(config: Config = Config.empty) :
30-
Rule(config, "use containsOnly() instead of asserting isEqual() on the result of single()") {
28+
AutoCorrectRule(config, "use containsOnly() instead of asserting isEqual() on the result of single()") {
29+
3130
override fun visitDotQualifiedExpression(expression: KtDotQualifiedExpression) {
3231
super.visitDotQualifiedExpression(expression)
3332

@@ -50,13 +49,11 @@ internal class DoNotAssertIsEqualOnTheResultOfSingle(config: Config = Config.emp
5049
)
5150

5251
if (autoCorrect) {
53-
argumentForAssertThatExpression.lastChild.delete() // Delete "single()"
54-
argumentForAssertThatExpression.lastChild.delete() // Delete "."
52+
// assertThat(x.single()).isEqualTo(y) -> assertThat(x).containsOnly(y)
53+
val collectionExpr = (argumentForAssertThatExpression as KtDotQualifiedExpression).receiverExpression.text
54+
val isEqualArgs = (isEqualExpression as? KtCallExpression)?.valueArgumentList?.text
5555

56-
val argumentsForIsEqualExpression = (isEqualExpression as? KtCallExpression)?.valueArgumentList?.text
57-
isEqualExpression.astReplace(
58-
KtPsiFactory(isEqualExpression).createExpression("containsOnly$argumentsForIsEqualExpression"),
59-
)
56+
pending.add(expression.text to "assertThat($collectionExpr).containsOnly$isEqualArgs")
6057
}
6158
}
6259

src/main/kotlin/com/faire/detekt/rules/DoNotUseHasSizeForEmptyListInAssert.kt

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
package com.faire.detekt.rules
22

3+
import com.faire.detekt.utils.AutoCorrectRule
34
import com.faire.detekt.utils.isAssertThat
45
import dev.detekt.api.Config
56
import dev.detekt.api.Entity
67
import dev.detekt.api.Finding
7-
import dev.detekt.api.Rule
88
import org.jetbrains.kotlin.psi.KtCallExpression
99
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
10-
import org.jetbrains.kotlin.psi.KtPsiFactory
11-
import org.jetbrains.kotlin.psi.psiUtil.astReplace
1210
import org.jetbrains.kotlin.resolve.calls.util.getCalleeExpressionIfAny
1311
import org.jetbrains.kotlin.resolve.calls.util.getValueArgumentsInParentheses
1412

@@ -34,7 +32,8 @@ import org.jetbrains.kotlin.resolve.calls.util.getValueArgumentsInParentheses
3432
* ```
3533
*/
3634
internal class DoNotUseHasSizeForEmptyListInAssert(config: Config = Config.empty) :
37-
Rule(config, "Do not call hasSize(0) on an empty collection, call isEmpty().") {
35+
AutoCorrectRule(config, "Do not call hasSize(0) on an empty collection, call isEmpty().") {
36+
3837
override fun visitDotQualifiedExpression(expression: KtDotQualifiedExpression) {
3938
super.visitDotQualifiedExpression(expression)
4039

@@ -55,8 +54,7 @@ internal class DoNotUseHasSizeForEmptyListInAssert(config: Config = Config.empty
5554
)
5655

5756
if (autoCorrect) {
58-
val isEmptyExpression = KtPsiFactory(hasSizeExpression).createExpression("isEmpty()")
59-
hasSizeExpression.astReplace(isEmptyExpression)
57+
pending.add(hasSizeExpression.text to "isEmpty()")
6058
}
6159
}
6260
}

src/main/kotlin/com/faire/detekt/rules/DoNotUseIsEqualToWhenArgumentIsOne.kt

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
package com.faire.detekt.rules
22

3+
import com.faire.detekt.utils.AutoCorrectRule
34
import com.faire.detekt.utils.isAssertThat
45
import com.faire.detekt.utils.usesSizeProperty
56
import dev.detekt.api.Config
67
import dev.detekt.api.Entity
78
import dev.detekt.api.Finding
8-
import dev.detekt.api.Rule
99
import org.jetbrains.kotlin.psi.KtCallExpression
1010
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
11-
import org.jetbrains.kotlin.psi.KtPsiFactory
12-
import org.jetbrains.kotlin.psi.psiUtil.astReplace
1311
import org.jetbrains.kotlin.psi.psiUtil.referenceExpression
1412

1513
private val ONE_TEXTS = setOf("1.0", "1", "1L", "1.0f", "1f", "1.0F", "0x1", "0b1")
@@ -29,7 +27,8 @@ private val ONE_TEXTS = setOf("1.0", "1", "1L", "1.0f", "1f", "1.0F", "0x1", "0b
2927
* ```
3028
*/
3129
internal class DoNotUseIsEqualToWhenArgumentIsOne(config: Config = Config.empty) :
32-
Rule(config, "Do not use isEqualTo(1), use isOne() instead.") {
30+
AutoCorrectRule(config, "Do not use isEqualTo(1), use isOne() instead.") {
31+
3332
override fun visitDotQualifiedExpression(expression: KtDotQualifiedExpression) {
3433
super.visitDotQualifiedExpression(expression)
3534

@@ -54,8 +53,7 @@ internal class DoNotUseIsEqualToWhenArgumentIsOne(config: Config = Config.empty)
5453
)
5554

5655
if (autoCorrect) {
57-
val isOneExpression = KtPsiFactory(isEqualToExpression).createExpression("isOne()")
58-
isEqualToExpression.astReplace(isOneExpression)
56+
pending.add(isEqualToExpression.text to "isOne()")
5957
}
6058
}
6159
}

src/main/kotlin/com/faire/detekt/rules/DoNotUseIsEqualToWhenArgumentIsZero.kt

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
package com.faire.detekt.rules
22

3+
import com.faire.detekt.utils.AutoCorrectRule
34
import com.faire.detekt.utils.isAssertThat
45
import com.faire.detekt.utils.usesSizeProperty
56
import dev.detekt.api.Config
67
import dev.detekt.api.Entity
78
import dev.detekt.api.Finding
8-
import dev.detekt.api.Rule
99
import org.jetbrains.kotlin.psi.KtCallExpression
1010
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
11-
import org.jetbrains.kotlin.psi.KtPsiFactory
12-
import org.jetbrains.kotlin.psi.psiUtil.astReplace
1311
import org.jetbrains.kotlin.psi.psiUtil.referenceExpression
1412

1513
private val ZERO_TEXTS = setOf("0.0", "0", "0L", "0.0f", "0f", "0.0F", "0x0", "0b0")
@@ -29,7 +27,8 @@ private val ZERO_TEXTS = setOf("0.0", "0", "0L", "0.0f", "0f", "0.0F", "0x0", "0
2927
* ```
3028
*/
3129
internal class DoNotUseIsEqualToWhenArgumentIsZero(config: Config = Config.empty) :
32-
Rule(config, "Do not use isEqualTo(0), use isZero() instead.") {
30+
AutoCorrectRule(config, "Do not use isEqualTo(0), use isZero() instead.") {
31+
3332
override fun visitDotQualifiedExpression(expression: KtDotQualifiedExpression) {
3433
super.visitDotQualifiedExpression(expression)
3534

@@ -54,8 +53,7 @@ internal class DoNotUseIsEqualToWhenArgumentIsZero(config: Config = Config.empty
5453
)
5554

5655
if (autoCorrect) {
57-
val isZeroExpression = KtPsiFactory(isEqualToExpression).createExpression("isZero()")
58-
isEqualToExpression.astReplace(isZeroExpression)
56+
pending.add(isEqualToExpression.text to "isZero()")
5957
}
6058
}
6159
}

src/main/kotlin/com/faire/detekt/rules/DoNotUsePropertyAccessInAssert.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
package com.faire.detekt.rules
22

3+
import com.faire.detekt.utils.AutoCorrectRule
34
import com.faire.detekt.utils.isAssertThat
45
import dev.detekt.api.Config
56
import dev.detekt.api.Entity
67
import dev.detekt.api.Finding
7-
import dev.detekt.api.Rule
88
import org.jetbrains.kotlin.psi.KtCallExpression
99
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
10-
import org.jetbrains.kotlin.psi.KtPsiFactory
11-
import org.jetbrains.kotlin.psi.psiUtil.astReplace
1210

1311
/**
1412
* Do not use property access syntax with assertion methods.
@@ -24,7 +22,11 @@ import org.jetbrains.kotlin.psi.psiUtil.astReplace
2422
*/
2523

2624
internal class DoNotUsePropertyAccessInAssert(config: Config = Config.empty) :
27-
Rule(config, "Do not use property access syntax with assertion methods. Do not remove the parenthesis.") {
25+
AutoCorrectRule(
26+
config,
27+
"Do not use property access syntax with assertion methods. Do not remove the parenthesis.",
28+
) {
29+
2830
override fun visitDotQualifiedExpression(expression: KtDotQualifiedExpression) {
2931
super.visitDotQualifiedExpression(expression)
3032

@@ -41,9 +43,7 @@ internal class DoNotUsePropertyAccessInAssert(config: Config = Config.empty) :
4143
)
4244

4345
if (autoCorrect) {
44-
val withParenthesisExpression = KtPsiFactory(selectorExpression)
45-
.createExpression("${selectorExpression.text}()")
46-
selectorExpression.astReplace(withParenthesisExpression)
46+
pending.add(selectorExpression.text to "${selectorExpression.text}()")
4747
}
4848
}
4949
}

src/main/kotlin/com/faire/detekt/rules/DoNotUseSingleOnFilter.kt

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package com.faire.detekt.rules
22

3-
import com.faire.detekt.utils.simplifyCollectionPatterns
3+
import com.faire.detekt.utils.AutoCorrectRule
44
import dev.detekt.api.Config
55
import dev.detekt.api.Entity
66
import dev.detekt.api.Finding
7-
import dev.detekt.api.Rule
7+
import org.jetbrains.kotlin.psi.KtCallExpression
88
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
99
import org.jetbrains.kotlin.psi.KtLambdaArgument
1010
import org.jetbrains.kotlin.psi.KtValueArgumentList
@@ -25,7 +25,8 @@ private val FILTER_REGEX = ".*\\.*filter\\s*(\\{|\\().+".toRegex()
2525
* This augments the `UnnecessaryFilter` detekt rule which does not cover `.single` as of 1.21.0.
2626
*/
2727
internal class DoNotUseSingleOnFilter(config: Config = Config.empty) :
28-
Rule(config, "Do not use single() with filter { ... }, use single { ... } instead") {
28+
AutoCorrectRule(config, "Do not use single() with filter { ... }, use single { ... } instead") {
29+
2930
override fun visitDotQualifiedExpression(expression: KtDotQualifiedExpression) {
3031
super.visitDotQualifiedExpression(expression)
3132

@@ -48,13 +49,21 @@ internal class DoNotUseSingleOnFilter(config: Config = Config.empty) :
4849
)
4950

5051
if (autoCorrect) {
51-
removeCallToSingle(expression)
52-
receiverExpression.simplifyCollectionPatterns("single")
52+
val receiverDotExpr = receiverExpression as? KtDotQualifiedExpression ?: return
53+
val filterCall = receiverDotExpr.selectorExpression as? KtCallExpression ?: return
54+
val filterCallText = filterCall.text
55+
val newCallText = filterCallText.replaceFirst("filter", "single")
56+
val baseReceiverText = receiverDotExpr.receiverExpression.text
57+
// Extract whitespace/dot between base receiver and filter call from the expression text
58+
val betweenBaseAndFilter = receiverDotExpr.text.removePrefix(baseReceiverText).removeSuffix(filterCallText)
59+
60+
pending.add(expression.text to "$baseReceiverText$betweenBaseAndFilter$newCallText")
5361
}
5462
}
63+
}
5564

56-
private fun removeCallToSingle(expression: KtDotQualifiedExpression) {
57-
expression.lastChild.delete()
58-
expression.lastChild.delete()
65+
private fun buildArgumentsText(call: KtCallExpression): String = if (call.lambdaArguments.isNotEmpty()) {
66+
" ${call.lambdaArguments.joinToString { it.text }}"
67+
} else {
68+
"(${call.valueArguments.joinToString { it.text }})"
5969
}
60-
}

src/main/kotlin/com/faire/detekt/rules/FilterNotNullOverMapNotNullForFiltering.kt

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package com.faire.detekt.rules
22

3-
import com.intellij.psi.impl.source.tree.LeafPsiElement
3+
import com.faire.detekt.utils.AutoCorrectRule
44
import dev.detekt.api.Config
55
import dev.detekt.api.Entity
66
import dev.detekt.api.Finding
7-
import dev.detekt.api.Rule
8-
import org.jetbrains.kotlin.lexer.KtTokens
97
import org.jetbrains.kotlin.psi.KtCallExpression
108
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
119
import org.jetbrains.kotlin.resolve.calls.util.getCalleeExpressionIfAny
@@ -24,7 +22,8 @@ import org.jetbrains.kotlin.resolve.calls.util.getCalleeExpressionIfAny
2422
* ```
2523
*/
2624
internal class FilterNotNullOverMapNotNullForFiltering(config: Config = Config.empty) :
27-
Rule(config, "Use filterNotNull() instead of mapNotNull { it }") {
25+
AutoCorrectRule(config, "Use filterNotNull() instead of mapNotNull { it }") {
26+
2827
override fun visitCallExpression(expression: KtCallExpression) {
2928
super.visitCallExpression(expression)
3029

@@ -43,22 +42,8 @@ internal class FilterNotNullOverMapNotNullForFiltering(config: Config = Config.e
4342
)
4443

4544
if (autoCorrect) {
46-
// Rename "mapNotNull" to "filterNotNull"
47-
val calleeLeaf = callee.node.findChildByType(KtTokens.IDENTIFIER)
48-
(calleeLeaf?.psi as? LeafPsiElement)?.rawReplaceWithText("filterNotNull")
49-
50-
// Remove lambda argument and any preceding whitespace
51-
val expressionNode = expression.node
52-
val lambdaArgNode = expression.lambdaArguments.first().node
53-
val blankNode = lambdaArgNode.treePrev?.takeIf { it.text.isBlank() }
54-
if (blankNode != null) {
55-
expressionNode.removeChild(blankNode)
56-
}
57-
expressionNode.removeChild(lambdaArgNode)
58-
59-
// Add empty parentheses
60-
expressionNode.addChild(LeafPsiElement(KtTokens.LPAR, "("), null)
61-
expressionNode.addChild(LeafPsiElement(KtTokens.RPAR, ")"), null)
45+
// mapNotNull { it } -> filterNotNull()
46+
pending.add(expression.text to "filterNotNull()")
6247
}
6348
}
6449
}

src/main/kotlin/com/faire/detekt/rules/NoEmptyLinesInConstructorParameters.kt

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
package com.faire.detekt.rules
22

3+
import com.faire.detekt.utils.AutoCorrectRule
34
import com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl
45
import dev.detekt.api.Config
56
import dev.detekt.api.Entity
67
import dev.detekt.api.Finding
7-
import dev.detekt.api.Rule
88
import org.jetbrains.kotlin.psi.KtParameterList
99
import org.jetbrains.kotlin.psi.KtPrimaryConstructor
1010
import org.jetbrains.kotlin.psi.KtSecondaryConstructor
1111
import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType
1212

13+
private val BLANK_LINE_REGEX = Regex("""\n([ \t]*\n)+""")
14+
1315
/**
1416
* Detects and removes empty lines within constructor parameter lists.
1517
*
@@ -53,7 +55,8 @@ import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType
5355
* ```
5456
*/
5557
internal class NoEmptyLinesInConstructorParameters(config: Config = Config.empty) :
56-
Rule(config, description = "Constructor parameter lists should not contain empty lines") {
58+
AutoCorrectRule(config, description = "Constructor parameter lists should not contain empty lines") {
59+
5760
override fun visitPrimaryConstructor(constructor: KtPrimaryConstructor) {
5861
super.visitPrimaryConstructor(constructor)
5962
checkConstructorParameters(constructor.valueParameterList)
@@ -84,17 +87,11 @@ internal class NoEmptyLinesInConstructorParameters(config: Config = Config.empty
8487
Finding(entity = Entity.from(parameterList), message = description),
8588
)
8689

87-
// Auto-correct by removing blank lines
8890
if (autoCorrect) {
89-
for (whitespaceNode in nodesWithBlankLines) {
90-
// Replace multiple consecutive newlines with a single newline
91-
// Preserve the indentation from the last line
92-
val lines = whitespaceNode.text.split("\n")
93-
val lastLine = lines.lastOrNull() ?: ""
94-
val correctedWhitespace = "\n$lastLine"
95-
96-
whitespaceNode.rawReplaceWithText(correctedWhitespace)
97-
}
98-
}
91+
val originalText = parameterList.text
92+
// Collapse blank lines: replace sequences of newlines (with optional whitespace) with a single newline
93+
val fixedText = BLANK_LINE_REGEX.replace(originalText, "\n")
94+
pending.add(originalText to fixedText)
95+
}
9996
}
10097
}

0 commit comments

Comments
 (0)