From 31a57467245b95b67907228d007e33dfb3ee253b Mon Sep 17 00:00:00 2001 From: Nicolay Mitropolsky Date: Fri, 10 Aug 2018 14:49:59 +0300 Subject: [PATCH] 183: implementation of `JvmElementActionsFactory.createChangeParametersActions` --- .../ChangeMethodParameters.kt.183 | 100 +++ .../KotlinElementActionsFactory.kt.183 | 490 ++++++++++++++ .../CommonIntentionActionsTest.kt.183 | 617 ++++++++++++++++++ 3 files changed, 1207 insertions(+) create mode 100644 idea/src/org/jetbrains/kotlin/idea/quickfix/crossLanguage/ChangeMethodParameters.kt.183 create mode 100644 idea/src/org/jetbrains/kotlin/idea/quickfix/crossLanguage/KotlinElementActionsFactory.kt.183 create mode 100644 idea/tests/org/jetbrains/kotlin/idea/quickfix/CommonIntentionActionsTest.kt.183 diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/crossLanguage/ChangeMethodParameters.kt.183 b/idea/src/org/jetbrains/kotlin/idea/quickfix/crossLanguage/ChangeMethodParameters.kt.183 new file mode 100644 index 00000000000..5890fc02762 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/crossLanguage/ChangeMethodParameters.kt.183 @@ -0,0 +1,100 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. 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.quickfix.crossLanguage + +import com.intellij.codeInsight.daemon.QuickFixBundle +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.StringUtil +import org.jetbrains.kotlin.descriptors.SourceElement +import org.jetbrains.kotlin.descriptors.annotations.Annotations +import org.jetbrains.kotlin.descriptors.impl.SimpleFunctionDescriptorImpl +import org.jetbrains.kotlin.descriptors.impl.ValueParameterDescriptorImpl +import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny +import org.jetbrains.kotlin.idea.core.ShortenReferences +import org.jetbrains.kotlin.idea.quickfix.KotlinQuickFixAction +import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers +import org.jetbrains.kotlin.js.descriptorUtils.getJetTypeFqName +import org.jetbrains.kotlin.load.java.NOT_NULL_ANNOTATIONS +import org.jetbrains.kotlin.load.java.NULLABLE_ANNOTATIONS +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.KtParameterList +import org.jetbrains.kotlin.psi.KtPsiFactory +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.types.KotlinType + +internal class ChangeMethodParameters( + target: KtNamedFunction, + private val request: List>, + private val isValid: () -> Boolean +) : KotlinQuickFixAction(target) { + + override fun getText(): String { + val parametersString = request.joinToString(", ", "(", ")") { (name, type) -> + "${name.asString()}: ${IdeDescriptorRenderers.SOURCE_CODE_SHORT_NAMES_NO_ANNOTATIONS.renderType(type)}" + } + + val shortenParameterString = StringUtil.shortenTextWithEllipsis(parametersString, 30, 5) + return QuickFixBundle.message("change.method.parameters.text", shortenParameterString) + } + + override fun getFamilyName(): String = QuickFixBundle.message("change.method.parameters.family") + + override fun isAvailable(project: Project, editor: Editor?, file: KtFile): Boolean = element != null && isValid() + + override fun invoke(project: Project, editor: Editor?, file: KtFile) { + val target = element ?: return + val functionDescriptor = target.resolveToDescriptorIfAny(BodyResolveMode.FULL) ?: return + + val newFunctionDescriptor = SimpleFunctionDescriptorImpl.create( + functionDescriptor.containingDeclaration, + functionDescriptor.annotations, + functionDescriptor.name, + functionDescriptor.kind, + SourceElement.NO_SOURCE + ).apply { + initialize( + functionDescriptor.extensionReceiverParameter?.type, + functionDescriptor.dispatchReceiverParameter, + functionDescriptor.typeParameters, + request.withIndex().map { (index, parameter) -> + ValueParameterDescriptorImpl( + this, null, index, Annotations.EMPTY, + Name.identifier(parameter.first.toString()), + parameter.second, false, + false, false, null, SourceElement.NO_SOURCE + ) + }, + functionDescriptor.returnType, + functionDescriptor.modality, + functionDescriptor.visibility + ) + } + + + val renderer = IdeDescriptorRenderers.SOURCE_CODE.withOptions { + defaultParameterValueRenderer = null + } + + val newFunction = KtPsiFactory(project).createFunction(renderer.render(newFunctionDescriptor)).apply { + valueParameters.forEach { param -> + param.annotationEntries.forEach { a -> + a.typeReference?.run { + val fqName = FqName(this.text) + if (fqName in (NULLABLE_ANNOTATIONS + NOT_NULL_ANNOTATIONS)) a.delete() + } + } + } + } + + val newParameterList = target.valueParameterList!!.replace(newFunction.valueParameterList!!) as KtParameterList + ShortenReferences.DEFAULT.process(newParameterList) + } + + +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/crossLanguage/KotlinElementActionsFactory.kt.183 b/idea/src/org/jetbrains/kotlin/idea/quickfix/crossLanguage/KotlinElementActionsFactory.kt.183 new file mode 100644 index 00000000000..61ae834bc87 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/crossLanguage/KotlinElementActionsFactory.kt.183 @@ -0,0 +1,490 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.quickfix.crossLanguage + +import com.intellij.codeInsight.daemon.QuickFixBundle +import com.intellij.codeInsight.daemon.impl.quickfix.CreateParameterFromUsageFix +import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.codeInsight.intention.QuickFixFactory +import com.intellij.lang.jvm.* +import com.intellij.lang.jvm.actions.* +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.StringUtilRt +import com.intellij.psi.* +import com.intellij.psi.codeStyle.SuggestedNameInfo +import com.intellij.psi.impl.source.tree.java.PsiReferenceExpressionImpl +import org.jetbrains.kotlin.asJava.classes.KtLightClassForFacade +import org.jetbrains.kotlin.asJava.classes.KtLightClassForSourceDeclaration +import org.jetbrains.kotlin.asJava.elements.KtLightElement +import org.jetbrains.kotlin.asJava.toLightMethods +import org.jetbrains.kotlin.asJava.unwrapped +import org.jetbrains.kotlin.caches.resolve.KotlinCacheService +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.descriptors.SourceElement +import org.jetbrains.kotlin.descriptors.TypeParameterDescriptor +import org.jetbrains.kotlin.descriptors.impl.ClassDescriptorImpl +import org.jetbrains.kotlin.descriptors.impl.MutablePackageFragmentDescriptor +import org.jetbrains.kotlin.idea.KotlinLanguage +import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade +import org.jetbrains.kotlin.idea.core.ShortenReferences +import org.jetbrains.kotlin.idea.core.appendModifier +import org.jetbrains.kotlin.idea.quickfix.AddModifierFix +import org.jetbrains.kotlin.idea.quickfix.RemoveModifierFix +import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.* +import org.jetbrains.kotlin.idea.quickfix.createFromUsage.createCallable.CreateCallableFromUsageFix +import org.jetbrains.kotlin.idea.resolve.ResolutionFacade +import org.jetbrains.kotlin.idea.util.approximateFlexibleTypes +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.load.java.components.TypeUsage +import org.jetbrains.kotlin.load.java.lazy.JavaResolverComponents +import org.jetbrains.kotlin.load.java.lazy.LazyJavaResolverContext +import org.jetbrains.kotlin.load.java.lazy.TypeParameterResolver +import org.jetbrains.kotlin.load.java.lazy.child +import org.jetbrains.kotlin.load.java.lazy.descriptors.LazyJavaTypeParameterDescriptor +import org.jetbrains.kotlin.load.java.lazy.types.JavaTypeAttributes +import org.jetbrains.kotlin.load.java.lazy.types.JavaTypeResolver +import org.jetbrains.kotlin.load.java.structure.JavaTypeParameter +import org.jetbrains.kotlin.load.java.structure.impl.JavaTypeImpl +import org.jetbrains.kotlin.load.java.structure.impl.JavaTypeParameterImpl +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.createSmartPointer +import org.jetbrains.kotlin.psi.psiUtil.visibilityModifierType +import org.jetbrains.kotlin.resolve.annotations.JVM_FIELD_ANNOTATION_FQ_NAME +import org.jetbrains.kotlin.resolve.annotations.JVM_STATIC_ANNOTATION_FQ_NAME +import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatform +import org.jetbrains.kotlin.storage.LockBasedStorageManager +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.Variance +import org.jetbrains.kotlin.types.typeUtil.supertypes + +class KotlinElementActionsFactory : JvmElementActionsFactory() { + companion object { + val javaPsiModifiersMapping = mapOf( + JvmModifier.PRIVATE to KtTokens.PRIVATE_KEYWORD, + JvmModifier.PUBLIC to KtTokens.PUBLIC_KEYWORD, + JvmModifier.PROTECTED to KtTokens.PUBLIC_KEYWORD, + JvmModifier.ABSTRACT to KtTokens.ABSTRACT_KEYWORD + ) + } + + private class FakeExpressionFromParameter(private val psiParam: PsiParameter) : PsiReferenceExpressionImpl() { + override fun getText(): String = psiParam.name!! + override fun getProject(): Project = psiParam.project + override fun getParent(): PsiElement = psiParam.parent + override fun getType(): PsiType? = psiParam.type + override fun isValid(): Boolean = true + override fun getContainingFile(): PsiFile = psiParam.containingFile + override fun getReferenceName(): String? = psiParam.name + override fun resolve(): PsiElement? = psiParam + } + + private class ModifierBuilder( + private val targetContainer: KtElement, + private val allowJvmStatic: Boolean = true + ) { + private val psiFactory = KtPsiFactory(targetContainer.project) + + val modifierList = psiFactory.createEmptyModifierList() + + private fun JvmModifier.transformAndAppend(): Boolean { + javaPsiModifiersMapping[this]?.let { + modifierList.appendModifier(it) + return true + } + + when (this) { + JvmModifier.STATIC -> { + if (allowJvmStatic && targetContainer is KtClassOrObject) { + addAnnotation(JVM_STATIC_ANNOTATION_FQ_NAME) + } + } + JvmModifier.ABSTRACT -> modifierList.appendModifier(KtTokens.ABSTRACT_KEYWORD) + JvmModifier.FINAL -> modifierList.appendModifier(KtTokens.FINAL_KEYWORD) + else -> return false + } + + return true + } + + var isValid = true + private set + + fun addJvmModifier(modifier: JvmModifier) { + isValid = isValid && modifier.transformAndAppend() + } + + fun addJvmModifiers(modifiers: Iterable) { + modifiers.forEach { addJvmModifier(it) } + } + + fun addAnnotation(fqName: FqName) { + if (!isValid) return + modifierList.add(psiFactory.createAnnotationEntry("@${fqName.asString()}")) + } + } + + class CreatePropertyFix( + private val targetClass: JvmClass, + contextElement: KtElement, + propertyInfo: PropertyInfo + ) : CreateCallableFromUsageFix(contextElement, listOf(propertyInfo)) { + override fun getFamilyName() = "Add property" + override fun getText(): String { + val info = callableInfos.first() as PropertyInfo + return buildString { + append("Add '") + if (info.isLateinitPreferred || info.modifierList?.hasModifier(KtTokens.LATEINIT_KEYWORD) == true) { + append("lateinit ") + } + append(if (info.writable) "var" else "val") + append("' property '${info.name}' to '${targetClass.name}'") + } + } + } + + private fun JvmClass.toKtClassOrFile(): KtElement? { + val psi = sourceElement + return when (psi) { + is KtClassOrObject -> psi + is KtLightClassForSourceDeclaration -> psi.kotlinOrigin + is KtLightClassForFacade -> psi.files.firstOrNull() + else -> null + } + } + + private inline fun JvmElement.toKtElement() = sourceElement?.unwrapped as? T + + private fun fakeParametersExpressions(parameters: List>>, project: Project): Array? = + when { + parameters.isEmpty() -> emptyArray() + else -> JavaPsiFacade + .getElementFactory(project) + .createParameterList( + parameters.map { it.first.names.firstOrNull() }.toTypedArray(), + parameters.map { JvmPsiConversionHelper.getInstance(project).asPsiType(it) ?: return null }.toTypedArray() + ) + .parameters + .map(::FakeExpressionFromParameter) + .toTypedArray() + } + + private fun PsiType.collectTypeParameters(): List { + val results = ArrayList() + accept( + object : PsiTypeVisitor() { + override fun visitArrayType(arrayType: PsiArrayType) { + arrayType.componentType.accept(this) + } + + override fun visitClassType(classType: PsiClassType) { + (classType.resolve() as? PsiTypeParameter)?.let { results += it } + classType.parameters.forEach { it.accept(this) } + } + + override fun visitWildcardType(wildcardType: PsiWildcardType) { + wildcardType.bound?.accept(this) + } + } + ) + return results + } + + private fun PsiType.resolveToKotlinType(resolutionFacade: ResolutionFacade): KotlinType? { + val typeParameters = collectTypeParameters() + val components = resolutionFacade.getFrontendService(JavaResolverComponents::class.java) + val rootContext = LazyJavaResolverContext(components, TypeParameterResolver.EMPTY) { null } + val dummyPackageDescriptor = MutablePackageFragmentDescriptor(resolutionFacade.moduleDescriptor, FqName("dummy")) + val dummyClassDescriptor = ClassDescriptorImpl( + dummyPackageDescriptor, + Name.identifier("Dummy"), + Modality.FINAL, + ClassKind.CLASS, + emptyList(), + SourceElement.NO_SOURCE, + false, + LockBasedStorageManager.NO_LOCKS + ) + val typeParameterResolver = object : TypeParameterResolver { + override fun resolveTypeParameter(javaTypeParameter: JavaTypeParameter): TypeParameterDescriptor? { + val psiTypeParameter = (javaTypeParameter as JavaTypeParameterImpl).psi + val index = typeParameters.indexOf(psiTypeParameter) + if (index < 0) return null + return LazyJavaTypeParameterDescriptor(rootContext.child(this), javaTypeParameter, index, dummyClassDescriptor) + } + } + val typeResolver = JavaTypeResolver(rootContext, typeParameterResolver) + val attributes = JavaTypeAttributes(TypeUsage.COMMON) + return typeResolver.transformJavaType(JavaTypeImpl.create(this), attributes).approximateFlexibleTypes(preferNotNull = true) + } + + private fun ExpectedTypes.toKotlinTypeInfo(resolutionFacade: ResolutionFacade): TypeInfo { + val candidateTypes = flatMapTo(LinkedHashSet()) { + val ktType = (it.theType as? PsiType)?.resolveToKotlinType(resolutionFacade) ?: return@flatMapTo emptyList() + when (it.theKind) { + ExpectedType.Kind.EXACT, ExpectedType.Kind.SUBTYPE -> listOf(ktType) + ExpectedType.Kind.SUPERTYPE -> listOf(ktType) + ktType.supertypes() + } + } + if (candidateTypes.isEmpty()) { + val nullableAnyType = resolutionFacade.moduleDescriptor.builtIns.nullableAnyType + return TypeInfo(nullableAnyType, Variance.INVARIANT) + } + return TypeInfo.ByExplicitCandidateTypes(candidateTypes.toList()) + } + + override fun createChangeModifierActions(target: JvmModifiersOwner, request: MemberRequest.Modifier): List { + val kModifierOwner = target.toKtElement() ?: return emptyList() + + val modifier = request.modifier + val shouldPresent = request.shouldPresent + //TODO: make similar to `createAddMethodActions` + val (kToken, shouldPresentMapped) = when { + modifier == JvmModifier.FINAL -> KtTokens.OPEN_KEYWORD to !shouldPresent + modifier == JvmModifier.PUBLIC && shouldPresent -> + kModifierOwner.visibilityModifierType() + ?.takeIf { it != KtTokens.DEFAULT_VISIBILITY_KEYWORD } + ?.let { it to false } ?: return emptyList() + else -> javaPsiModifiersMapping[modifier] to shouldPresent + } + if (kToken == null) return emptyList() + + val action = if (shouldPresentMapped) + AddModifierFix.createIfApplicable(kModifierOwner, kToken) + else + RemoveModifierFix(kModifierOwner, kToken, false) + return listOfNotNull(action) + } + + override fun createAddConstructorActions(targetClass: JvmClass, request: CreateConstructorRequest): List { + val targetKtClass = targetClass.toKtClassOrFile() as? KtClass ?: return emptyList() + + val modifierBuilder = ModifierBuilder(targetKtClass).apply { addJvmModifiers(request.modifiers) } + if (!modifierBuilder.isValid) return emptyList() + val resolutionFacade = targetKtClass.getResolutionFacade() + val nullableAnyType = resolutionFacade.moduleDescriptor.builtIns.nullableAnyType + val helper = JvmPsiConversionHelper.getInstance(targetKtClass.project) + val parameters = request.parameters as List>> + val parameterInfos = parameters.mapIndexed { index, param: Pair> -> + val ktType = helper.asPsiType(param)?.resolveToKotlinType(resolutionFacade) ?: nullableAnyType + val name = param.first.names.firstOrNull() ?: "arg${index + 1}" + ParameterInfo(TypeInfo(ktType, Variance.IN_VARIANCE), listOf(name)) + } + val needPrimary = !targetKtClass.hasExplicitPrimaryConstructor() + val constructorInfo = ConstructorInfo( + parameterInfos, + targetKtClass, + isPrimary = needPrimary, + modifierList = modifierBuilder.modifierList, + withBody = true + ) + val targetClassName = targetClass.name + val addConstructorAction = object : CreateCallableFromUsageFix(targetKtClass, listOf(constructorInfo)) { + override fun getFamilyName() = "Add method" + override fun getText() = "Add ${if (needPrimary) "primary" else "secondary"} constructor to '$targetClassName'" + } + + val changePrimaryConstructorAction = run { + val primaryConstructor = targetKtClass.primaryConstructor ?: return@run null + val lightMethod = primaryConstructor.toLightMethods().firstOrNull() ?: return@run null + val project = targetKtClass.project + val fakeParametersExpressions = fakeParametersExpressions(parameters, project) ?: return@run null + QuickFixFactory.getInstance() + .createChangeMethodSignatureFromUsageFix( + lightMethod, + fakeParametersExpressions, + PsiSubstitutor.EMPTY, + targetKtClass, + false, + 2 + ).takeIf { it.isAvailable(project, null, targetKtClass.containingFile) } + } + + return listOfNotNull(changePrimaryConstructorAction, addConstructorAction) + } + + override fun createAddPropertyActions(targetClass: JvmClass, request: MemberRequest.Property): List { + val targetContainer = targetClass.toKtClassOrFile() ?: return emptyList() + + val modifierBuilder = ModifierBuilder(targetContainer).apply { addJvmModifier(request.visibilityModifier) } + if (!modifierBuilder.isValid) return emptyList() + + val resolutionFacade = targetContainer.getResolutionFacade() + val nullableAnyType = resolutionFacade.moduleDescriptor.builtIns.nullableAnyType + val ktType = (request.propertyType as? PsiType)?.resolveToKotlinType(resolutionFacade) ?: nullableAnyType + val propertyInfo = PropertyInfo( + request.propertyName, + TypeInfo.Empty, + TypeInfo(ktType, Variance.INVARIANT), + request.setterRequired, + listOf(targetContainer), + modifierList = modifierBuilder.modifierList, + withInitializer = true + ) + val propertyInfos = if (request.setterRequired) { + listOf(propertyInfo, propertyInfo.copyProperty(isLateinitPreferred = true)) + } + else { + listOf(propertyInfo) + } + return propertyInfos.map { CreatePropertyFix(targetClass, targetContainer, it) } + } + + override fun createAddFieldActions(targetClass: JvmClass, request: CreateFieldRequest): List { + val targetContainer = targetClass.toKtClassOrFile() ?: return emptyList() + + val resolutionFacade = targetContainer.getResolutionFacade() + val typeInfo = request.fieldType.toKotlinTypeInfo(resolutionFacade) + val writable = JvmModifier.FINAL !in request.modifiers + + fun propertyInfo(lateinit: Boolean) = PropertyInfo( + request.fieldName, + TypeInfo.Empty, + typeInfo, + writable, + listOf(targetContainer), + isLateinitPreferred = false, // Dont set it to `lateinit` because it works via templates that brings issues in batch field adding + isForCompanion = JvmModifier.STATIC in request.modifiers, + modifierList = ModifierBuilder(targetContainer, allowJvmStatic = false).apply { + addJvmModifiers(request.modifiers) + if (modifierList.children.none { it.node.elementType in KtTokens.VISIBILITY_MODIFIERS }) + addJvmModifier(JvmModifier.PUBLIC) + if (lateinit) + modifierList.appendModifier(KtTokens.LATEINIT_KEYWORD) + if (!request.modifiers.contains(JvmModifier.PRIVATE) && !lateinit) + addAnnotation(JVM_FIELD_ANNOTATION_FQ_NAME) + }.modifierList, + withInitializer = !lateinit + ) + + val propertyInfos = if (writable) { + listOf(propertyInfo(false), propertyInfo(true)) + } + else { + listOf(propertyInfo(false)) + } + return propertyInfos.map { CreatePropertyFix(targetClass, targetContainer, it) } + } + + override fun createAddMethodActions(targetClass: JvmClass, request: CreateMethodRequest): List { + val targetContainer = targetClass.toKtClassOrFile() ?: return emptyList() + + val modifierBuilder = ModifierBuilder(targetContainer).apply { addJvmModifiers(request.modifiers) } + if (!modifierBuilder.isValid) return emptyList() + + val resolutionFacade = KotlinCacheService.getInstance(targetContainer.project) + .getResolutionFacadeByFile(targetContainer.containingFile, JvmPlatform) ?: return emptyList() + val returnTypeInfo = request.returnType.toKotlinTypeInfo(resolutionFacade) + val parameters = request.parameters as List>> + val parameterInfos = parameters.map { (suggestedNames, expectedTypes) -> + ParameterInfo(expectedTypes.toKotlinTypeInfo(resolutionFacade), suggestedNames.names.toList()) + } + val methodName = request.methodName + val functionInfo = FunctionInfo( + methodName, + TypeInfo.Empty, + returnTypeInfo, + listOf(targetContainer), + parameterInfos, + isForCompanion = JvmModifier.STATIC in request.modifiers, + modifierList = modifierBuilder.modifierList, + preferEmptyBody = true + ) + val targetClassName = targetClass.name + val action = object : CreateCallableFromUsageFix(targetContainer, listOf(functionInfo)) { + override fun getFamilyName() = "Add method" + override fun getText() = "Add method '$methodName' to '$targetClassName'" + } + return listOf(action) + } + + override fun createAddAnnotationActions(target: JvmModifiersOwner, request: AnnotationRequest): List { + val declaration = (target as? KtLightElement<*, *>)?.kotlinOrigin as? KtModifierListOwner ?: return emptyList() + if (declaration.language != KotlinLanguage.INSTANCE) return emptyList() + return listOf(CreateAnnotationAction(declaration, request)) + } + + private class CreateAnnotationAction( + target: KtModifierListOwner, + val request: AnnotationRequest + ) : IntentionAction { + + private val pointer = target.createSmartPointer() + + override fun startInWriteAction(): Boolean = true + + override fun getText(): String = + QuickFixBundle.message("create.annotation.text", StringUtilRt.getShortName(request.qualifiedName)) + + override fun getFamilyName(): String = QuickFixBundle.message("create.annotation.family") + + override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean = pointer.element != null + + + override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { + val target = pointer.element ?: return + val kotlinAnnotation = JavaPsiFacade.getInstance(project).findClass( + request.qualifiedName, + target.resolveScope + )?.language == KotlinLanguage.INSTANCE + val entry = target.addAnnotationEntry( + KtPsiFactory(target) + .createAnnotationEntry( + "@${request.qualifiedName}${ + request.attributes.mapIndexed { i, p -> + if (!kotlinAnnotation && i == 0 && p.name == "value") + renderAttributeValue(p.value).toString() + else + "${p.name} = ${renderAttributeValue(p.value)}" + }.joinToString(", ", "(", ")") + }" + ) + ) + + ShortenReferences.DEFAULT.process(entry) + } + + private fun renderAttributeValue(annotationAttributeRequest: AnnotationAttributeValueRequest) = + when (annotationAttributeRequest) { + is AnnotationAttributeValueRequest.PrimitiveValue -> annotationAttributeRequest.value + is AnnotationAttributeValueRequest.StringValue -> "\"" + annotationAttributeRequest.value + "\"" + } + + } + + override fun createChangeParametersActions(target: JvmMethod, request: ChangeParametersRequest): List { + val ktNamedFunction = (target as? KtLightElement<*, *>)?.kotlinOrigin as? KtNamedFunction ?: return emptyList() + + val helper = JvmPsiConversionHelper.getInstance(target.project) + + val params = request.expectedParameters.map { ep -> + val name = ep.semanticNames.singleOrNull() ?: return emptyList() + val expectedType = ep.expectedTypes.singleOrNull() ?: return emptyList() + + val kotlinType = + helper.convertType(expectedType.theType).resolveToKotlinType(ktNamedFunction.getResolutionFacade()) ?: return emptyList() + Name.identifier(name) to kotlinType + } + return listOf(ChangeMethodParameters(ktNamedFunction, params, { request.isValid })) + } +} + +private fun JvmPsiConversionHelper.asPsiType(param: Pair>): PsiType? = + param.second.firstOrNull()?.theType?.let { convertType(it) } diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/CommonIntentionActionsTest.kt.183 b/idea/tests/org/jetbrains/kotlin/idea/quickfix/CommonIntentionActionsTest.kt.183 new file mode 100644 index 00000000000..29156c97398 --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/CommonIntentionActionsTest.kt.183 @@ -0,0 +1,617 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.quickfix + +import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.lang.jvm.JvmClass +import com.intellij.lang.jvm.JvmElement +import com.intellij.lang.jvm.JvmMethod +import com.intellij.lang.jvm.JvmModifier +import com.intellij.lang.jvm.actions.* +import com.intellij.lang.jvm.types.JvmSubstitutor +import com.intellij.lang.jvm.types.JvmType +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Pair.pair +import com.intellij.psi.* +import com.intellij.psi.codeStyle.SuggestedNameInfo +import com.intellij.testFramework.fixtures.CodeInsightTestFixture +import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase +import org.jetbrains.kotlin.asJava.toLightElements +import org.jetbrains.kotlin.idea.search.allScope +import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor +import org.jetbrains.kotlin.psi.KtModifierListOwner +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UParameter +import org.jetbrains.uast.UastContext +import org.jetbrains.uast.toUElement +import org.junit.Assert + +class CommonIntentionActionsTest : LightPlatformCodeInsightFixtureTestCase() { + private class SimpleMethodRequest( + project: Project, + private val methodName: String, + private val modifiers: Collection = emptyList(), + private val returnType: ExpectedTypes = emptyList(), + private val annotations: Collection = emptyList(), + parameters: List = emptyList(), + private val targetSubstitutor: JvmSubstitutor = PsiJvmSubstitutor(project, PsiSubstitutor.EMPTY) + ) : CreateMethodRequest { + private val expectedParameters = parameters + + override fun getTargetSubstitutor(): JvmSubstitutor = targetSubstitutor + + override fun getModifiers() = modifiers + + override fun getMethodName() = methodName + + override fun getAnnotations() = annotations + + override fun getExpectedParameters(): List = expectedParameters + + override fun getReturnType() = returnType + + override fun isValid(): Boolean = true + + } + + private class NameInfo(vararg names: String) : SuggestedNameInfo(names) + + override fun getProjectDescriptor() = KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE_FULL_JDK + + fun testMakeNotFinal() { + myFixture.configureByText("foo.kt", """ + class Foo { + fun bar(){} + } + """) + + myFixture.launchAction( + createModifierActions( + myFixture.atCaret(), MemberRequest.Modifier(JvmModifier.FINAL, false) + ).findWithText("Make 'bar' open") + ) + myFixture.checkResult(""" + class Foo { + open fun bar(){} + } + """) + } + + fun testMakePrivate() { + myFixture.configureByText("foo.kt", """ + class Foo { + fun bar(){} + } + """) + + myFixture.launchAction( + createModifierActions( + myFixture.atCaret(), MemberRequest.Modifier(JvmModifier.PRIVATE, true) + ).findWithText("Make 'Foo' private") + ) + myFixture.checkResult(""" + private class Foo { + fun bar(){} + } + """) + } + + fun testMakeNotPrivate() { + myFixture.configureByText("foo.kt", """ + private class Foo { + fun bar(){} + } + """.trim()) + + myFixture.launchAction( + createModifierActions( + myFixture.atCaret(), MemberRequest.Modifier(JvmModifier.PRIVATE, false) + ).findWithText("Remove 'private' modifier") + ) + myFixture.checkResult(""" + class Foo { + fun bar(){} + } + """.trim(), true) + } + + fun testMakePrivatePublic() { + myFixture.configureByText( + "foo.kt", """class Foo { + | private fun bar(){} + |}""".trim().trimMargin() + ) + + myFixture.launchAction( + createModifierActions( + myFixture.atCaret(), MemberRequest.Modifier(JvmModifier.PUBLIC, true) + ).findWithText("Remove 'private' modifier") + ) + myFixture.checkResult( + """class Foo { + | fun bar(){} + |}""".trim().trimMargin(), true + ) + } + + fun testMakeProtectedPublic() { + myFixture.configureByText( + "foo.kt", """open class Foo { + | protected fun bar(){} + |}""".trim().trimMargin() + ) + + myFixture.launchAction( + createModifierActions( + myFixture.atCaret(), MemberRequest.Modifier(JvmModifier.PUBLIC, true) + ).findWithText("Remove 'protected' modifier") + ) + myFixture.checkResult( + """open class Foo { + | fun bar(){} + |}""".trim().trimMargin(), true + ) + } + + fun testMakeInternalPublic() { + myFixture.configureByText( + "foo.kt", """class Foo { + | internal fun bar(){} + |}""".trim().trimMargin() + ) + + myFixture.launchAction( + createModifierActions( + myFixture.atCaret(), MemberRequest.Modifier(JvmModifier.PUBLIC, true) + ).findWithText("Remove 'internal' modifier") + ) + myFixture.checkResult( + """class Foo { + | fun bar(){} + |}""".trim().trimMargin(), true + ) + } + + fun testAddAnnotation() { + myFixture.configureByText( + "foo.kt", """class Foo { + | fun bar(){} + |}""".trim().trimMargin() + ) + + myFixture.launchAction( + createAddAnnotationActions( + myFixture.findElementByText("bar", KtModifierListOwner::class.java).toLightElements().single() as PsiMethod, + annotationRequest("kotlin.jvm.JvmName", stringAttribute("name", "foo")) + ).single() + ) + myFixture.checkResult( + """class Foo { + | @JvmName(name = "foo") + | fun bar(){} + |}""".trim().trimMargin(), true + ) + } + + fun testAddJavaAnnotationValue() { + + myFixture.addFileToProject( + "pkg/myannotation/JavaAnnotation.java", """ + package pkg.myannotation + + public @interface JavaAnnotation { + String value(); + int param() default 0; + } + """.trimIndent() + ) + + myFixture.configureByText( + "foo.kt", """class Foo { + | fun bar(){} + | fun baz(){} + |}""".trim().trimMargin() + ) + + myFixture.launchAction( + createAddAnnotationActions( + myFixture.findElementByText("bar", KtModifierListOwner::class.java).toLightElements().single() as PsiMethod, + annotationRequest("pkg.myannotation.JavaAnnotation", stringAttribute("value", "foo"), intAttribute("param", 2)) + ).single() + ) + myFixture.launchAction( + createAddAnnotationActions( + myFixture.findElementByText("baz", KtModifierListOwner::class.java).toLightElements().single() as PsiMethod, + annotationRequest("pkg.myannotation.JavaAnnotation", intAttribute("param", 2), stringAttribute("value", "foo")) + ).single() + ) + myFixture.checkResult( + """import pkg.myannotation.JavaAnnotation + | + |class Foo { + | @JavaAnnotation("foo", param = 2) + | fun bar(){} + | @JavaAnnotation(param = 2, value = "foo") + | fun baz(){} + |}""".trim().trimMargin(), true + ) + } + + fun testDontMakePublicPublic() { + myFixture.configureByText( + "foo.kt", """class Foo { + | fun bar(){} + |}""".trim().trimMargin() + ) + + assertEmpty(createModifierActions(myFixture.atCaret(), MemberRequest.Modifier(JvmModifier.PUBLIC, true))) + } + + fun testDontMakeFunInObjectsOpen() { + myFixture.configureByText("foo.kt", """ + object Foo { + fun bar(){} + } + """.trim()) + assertEmpty(createModifierActions(myFixture.atCaret(), MemberRequest.Modifier(JvmModifier.FINAL, false))) + } + + fun testAddVoidVoidMethod() { + myFixture.configureByText("foo.kt", """ + |class Foo { + | fun bar() {} + |} + """.trim().trimMargin()) + + myFixture.launchAction( + createMethodActions( + myFixture.atCaret(), + methodRequest(project, "baz", JvmModifier.PRIVATE, PsiType.VOID) + ).findWithText("Add method 'baz' to 'Foo'") + ) + myFixture.checkResult(""" + |class Foo { + | fun bar() {} + | private fun baz() { + | + | } + |} + """.trim().trimMargin(), true) + } + + fun testAddIntIntMethod() { + myFixture.configureByText("foo.kt", """ + |class Foo { + | fun bar() {} + |} + """.trim().trimMargin()) + + myFixture.launchAction( + createMethodActions( + myFixture.atCaret(), + SimpleMethodRequest(project, + methodName = "baz", + modifiers = listOf(JvmModifier.PUBLIC), + returnType = expectedTypes(PsiType.INT), + parameters = expectedParams(PsiType.INT)) + ).findWithText("Add method 'baz' to 'Foo'") + ) + myFixture.checkResult(""" + |class Foo { + | fun bar() {} + | fun baz(param0: Int): Int { + | TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + | } + |} + """.trim().trimMargin(), true) + } + + fun testAddIntPrimaryConstructor() { + myFixture.configureByText("foo.kt", """ + |class Foo { + |} + """.trim().trimMargin()) + + myFixture.launchAction( + createConstructorActions( + myFixture.atCaret(), constructorRequest(project, listOf(pair("param0", PsiType.INT as PsiType))) + ).findWithText("Add primary constructor to 'Foo'") + ) + myFixture.checkResult(""" + |class Foo(param0: Int) { + |} + """.trim().trimMargin(), true) + } + + fun testAddIntSecondaryConstructor() { + myFixture.configureByText("foo.kt", """ + |class Foo() { + |} + """.trim().trimMargin()) + + myFixture.launchAction( + createConstructorActions( + myFixture.atCaret(), + constructorRequest(project, listOf(pair("param0", PsiType.INT as PsiType))) + ).findWithText("Add secondary constructor to 'Foo'") + ) + myFixture.checkResult(""" + |class Foo() { + | constructor(param0: Int) { + | + | } + |} + """.trim().trimMargin(), true) + } + + fun testChangePrimaryConstructorInt() { + myFixture.configureByText("foo.kt", """ + |class Foo() { + |} + """.trim().trimMargin()) + + myFixture.launchAction( + createConstructorActions( + myFixture.atCaret(), + constructorRequest(project, listOf(pair("param0", PsiType.INT as PsiType))) + ).findWithText("Add 'int' as 1st parameter to method 'Foo'") + ) + myFixture.checkResult(""" + |class Foo(param0: Int) { + |} + """.trim().trimMargin(), true) + } + + fun testRemoveConstructorParameters() { + myFixture.configureByText("foo.kt", """ + |class Foo(i: Int) { + |} + """.trim().trimMargin()) + + myFixture.launchAction( + createConstructorActions( + myFixture.atCaret(), + constructorRequest(project, emptyList()) + ).findWithText("Remove 1st parameter from method 'Foo'") + ) + myFixture.checkResult(""" + |class Foo() { + |} + """.trim().trimMargin(), true) + } + + fun testAddStringVarProperty() { + myFixture.configureByText("foo.kt", """ + |class Foo { + | fun bar() {} + |} + """.trim().trimMargin()) + + myFixture.launchAction( + createPropertyActions( + myFixture.atCaret(), + MemberRequest.Property( + propertyName = "baz", + visibilityModifier = JvmModifier.PUBLIC, + propertyType = PsiType.getTypeByName("java.lang.String", project, project.allScope()), + getterRequired = true, + setterRequired = true + ) + ).findWithText("Add 'var' property 'baz' to 'Foo'") + ) + myFixture.checkResult(""" + |class Foo { + | var baz: String = TODO("initialize me") + | + | fun bar() {} + |} + """.trim().trimMargin(), true) + } + + fun testAddLateInitStringVarProperty() { + myFixture.configureByText("foo.kt", """ + |class Foo { + | fun bar() {} + |} + """.trim().trimMargin()) + + myFixture.launchAction( + createPropertyActions( + myFixture.atCaret(), + MemberRequest.Property( + propertyName = "baz", + visibilityModifier = JvmModifier.PUBLIC, + propertyType = PsiType.getTypeByName("java.lang.String", project, project.allScope()), + getterRequired = true, + setterRequired = true + ) + ).findWithText("Add 'lateinit var' property 'baz' to 'Foo'") + ) + myFixture.checkResult(""" + |class Foo { + | lateinit var baz: String + | + | fun bar() {} + |} + """.trim().trimMargin(), true) + } + + fun testAddStringVarField() { + myFixture.configureByText( + "foo.kt", """ + |class Foo { + | fun bar() {} + |} + """.trim().trimMargin() + ) + myFixture.launchAction( + createFieldActions( + myFixture.atCaret(), + FieldRequest(project, emptyList(), "java.util.Date", "baz") + ).findWithText("Add 'var' property 'baz' to 'Foo'") + ) + myFixture.checkResult( + """ + |import java.util.Date + | + |class Foo { + | @JvmField + | var baz: Date = TODO("initialize me") + | + | fun bar() {} + |} + """.trim().trimMargin(), true + ) + } + + fun testAddLateInitStringVarField() { + myFixture.configureByText( + "foo.kt", """ + |class Foo { + | fun bar() {} + |} + """.trim().trimMargin() + ) + + myFixture.launchAction( + createFieldActions( + myFixture.atCaret(), + FieldRequest(project, listOf(JvmModifier.PRIVATE), "java.lang.String", "baz") + ).findWithText("Add 'lateinit var' property 'baz' to 'Foo'") + ) + myFixture.checkResult( + """ + |class Foo { + | private lateinit var baz: String + | + | fun bar() {} + |} + """.trim().trimMargin(), true + ) + } + + + + private fun createPropertyActions(atCaret: JvmClass, property: MemberRequest.Property): List = + com.intellij.lang.jvm.actions.EP_NAME.extensions.flatMap { it.createAddPropertyActions(atCaret, property) } + + private fun createFieldActions(atCaret: JvmClass, fieldRequest: CreateFieldRequest): List = + com.intellij.lang.jvm.actions.EP_NAME.extensions.flatMap { it.createAddFieldActions(atCaret, fieldRequest) } + + fun testAddStringValProperty() { + myFixture.configureByText("foo.kt", """ + |class Foo { + | fun bar() {} + |} + """.trim().trimMargin()) + + myFixture.launchAction( + createPropertyActions( + myFixture.atCaret(), + MemberRequest.Property( + propertyName = "baz", + visibilityModifier = JvmModifier.PUBLIC, + propertyType = PsiType.getTypeByName("java.lang.String", project, project.allScope()), + getterRequired = true, + setterRequired = false + ) + ).findWithText("Add 'val' property 'baz' to 'Foo'") + ) + myFixture.checkResult(""" + |class Foo { + | val baz: String = TODO("initialize me") + | + | fun bar() {} + |} + """.trim().trimMargin(), true) + } + + fun testSetParameters() { + myFixture.configureByText( + "foo.kt", """ + |class Foo { + | fun bar() {} + |} + """.trim().trimMargin() + ) + + + myFixture.launchAction( + com.intellij.lang.jvm.actions.createChangeParametersActions( + myFixture.atCaret().javaPsi, + setMethodParametersRequest( + linkedMapOf( + "i" to PsiType.INT, + "file" to PsiType.getTypeByName("java.io.File", project, myFixture.file.resolveScope) + ).entries + ) + ).findWithText("Change method parameters to '(i: Int, file: File)'") + ) + myFixture.checkResult( + """ + import java.io.File + + class Foo { + fun bar(i: Int, file: File) {} + } + """.trimIndent(), true + ) + } + + private fun makeParams(vararg psyTypes: PsiType): List { + val uastContext = UastContext(myFixture.project) + val factory = JavaPsiFacade.getElementFactory(myFixture.project) + val parameters = psyTypes.mapIndexed { index, psiType -> factory.createParameter("param$index", psiType) } + return parameters.map { uastContext.convertElement(it, null, UParameter::class.java) as UParameter } + } + + private fun expectedTypes(vararg psiTypes: PsiType) = psiTypes.map { expectedType(it) } + + private fun expectedParams(vararg psyTypes: PsiType) = + psyTypes.mapIndexed { index, psiType -> expectedParameter(expectedTypes(psiType), "param$index") } + + private inline fun CodeInsightTestFixture.atCaret() = elementAtCaret.toUElement() as T + + @Suppress("CAST_NEVER_SUCCEEDS") + private fun List.findWithText(text: String): IntentionAction = + this.firstOrNull { it.text == text } ?: + Assert.fail("intention with text '$text' was not found, only ${this.joinToString { "\"${it.text}\"" }} available") as Nothing + + class FieldRequest( + private val project: Project, + val modifiers: List, + val type: String, + val name: String + ) : CreateFieldRequest { + override fun getTargetSubstitutor(): JvmSubstitutor = PsiJvmSubstitutor(project, PsiSubstitutor.EMPTY) + + override fun getModifiers(): Collection = modifiers + + override fun isConstant(): Boolean = false + + override fun getFieldType(): List = + com.intellij.lang.jvm.actions.expectedTypes(PsiType.getTypeByName(type, project, project.allScope())) + + override fun getFieldName(): String = name + + override fun isValid(): Boolean = true + } + + +} + +