diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/commonUtils.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/commonUtils.kt index 77737f40b11..4be8a3e0f41 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/commonUtils.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/commonUtils.kt @@ -20,7 +20,9 @@ import com.intellij.psi.PsiWhiteSpace import com.intellij.psi.search.LocalSearchScope import com.intellij.psi.search.searches.ReferencesSearch import org.jetbrains.kotlin.KtNodeTypes +import org.jetbrains.kotlin.descriptors.FunctionDescriptor import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.imports.importableFqName import org.jetbrains.kotlin.idea.references.KtSimpleNameReference import org.jetbrains.kotlin.idea.references.mainReference import org.jetbrains.kotlin.idea.references.readWriteAccess @@ -32,6 +34,7 @@ import org.jetbrains.kotlin.psi.psiUtil.getQualifiedExpressionForSelector import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.psi.psiUtil.siblings import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall import org.jetbrains.kotlin.resolve.constants.evaluate.ConstantExpressionEvaluator import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance @@ -169,3 +172,19 @@ fun PsiChildRange.withoutLastStatement(): PsiChildRange { val newLast = last!!.siblings(forward = false, withItself = false).first { it !is PsiWhiteSpace } return PsiChildRange(first, newLast) } + +fun KtExpression?.extractStaticFunctionCallArguments(functionFqName: String): List? { + val callExpression = when (this) { + is KtDotQualifiedExpression -> selectorExpression as? KtCallExpression + is KtCallExpression -> this + else -> null + } ?: return null + + val bindingContext = callExpression.analyze(BodyResolveMode.PARTIAL) + val resolvedCall = callExpression.getResolvedCall(bindingContext) ?: return null + val functionDescriptor = resolvedCall.resultingDescriptor as? FunctionDescriptor ?: return null + if (functionDescriptor.dispatchReceiverParameter != null || functionDescriptor.extensionReceiverParameter != null) return null + if (functionDescriptor.importableFqName?.asString() != functionFqName) return null + + return resolvedCall.valueArgumentsByIndex?.map { it?.arguments?.singleOrNull()?.getArgumentExpression() } +} diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/MaxOrMinTransformation.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/MaxOrMinTransformation.kt index 50a9e4ef942..10b20165b48 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/MaxOrMinTransformation.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/loopToCallChain/result/MaxOrMinTransformation.kt @@ -17,13 +17,14 @@ package org.jetbrains.kotlin.idea.intentions.loopToCallChain.result import org.jetbrains.kotlin.idea.intentions.loopToCallChain.* +import org.jetbrains.kotlin.idea.intentions.loopToCallChain.sequence.MapTransformation import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.blockExpressionsOrSingle +import org.jetbrains.kotlin.utils.singletonOrEmptyList class MaxOrMinTransformation( loop: KtForExpression, - private val inputVariable: KtCallableDeclaration, initialization: VariableInitialization, private val isMax: Boolean ) : AssignToVariableResultTransformation(loop, initialization) { @@ -58,10 +59,12 @@ class MaxOrMinTransformation( */ object Matcher : TransformationMatcher { override val indexVariableAllowed: Boolean - get() = false + get() = true override fun match(state: MatchingState): TransformationMatch.Result? { - return matchIfAssign(state) ?: matchAssignIf(state) + return matchIfAssign(state) + ?: matchAssignIf(state) + ?: matchMathMaxOrMin(state) } private fun matchIfAssign(state: MatchingState): TransformationMatch.Result? { @@ -84,6 +87,46 @@ class MaxOrMinTransformation( return match(ifExpression.condition, assignment.left, ifExpression.then, ifExpression.`else`, state.inputVariable, state.outerLoop) } + private fun matchMathMaxOrMin(state: MatchingState): TransformationMatch.Result? { + val assignment = state.statements.singleOrNull() as? KtBinaryExpression ?: return null + if (assignment.operationToken != KtTokens.EQ) return null + val assignmentTarget = assignment.left ?: return null + + val variableInitialization = assignmentTarget.isVariableInitializedBeforeLoop(state.outerLoop, checkNoOtherUsagesInLoop = false) + ?: return null + + return matchMathMaxOrMin(variableInitialization, assignment, state, isMax = true) + ?: matchMathMaxOrMin(variableInitialization, assignment, state, isMax = false) + } + + private fun matchMathMaxOrMin( + variableInitialization: VariableInitialization, + assignment: KtBinaryExpression, + state: MatchingState, + isMax: Boolean + ): TransformationMatch.Result? { + val functionName = if (isMax) "max" else "min" + val arguments = assignment.right.extractStaticFunctionCallArguments("java.lang.Math." + functionName) ?: return null + if (arguments.size != 2) return null + val value = if (arguments[0].isVariableReference(variableInitialization.variable)) { + arguments[1] ?: return null + } + else if (arguments[1].isVariableReference(variableInitialization.variable)) { + arguments[0] ?: return null + } + else { + return null + } + + val mapTransformation = if (value.isVariableReference(state.inputVariable)) + null + else + MapTransformation(state.outerLoop, state.inputVariable, state.indexVariable, value, mapNotNull = false) + + val transformation = MaxOrMinTransformation(state.outerLoop, variableInitialization, isMax) + return TransformationMatch.Result(transformation, mapTransformation.singletonOrEmptyList()) + } + private fun match( condition: KtExpression?, assignmentTarget: KtExpression?, @@ -124,7 +167,7 @@ class MaxOrMinTransformation( if (valueToBeVariable != null && !valueToBeVariable.isVariableReference(variableInitialization.variable)) return null val isMax = (comparison == KtTokens.GT || comparison == KtTokens.GTEQ) xor (otherHand == left) - val transformation = MaxOrMinTransformation(loop, inputVariable, variableInitialization, isMax) + val transformation = MaxOrMinTransformation(loop, variableInitialization, isMax) return TransformationMatch.Result(transformation) } } diff --git a/idea/testData/intentions/loopToCallChain/max4.kt b/idea/testData/intentions/loopToCallChain/max4.kt new file mode 100644 index 00000000000..22cf6eaf743 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/max4.kt @@ -0,0 +1,10 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'max()'" +// IS_APPLICABLE_2: false +fun getMaxLineWidth(list: List): Float { + var max = 0.0f + for (item in list) { + max = Math.max(item, max) + } + return max +} diff --git a/idea/testData/intentions/loopToCallChain/max4.kt.after b/idea/testData/intentions/loopToCallChain/max4.kt.after new file mode 100644 index 00000000000..92f46598674 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/max4.kt.after @@ -0,0 +1,8 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'max()'" +// IS_APPLICABLE_2: false +fun getMaxLineWidth(list: List): Float { + val max = list.max() + ?: 0.0f + return max +} diff --git a/idea/testData/intentions/loopToCallChain/max5.kt b/idea/testData/intentions/loopToCallChain/max5.kt new file mode 100644 index 00000000000..60dabc98816 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/max5.kt @@ -0,0 +1,12 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'map{}.max()'" +// INTENTION_TEXT_2: "Replace with 'asSequence().map{}.max()'" +fun getMaxLineWidth(count: Int): Double { + var max = 0.0 + for (i in 0..count-1) { + max = Math.max(max, getLineWidth(i)) + } + return max +} + +fun getLineWidth(i: Int): Double = TODO() diff --git a/idea/testData/intentions/loopToCallChain/max5.kt.after b/idea/testData/intentions/loopToCallChain/max5.kt.after new file mode 100644 index 00000000000..75d3d4da511 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/max5.kt.after @@ -0,0 +1,12 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'map{}.max()'" +// INTENTION_TEXT_2: "Replace with 'asSequence().map{}.max()'" +fun getMaxLineWidth(count: Int): Double { + val max = (0..count-1) + .map { getLineWidth(it) } + .max() + ?: 0.0 + return max +} + +fun getLineWidth(i: Int): Double = TODO() diff --git a/idea/testData/intentions/loopToCallChain/max5.kt.after2 b/idea/testData/intentions/loopToCallChain/max5.kt.after2 new file mode 100644 index 00000000000..c60e7680fe9 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/max5.kt.after2 @@ -0,0 +1,13 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'map{}.max()'" +// INTENTION_TEXT_2: "Replace with 'asSequence().map{}.max()'" +fun getMaxLineWidth(count: Int): Double { + val max = (0..count-1) + .asSequence() + .map { getLineWidth(it) } + .max() + ?: 0.0 + return max +} + +fun getLineWidth(i: Int): Double = TODO() diff --git a/idea/testData/intentions/loopToCallChain/max6.kt b/idea/testData/intentions/loopToCallChain/max6.kt new file mode 100644 index 00000000000..efb68ce2922 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/max6.kt @@ -0,0 +1,10 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'mapIndexed{}.max()'" +// INTENTION_TEXT_2: "Replace with 'asSequence().mapIndexed{}.max()'" +fun getMaxLineWidth(list: List): Double { + var max = 0.0 + for ((i, item) in list.withIndex()) { + max = Math.max(max, item * i) + } + return max +} diff --git a/idea/testData/intentions/loopToCallChain/max6.kt.after b/idea/testData/intentions/loopToCallChain/max6.kt.after new file mode 100644 index 00000000000..1e536219e18 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/max6.kt.after @@ -0,0 +1,10 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'mapIndexed{}.max()'" +// INTENTION_TEXT_2: "Replace with 'asSequence().mapIndexed{}.max()'" +fun getMaxLineWidth(list: List): Double { + val max = list + .mapIndexed { i, item -> item * i } + .max() + ?: 0.0 + return max +} diff --git a/idea/testData/intentions/loopToCallChain/max6.kt.after2 b/idea/testData/intentions/loopToCallChain/max6.kt.after2 new file mode 100644 index 00000000000..f96775edf36 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/max6.kt.after2 @@ -0,0 +1,11 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'mapIndexed{}.max()'" +// INTENTION_TEXT_2: "Replace with 'asSequence().mapIndexed{}.max()'" +fun getMaxLineWidth(list: List): Double { + val max = list + .asSequence() + .mapIndexed { i, item -> item * i } + .max() + ?: 0.0 + return max +} diff --git a/idea/testData/intentions/loopToCallChain/max7.kt b/idea/testData/intentions/loopToCallChain/max7.kt new file mode 100644 index 00000000000..753af0e7062 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/max7.kt @@ -0,0 +1,14 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'map{}.max()'" +// INTENTION_TEXT_2: "Replace with 'asSequence().map{}.max()'" +import java.lang.Math.max + +fun getMaxLineWidth(count: Int): Double { + var m = 0.0 + for (i in 0..count-1) { + m = max(m, getLineWidth(i)) + } + return m +} + +fun getLineWidth(i: Int): Double = TODO() diff --git a/idea/testData/intentions/loopToCallChain/max7.kt.after b/idea/testData/intentions/loopToCallChain/max7.kt.after new file mode 100644 index 00000000000..0e8d03769e8 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/max7.kt.after @@ -0,0 +1,14 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'map{}.max()'" +// INTENTION_TEXT_2: "Replace with 'asSequence().map{}.max()'" +import java.lang.Math.max + +fun getMaxLineWidth(count: Int): Double { + val m = (0..count-1) + .map { getLineWidth(it) } + .max() + ?: 0.0 + return m +} + +fun getLineWidth(i: Int): Double = TODO() diff --git a/idea/testData/intentions/loopToCallChain/max7.kt.after2 b/idea/testData/intentions/loopToCallChain/max7.kt.after2 new file mode 100644 index 00000000000..f593ce9d763 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/max7.kt.after2 @@ -0,0 +1,15 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'map{}.max()'" +// INTENTION_TEXT_2: "Replace with 'asSequence().map{}.max()'" +import java.lang.Math.max + +fun getMaxLineWidth(count: Int): Double { + val m = (0..count-1) + .asSequence() + .map { getLineWidth(it) } + .max() + ?: 0.0 + return m +} + +fun getLineWidth(i: Int): Double = TODO() diff --git a/idea/testData/intentions/loopToCallChain/min5.kt b/idea/testData/intentions/loopToCallChain/min5.kt new file mode 100644 index 00000000000..7f32fec1106 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/min5.kt @@ -0,0 +1,12 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'map{}.min()'" +// INTENTION_TEXT_2: "Replace with 'asSequence().map{}.min()'" +fun getMinLineWidth(lineCount: Int): Double { + var min_width = Double.MAX_VALUE + for (i in 0..lineCount - 1) { + min_width = Math.min(getLineWidth(i), min_width) + } + return min_width +} + +fun getLineWidth(i: Int): Double = TODO() diff --git a/idea/testData/intentions/loopToCallChain/min5.kt.after b/idea/testData/intentions/loopToCallChain/min5.kt.after new file mode 100644 index 00000000000..de805321864 --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/min5.kt.after @@ -0,0 +1,12 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'map{}.min()'" +// INTENTION_TEXT_2: "Replace with 'asSequence().map{}.min()'" +fun getMinLineWidth(lineCount: Int): Double { + val min_width = (0..lineCount - 1) + .map { getLineWidth(it) } + .min() + ?: Double.MAX_VALUE + return min_width +} + +fun getLineWidth(i: Int): Double = TODO() diff --git a/idea/testData/intentions/loopToCallChain/min5.kt.after2 b/idea/testData/intentions/loopToCallChain/min5.kt.after2 new file mode 100644 index 00000000000..2d1336352cb --- /dev/null +++ b/idea/testData/intentions/loopToCallChain/min5.kt.after2 @@ -0,0 +1,13 @@ +// WITH_RUNTIME +// INTENTION_TEXT: "Replace with 'map{}.min()'" +// INTENTION_TEXT_2: "Replace with 'asSequence().map{}.min()'" +fun getMinLineWidth(lineCount: Int): Double { + val min_width = (0..lineCount - 1) + .asSequence() + .map { getLineWidth(it) } + .min() + ?: Double.MAX_VALUE + return min_width +} + +fun getLineWidth(i: Int): Double = TODO() diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTest2Generated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTest2Generated.java index 38a97468b92..d7e315c4828 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTest2Generated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTest2Generated.java @@ -965,6 +965,30 @@ public class IntentionTest2Generated extends AbstractIntentionTest2 { doTest(fileName); } + @TestMetadata("max4.kt") + public void testMax4() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/max4.kt"); + doTest(fileName); + } + + @TestMetadata("max5.kt") + public void testMax5() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/max5.kt"); + doTest(fileName); + } + + @TestMetadata("max6.kt") + public void testMax6() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/max6.kt"); + doTest(fileName); + } + + @TestMetadata("max7.kt") + public void testMax7() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/max7.kt"); + doTest(fileName); + } + @TestMetadata("min1.kt") public void testMin1() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/min1.kt"); @@ -989,6 +1013,12 @@ public class IntentionTest2Generated extends AbstractIntentionTest2 { doTest(fileName); } + @TestMetadata("min5.kt") + public void testMin5() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/min5.kt"); + doTest(fileName); + } + @TestMetadata("moveInitializationsCloserToLoop.kt") public void testMoveInitializationsCloserToLoop() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/moveInitializationsCloserToLoop.kt"); diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java index 7527474e458..f903a2161b5 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java @@ -8125,6 +8125,30 @@ public class IntentionTestGenerated extends AbstractIntentionTest { doTest(fileName); } + @TestMetadata("max4.kt") + public void testMax4() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/max4.kt"); + doTest(fileName); + } + + @TestMetadata("max5.kt") + public void testMax5() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/max5.kt"); + doTest(fileName); + } + + @TestMetadata("max6.kt") + public void testMax6() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/max6.kt"); + doTest(fileName); + } + + @TestMetadata("max7.kt") + public void testMax7() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/max7.kt"); + doTest(fileName); + } + @TestMetadata("min1.kt") public void testMin1() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/min1.kt"); @@ -8149,6 +8173,12 @@ public class IntentionTestGenerated extends AbstractIntentionTest { doTest(fileName); } + @TestMetadata("min5.kt") + public void testMin5() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/min5.kt"); + doTest(fileName); + } + @TestMetadata("moveInitializationsCloserToLoop.kt") public void testMoveInitializationsCloserToLoop() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/loopToCallChain/moveInitializationsCloserToLoop.kt");