Suggested Rename and Change Signature implementation for Kotlin

This commit is contained in:
Valentin Kipyatkov
2020-02-26 09:38:24 +01:00
parent a01b840eab
commit b9a9000569
12 changed files with 4055 additions and 0 deletions
+1
View File
@@ -88,5 +88,6 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
<fileTypeUsageSchemaDescriptor schema="Gradle Script" implementationClass="org.jetbrains.kotlin.idea.core.script.KotlinGradleScriptFileTypeSchemaDetector"/>
<completion.ml.model implementation="org.jetbrains.kotlin.idea.completion.ml.KotlinMLRankingProvider"/>
<suggestedRefactoringSupport language="kotlin" implementationClass="org.jetbrains.kotlin.idea.refactoring.suggested.KotlinSuggestedRefactoringSupport"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,77 @@
package org.jetbrains.kotlin.idea.refactoring.suggested
import com.intellij.refactoring.suggested.SuggestedRefactoringSupport
import com.intellij.refactoring.suggested.SignatureChangePresentationModel.Effect
import com.intellij.refactoring.suggested.SignatureChangePresentationModel.TextFragment.Leaf
import com.intellij.refactoring.suggested.SignaturePresentationBuilder
internal class KotlinSignaturePresentationBuilder(
signature: SuggestedRefactoringSupport.Signature,
otherSignature: SuggestedRefactoringSupport.Signature,
isOldSignature: Boolean
) : SignaturePresentationBuilder(signature, otherSignature, isOldSignature)
{
override fun buildPresentation() {
val declarationType = (signature.additionalData as KotlinSignatureAdditionalData).declarationType
val keyword = declarationType.prefixKeyword
if (keyword != null) {
fragments += Leaf("$keyword ")
}
signature.receiverType?.let {
when (val effect = effect(it, otherSignature.receiverType)) {
Effect.Modified -> {
fragments += Leaf(it, effect)
fragments += Leaf(".")
}
else -> {
fragments += Leaf("$it.", effect)
}
}
}
val name = if (declarationType == DeclarationType.SECONDARY_CONSTRUCTOR) "constructor" else signature.name
fragments += Leaf(name, effect(signature.name, otherSignature.name))
if (declarationType.isFunction) {
buildParameterList { fragments, parameter, correspondingParameter ->
if (parameter.modifiers.isNotEmpty()) {
fragments += leaf(parameter.modifiers, correspondingParameter?.modifiers ?: parameter.modifiers)
fragments += Leaf(" ")
}
fragments += leaf(parameter.name, correspondingParameter?.name ?: parameter.name)
fragments += Leaf(": ")
fragments += leaf(parameter.type, correspondingParameter?.type ?: parameter.type)
val defaultValue = parameter.defaultValue
if (defaultValue != null) {
val defaultValueEffect = if (correspondingParameter != null)
effect(defaultValue, correspondingParameter.defaultValue)
else
Effect.None
fragments += Leaf(" = ", defaultValueEffect.takeIf { it != Effect.Modified } ?: Effect.None)
fragments += Leaf(defaultValue, defaultValueEffect)
}
}
} else {
require(signature.parameters.isEmpty())
}
signature.type?.let { type ->
when (val effect = effect(type, otherSignature.type)) {
Effect.Added, Effect.Removed, Effect.None -> {
fragments += Leaf(": ${signature.type}", effect)
}
Effect.Modified -> {
fragments += Leaf(": ")
fragments += Leaf(type, effect)
}
}
}
}
}
@@ -0,0 +1,161 @@
package org.jetbrains.kotlin.idea.refactoring.suggested
import com.intellij.psi.PsiNamedElement
import com.intellij.refactoring.suggested.*
import com.intellij.refactoring.suggested.SuggestedRefactoringSupport.Parameter
import com.intellij.refactoring.suggested.SuggestedRefactoringSupport.Signature
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.idea.caches.project.forcedModuleInfo
import org.jetbrains.kotlin.idea.caches.project.getModuleInfo
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
import org.jetbrains.kotlin.idea.refactoring.isInterfaceClass
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtCallableDeclaration
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.hasBody
import org.jetbrains.kotlin.renderer.DescriptorRenderer
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.isError
class KotlinSuggestedRefactoringAvailability(refactoringSupport: SuggestedRefactoringSupport) :
SuggestedRefactoringAvailability(refactoringSupport)
{
override fun refineSignaturesWithResolve(state: SuggestedRefactoringState): SuggestedRefactoringState {
val newDeclaration = state.declaration as? KtCallableDeclaration ?: return state
val oldDeclaration = state.createRestoredDeclarationCopy(refactoringSupport) as KtCallableDeclaration
oldDeclaration.containingKtFile.forcedModuleInfo = newDeclaration.getModuleInfo()
val descriptorWithOldSignature = oldDeclaration.resolveToDescriptorIfAny() as CallableDescriptor
val descriptorWithNewSignature = newDeclaration.resolveToDescriptorIfAny() as CallableDescriptor
val oldSignature = state.oldSignature
val newSignature = state.newSignature
val (oldReturnType, newReturnType) = refineType(
oldSignature.type,
newSignature.type,
descriptorWithOldSignature.returnType,
descriptorWithNewSignature.returnType
)
val improvedOldParameterTypesById = mutableMapOf<Any, String>()
val improvedNewParameterTypesById = mutableMapOf<Any, String>()
for (oldParameter in oldSignature.parameters) {
val id = oldParameter.id
val newParameter = newSignature.parameterById(id) ?: continue
val oldIndex = oldSignature.parameterIndex(oldParameter)
val newIndex = newSignature.parameterIndex(newParameter)
val (oldType, newType) = refineType(
oldParameter.type,
newParameter.type,
descriptorWithOldSignature.valueParameters[oldIndex].type,
descriptorWithNewSignature.valueParameters[newIndex].type
)
improvedOldParameterTypesById[id] = oldType!!
improvedNewParameterTypesById[id] = newType!!
}
val oldParameters = oldSignature.parameters.map { it.copy(type = improvedOldParameterTypesById[it.id] ?: it.type) }
val newParameters = newSignature.parameters.map { it.copy(type = improvedNewParameterTypesById[it.id] ?: it.type) }
val oldAdditionalData = oldSignature.additionalData as KotlinSignatureAdditionalData?
val newAdditionalData = newSignature.additionalData as KotlinSignatureAdditionalData?
val (oldReceiverType, newReceiverType) = refineType(
oldAdditionalData?.receiverType,
newAdditionalData?.receiverType,
descriptorWithOldSignature.extensionReceiverParameter?.type,
descriptorWithNewSignature.extensionReceiverParameter?.type
)
return state.copy(
oldSignature = Signature.create(
oldSignature.name,
oldReturnType,
oldParameters,
oldAdditionalData?.copy(receiverType = oldReceiverType)
)!!,
newSignature = Signature.create(
newSignature.name,
newReturnType,
newParameters,
newAdditionalData?.copy(receiverType = newReceiverType)
)!!
)
}
private fun refineType(
oldTypeInCode: String?,
newTypeInCode: String?,
oldTypeResolved: KotlinType?,
newTypeResolved: KotlinType?
): Pair<String?, String?> {
if (oldTypeResolved?.isError == true || newTypeResolved?.isError == true) {
return oldTypeInCode to newTypeInCode
}
val oldTypeFQ = oldTypeResolved?.fqText()
val newTypeFQ = newTypeResolved?.fqText()
if (oldTypeInCode != newTypeInCode) {
if (oldTypeFQ == newTypeFQ) {
return newTypeInCode to newTypeInCode
}
} else {
if (oldTypeFQ != newTypeFQ) {
return oldTypeFQ to newTypeFQ
}
}
return oldTypeInCode to newTypeInCode
}
private fun KotlinType.fqText() = DescriptorRenderer.FQ_NAMES_IN_TYPES.renderType(this)
override fun detectAvailableRefactoring(state: SuggestedRefactoringState): SuggestedRefactoringData? {
val declaration = state.declaration
if (declaration !is KtCallableDeclaration || KotlinSuggestedRefactoringSupport.isOnlyRenameSupported(declaration)) {
return SuggestedRenameData(declaration as PsiNamedElement, state.oldSignature.name)
}
val oldSignature = state.oldSignature
val newSignature = state.newSignature
val updateUsagesData = SuggestedChangeSignatureData.create(state, USAGES)
if (hasParameterAddedRemovedOrReordered(oldSignature, newSignature)) return updateUsagesData
// for non-virtual function we can add or remove receiver for usages but not change its type
if ((oldSignature.receiverType == null) != (newSignature.receiverType == null)) return updateUsagesData
val (nameChanges, renameData) = nameChanges(oldSignature, newSignature, declaration, declaration.valueParameters)
if (hasTypeChanges(oldSignature, newSignature)) {
return if (nameChanges > 0)
updateUsagesData
else
declaration.overridesName()?.let { updateUsagesData.copy(nameOfStuffToUpdate = it) }
}
return when {
renameData != null -> renameData
nameChanges > 0 -> updateUsagesData
else -> null
}
}
private fun KtCallableDeclaration.overridesName(): String? {
return when {
hasModifier(KtTokens.ABSTRACT_KEYWORD) -> IMPLEMENTATIONS
hasModifier(KtTokens.OPEN_KEYWORD) -> OVERRIDES
containingClassOrObject?.isInterfaceClass() == true -> if (hasBody()) OVERRIDES else IMPLEMENTATIONS
hasModifier(KtTokens.EXPECT_KEYWORD) -> "actual declarations"
else -> null
}
}
override fun hasTypeChanges(oldSignature: Signature, newSignature: Signature): Boolean {
return super.hasTypeChanges(oldSignature, newSignature) || oldSignature.receiverType != newSignature.receiverType
}
override fun hasParameterTypeChanges(oldParam: Parameter, newParam: Parameter): Boolean {
return super.hasParameterTypeChanges(oldParam, newParam) || oldParam.modifiers != newParam.modifiers
}
}
@@ -0,0 +1,157 @@
package org.jetbrains.kotlin.idea.refactoring.suggested
import com.intellij.psi.PsiElement
import com.intellij.refactoring.suggested.SuggestedChangeSignatureData
import com.intellij.refactoring.suggested.SuggestedRefactoringExecution
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
import org.jetbrains.kotlin.idea.refactoring.changeSignature.*
import org.jetbrains.kotlin.psi.KtCallableDeclaration
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.types.isError
import org.jetbrains.kotlin.utils.addIfNotNull
class KotlinSuggestedRefactoringExecution(
refactoringSupport: KotlinSuggestedRefactoringSupport
) : SuggestedRefactoringExecution(refactoringSupport) {
private data class PrepareChangeSignatureResult(
val returnTypeInfo: KotlinTypeInfo,
val parameterTypeInfos: List<KotlinTypeInfo>
)
override fun prepareChangeSignature(data: SuggestedChangeSignatureData): Any {
val declaration = data.declaration as KtCallableDeclaration
val descriptorWithNewSignature = declaration.resolveToDescriptorIfAny() as CallableDescriptor
val newSignature = data.newSignature
val returnType = descriptorWithNewSignature.returnType
val returnTypeInfo = if (returnType != null && !returnType.isError) {
KotlinTypeInfo(type = returnType, isCovariant = true)
} else {
KotlinTypeInfo(text = newSignature.type, isCovariant = true)
}
require(descriptorWithNewSignature.valueParameters.size == newSignature.parameters.size)
val parameterTypeInfos = descriptorWithNewSignature.valueParameters.zip(newSignature.parameters)
.map { (parameterDescriptor, parameterData) ->
val type = parameterDescriptor.varargElementType ?: parameterDescriptor.type
if (!type.isError) {
KotlinTypeInfo(type = type, isCovariant = true)
} else {
KotlinTypeInfo(text = parameterData.type, isCovariant = true)
}
}
return PrepareChangeSignatureResult(returnTypeInfo, parameterTypeInfos)
}
override fun performChangeSignature(
data: SuggestedChangeSignatureData,
newParameterValues: List<NewParameterValue>,
preparedData: Any?
) {
val (returnTypeInfo, parameterTypeInfos) = preparedData as PrepareChangeSignatureResult
val declaration = data.declaration as KtDeclaration
val descriptor = declaration.resolveToDescriptorIfAny() as CallableDescriptor
val project = declaration.project
val configuration = object : KotlinChangeSignatureConfiguration {
override fun performSilently(affectedFunctions: Collection<PsiElement>) = true
}
val changeSignature = KotlinChangeSignature(project, descriptor, configuration, declaration, null)
val methodDescriptor = changeSignature.adjustDescriptor(listOf(descriptor))!!
val parameters = mutableListOf<KotlinParameterInfo>()
var newParameterValueIndex = 0
val receiver: KotlinParameterInfo? = if (data.newSignature.receiverType != null) {
val newTypeInfo = KotlinTypeInfo(text = data.newSignature.receiverType, isCovariant = false)
if (data.oldSignature.receiverType != null) {
methodDescriptor.receiver!!.apply { currentTypeInfo = newTypeInfo }
} else {
KotlinParameterInfo(descriptor, -1, "", newTypeInfo)
.withDefaultValue(newParameterValues[newParameterValueIndex++])
}
} else {
null
}
parameters.addIfNotNull(receiver)
val psiFactory = KtPsiFactory(project)
for ((index, parameter) in data.newSignature.parameters.withIndex()) {
val initialIndex = data.oldSignature.parameterById(parameter.id)
?.let { data.oldSignature.parameterIndex(it) }
val defaultValue = parameter.defaultValue?.let { psiFactory.createExpression(it) }
val modifierList = parameter.modifiers
.takeIf { it.isNotEmpty() }
?.let { psiFactory.createModifierList(it) }
val parameterInfo = if (initialIndex == null) {
KotlinParameterInfo(
descriptor,
-1,
parameter.name,
parameterTypeInfos[index],
defaultValueForParameter = defaultValue,
modifierList = modifierList
).withDefaultValue(newParameterValues[newParameterValueIndex++])
} else {
KotlinParameterInfo(
descriptor,
initialIndex,
parameter.name,
methodDescriptor.parameters[initialIndex].originalTypeInfo,
defaultValueForParameter = defaultValue,
modifierList = modifierList
).apply {
currentTypeInfo = parameterTypeInfos[index]
}
}
parameters.add(parameterInfo)
}
val changeInfo = KotlinChangeInfo(
methodDescriptor,
context = declaration,
name = data.newSignature.name,
newReturnTypeInfo = returnTypeInfo,
parameterInfos = parameters,
receiver = receiver
)
val processor = KotlinChangeSignatureProcessor(project, changeInfo, "")
processor.run()
}
//TODO: we can't just set defaultValueForCall due to bug in KotlinParameterInfo
private fun KotlinParameterInfo.withDefaultValue(value: NewParameterValue): KotlinParameterInfo {
when (value) {
is NewParameterValue.AnyVariable -> {
isUseAnySingleVariable = true
return this
}
is NewParameterValue.Expression -> {
val defaultValueForCall = value.expression as KtExpression
return KotlinParameterInfo(
callableDescriptor, originalIndex, name, originalTypeInfo,
defaultValueForParameter, defaultValueForCall, valOrVar, modifierList
).apply {
currentTypeInfo = this@withDefaultValue.currentTypeInfo
}
}
is NewParameterValue.None -> {
defaultValueForCall = null
return this
}
}
}
}
@@ -0,0 +1,66 @@
package org.jetbrains.kotlin.idea.refactoring.suggested
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.refactoring.suggested.SuggestedRefactoringState
import com.intellij.refactoring.suggested.SuggestedRefactoringStateChanges
import com.intellij.refactoring.suggested.SuggestedRefactoringSupport
import com.intellij.refactoring.suggested.SuggestedRefactoringSupport.Parameter
import com.intellij.refactoring.suggested.SuggestedRefactoringSupport.Signature
import org.jetbrains.kotlin.lexer.KtModifierKeywordToken
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtCallableDeclaration
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.psi.KtParameter
import org.jetbrains.kotlin.psi.psiUtil.children
class KotlinSuggestedRefactoringStateChanges(refactoringSupport: SuggestedRefactoringSupport) :
SuggestedRefactoringStateChanges(refactoringSupport)
{
override fun createInitialState(declaration: PsiElement): SuggestedRefactoringState? {
declaration as KtDeclaration
if (declaration.hasModifier(KtTokens.OVERRIDE_KEYWORD)) return null // currently not supported
if (declaration.hasModifier(KtTokens.ACTUAL_KEYWORD)) return null // currently not supported
return super.createInitialState(declaration)
}
override fun signature(declaration: PsiElement, prevState: SuggestedRefactoringState?): Signature? {
declaration as KtDeclaration
val name = declaration.name ?: return null
if (declaration !is KtCallableDeclaration || KotlinSuggestedRefactoringSupport.isOnlyRenameSupported(declaration)) {
return Signature.create(name, null, emptyList(), null)
}
val parameters = declaration.valueParameters.map { it.extractParameterData() ?: return null }
val type = declaration.typeReference?.text
val receiverType = declaration.receiverTypeReference?.text
val signature = Signature.create(
name,
type,
parameters,
KotlinSignatureAdditionalData(DeclarationType.fromDeclaration(declaration), receiverType)
) ?: return null
return if (prevState == null) signature else matchParametersWithPrevState(signature, declaration, prevState)
}
override fun parameterMarkerRanges(declaration: PsiElement): List<TextRange?> {
if (declaration !is KtFunction) return emptyList()
return declaration.valueParameters.map { it.colon?.textRange }
}
private fun KtParameter.extractParameterData(): Parameter? {
val modifiers = modifierList?.node?.children()
?.filter { it.elementType is KtModifierKeywordToken && it.text in modifiersToInclude }
?.joinToString(separator = " ") { it.text } ?: ""
return Parameter(
Any(),
name ?: return null,
typeReference?.text ?: return null,
KotlinParameterAdditionalData(defaultValue?.text/*TODO: strip comments etc*/, modifiers)
)
}
private val modifiersToInclude = setOf(KtTokens.VARARG_KEYWORD.value)
}
@@ -0,0 +1,153 @@
package org.jetbrains.kotlin.idea.refactoring.suggested
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.refactoring.suggested.SuggestedRefactoringSupport
import com.intellij.refactoring.suggested.SuggestedRefactoringSupport.Parameter
import com.intellij.refactoring.suggested.SuggestedRefactoringSupport.Signature
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
class KotlinSuggestedRefactoringSupport : SuggestedRefactoringSupport {
override fun isDeclaration(psiElement: PsiElement): Boolean {
if (psiElement !is KtDeclaration) return false
if (psiElement is KtParameter && psiElement.ownerFunction != null) return false
return true
}
override fun signatureRange(declaration: PsiElement): TextRange? {
when (declaration) {
is KtPrimaryConstructor -> return declaration.textRange
is KtSecondaryConstructor -> return declaration.valueParameterList?.textRange
is KtCallableDeclaration -> {
if (isOnlyRenameSupported(declaration)) {
return declaration.nameIdentifier?.textRange
}
val start = declaration.receiverTypeReference?.textRange?.startOffset
?: declaration.nameIdentifier?.textRange?.startOffset
?: return null
val end = (declaration.typeReference ?: declaration.valueParameterList ?: declaration.nameIdentifier)
?.textRange?.endOffset
?: return null
return TextRange(start, end)
}
is KtNamedDeclaration -> return declaration.nameIdentifier?.textRange
else -> return null
}
}
override fun importsRange(psiFile: PsiFile): TextRange? {
return (psiFile as KtFile).importList?.textRange
}
override fun nameRange(declaration: PsiElement): TextRange? {
val identifier = when (declaration) {
is KtPrimaryConstructor -> declaration.containingClassOrObject?.nameIdentifier
is KtSecondaryConstructor -> declaration.getConstructorKeyword()
is KtNamedDeclaration -> declaration.nameIdentifier
else -> null
}
return identifier?.textRange
}
override fun hasSyntaxError(declaration: PsiElement): Boolean {
if (super.hasSyntaxError(declaration)) return true
// do not suggest renaming of local variable which has neither type nor initializer
// it's important because such variable declarations may appear on typing "val name = " before an expression
if (declaration is KtProperty && declaration.isLocal) {
if (declaration.typeReference == null && declaration.initializer == null) return true
}
return false
}
override fun isIdentifierStart(c: Char) = c.isJavaIdentifierStart()
override fun isIdentifierPart(c: Char) = c.isJavaIdentifierPart()
override val stateChanges = KotlinSuggestedRefactoringStateChanges(this)
override val availability = KotlinSuggestedRefactoringAvailability(this)
override val ui get() = KotlinSuggestedRefactoringUI
override val execution = KotlinSuggestedRefactoringExecution(this)
companion object {
fun isOnlyRenameSupported(declaration: KtCallableDeclaration): Boolean {
// for local variable - only rename
return declaration is KtVariableDeclaration && KtPsiUtil.isLocal(declaration)
}
}
}
enum class DeclarationType {
FUN {
override val prefixKeyword get() = "fun"
override val isFunction get() = true
},
VAL {
override val prefixKeyword get() = "val"
override val isFunction get() = false
},
VAR {
override val prefixKeyword get() = "var"
override val isFunction get() = false
},
PRIMARY_CONSTRUCTOR {
override val prefixKeyword: String?
get() = null
override val isFunction get() = true
},
SECONDARY_CONSTRUCTOR {
override val prefixKeyword: String?
get() = null
override val isFunction get() = true
},
OTHER {
override val prefixKeyword: String?
get() = null
override val isFunction get() = false
}
;
abstract val prefixKeyword: String?
abstract val isFunction: Boolean
companion object {
fun fromDeclaration(declaration: KtDeclaration): DeclarationType = when (declaration) {
is KtPrimaryConstructor -> PRIMARY_CONSTRUCTOR
is KtSecondaryConstructor -> SECONDARY_CONSTRUCTOR
is KtNamedFunction -> FUN
is KtProperty -> if (declaration.isVar) VAR else VAL
else -> OTHER
}
}
}
internal data class KotlinSignatureAdditionalData(
val declarationType: DeclarationType,
val receiverType: String?
) : SuggestedRefactoringSupport.SignatureAdditionalData
internal data class KotlinParameterAdditionalData(
val defaultValue: String?,
val modifiers: String
) : SuggestedRefactoringSupport.ParameterAdditionalData
internal val Signature.receiverType: String?
get() = (additionalData as KotlinSignatureAdditionalData?)?.receiverType
internal val Parameter.defaultValue: String?
get() = (additionalData as KotlinParameterAdditionalData?)?.defaultValue
internal val Parameter.modifiers: String
get() = (additionalData as KotlinParameterAdditionalData?)?.modifiers ?: ""
@@ -0,0 +1,46 @@
package org.jetbrains.kotlin.idea.refactoring.suggested
import com.intellij.psi.PsiCodeFragment
import com.intellij.refactoring.suggested.SuggestedChangeSignatureData
import com.intellij.refactoring.suggested.SuggestedRefactoringExecution.NewParameterValue
import com.intellij.refactoring.suggested.SuggestedRefactoringSupport.Signature
import com.intellij.refactoring.suggested.SuggestedRefactoringUI
import com.intellij.refactoring.suggested.SignaturePresentationBuilder
import org.jetbrains.kotlin.psi.KtExpressionCodeFragment
import org.jetbrains.kotlin.psi.KtPsiFactory
object KotlinSuggestedRefactoringUI : SuggestedRefactoringUI() {
override fun createSignaturePresentationBuilder(
signature: Signature,
otherSignature: Signature,
isOldSignature: Boolean
): SignaturePresentationBuilder {
return KotlinSignaturePresentationBuilder(signature, otherSignature, isOldSignature)
}
override fun extractNewParameterData(data: SuggestedChangeSignatureData): List<NewParameterData> {
val newParameters = mutableListOf<NewParameterData>()
val declaration = data.declaration
val factory = KtPsiFactory(declaration.project)
fun createCodeFragment() = factory.createExpressionCodeFragment("", declaration)
if (data.newSignature.receiverType != null && data.oldSignature.receiverType == null) {
newParameters.add(NewParameterData("<receiver>", createCodeFragment(), false/*TODO*/))
}
for (parameter in data.newSignature.parameters) {
if (data.oldSignature.parameterById(parameter.id) == null) {
newParameters.add(NewParameterData(parameter.name, createCodeFragment(), false/*TODO*/))
}
}
return newParameters
}
override fun extractValue(fragment: PsiCodeFragment): NewParameterValue.Expression? {
return (fragment as KtExpressionCodeFragment).getContentElement()
?.let { NewParameterValue.Expression(it) }
}
}
@@ -0,0 +1,874 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.refactoring.suggested
import com.intellij.refactoring.suggested.SuggestedRefactoringSupport.Parameter
import com.intellij.refactoring.suggested.SuggestedRefactoringSupport.Signature
import com.intellij.refactoring.suggested.BaseSignatureChangePresentationTest
class KotlinSignatureChangePresentationTest : BaseSignatureChangePresentationTest() {
override val refactoringSupport = KotlinSuggestedRefactoringSupport()
private fun signature(
name: String,
type: String?,
parameters: List<Parameter>,
receiverType: String? = null
): Signature? {
return Signature.create(
name,
type,
parameters,
KotlinSignatureAdditionalData(DeclarationType.FUN, receiverType)
)
}
fun testAddParameters() {
val oldSignature = signature(
"foo",
"String",
listOf(Parameter(0, "p1", "Int")),
"Any"
)!!
val newSignature = signature(
"foo",
"String",
listOf(
Parameter(Any(), "p0", "Any"),
Parameter(0, "p1", "Int"),
Parameter(Any(), "p2", "Long")
),
"Any"
)!!
doTest(
oldSignature,
newSignature,
"""
Old:
'fun '
'Any.'
'foo'
'('
LineBreak('', true)
Group:
'p1'
': '
'Int'
LineBreak('', false)
')'
': String'
New:
'fun '
'Any.'
'foo'
'('
LineBreak('', true)
Group (added):
'p0'
': '
'Any'
','
LineBreak(' ', true)
Group:
'p1'
': '
'Int'
','
LineBreak(' ', true)
Group (added):
'p2'
': '
'Long'
LineBreak('', false)
')'
': String'
""".trimIndent()
)
}
fun testSwapParameters() {
val oldSignature = signature(
"foo",
null,
listOf(
Parameter(0, "p1", "Int"),
Parameter(1, "p2", "Long")
)
)!!
val newSignature = signature(
"foo",
null,
listOf(
Parameter(1, "p2", "Long"),
Parameter(0, "p1", "Int")
)
)!!
doTest(
oldSignature,
newSignature,
"""
Old:
'fun '
'foo'
'('
LineBreak('', true)
Group (moved):
'p1'
': '
'Int'
','
LineBreak(' ', true)
Group:
'p2'
': '
'Long'
LineBreak('', false)
')'
New:
'fun '
'foo'
'('
LineBreak('', true)
Group:
'p2'
': '
'Long'
','
LineBreak(' ', true)
Group (moved):
'p1'
': '
'Int'
LineBreak('', false)
')'
""".trimIndent()
)
}
fun testMoveParameter() {
val oldSignature = signature(
"foo",
null,
listOf(
Parameter(0, "p1", "Int"),
Parameter(1, "p2", "Long"),
Parameter(2, "p3", "Any")
)
)!!
val newSignature = signature(
"foo",
null,
listOf(
Parameter(1, "p2", "Long"),
Parameter(2, "p3", "Any"),
Parameter(0, "p1", "Int")
)
)!!
doTest(
oldSignature,
newSignature,
"""
Old:
'fun '
'foo'
'('
LineBreak('', true)
Group (moved):
'p1'
': '
'Int'
','
LineBreak(' ', true)
Group:
'p2'
': '
'Long'
','
LineBreak(' ', true)
Group:
'p3'
': '
'Any'
LineBreak('', false)
')'
New:
'fun '
'foo'
'('
LineBreak('', true)
Group:
'p2'
': '
'Long'
','
LineBreak(' ', true)
Group:
'p3'
': '
'Any'
','
LineBreak(' ', true)
Group (moved):
'p1'
': '
'Int'
LineBreak('', false)
')'
""".trimIndent()
)
}
fun testReorderParameters() {
val oldSignature = signature(
"foo",
null,
listOf(
Parameter(0, "p1", "Int"),
Parameter(1, "p2", "Long"),
Parameter(2, "p3", "Any"),
Parameter(3, "p4", "Any")
)
)!!
val newSignature = signature(
"foo",
null,
listOf(
Parameter(1, "p2", "Long"),
Parameter(3, "p4", "Any"),
Parameter(2, "p3", "Any"),
Parameter(0, "p1", "Int")
)
)!!
doTest(
oldSignature,
newSignature,
"""
Old:
'fun '
'foo'
'('
LineBreak('', true)
Group (id = 0, moved):
'p1'
': '
'Int'
','
LineBreak(' ', true)
Group:
'p2'
': '
'Long'
','
LineBreak(' ', true)
Group (id = 2, moved):
'p3'
': '
'Any'
','
LineBreak(' ', true)
Group:
'p4'
': '
'Any'
LineBreak('', false)
')'
New:
'fun '
'foo'
'('
LineBreak('', true)
Group:
'p2'
': '
'Long'
','
LineBreak(' ', true)
Group:
'p4'
': '
'Any'
','
LineBreak(' ', true)
Group (id = 2, moved):
'p3'
': '
'Any'
','
LineBreak(' ', true)
Group (id = 0, moved):
'p1'
': '
'Int'
LineBreak('', false)
')'
""".trimIndent()
)
}
fun testChangeFunctionName() {
val oldSignature = signature("foo", null, emptyList())!!
val newSignature = signature("bar", null, emptyList())!!
doTest(
oldSignature,
newSignature,
"""
Old:
'fun '
'foo' (modified)
'('
LineBreak('', false)
')'
New:
'fun '
'bar' (modified)
'('
LineBreak('', false)
')'
""".trimIndent()
)
}
fun testChangeReturnType() {
val oldSignature = signature("foo", "Any", emptyList())!!
val newSignature = signature("foo", "String", emptyList())!!
doTest(
oldSignature,
newSignature,
"""
Old:
'fun '
'foo'
'('
LineBreak('', false)
')'
': '
'Any' (modified)
New:
'fun '
'foo'
'('
LineBreak('', false)
')'
': '
'String' (modified)
""".trimIndent()
)
}
fun testAddReturnType() {
val oldSignature = signature("foo", null, emptyList())!!
val newSignature = signature("foo", "String", emptyList())!!
doTest(
oldSignature,
newSignature,
"""
Old:
'fun '
'foo'
'('
LineBreak('', false)
')'
New:
'fun '
'foo'
'('
LineBreak('', false)
')'
': String' (added)
""".trimIndent()
)
}
fun testRemoveReturnType() {
val oldSignature = signature("foo", "Any", emptyList())!!
val newSignature = signature("foo", null, emptyList())!!
doTest(
oldSignature,
newSignature,
"""
Old:
'fun '
'foo'
'('
LineBreak('', false)
')'
': Any' (removed)
New:
'fun '
'foo'
'('
LineBreak('', false)
')'
""".trimIndent()
)
}
fun testChangeParameterName() {
val oldSignature = signature(
"foo",
null,
listOf(
Parameter(0, "p1", "Int"),
Parameter(1, "p2", "Long")
)
)!!
val newSignature = signature(
"foo",
null,
listOf(
Parameter(0, "p1New", "Int"),
Parameter(1, "p2", "Long")
)
)!!
doTest(
oldSignature,
newSignature,
"""
Old:
'fun '
'foo'
'('
LineBreak('', true)
Group:
'p1' (modified)
': '
'Int'
','
LineBreak(' ', true)
Group:
'p2'
': '
'Long'
LineBreak('', false)
')'
New:
'fun '
'foo'
'('
LineBreak('', true)
Group:
'p1New' (modified)
': '
'Int'
','
LineBreak(' ', true)
Group:
'p2'
': '
'Long'
LineBreak('', false)
')'
""".trimIndent()
)
}
fun testChangeTwoParameterNames() {
val oldSignature = signature(
"foo",
null,
listOf(
Parameter(0, "p1", "Int"),
Parameter(1, "p2", "Long")
)
)!!
val newSignature = signature(
"foo",
null,
listOf(
Parameter(0, "p1New", "Int"),
Parameter(1, "p2New", "Long")
)
)!!
doTest(
oldSignature,
newSignature,
"""
Old:
'fun '
'foo'
'('
LineBreak('', true)
Group:
'p1' (modified)
': '
'Int'
','
LineBreak(' ', true)
Group:
'p2' (modified)
': '
'Long'
LineBreak('', false)
')'
New:
'fun '
'foo'
'('
LineBreak('', true)
Group:
'p1New' (modified)
': '
'Int'
','
LineBreak(' ', true)
Group:
'p2New' (modified)
': '
'Long'
LineBreak('', false)
')'
""".trimIndent()
)
}
fun testMoveAndRenameParameter() {
val oldSignature = signature(
"foo",
null,
listOf(
Parameter(0, "p1", "Int"),
Parameter(1, "p2", "Long"),
Parameter(2, "p3", "Any")
)
)!!
val newSignature = signature(
"foo",
null,
listOf(
Parameter(1, "p2", "Long"),
Parameter(2, "p3", "Any"),
Parameter(0, "p1New", "Int")
)
)!!
doTest(
oldSignature,
newSignature,
"""
Old:
'fun '
'foo'
'('
LineBreak('', true)
Group (moved):
'p1' (modified)
': '
'Int'
','
LineBreak(' ', true)
Group:
'p2'
': '
'Long'
','
LineBreak(' ', true)
Group:
'p3'
': '
'Any'
LineBreak('', false)
')'
New:
'fun '
'foo'
'('
LineBreak('', true)
Group:
'p2'
': '
'Long'
','
LineBreak(' ', true)
Group:
'p3'
': '
'Any'
','
LineBreak(' ', true)
Group (moved):
'p1New' (modified)
': '
'Int'
LineBreak('', false)
')'
""".trimIndent()
)
}
fun testMoveParameterAndChangeFunctionName() {
val oldSignature = signature(
"foo",
null,
listOf(
Parameter(0, "p1", "Int"),
Parameter(1, "p2", "Long"),
Parameter(2, "p3", "Any")
)
)!!
val newSignature = signature(
"fooNew",
null,
listOf(
Parameter(1, "p2", "Long"),
Parameter(2, "p3", "Any"),
Parameter(0, "p1", "Int")
)
)!!
doTest(
oldSignature,
newSignature,
"""
Old:
'fun '
'foo' (modified)
'('
LineBreak('', true)
Group (id = 0, moved):
'p1'
': '
'Int'
','
LineBreak(' ', true)
Group:
'p2'
': '
'Long'
','
LineBreak(' ', true)
Group:
'p3'
': '
'Any'
LineBreak('', false)
')'
New:
'fun '
'fooNew' (modified)
'('
LineBreak('', true)
Group:
'p2'
': '
'Long'
','
LineBreak(' ', true)
Group:
'p3'
': '
'Any'
','
LineBreak(' ', true)
Group (id = 0, moved):
'p1'
': '
'Int'
LineBreak('', false)
')'
""".trimIndent()
)
}
fun testAddReceiver() {
val oldSignature = signature(
"foo",
null,
emptyList()
)!!
val newSignature = signature(
"foo",
null,
emptyList(),
"Any"
)!!
doTest(
oldSignature,
newSignature,
"""
Old:
'fun '
'foo'
'('
LineBreak('', false)
')'
New:
'fun '
'Any.' (added)
'foo'
'('
LineBreak('', false)
')'
""".trimIndent()
)
}
fun testRemoveReceiver() {
val oldSignature = signature(
"foo",
null,
emptyList(),
"Any"
)!!
val newSignature = signature(
"foo",
null,
emptyList()
)!!
doTest(
oldSignature,
newSignature,
"""
Old:
'fun '
'Any.' (removed)
'foo'
'('
LineBreak('', false)
')'
New:
'fun '
'foo'
'('
LineBreak('', false)
')'
""".trimIndent()
)
}
fun testChangeReceiverType() {
val oldSignature = signature(
"foo",
null,
emptyList(),
"Any"
)!!
val newSignature = signature(
"foo",
null,
emptyList(),
"String"
)!!
doTest(
oldSignature,
newSignature,
"""
Old:
'fun '
'Any' (modified)
'.'
'foo'
'('
LineBreak('', false)
')'
New:
'fun '
'String' (modified)
'.'
'foo'
'('
LineBreak('', false)
')'
""".trimIndent()
)
}
fun testChangeDefaultValues() {
val oldSignature = signature(
"foo",
null,
listOf(
Parameter(0, "p1", "Int", KotlinParameterAdditionalData("1", "")),
Parameter(1, "p2", "Int", KotlinParameterAdditionalData("2", "")),
Parameter(2, "p3", "Int", KotlinParameterAdditionalData("3", "")),
Parameter(3, "p4", "Int")
)
)!!
val newSignature = signature(
"foo",
null,
listOf(
Parameter(0, "p1", "Int", KotlinParameterAdditionalData("1", "")),
Parameter(1, "p2", "Int", KotlinParameterAdditionalData("22", "")),
Parameter(2, "p3", "Int"),
Parameter(3, "p4", "Int", KotlinParameterAdditionalData("4", ""))
)
)!!
doTest(
oldSignature,
newSignature,
"""
Old:
'fun '
'foo'
'('
LineBreak('', true)
Group:
'p1'
': '
'Int'
' = '
'1'
','
LineBreak(' ', true)
Group:
'p2'
': '
'Int'
' = '
'2' (modified)
','
LineBreak(' ', true)
Group:
'p3'
': '
'Int'
' = ' (removed)
'3' (removed)
','
LineBreak(' ', true)
Group:
'p4'
': '
'Int'
LineBreak('', false)
')'
New:
'fun '
'foo'
'('
LineBreak('', true)
Group:
'p1'
': '
'Int'
' = '
'1'
','
LineBreak(' ', true)
Group:
'p2'
': '
'Int'
' = '
'22' (modified)
','
LineBreak(' ', true)
Group:
'p3'
': '
'Int'
','
LineBreak(' ', true)
Group:
'p4'
': '
'Int'
' = ' (added)
'4' (added)
LineBreak('', false)
')'
""".trimIndent()
)
}
}
@@ -0,0 +1,371 @@
package org.jetbrains.kotlin.idea.refactoring.suggested
import com.intellij.openapi.fileTypes.LanguageFileType
import com.intellij.refactoring.suggested.BaseSuggestedRefactoringAvailabilityTest
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.resolve.ImportPath
class KotlinSuggestedRefactoringAvailabilityTest : BaseSuggestedRefactoringAvailabilityTest() {
override val fileType: LanguageFileType
get() = KotlinFileType.INSTANCE
override fun getProjectDescriptor() = KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE
fun testNotAvailableWithSyntaxError() {
doTest(
"""
fun foo(p1: Int<caret>) {
foo(1)
}
fun bar() {
foo(2)
}
""".trimIndent(),
{
myFixture.type(", p2: Any")
},
{
myFixture.type(", p")
},
expectedAvailability = Availability.Disabled
)
}
fun testInsertTrailingComma() {
doTest(
"""
fun foo(p1: Int<caret>) {
foo(1)
}
fun bar() {
foo(2)
}
""".trimIndent(),
{
myFixture.type(",")
},
expectedAvailability = Availability.NotAvailable
)
}
fun testChangeNonVirtualPropertyType() {
doTest(
"val v: <caret>String = \"\"",
{
replaceTextAtCaret("String", "Any")
},
expectedAvailability = Availability.Disabled
)
}
fun testChangeParameterTypeNonVirtual() {
doTest(
"fun foo(p: <caret>String) {}",
{
replaceTextAtCaret("String", "Any")
},
expectedAvailability = Availability.Disabled
)
}
fun testChangeReturnTypeNonVirtual() {
doTest(
"fun foo(): <caret>String = \"\"",
{
replaceTextAtCaret("String", "Any")
},
expectedAvailability = Availability.Disabled
)
}
fun testChangeLocalVariableType() {
doTest(
"""
fun foo() {
val local: <caret>Int
local = 10
}
""".trimIndent(),
{
replaceTextAtCaret("Int", "Long")
},
expectedAvailability = Availability.NotAvailable
)
}
fun testAddLocalVariableType() {
doTest(
"""
fun foo() {
var local<caret> = 10
}
""".trimIndent(),
{
myFixture.type(": Long")
},
expectedAvailability = Availability.NotAvailable
)
}
fun testTypeLocalVariableBeforeExpression() {
doTest(
"""
""".trimIndent(),
{
myFixture.type("val ")
},
{
myFixture.type("cod")
},
{
myFixture.type("e = ")
},
expectedAvailability = Availability.NotAvailable
)
}
fun testChangeParameterTypeAndName() {
doTest(
"""
interface I {
fun foo(p: <caret>Int)
}
""".trimIndent(),
{
replaceTextAtCaret("Int", "String")
editor.caretModel.moveToOffset(editor.caretModel.offset - "p: ".length)
replaceTextAtCaret("p", "pNew")
},
expectedAvailability = Availability.Available(changeSignatureAvailableTooltip("foo", "usages"))
)
}
fun testRenameTwoParameters() {
doTest(
"""
interface I {
fun foo(<caret>p1: Int, p2: Int)
}
""".trimIndent(),
{
replaceTextAtCaret("p1", "p1New")
},
{
editor.caretModel.moveToOffset(editor.caretModel.offset + "p1New: Int, ".length)
replaceTextAtCaret("p2", "p2New")
},
expectedAvailability = Availability.Available(changeSignatureAvailableTooltip("foo", "usages"))
)
}
fun testChangeParameterType() {
doTest(
"""
class C {
open fun foo(p1: <caret>Int, p2: Int) {
}
}
""".trimIndent(),
{
replaceTextAtCaret("Int", "Any")
},
expectedAvailability = Availability.Available(changeSignatureAvailableTooltip("foo", "overrides"))
)
}
fun testChangeParameterTypeForAbstract() {
doTest(
"""
abstract class C {
abstract fun foo(p1: <caret>Int, p2: Int)
}
""".trimIndent(),
{
replaceTextAtCaret("Int", "Any")
},
expectedAvailability = Availability.Available(changeSignatureAvailableTooltip("foo", "implementations"))
)
}
fun testChangeParameterTypeInInterface() {
doTest(
"""
interface I {
fun foo(p1: <caret>Int, p2: Int)
}
""".trimIndent(),
{
replaceTextAtCaret("Int", "Any")
},
expectedAvailability = Availability.Available(changeSignatureAvailableTooltip("foo", "implementations"))
)
}
fun testChangeParameterTypeInInterfaceWithBody() {
doTest(
"""
interface I {
fun foo(p1: <caret>Int, p2: Int) {}
}
""".trimIndent(),
{
replaceTextAtCaret("Int", "Any")
},
expectedAvailability = Availability.Available(changeSignatureAvailableTooltip("foo", "overrides"))
)
}
fun testChangeTypeOfPropertyWithImplementationInInterface() {
doTest(
"""
interface I {
val p: <caret>Int
get() = 0
}
""".trimIndent(),
{
replaceTextAtCaret("Int", "Any")
},
expectedAvailability = Availability.Available(changeSignatureAvailableTooltip("p", "overrides"))
)
}
fun testSpecifyExplicitType() {
doTest(
"""
open class C {
open fun foo()<caret> = 1
}
""".trimIndent(),
{
myFixture.type(": Int")
},
expectedAvailability = Availability.Available(changeSignatureAvailableTooltip("foo", "overrides")),
expectedAvailabilityAfterResolve = Availability.NotAvailable
)
}
fun testRemoveExplicitType() {
doTest(
"""
open class C {
open fun foo(): Int<caret> = 1
}
""".trimIndent(),
{
deleteTextBeforeCaret(": Int")
},
expectedAvailability = Availability.Available(changeSignatureAvailableTooltip("foo", "overrides")),
expectedAvailabilityAfterResolve = Availability.NotAvailable
)
}
fun testImportNestedClass() {
doTest(
"""
package ppp
class C {
class Nested
}
interface I {
fun foo(): <caret>C.Nested
}
""".trimIndent(),
{
addImport("ppp.C.Nested")
},
{
replaceTextAtCaret("C.Nested", "Nested")
},
expectedAvailability = Availability.Available(changeSignatureAvailableTooltip("foo", "implementations")),
expectedAvailabilityAfterResolve = Availability.NotAvailable
)
}
fun testImportNestedClassForReceiverType() {
doTest(
"""
package ppp
class C {
class Nested
}
interface I {
fun <caret>C.Nested.foo()
}
""".trimIndent(),
{
addImport("ppp.C.Nested")
},
{
replaceTextAtCaret("C.Nested", "Nested")
},
expectedAvailability = Availability.Available(changeSignatureAvailableTooltip("foo", "implementations")),
expectedAvailabilityAfterResolve = Availability.NotAvailable
)
}
fun testImportNestedClassForParameterType() {
doTest(
"""
package ppp
class C {
class Nested
}
interface I {
fun foo(p: <caret>C.Nested)
}
""".trimIndent(),
{
addImport("ppp.C.Nested")
},
{
replaceTextAtCaret("C.Nested", "Nested")
},
expectedAvailability = Availability.Available(changeSignatureAvailableTooltip("foo", "implementations")),
expectedAvailabilityAfterResolve = Availability.NotAvailable
)
}
fun testImportAnotherType() {
doTest(
"""
import java.util.Date
interface I {
fun foo(): <caret>Date
}
""".trimIndent(),
{
replaceTextAtCaret("Date", "java.sql.Date")
},
{
removeImport("java.util.Date")
addImport("java.sql.Date")
},
{
replaceTextAtCaret("java.sql.Date", "Date")
},
expectedAvailability = Availability.NotAvailable,
expectedAvailabilityAfterResolve = Availability.Available((changeSignatureAvailableTooltip("foo", "implementations")))
)
}
private fun addImport(fqName: String) {
(file as KtFile).importList!!.add(KtPsiFactory(project).createImportDirective(ImportPath.fromString(fqName)))
}
private fun removeImport(fqName: String) {
(file as KtFile).importList!!.imports.first { it.importedFqName?.asString() == fqName }.delete()
}
}
@@ -0,0 +1,201 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.refactoring.suggested
import com.intellij.lang.Language
import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.command.executeCommand
import com.intellij.openapi.fileTypes.FileType
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.refactoring.suggested.SuggestedRefactoringSupport.Parameter
import com.intellij.refactoring.suggested.SuggestedRefactoringSupport.Signature
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.KotlinLanguage
import com.intellij.refactoring.suggested.BaseSuggestedRefactoringChangeCollectorTest
import org.jetbrains.kotlin.psi.*
class KotlinSuggestedRefactoringChangeCollectorTest : BaseSuggestedRefactoringChangeCollectorTest<KtNamedFunction>() {
override val fileType: FileType
get() = KotlinFileType.INSTANCE
override val language: Language
get() = KotlinLanguage.INSTANCE
override fun addDeclaration(file: PsiFile, text: String): KtNamedFunction {
val psiFactory = KtPsiFactory(project)
return (file as KtFile).add(psiFactory.createDeclaration(text)) as KtNamedFunction
}
override fun Signature.presentation(labelForParameterId: (Any) -> String?): String {
return buildString {
append("fun ")
val receiverType = (additionalData as KotlinSignatureAdditionalData?)?.receiverType
if (receiverType != null) {
append(receiverType)
append(".")
}
append(name)
append("(")
parameters.joinTo(this, separator = ", ") { it.presentation(labelForParameterId(it.id)) }
append(")")
if (type != null) {
append(": ")
append(type)
}
}
}
private fun Parameter.presentation(label: String?): String {
return buildString {
if (modifiers.isNotEmpty()) {
append(modifiers)
append(" ")
}
append(name)
append(": ")
append(type)
if (label != null) {
append(" (")
append(label)
append(")")
}
}
}
private fun createType(text: String): KtTypeReference {
return KtPsiFactory(project).createType(text)
}
private fun createParameter(text: String): KtParameter {
return KtPsiFactory(project).createParameter(text)
}
fun testAddParameter() {
doTest(
"fun foo(p1: Int) {}",
{ it.valueParameterList!!.addParameter(createParameter("p2: Int")) },
expectedOldSignature = "fun foo(p1: Int)",
expectedNewSignature = "fun foo(p1: Int (initialIndex = 0), p2: Int (new))"
)
}
fun testRemoveParameter() {
doTest(
"fun foo(p1: Int, p2: Int) {}",
{ it.valueParameterList!!.removeParameter(0) },
expectedOldSignature = "fun foo(p1: Int, p2: Int)",
expectedNewSignature = "fun foo(p2: Int (initialIndex = 1))"
)
}
fun testChangeParameterType() {
doTest(
"fun foo(p1: Int, p2: Int) {}",
{ it.valueParameters[1].typeReference = createType("Any?") },
expectedOldSignature = "fun foo(p1: Int, p2: Int)",
expectedNewSignature = "fun foo(p1: Int (initialIndex = 0), p2: Any? (initialIndex = 1))"
)
}
fun testChangeParameterNames() {
doTest(
"fun foo(p1: Int, p2: Int) {}",
{ it.valueParameters[0].setName("newP1") },
{ it.valueParameters[1].setName("newP2") },
expectedOldSignature = "fun foo(p1: Int, p2: Int)",
expectedNewSignature = "fun foo(newP1: Int (initialIndex = 0), newP2: Int (initialIndex = 1))"
)
}
fun testReplaceParameter() {
doTest(
"fun foo(p1: Int, p2: Int) {}",
{ it.valueParameters[0].replace(createParameter("newP1: Long")) },
expectedOldSignature = "fun foo(p1: Int, p2: Int)",
expectedNewSignature = "fun foo(newP1: Long (new), p2: Int (initialIndex = 1))"
)
}
fun testReorderParametersChangeTypesAndNames() {
doTest(
"fun foo(p1: Int, p2: Int, p3: Int) {}",
{
editor.caretModel.moveToOffset(it.valueParameters[2].textOffset)
myFixture.performEditorAction(IdeActions.MOVE_ELEMENT_LEFT)
myFixture.performEditorAction(IdeActions.MOVE_ELEMENT_LEFT)
},
{
executeCommand {
runWriteAction {
it.valueParameters[0].typeReference = createType("Any?")
it.valueParameters[1].typeReference = createType("Long")
it.valueParameters[2].typeReference = createType("Double")
}
}
},
{
executeCommand {
runWriteAction {
it.valueParameters[1].setName("newName")
}
}
},
wrapIntoCommandAndWriteAction = false,
expectedOldSignature = "fun foo(p1: Int, p2: Int, p3: Int)",
expectedNewSignature = "fun foo(p3: Any? (initialIndex = 2), newName: Long (initialIndex = 0), p2: Double (initialIndex = 1))"
)
}
fun testReorderParametersByCutPaste() {
doTest(
"fun foo(p1: Int, p2: String, p3: Char)",
{
val offset = it.valueParameters[1].textRange.endOffset
editor.caretModel.moveToOffset(offset)
editor.selectionModel.setSelection(offset, offset + ", p3: Char".length)
myFixture.performEditorAction(IdeActions.ACTION_EDITOR_CUT)
},
{
val offset = it.valueParameters[0].textRange.endOffset
editor.caretModel.moveToOffset(offset)
myFixture.performEditorAction(IdeActions.ACTION_EDITOR_PASTE)
},
wrapIntoCommandAndWriteAction = false,
expectedOldSignature = "fun foo(p1: Int, p2: String, p3: Char)",
expectedNewSignature = "fun foo(p1: Int (initialIndex = 0), p3: Char (initialIndex = 2), p2: String (initialIndex = 1))"
)
}
fun testReorderParametersByCutPasteAfterChangingName() {
doTest(
"fun foo(p1: Int, p2: String, p3: Char)",
{
executeCommand {
runWriteAction {
it.valueParameters[2].setName("p3New")
PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document)
}
}
},
{
val offset = it.valueParameters[1].textRange.endOffset
editor.caretModel.moveToOffset(offset)
editor.selectionModel.setSelection(offset, offset + ", p3New: Char".length)
myFixture.performEditorAction(IdeActions.ACTION_EDITOR_CUT)
},
{
val offset = it.valueParameters[0].textRange.endOffset
editor.caretModel.moveToOffset(offset)
myFixture.performEditorAction(IdeActions.ACTION_EDITOR_PASTE)
},
wrapIntoCommandAndWriteAction = false,
expectedOldSignature = "fun foo(p1: Int, p2: String, p3: Char)",
expectedNewSignature = "fun foo(p1: Int (initialIndex = 0), p3New: Char (initialIndex = 2), p2: String (initialIndex = 1))"
)
}
}
@@ -0,0 +1,395 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.refactoring.suggested
import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.command.executeCommand
import com.intellij.openapi.fileTypes.FileType
import org.jetbrains.kotlin.idea.KotlinFileType
import com.intellij.refactoring.suggested.BaseSuggestedRefactoringChangeListenerTest
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.findDescendantOfType
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.resolve.ImportPath
class KotlinSuggestedRefactoringChangeListenerTest : BaseSuggestedRefactoringChangeListenerTest() {
override val fileType: FileType
get() = KotlinFileType.INSTANCE
fun test1() {
setup("fun foo(<caret>) {}")
perform("editingStarted: 'foo()'") { myFixture.type("p") }
perform("nextSignature: 'foo(p)'") { commitAll() }
perform { myFixture.type(":") }
perform { myFixture.type(" S") }
perform { myFixture.type("tr") }
perform("nextSignature: 'foo(p: Str)'") { commitAll() }
perform { myFixture.type("ing") }
perform("nextSignature: 'foo(p: String)'") { commitAll() }
perform {
perform { myFixture.type(", ") }
commitAll()
}
}
fun testCompletion() {
setup("fun foo(<caret>) {}")
perform("editingStarted: 'foo()'") { myFixture.type("p: DoubleArra") }
perform("nextSignature: 'foo(p: DoubleArra)'", "nextSignature: 'foo(p: DoubleArray)'") { myFixture.completeBasic() }
}
fun testChangeOutsideSignature() {
setup("fun foo(<caret>) {}")
perform("editingStarted: 'foo()'") { myFixture.type("p: A") }
perform("reset") {
insertString(editor.document.textLength, "\nval")
}
}
fun testEditOtherSignature() {
setup("fun foo(<caret>) {}\nfun bar() = 0")
val otherFunction = (file as KtFile).declarations[1] as KtNamedFunction
val offset = otherFunction.valueParameterList!!.startOffset + 1
val marker = editor.document.createRangeMarker(offset, offset)
perform("editingStarted: 'foo()'") { myFixture.type("p: A") }
perform("nextSignature: 'foo(p: A)'") { commitAll() }
perform("reset", "editingStarted: 'bar()'", "nextSignature: 'bar(p1: String)'") {
assert(marker.isValid)
insertString(marker.startOffset, "p1: String")
commitAll()
}
}
fun testChangeInAnotherFile() {
setup("fun foo(<caret>) {}")
perform("editingStarted: 'foo()'") { myFixture.type("p: A") }
perform("reset") {
setup("")
myFixture.type(" ")
}
}
fun testAddImport() {
setup("fun foo(<caret>) {}")
perform("editingStarted: 'foo()'", "nextSignature: 'foo(p: Any)'") {
myFixture.type("p: Any")
commitAll()
}
perform("nextSignature: 'foo(p: Any)'", "nextSignature: 'foo(p: Any)'") {
addImport("java.util.ArrayList")
}
perform("nextSignature: 'foo(p: Any, p2: String)'") {
myFixture.type(", p2: String")
commitAll()
}
perform("nextSignature: 'foo(p: Any, p2: String)'", "nextSignature: 'foo(p: Any, p2: String)'") {
addImport("java.util.Date")
}
}
fun testAddImportWithBlankLineInsertion() {
setup(
"""
import foo.bar
fun foo(<caret>) {}
""".trimIndent()
)
perform("editingStarted: 'foo()'", "nextSignature: 'foo(p: ArrayList)'") {
myFixture.type("p: ArrayList")
commitAll()
}
perform("nextSignature: 'foo(p: ArrayList)'", "nextSignature: 'foo(p: ArrayList)'") {
addImport("java.util.ArrayList")
}
perform("nextSignature: 'foo(p: ArrayList<String>)'") {
myFixture.type("<String>")
commitAll()
}
perform("nextSignature: 'foo(p: ArrayList<String>, p2: Any)'") {
myFixture.type(", p2: Any")
commitAll()
}
}
fun testAddImportWithBlankLinesRemoval() {
setup(
"""
import foo.bar
fun foo(<caret>) {}
""".trimIndent()
)
perform("editingStarted: 'foo()'", "nextSignature: 'foo(p: ArrayList)'") {
myFixture.type("p: ArrayList")
commitAll()
}
perform("nextSignature: 'foo(p: ArrayList)'", "nextSignature: 'foo(p: ArrayList)'") {
addImport("java.util.ArrayList")
}
perform("nextSignature: 'foo(p: ArrayList<String>)'") {
myFixture.type("<String>")
commitAll()
}
perform("nextSignature: 'foo(p: ArrayList<String>, p2: Any)'") {
myFixture.type(", p2: Any")
commitAll()
}
}
fun testReorderParameters() {
setup("fun foo(p1: String, p2: Any, p3<caret>: Int) {}")
perform("editingStarted: 'foo(p1: String, p2: Any, p3: Int)'") {
myFixture.performEditorAction(IdeActions.MOVE_ELEMENT_LEFT)
}
perform("nextSignature: 'foo(p1: String, p3: Int, p2: Any)'") {
myFixture.performEditorAction(IdeActions.MOVE_ELEMENT_LEFT)
}
perform("nextSignature: 'foo(p3: Int, p1: String, p2: Any)'") {
commitAll()
}
perform("nextSignature: 'foo(p1: String, p3: Int, p2: Any)'") {
myFixture.performEditorAction(IdeActions.MOVE_ELEMENT_RIGHT)
commitAll()
}
}
fun testAddParameterViaPsi() {
setup("fun foo(p1: Int) {}")
val function = (file as KtFile).declarations.single() as KtFunction
perform(
"editingStarted: 'foo(p1: Int)'",
"nextSignature: 'foo(p1: Int,)'",
"nextSignature: 'foo(p1: Int,p2: Int)'",
"nextSignature: 'foo(p1: Int, p2: Int)'"
) {
executeCommand {
runWriteAction {
function.valueParameterList!!.addParameter(KtPsiFactory(project).createParameter("p2: Int"))
}
}
}
}
fun testCommentTyping() {
setup("fun foo(<caret>) {}")
perform("editingStarted: 'foo()'", "nextSignature: 'foo(p1: Any)'") {
myFixture.type("p1: Any")
commitAll()
}
perform {
myFixture.type("/*")
commitAll()
}
perform {
myFixture.type(" this is comment for parameter")
commitAll()
}
perform("nextSignature: 'foo(p1: Any/* this is comment for parameter*/)'") {
myFixture.type("*/")
commitAll()
}
perform {
myFixture.type(", p2: Int /*")
commitAll()
}
perform {
myFixture.type("this is comment for another parameter")
commitAll()
}
perform("nextSignature: 'foo(p1: Any/* this is comment for parameter*/, p2: Int /*this is comment for another parameter*/)'") {
myFixture.type("*/")
commitAll()
}
}
fun testAddReturnType() {
setup(
"""
interface I {
fun foo()<caret>
}
""".trimIndent()
)
perform("editingStarted: 'foo()'") { myFixture.type(": String") }
perform("nextSignature: 'foo(): String'") { commitAll() }
}
fun testNewLocal() {
setup(
"""
fun foo() {
<caret>
print(a)
}
""".trimIndent()
)
perform {
myFixture.type("val a")
commitAll()
myFixture.type("bcd")
commitAll()
}
}
fun testNewFunction() {
setup(
"""
interface I {
<caret>
}
""".trimIndent()
)
perform {
myFixture.type("fun foo_bar123(_p1: Int)")
commitAll()
}
}
fun testNewProperty() {
setup(
"""
interface I {
<caret>
}
""".trimIndent()
)
perform {
myFixture.type("val prop: I")
commitAll()
myFixture.type("nt")
commitAll()
}
}
fun testNewLocalWithNewUsage() {
setup(
"""
fun foo() {
<caret>
}
""".trimIndent()
)
perform {
myFixture.type("val a = 10")
myFixture.performEditorAction(IdeActions.ACTION_EDITOR_ENTER)
myFixture.type("print(a)")
commitAll()
}
perform("editingStarted: 'a'", "nextSignature: 'abcd'") {
val variable = file.findDescendantOfType<KtProperty>()!!
myFixture.editor.caretModel.moveToOffset(variable.nameIdentifier!!.endOffset)
myFixture.type("bcd")
commitAll()
}
}
fun testNewLocalBeforeExpression() {
setup(
"""
fun foo(p: Int) {
<caret>p * p
}
""".trimIndent()
)
perform {
myFixture.type("val a")
commitAll()
}
perform {
myFixture.type("bcd = ")
commitAll()
}
}
fun testNewClassWithConstructor() {
setup("")
perform {
myFixture.type("class C")
commitAll()
}
perform {
myFixture.type("(p: Int)")
commitAll()
}
}
fun testNewSecondaryConstructor() {
setup(
"""
class C {
<caret>
}
""".trimIndent()
)
perform {
myFixture.type("constructor(p1: Int)")
commitAll()
}
perform {
myFixture.type("(, p2: String)")
commitAll()
}
}
fun testRenameComponentVar() {
setup(
"""
fun f() {
val (<caret>a, b) = f()
}
""".trimIndent()
)
perform("editingStarted: 'a'", "nextSignature: 'newa'") {
myFixture.type("new")
commitAll()
}
}
private fun addImport(fqName: String) {
executeCommand {
runWriteAction {
(file as KtFile).importList!!.add(KtPsiFactory(project).createImportDirective(ImportPath.fromString(fqName)))
}
}
}
}