[kotlin] K2: consider properties in context conversion intentions

KTIJ-33555
KTIJ-33556
KTIJ-34531

GitOrigin-RevId: 8ca65b88632b7ea3258fc7bb331fe8d55d4282fe
This commit is contained in:
Pavel Kirpichenkov
2025-06-11 17:32:38 +00:00
committed by intellij-monorepo-bot
parent 10d6fe208f
commit bef84b9b2e
9 changed files with 104 additions and 20 deletions
@@ -9,6 +9,8 @@ import org.jetbrains.kotlin.idea.k2.codeinsight.intentions.contexts.ContextParam
import org.jetbrains.kotlin.idea.k2.codeinsight.intentions.contexts.ContextParameterUtils.isConvertibleContextParameter
import org.jetbrains.kotlin.idea.k2.codeinsight.intentions.contexts.ContextParameterUtils.runChangeSignatureForParameter
import org.jetbrains.kotlin.idea.k2.refactoring.changeSignature.KotlinChangeInfo
import org.jetbrains.kotlin.psi.KtContextReceiverList
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtParameter
class ConvertContextParameterToRegularParameterIntention : SelfTargetingIntention<KtParameter>(
@@ -18,6 +20,7 @@ class ConvertContextParameterToRegularParameterIntention : SelfTargetingIntentio
override fun isApplicableTo(element: KtParameter, caretOffset: Int): Boolean {
return isConvertibleContextParameter(element)
&& (element.parent as? KtContextReceiverList)?.ownerDeclaration is KtNamedFunction
}
override fun applyTo(element: KtParameter, editor: Editor?) {
@@ -11,10 +11,12 @@ import org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.idea.base.projectStructure.languageVersionSettings
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.idea.codeinsight.api.classic.intentions.SelfTargetingIntention
import org.jetbrains.kotlin.idea.k2.codeinsight.intentions.contexts.ContextParameterUtils.getContextParameters
import org.jetbrains.kotlin.idea.k2.refactoring.changeSignature.KotlinChangeInfo
import org.jetbrains.kotlin.idea.k2.refactoring.changeSignature.KotlinChangeSignatureProcessor
import org.jetbrains.kotlin.idea.k2.refactoring.changeSignature.KotlinMethodDescriptor
import org.jetbrains.kotlin.idea.refactoring.rename.KotlinMemberInplaceRenameHandler
import org.jetbrains.kotlin.psi.KtCallableDeclaration
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtTypeReference
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
@@ -27,19 +29,20 @@ class ConvertReceiverParameterToContextParameterIntention : SelfTargetingIntenti
override fun startInWriteAction(): Boolean = false
override fun isApplicableTo(element: KtTypeReference, caretOffset: Int): Boolean {
return element.languageVersionSettings.supportsFeature(LanguageFeature.ContextParameters)
if (!element.languageVersionSettings.supportsFeature(LanguageFeature.ContextParameters)) return false
return (element.parent as? KtNamedFunction)?.receiverTypeReference == element // disabled for properties, TODO KTIJ-34531
}
override fun applyTo(element: KtTypeReference, editor: Editor?) {
val ktFunction = element.getStrictParentOfType<KtNamedFunction>() ?: return
val methodDescriptor = KotlinMethodDescriptor(ktFunction)
val ktCallable = element.getStrictParentOfType<KtCallableDeclaration>() ?: return
val methodDescriptor = KotlinMethodDescriptor(ktCallable)
val changeInfo = KotlinChangeInfo(methodDescriptor)
if (!configureChangeInfo(changeInfo)) return
object : KotlinChangeSignatureProcessor(element.project, changeInfo) {
override fun performRefactoring(usages: Array<out UsageInfo?>) {
super.performRefactoring(usages)
DumbService.getInstance(element.project).smartInvokeLater {
renameLastContextParameter(ktFunction, editor)
renameLastContextParameter(ktCallable, editor)
}
}
}.also {
@@ -54,11 +57,11 @@ class ConvertReceiverParameterToContextParameterIntention : SelfTargetingIntenti
return true
}
private fun renameLastContextParameter(ktFunction: KtNamedFunction, editor: Editor?) {
if (!ktFunction.isValid || editor == null || editor.isDisposed) return
val lastContextParameter = ktFunction.contextReceiverList?.contextParameters()?.lastOrNull() ?: return
private fun renameLastContextParameter(ktCallable: KtCallableDeclaration, editor: Editor?) {
if (!ktCallable.isValid || editor == null || editor.isDisposed) return
val lastContextParameter = ktCallable.getContextParameters()?.lastOrNull() ?: return
editor.caretModel.moveToOffset(lastContextParameter.startOffset)
PsiDocumentManager.getInstance(ktFunction.project).doPostponedOperationsAndUnblockDocument(editor.document)
PsiDocumentManager.getInstance(ktCallable.project).doPostponedOperationsAndUnblockDocument(editor.document)
KotlinMemberInplaceRenameHandler().doRename(lastContextParameter, editor, null)
}
}
@@ -14193,6 +14193,11 @@ public abstract class K2IntentionTestGenerated extends AbstractK2IntentionTest {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
@TestMetadata("contextProperty.kt")
public void testContextProperty() throws Exception {
runTest("../../../idea/tests/testData/intentions/contextParameters/contextParameterToReceiver/contextProperty.kt");
}
@TestMetadata("contextUsage1.kt")
public void testContextUsage1() throws Exception {
runTest("../../../idea/tests/testData/intentions/contextParameters/contextParameterToReceiver/contextUsage1.kt");
@@ -14386,6 +14391,11 @@ public abstract class K2IntentionTestGenerated extends AbstractK2IntentionTest {
runTest("../../../idea/tests/testData/intentions/contextParameters/receiverToContextParameter/contextMemberExtensionFunCallInBody.kt");
}
@TestMetadata("contextValPropertyWithReceiver.kt")
public void testContextValPropertyWithReceiver() throws Exception {
runTest("../../../idea/tests/testData/intentions/contextParameters/receiverToContextParameter/contextValPropertyWithReceiver.kt");
}
@TestMetadata("explicitExtensionArgument.kt")
public void testExplicitExtensionArgument() throws Exception {
runTest("../../../idea/tests/testData/intentions/contextParameters/receiverToContextParameter/explicitExtensionArgument.kt");
@@ -14426,6 +14436,16 @@ public abstract class K2IntentionTestGenerated extends AbstractK2IntentionTest {
runTest("../../../idea/tests/testData/intentions/contextParameters/receiverToContextParameter/memberFunCallInBody.kt");
}
@TestMetadata("valPropertyWithReceiver.kt")
public void testValPropertyWithReceiver() throws Exception {
runTest("../../../idea/tests/testData/intentions/contextParameters/receiverToContextParameter/valPropertyWithReceiver.kt");
}
@TestMetadata("varPropertyWithReceiver.kt")
public void testVarPropertyWithReceiver() throws Exception {
runTest("../../../idea/tests/testData/intentions/contextParameters/receiverToContextParameter/varPropertyWithReceiver.kt");
}
@TestMetadata("withAnotherContextBefore.kt")
public void testWithAnotherContextBefore() throws Exception {
runTest("../../../idea/tests/testData/intentions/contextParameters/receiverToContextParameter/withAnotherContextBefore.kt");
@@ -10,13 +10,8 @@ import org.jetbrains.kotlin.idea.k2.refactoring.changeSignature.KotlinMethodDesc
import org.jetbrains.kotlin.idea.k2.refactoring.changeSignature.KotlinParameterInfo
import org.jetbrains.kotlin.idea.refactoring.isAbstract
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtContextReceiverList
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtParameter
import org.jetbrains.kotlin.psi.KtParameterList
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.utils.addToStdlib.ifFalse
object ContextParameterUtils {
/**
@@ -26,8 +21,7 @@ object ContextParameterUtils {
if (!ktParameter.languageVersionSettings.supportsFeature(LanguageFeature.ContextParameters)) return false
val contextParameterList = ktParameter.parent as? KtContextReceiverList ?: return false
val contextParameterListOwner = contextParameterList.ownerDeclaration
// Change Signature support for context properties is required KTIJ-34042
if (contextParameterListOwner !is KtNamedFunction) return false
if (contextParameterListOwner !is KtCallableDeclaration) return false
if (contextParameterListOwner.isOpenAbstractOrOverride()) return false
return true
@@ -36,7 +30,7 @@ object ContextParameterUtils {
fun isValueParameterConvertibleToContext(ktParameter: KtParameter): Boolean {
if (!ktParameter.languageVersionSettings.supportsFeature(LanguageFeature.ContextParameters)) return false
val valueParameterList = ktParameter.parent as? KtParameterList ?: return false
val owner = valueParameterList.ownerFunction as? KtNamedFunction ?: return false
val owner = valueParameterList.ownerFunction as? KtCallableDeclaration ?: return false
if (owner.isOpenAbstractOrOverride()) return false
return true
@@ -47,10 +41,10 @@ object ContextParameterUtils {
* The Change Signature refactoring runs with this info if [configureChangeInfo] returns `true`.
*/
fun runChangeSignatureForParameter(element: KtParameter, configureChangeInfo: (KotlinChangeInfo) -> Boolean) {
val ktFunction = element.getStrictParentOfType<KtNamedFunction>() ?: return
val methodDescriptor = KotlinMethodDescriptor(ktFunction)
val ktCallable = element.getStrictParentOfType<KtCallableDeclaration>() ?: return
val methodDescriptor = KotlinMethodDescriptor(ktCallable)
val changeInfo = KotlinChangeInfo(methodDescriptor)
configureChangeInfo(changeInfo).ifFalse { return }
if (!configureChangeInfo(changeInfo)) return
KotlinChangeSignatureProcessor(element.project, changeInfo).also {
it.prepareSuccessfulSwingThreadCallback = Runnable { }
}.run()
@@ -72,6 +66,20 @@ object ContextParameterUtils {
!it.isContextParameter && it.oldName == ktParameter.name
}
/**
* Utility for getting context parameters from a callable declaration.
* Returns the list of context parameters if the declaration is a function or a property with context parameters, and null otherwise.
*
* The utility mitigates the awkward declaration of context parameters in the Kotlin PSI hierarchy.
*/
fun KtCallableDeclaration.getContextParameters(): List<KtParameter>? {
return when (this) {
is KtNamedFunction -> contextReceiverList?.contextParameters()
is KtProperty -> contextReceiverList?.contextParameters()
else -> null
}
}
// to avoid overrides and overridable declarations KTIJ-34463
private fun KtDeclaration.isOpenAbstractOrOverride(): Boolean =
isAbstract() || hasModifier(KtTokens.OPEN_KEYWORD) || hasModifier(KtTokens.OVERRIDE_KEYWORD)
@@ -0,0 +1,17 @@
// COMPILER_ARGUMENTS: -Xcontext-parameters
context(<caret>c1: String)
var prop: Int
get() = c1.length
set(value) {
println(c1)
foo()
c1.bar()
}
context(c: String)
fun foo() {
}
fun String.bar() {
}
@@ -0,0 +1,16 @@
// COMPILER_ARGUMENTS: -Xcontext-parameters
var String.prop: Int
get() = length
set(value) {
println(this)
foo()
bar()
}
context(c: String)
fun foo() {
}
fun String.bar() {
}
@@ -0,0 +1,6 @@
// COMPILER_ARGUMENTS: -Xcontext-parameters
// IS_APPLICABLE: false
context(i: Int)
val <caret>String.foo: Int
get() = i + length
@@ -0,0 +1,5 @@
// COMPILER_ARGUMENTS: -Xcontext-parameters
// IS_APPLICABLE: false
val <caret>String.foo: Int
get() = length
@@ -0,0 +1,6 @@
// COMPILER_ARGUMENTS: -Xcontext-parameters
// IS_APPLICABLE: false
var <caret>String.foo: Int
get() = length
set(value) { println(this) }