[kotlin] support context parameters in extract function refactoring

^KTIJ-34083 fixed

GitOrigin-RevId: f0d161b30b4b31ce4eda72f5e2c90dec9d622bb5
This commit is contained in:
Anna Kozlova
2025-07-14 20:12:03 +00:00
committed by intellij-monorepo-bot
parent 5795a4c736
commit b07eb7f354
13 changed files with 219 additions and 6 deletions
@@ -253,6 +253,8 @@ internal class MutableParameter(
private val originalType: KotlinType,
private val possibleTypes: Set<KotlinType>
) : Parameter, IMutableParameter<KotlinType> {
override val contextParameter: Boolean = false
// All modifications happen in the same thread
private var writable: Boolean = true
private val defaultTypes = LinkedHashSet<KotlinType>()
@@ -1610,6 +1610,35 @@ public abstract class ExtractionTestGenerated extends AbstractExtractionTest {
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("testData/refactoring/extractFunction/contextParameters")
public static class ContextParameters extends AbstractExtractionTest {
@java.lang.Override
@org.jetbrains.annotations.NotNull
public final KotlinPluginMode getPluginMode() {
return KotlinPluginMode.K1;
}
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doExtractFunctionTest, this, testDataFilePath);
}
@TestMetadata("base.kt")
public void testBase() throws Exception {
runTest("testData/refactoring/extractFunction/contextParameters/base.kt");
}
@TestMetadata("unnamed.kt")
public void testUnnamed() throws Exception {
runTest("testData/refactoring/extractFunction/contextParameters/unnamed.kt");
}
@TestMetadata("withProvidedContext.kt")
public void testWithProvidedContext() throws Exception {
runTest("testData/refactoring/extractFunction/contextParameters/withProvidedContext.kt");
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("testData/refactoring/extractFunction/controlFlow")
public abstract static class ControlFlow extends AbstractExtractionTest {
@@ -0,0 +1,15 @@
// COMPILER_ARGUMENTS: -Xcontext-parameters
context(a: String)
fun bar(p: Int) {}
context(i: Int)
val prop: Int
get() = 42
context(a: String, b: Int)
fun m() {
val i = prop
<selection>bar(i)</selection>
}
// IGNORE_K1
@@ -0,0 +1,19 @@
// COMPILER_ARGUMENTS: -Xcontext-parameters
context(a: String)
fun bar(p: Int) {}
context(i: Int)
val prop: Int
get() = 42
context(a: String, b: Int)
fun m() {
val i = prop
__dummyTestFun__(i)
}
context(a: String) private fun __dummyTestFun__(i: Int) {
bar(i)
}
// IGNORE_K1
@@ -0,0 +1,15 @@
// COMPILER_ARGUMENTS: -Xcontext-parameters
context(_: String)
fun bar(p: Int) {}
context(_: Int)
val prop: Int
get() = 42
context(a: String, b: Int)
fun m() {
val i = prop
<selection>bar(i)</selection>
}
// IGNORE_K1
@@ -0,0 +1,19 @@
// COMPILER_ARGUMENTS: -Xcontext-parameters
context(_: String)
fun bar(p: Int) {}
context(_: Int)
val prop: Int
get() = 42
context(a: String, b: Int)
fun m() {
val i = prop
__dummyTestFun__(i)
}
context(_: String) private fun __dummyTestFun__(i: Int) {
bar(i)
}
// IGNORE_K1
@@ -0,0 +1,19 @@
// COMPILER_ARGUMENTS: -Xcontext-parameters
context(a: String)
fun bar(p: Int) {}
context(i: Int)
val prop: Int
get() = 42
context(a: String, b: Int)
fun m() {
val i = prop
object: Function0<Int> {
override fun invoke(): Int {
<selection>bar(i)</selection>
return 1
}
}
}
@@ -0,0 +1,23 @@
// COMPILER_ARGUMENTS: -Xcontext-parameters
context(a: String)
fun bar(p: Int) {}
context(i: Int)
val prop: Int
get() = 42
context(a: String, b: Int)
fun m() {
val i = prop
object: Function0<Int> {
override fun invoke(): Int {
__dummyTestFun__()
return 1
}
private fun __dummyTestFun__() {
bar(i)
}
}
}
@@ -457,7 +457,7 @@ abstract class ExtractFunctionGenerator<KotlinType, ExtractionResult : IExtracti
if (generatorOptions.inTempFile) return config.createExtractionResult(declaration, Collections.emptyMap())
val replaceInitialOccurrence = {
val arguments = descriptor.parameters.map { it.argumentText }
val arguments = descriptor.parameters.filter { !it.contextParameter }.map { it.argumentText }
makeCall(descriptor, declaration, descriptor.controlFlow, descriptor.extractionData.originalRange, arguments)
}
@@ -558,6 +558,17 @@ abstract class ExtractFunctionGenerator<KotlinType, ExtractionResult : IExtracti
else -> CallableBuilder.Target.READ_ONLY_PROPERTY
}
return CallableBuilder(builderTarget).apply {
val typeDescriptor = createTypeDescriptor(descriptor.extractionData)
val contextParameters = descriptor.parameters.filter { it.contextParameter }
if (contextParameters.isNotEmpty()) {
val contextString = contextParameters.joinToString(prefix = "context(", postfix = ")") {
it.name + ": " + typeDescriptor.renderType(it.parameterType, isReceiver = false, Variance.IN_VARIANCE)
}
modifier(contextString)
}
val visibility = descriptor.visibility?.value ?: ""
fun TypeParameter.isReified() = originalDeclaration.hasModifier(KtTokens.REIFIED_KEYWORD)
@@ -600,7 +611,6 @@ abstract class ExtractFunctionGenerator<KotlinType, ExtractionResult : IExtracti
}
)
val typeDescriptor = createTypeDescriptor(descriptor.extractionData)
descriptor.receiverParameter?.let {
val receiverType = it.parameterType
val receiverTypeAsString = typeDescriptor.renderType(receiverType, isReceiver = true, Variance.IN_VARIANCE)
@@ -609,7 +619,7 @@ abstract class ExtractFunctionGenerator<KotlinType, ExtractionResult : IExtracti
name(descriptor.name)
descriptor.parameters.forEach { parameter ->
descriptor.parameters.filter { !it.contextParameter }.forEach { parameter ->
param(parameter.name, typeDescriptor.renderType(parameter.parameterType, isReceiver = false, Variance.IN_VARIANCE))
}
@@ -6,6 +6,7 @@ interface IParameter<KotlinType> {
val name: String
val mirrorVarName: String?
val receiverCandidate: Boolean
val contextParameter: Boolean
val parameterType: KotlinType
@@ -49,7 +49,8 @@ internal class MutableParameter(
override val originalDescriptor: PsiNamedElement,
override val receiverCandidate: Boolean,
private val originalType: KaType,
private val scope: KtElement
private val scope: KtElement,
override val contextParameter: Boolean
) : Parameter, IMutableParameter<KaType> {
private val typePredicates = mutableSetOf<TypePredicate>()
@@ -21,6 +21,7 @@ import org.jetbrains.kotlin.analysis.api.resolution.symbol
import org.jetbrains.kotlin.analysis.api.components.KaDiagnosticCheckerFilter
import org.jetbrains.kotlin.analysis.api.fir.diagnostics.KaFirDiagnostic
import org.jetbrains.kotlin.analysis.api.impl.base.components.KaBaseIllegalPsiException
import org.jetbrains.kotlin.analysis.api.resolution.KaErrorCallInfo
import org.jetbrains.kotlin.analysis.api.signatures.KaCallableSignature
import org.jetbrains.kotlin.analysis.api.symbols.*
import org.jetbrains.kotlin.analysis.api.types.KaClassType
@@ -81,6 +82,7 @@ import org.jetbrains.kotlin.types.expressions.OperatorConventions
import org.jetbrains.kotlin.idea.k2.refactoring.introduce.K2SemanticMatcher.isSemanticMatch
import org.jetbrains.kotlin.psi.KtReferenceExpression
import org.jetbrains.kotlin.psi.psiUtil.findLabelAndCall
import org.jetbrains.kotlin.utils.addIfNotNull
/**
* Represents a parameter candidate as it's original declaration and a reference in code.
@@ -102,6 +104,7 @@ private class ParameterWithReference(val parameterOrigin: PsiNamedElement, val r
}
}
@OptIn(KaExperimentalApi::class)
context(KaSession)
internal fun ExtractionData.inferParametersInfo(
virtualBlock: KtBlockExpression,
@@ -131,6 +134,34 @@ internal fun ExtractionData.inferParametersInfo(
}
val unknownContextParameters = mutableSetOf<KtParameter>()
analyze(virtualBlock) {
for (referenceExpression in virtualBlock.collectDescendantsOfType<KtReferenceExpression> { it.resolveResult != null }) {
val call = referenceExpression.resolveToCall()
if (call is KaErrorCallInfo) {
val diagnostic = call.diagnostic
if (diagnostic is KaFirDiagnostic.NoContextArgument) {
unknownContextParameters.addIfNotNull((diagnostic.symbol as? KaContextParameterSymbol)?.psi as? KtParameter)
}
}
}
}
unknownContextParameters.forEach {
val name = it.name ?: "_"
val parameter = MutableParameter(
name,
it.ownerDeclaration as KtNamedDeclaration,
false,
it.returnType,
targetSibling as KtElement,
contextParameter = true
)
parameter.refCount++
parameter.currentName = name
info.parameters.add(parameter)
}
val varNameValidator = KotlinDeclarationNameValidator(
commonParent,
true,
@@ -252,7 +283,7 @@ private fun ExtractionData.registerParameter(
if (extractThis || extractOrdinaryParameter || extractFunctionRef) {
val parameterExpression = getParameterArgumentExpression(originalRef, receiverToExtract, refInfo.smartCast)
val parameter = extractedDescriptorToParameter.getOrPut(ParameterWithReference(elementToExtract, originalRef.takeUnless { extractThis })) {
var argumentText =
val argumentText =
calculateArgumentText(
hasThisReceiver,
extractThis,
@@ -269,7 +300,7 @@ private fun ExtractionData.registerParameter(
receiverToExtract
)
MutableParameter(argumentText, elementToExtract, extractThis, originalType, targetSibling as KtElement)
MutableParameter(argumentText, elementToExtract, extractThis, originalType, targetSibling as KtElement, contextParameter = false)
}
// TODO add type predicate based on called functions https://youtrack.jetbrains.com/issue/KTIJ-29166
@@ -379,6 +379,35 @@ public abstract class K2IntroduceFunctionTestGenerated extends AbstractK2Introdu
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("../../idea/tests/testData/refactoring/extractFunction/contextParameters")
public static class ContextParameters extends AbstractK2IntroduceFunctionTest {
@java.lang.Override
@org.jetbrains.annotations.NotNull
public final KotlinPluginMode getPluginMode() {
return KotlinPluginMode.K2;
}
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doExtractFunctionTest, this, testDataFilePath);
}
@TestMetadata("base.kt")
public void testBase() throws Exception {
runTest("../../idea/tests/testData/refactoring/extractFunction/contextParameters/base.kt");
}
@TestMetadata("unnamed.kt")
public void testUnnamed() throws Exception {
runTest("../../idea/tests/testData/refactoring/extractFunction/contextParameters/unnamed.kt");
}
@TestMetadata("withProvidedContext.kt")
public void testWithProvidedContext() throws Exception {
runTest("../../idea/tests/testData/refactoring/extractFunction/contextParameters/withProvidedContext.kt");
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("../../idea/tests/testData/refactoring/extractFunction/controlFlow")
public abstract static class ControlFlow extends AbstractK2IntroduceFunctionTest {