Refactoring: make "loop to call chain" AbstractKotlinInspection

This commit is contained in:
Mikhail Glukhikh
2017-12-21 12:51:45 +03:00
parent f44fba746e
commit 3f1a3dfeb2
9 changed files with 147 additions and 129 deletions
@@ -1,3 +0,0 @@
fun foo(list: List<String>): String? {
<spot>return list.firstOrNull { it.length > 0 }</spot>
}
@@ -1,8 +0,0 @@
fun foo(list: List<String>): String? {
<spot>for (s in list) {
if (s.length > 0) {
return s
}
}
return null</spot>
}
@@ -1,5 +0,0 @@
<html>
<body>
This intention converts a for-loop into a sequence of stdlib-operations (like "map", "filter" etc)
</body>
</html>
@@ -1,7 +0,0 @@
fun foo(list: List<String>) {
val sum = <spot>list
.asSequence()
.map { it.calcSomething() }
.filter { it > 0 }
.sumBy { it }</spot>
}
@@ -1,9 +0,0 @@
fun foo(list: List<String>) {
val sum = 0
<spot>for (s in list) {
val x = s.calcSomething()
if (x > 0) {
sum += x
}
}</spot>
}
@@ -1,5 +0,0 @@
<html>
<body>
This intention converts a for-loop into a sequence of stdlib-operations (like "map", "filter" etc) with lazy evaluation (using 'asSequence()' method)
</body>
</html>
-10
View File
@@ -1032,16 +1032,6 @@
<category>Kotlin</category>
</intentionAction>
<intentionAction>
<className>org.jetbrains.kotlin.idea.intentions.loopToCallChain.LoopToCallChainIntention</className>
<category>Kotlin</category>
</intentionAction>
<intentionAction>
<className>org.jetbrains.kotlin.idea.intentions.loopToCallChain.LoopToLazyCallChainIntention</className>
<category>Kotlin</category>
</intentionAction>
<intentionAction>
<className>org.jetbrains.kotlin.idea.intentions.loopToCallChain.UseWithIndexIntention</className>
<category>Kotlin</category>
@@ -0,0 +1,147 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.intentions.loopToCallChain
import com.intellij.codeInspection.*
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import org.jetbrains.kotlin.idea.core.moveCaret
import org.jetbrains.kotlin.idea.inspections.AbstractKotlinInspection
import org.jetbrains.kotlin.idea.inspections.findExistingEditor
import org.jetbrains.kotlin.idea.intentions.SelfTargetingRangeIntention
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence.AsSequenceTransformation
import org.jetbrains.kotlin.psi.KtForExpression
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.KtVisitorVoid
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
import org.jetbrains.kotlin.psi.psiUtil.startOffset
class LoopToCallChainInspection : AbstractKotlinInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession) =
object : KtVisitorVoid() {
val nonLazyIntention = LoopToCallChainIntention()
val lazyIntention = LoopToLazyCallChainIntention()
override fun visitForExpression(expression: KtForExpression) {
super.visitForExpression(expression)
val nonLazyApplicable = nonLazyIntention.applicabilityRange(expression) != null
val lazyApplicable = lazyIntention.applicabilityRange(expression) != null
if (!nonLazyApplicable && !lazyApplicable) return
val fixes = mutableListOf<Fix>()
if (nonLazyApplicable) {
fixes += Fix(lazy = false, text = nonLazyIntention.text)
}
if (lazyApplicable) {
fixes += Fix(lazy = true, text = lazyIntention.text)
}
holder.registerProblem(
expression.forKeyword,
"Loop can be replaced with stdlib operations",
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
*fixes.toTypedArray()
)
}
}
class Fix(val lazy: Boolean, val text: String = "") : LocalQuickFix {
override fun getFamilyName(): String {
return if (lazy) {
"Replace with stdlib operations with use of 'asSequence()'"
}
else {
"Replace with stdlib operations"
}
}
override fun getName() = text
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val expression = descriptor.psiElement.getParentOfType<KtForExpression>(strict = true) ?: return
applyFix(expression)
}
fun applyFix(expression: KtForExpression, editor: Editor? = expression.findExistingEditor()) {
val match = match(expression, lazy, true) ?: return
val result = convertLoop(expression, match)
val offset = when (result) {
// if result is variable declaration, put the caret onto its name to allow quick inline
is KtProperty -> result.nameIdentifier?.startOffset ?: result.startOffset
else -> result.startOffset
}
editor?.moveCaret(offset)
}
}
}
class LoopToCallChainIntention : AbstractLoopToCallChainIntention(
lazy = false,
text = "Replace with stdlib operations"
)
class LoopToLazyCallChainIntention : AbstractLoopToCallChainIntention(
lazy = true,
text = "Replace with stdlib operations with use of 'asSequence()'"
)
abstract class AbstractLoopToCallChainIntention(
private val lazy: Boolean,
text: String
) : SelfTargetingRangeIntention<KtForExpression>(
KtForExpression::class.java,
text
) {
override fun applicabilityRange(element: KtForExpression): TextRange? {
val match = match(element, lazy, false)
text = if (match != null) "Replace with '${match.transformationMatch.buildPresentation()}'" else defaultText
return if (match != null) element.forKeyword.textRange else null
}
private fun TransformationMatch.Result.buildPresentation(): String {
return buildPresentation(sequenceTransformations + resultTransformation, null)
}
private fun buildPresentation(transformations: List<Transformation>, prevPresentation: String?): String {
if (transformations.size > MAX) {
if (transformations[0] is AsSequenceTransformation) {
return buildPresentation(transformations.drop(1), transformations[0].presentation)
}
return buildPresentation(transformations.drop(transformations.size - MAX), prevPresentation?.let { it + ".." } ?: "..")
}
var result: String? = prevPresentation
for (transformation in transformations) {
result = transformation.buildPresentation(result)
}
return result!!
}
override fun applyTo(element: KtForExpression, editor: Editor?) {
LoopToCallChainInspection.Fix(lazy).applyFix(element, editor)
}
companion object {
const val MAX = 3
}
}
@@ -1,82 +0,0 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.intentions.loopToCallChain
import com.intellij.codeInsight.intention.LowPriorityAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.util.TextRange
import org.jetbrains.kotlin.idea.core.moveCaret
import org.jetbrains.kotlin.idea.inspections.IntentionBasedInspection
import org.jetbrains.kotlin.idea.intentions.SelfTargetingRangeIntention
import org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence.AsSequenceTransformation
import org.jetbrains.kotlin.psi.KtForExpression
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.psiUtil.startOffset
class LoopToCallChainInspection : IntentionBasedInspection<KtForExpression>(
listOf(IntentionData(LoopToCallChainIntention::class), IntentionData(LoopToLazyCallChainIntention::class)),
problemText = "Loop can be replaced with stdlib operations"
)
class LoopToCallChainIntention : AbstractLoopToCallChainIntention(lazy = false, text = "Replace with stdlib operations")
class LoopToLazyCallChainIntention : AbstractLoopToCallChainIntention(lazy = true, text = "Replace with stdlib operations with use of 'asSequence()'"), LowPriorityAction
abstract class AbstractLoopToCallChainIntention(private val lazy: Boolean, text: String) : SelfTargetingRangeIntention<KtForExpression>(
KtForExpression::class.java,
text
) {
override fun applicabilityRange(element: KtForExpression): TextRange? {
val match = match(element, lazy, false)
text = if (match != null) "Replace with '${match.transformationMatch.buildPresentation()}'" else defaultText
return if (match != null) element.forKeyword.textRange else null
}
private fun TransformationMatch.Result.buildPresentation(): String {
return buildPresentation(sequenceTransformations + resultTransformation, null)
}
private fun buildPresentation(transformations: List<Transformation>, prevPresentation: String?): String {
val MAX = 3
if (transformations.size > MAX) {
if (transformations[0] is AsSequenceTransformation) {
return buildPresentation(transformations.drop(1), transformations[0].presentation)
}
return buildPresentation(transformations.drop(transformations.size - MAX), prevPresentation?.let { it + ".." } ?: "..")
}
var result: String? = prevPresentation
for (transformation in transformations) {
result = transformation.buildPresentation(result)
}
return result!!
}
override fun applyTo(element: KtForExpression, editor: Editor?) {
val match = match(element, lazy, true)!!
val result = convertLoop(element, match)
val offset = when (result) {
// if result is variable declaration, put the caret onto its name to allow quick inline
is KtProperty -> result.nameIdentifier?.startOffset ?: result.startOffset
else -> result.startOffset
}
editor?.moveCaret(offset)
}
}