diff --git a/plugins/kotlin/code-insight/inspections-k2/resources/kotlin.code-insight.inspections.k2.xml b/plugins/kotlin/code-insight/inspections-k2/resources/kotlin.code-insight.inspections.k2.xml index b104b796625c..67c0d6a3a3fe 100644 --- a/plugins/kotlin/code-insight/inspections-k2/resources/kotlin.code-insight.inspections.k2.xml +++ b/plugins/kotlin/code-insight/inspections-k2/resources/kotlin.code-insight.inspections.k2.xml @@ -996,5 +996,13 @@ language="kotlin" key="inspection.copy.without.named.arguments.display.name" bundle="messages.KotlinBundle"/> + + diff --git a/plugins/kotlin/code-insight/inspections-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/inspections/ReplaceToWithInfixFormInspection.kt b/plugins/kotlin/code-insight/inspections-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/inspections/ReplaceToWithInfixFormInspection.kt new file mode 100644 index 000000000000..d599e81851ab --- /dev/null +++ b/plugins/kotlin/code-insight/inspections-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/inspections/ReplaceToWithInfixFormInspection.kt @@ -0,0 +1,76 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.kotlin.idea.k2.codeinsight.inspections + +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.codeInspection.util.InspectionMessage +import com.intellij.codeInspection.util.IntentionFamilyName +import com.intellij.modcommand.ModPsiUpdater +import com.intellij.openapi.project.Project +import org.jetbrains.kotlin.analysis.api.KaSession +import org.jetbrains.kotlin.analysis.api.resolution.successfulFunctionCallOrNull +import org.jetbrains.kotlin.analysis.api.resolution.symbol +import org.jetbrains.kotlin.analysis.api.symbols.KaNamedFunctionSymbol +import org.jetbrains.kotlin.idea.base.resources.KotlinBundle +import org.jetbrains.kotlin.idea.codeinsight.api.applicable.asUnit +import org.jetbrains.kotlin.idea.codeinsight.api.applicable.inspections.KotlinApplicableInspectionBase +import org.jetbrains.kotlin.idea.codeinsight.api.applicable.inspections.KotlinModCommandQuickFix +import org.jetbrains.kotlin.idea.codeinsight.utils.callExpression +import org.jetbrains.kotlin.psi.* + +private const val TO_FUNCTION_NAME = "to" + +internal class ReplaceToWithInfixFormInspection : KotlinApplicableInspectionBase.Simple() { + override fun buildVisitor( + holder: ProblemsHolder, + isOnTheFly: Boolean, + ): KtVisitor<*, *> = dotQualifiedExpressionVisitor { + visitTargetElement(it, holder, isOnTheFly) + } + + override fun getProblemDescription( + element: KtDotQualifiedExpression, + context: Unit, + ): @InspectionMessage String = KotlinBundle.message("inspection.replace.to.with.infix.form.display.name") + + override fun isApplicableByPsi(element: KtDotQualifiedExpression): Boolean { + val callExpression = element.callExpression ?: return false + return callExpression.valueArguments.size == 1 && + callExpression.typeArgumentList == null && + element.calleeName == TO_FUNCTION_NAME + } + + override fun KaSession.prepareContext(element: KtDotQualifiedExpression): Unit? { + val call = element.resolveToCall()?.successfulFunctionCallOrNull() ?: return null + val functionSymbol = call.symbol as? KaNamedFunctionSymbol ?: return null + return functionSymbol.isInfix.asUnit + } + + override fun createQuickFix( + element: KtDotQualifiedExpression, + context: Unit, + ): KotlinModCommandQuickFix = ReplaceToWithInfixFormQuickFix() +} + +private class ReplaceToWithInfixFormQuickFix : KotlinModCommandQuickFix() { + override fun getFamilyName(): @IntentionFamilyName String = + KotlinBundle.message("replace.to.with.infix.form.quickfix.text") + + override fun applyFix( + project: Project, + element: KtDotQualifiedExpression, + updater: ModPsiUpdater, + ) { + val argumentExpression = element.callExpression?.valueArguments?.first()?.getArgumentExpression() ?: return + val newExpression = createInfixExpression(project, element.receiverExpression, argumentExpression) + element.replace(newExpression) + } + + private fun createInfixExpression( + project: Project, + receiver: KtExpression, + argument: KtExpression, + ): KtExpression = KtPsiFactory(project).createExpressionByPattern("$0 to $1", receiver, argument) +} + +private val KtQualifiedExpression.calleeName: String? + get() = (callExpression?.calleeExpression as? KtNameReferenceExpression)?.text diff --git a/plugins/kotlin/code-insight/inspections-k2/tests/test/org/jetbrains/kotlin/idea/k2/inspections/tests/K2LocalInspectionTestGenerated.java b/plugins/kotlin/code-insight/inspections-k2/tests/test/org/jetbrains/kotlin/idea/k2/inspections/tests/K2LocalInspectionTestGenerated.java index fa015016f194..b363170c73a6 100644 --- a/plugins/kotlin/code-insight/inspections-k2/tests/test/org/jetbrains/kotlin/idea/k2/inspections/tests/K2LocalInspectionTestGenerated.java +++ b/plugins/kotlin/code-insight/inspections-k2/tests/test/org/jetbrains/kotlin/idea/k2/inspections/tests/K2LocalInspectionTestGenerated.java @@ -13833,6 +13833,35 @@ public abstract class K2LocalInspectionTestGenerated extends AbstractK2LocalInsp } } + @RunWith(JUnit3RunnerWithInners.class) + @TestMetadata("../../../idea/tests/testData/inspectionsLocal/replaceToWithInfixForm") + public static class ReplaceToWithInfixForm extends AbstractK2LocalInspectionTest { + @java.lang.Override + @org.jetbrains.annotations.NotNull + public final KotlinPluginMode getPluginMode() { + return KotlinPluginMode.K2; + } + + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, this, testDataFilePath); + } + + @TestMetadata("base.kt") + public void testBase() throws Exception { + runTest("../../../idea/tests/testData/inspectionsLocal/replaceToWithInfixForm/base.kt"); + } + + @TestMetadata("nonPair.kt") + public void testNonPair() throws Exception { + runTest("../../../idea/tests/testData/inspectionsLocal/replaceToWithInfixForm/nonPair.kt"); + } + + @TestMetadata("typeArguments.kt") + public void testTypeArguments() throws Exception { + runTest("../../../idea/tests/testData/inspectionsLocal/replaceToWithInfixForm/typeArguments.kt"); + } + } + @RunWith(JUnit3RunnerWithInners.class) @TestMetadata("../../../idea/tests/testData/inspectionsLocal/addOperatorModifier") public static class AddOperatorModifier extends AbstractK2LocalInspectionTest { diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceToWithInfixForm/.k2Inspection b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceToWithInfixForm/.k2Inspection new file mode 100644 index 000000000000..3690bdcf24e8 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/replaceToWithInfixForm/.k2Inspection @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.k2.codeinsight.inspections.ReplaceToWithInfixFormInspection \ No newline at end of file diff --git a/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/codeinsight/GenerateK2InspectionTests.kt b/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/codeinsight/GenerateK2InspectionTests.kt index c7fa67a6d623..da87b47e65e6 100644 --- a/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/codeinsight/GenerateK2InspectionTests.kt +++ b/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/codeinsight/GenerateK2InspectionTests.kt @@ -90,6 +90,7 @@ internal fun MutableTWorkspace.generateK2InspectionTests() { model("${idea}/inspectionsLocal/canBeVal") model("${idea}/inspectionsLocal/replaceSubstring") model("${idea}/inspectionsLocal/replaceWithIgnoreCaseEquals") + model("${idea}/inspectionsLocal/replaceToWithInfixForm") model("${idea}/inspectionsLocal/addOperatorModifier") model("${idea}/inspectionsLocal/kotlinUnreachableCode") model("${idea}/inspectionsLocal/removeRedundantLabel")