From 747f397f4006bbed8ea7b91089caae0a71dc6189 Mon Sep 17 00:00:00 2001 From: Vyacheslav Gerasimov Date: Thu, 18 Jan 2018 02:17:02 +0300 Subject: [PATCH] 172: Revert "Quick Fixes: Drop deprecated KotlinCommonIntentionActionsFactory" This reverts commit ba0a91d --- idea/src/META-INF/plugin.xml.172 | 2 + ...KotlinCommonIntentionActionsFactory.kt.172 | 261 +++++++++++++++ .../CommonIntentionActionsTest.kt.172 | 307 ++++++++++++++++++ 3 files changed, 570 insertions(+) create mode 100644 idea/src/org/jetbrains/kotlin/idea/intentions/KotlinCommonIntentionActionsFactory.kt.172 create mode 100644 idea/tests/org/jetbrains/kotlin/idea/quickfix/CommonIntentionActionsTest.kt.172 diff --git a/idea/src/META-INF/plugin.xml.172 b/idea/src/META-INF/plugin.xml.172 index 1ca575190cc..84fc15c617a 100644 --- a/idea/src/META-INF/plugin.xml.172 +++ b/idea/src/META-INF/plugin.xml.172 @@ -2786,6 +2786,8 @@ + + diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/KotlinCommonIntentionActionsFactory.kt.172 b/idea/src/org/jetbrains/kotlin/idea/intentions/KotlinCommonIntentionActionsFactory.kt.172 new file mode 100644 index 00000000000..6e043e9bb23 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/KotlinCommonIntentionActionsFactory.kt.172 @@ -0,0 +1,261 @@ +/* + * 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.intentions + +import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.codeInsight.intention.JvmCommonIntentionActionsFactory +import com.intellij.codeInsight.intention.MethodInsertionInfo +import com.intellij.codeInsight.intention.QuickFixFactory +import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.* +import com.intellij.psi.impl.source.tree.java.PsiReferenceExpressionImpl +import org.jetbrains.kotlin.asJava.LightClassUtil +import org.jetbrains.kotlin.asJava.elements.KtLightElement +import org.jetbrains.kotlin.builtins.DefaultBuiltIns +import org.jetbrains.kotlin.descriptors.Visibilities +import org.jetbrains.kotlin.idea.core.ShortenReferences +import org.jetbrains.kotlin.idea.core.insertMembersAfter +import org.jetbrains.kotlin.idea.quickfix.AddModifierFix +import org.jetbrains.kotlin.idea.quickfix.RemoveModifierFix +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.platform.JavaToKotlinClassMap +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.KtPsiFactory.CallableBuilder +import org.jetbrains.kotlin.psi.KtPsiFactory.CallableBuilder.Target.CONSTRUCTOR +import org.jetbrains.kotlin.psi.KtPsiFactory.CallableBuilder.Target.FUNCTION +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.uast.UClass +import org.jetbrains.uast.UDeclaration +import org.jetbrains.uast.UElement + +class KotlinCommonIntentionActionsFactory : JvmCommonIntentionActionsFactory() { + override fun createChangeModifierAction(declaration: UDeclaration, modifier: String, shouldPresent: Boolean): IntentionAction? { + val kModifierOwner = declaration.asKtElement() + ?: throw IllegalArgumentException("$declaration is expected to contain KtLightElement with KtModifierListOwner") + + val (kToken, shouldPresentMapped) = if (PsiModifier.FINAL == modifier) + KtTokens.OPEN_KEYWORD to !shouldPresent + else + javaPsiModifiersMapping[modifier] to shouldPresent + + if (kToken == null) return null + return if (shouldPresentMapped) + AddModifierFix.createIfApplicable(kModifierOwner, kToken) + else + RemoveModifierFix(kModifierOwner, kToken, false) + } + + override fun createAddBeanPropertyActions(uClass: UClass, + propertyName: String, + visibilityModifier: String, + propertyType: PsiType, + setterRequired: Boolean, + getterRequired: Boolean): List { + + fun addPropertyFix(lateinit: Boolean = false) = + Fix(uClass, + "Add property", + "Add '${if (lateinit) "lateinit " else ""}" + + "${if (setterRequired) "var" else "val"}' property '$propertyName' to '${uClass.name}'") + { uClass -> + val visibilityStr = javaVisibilityMapping.getValue(visibilityModifier) + val psiFactory = KtPsiFactory(uClass) + val modifiersString = if (lateinit) "lateinit $visibilityStr" else visibilityStr + val function = psiFactory.createProperty( + modifiersString, + propertyName, + typeString(propertyType), + setterRequired, + if (lateinit) null else "TODO(\"initialize me\")") + val ktClassOrObject = uClass.asKtElement()!! + insertMembersAfter(null, ktClassOrObject, listOf(function), null) + } + + if (setterRequired) + return listOf(addPropertyFix(), addPropertyFix(lateinit = true)) + else + return listOf(addPropertyFix()) + } + + override fun createAddCallableMemberActions(info: MethodInsertionInfo): List = + when (info) { + is MethodInsertionInfo.Method -> + createAddMethodAction(info)?.let { listOf(it) } ?: emptyList() + + is MethodInsertionInfo.Constructor -> + createAddConstructorActions(info) + } + + companion object { + val javaPsiModifiersMapping = mapOf( + PsiModifier.PRIVATE to KtTokens.PRIVATE_KEYWORD, + PsiModifier.PUBLIC to KtTokens.PUBLIC_KEYWORD, + PsiModifier.PROTECTED to KtTokens.PUBLIC_KEYWORD, + PsiModifier.ABSTRACT to KtTokens.ABSTRACT_KEYWORD + ) + + val javaVisibilityMapping: Map = mapOf( + PsiModifier.PRIVATE to Visibilities.PRIVATE.displayName, + PsiModifier.PUBLIC to "", + PsiModifier.PROTECTED to Visibilities.PROTECTED.displayName, + PsiModifier.PACKAGE_LOCAL to Visibilities.INTERNAL.displayName + ).withDefault { Visibilities.DEFAULT_VISIBILITY.displayName } + + fun typeString(str: PsiType): String { + var typeName: String? = when (str) { + PsiType.VOID -> "" + PsiType.INT -> "kotlin.Int" + PsiType.LONG -> "kotlin.Long" + PsiType.SHORT -> "kotlin.Short" + PsiType.BOOLEAN -> "kotlin.Boolean" + PsiType.BYTE -> "kotlin.Byte" + PsiType.CHAR -> "kotlin.Char" + PsiType.DOUBLE -> "kotlin.Double" + PsiType.FLOAT -> "kotlin.Float" + else -> null + } + if (typeName == null) + typeName = JavaToKotlinClassMap.mapJavaToKotlin(FqName(str.canonicalText), DefaultBuiltIns.Instance)?.fqNameSafe?.asString() + + return typeName ?: str.canonicalText + } + } + + private inline fun UElement.asKtElement(): T? = + (psi as? KtLightElement<*, *>?)?.kotlinOrigin as? T + + private fun CallableBuilder.paramsFromInfo(info: MethodInsertionInfo) { + for ((index, param) in info.parameters.withIndex()) { + param(param.name ?: "arg${index + 1}", typeString(param.type)) + } + } + + private fun createAddMethodAction(info: MethodInsertionInfo.Method): IntentionAction? { + val visibilityStr = info.modifiers.map { javaVisibilityMapping.get(it) ?: it }.joinToString(" ") + val functionString = CallableBuilder(FUNCTION).apply { + modifier(visibilityStr) + typeParams() + name(info.name) + paramsFromInfo(info) + info.returnType.let { + when (it) { + PsiType.VOID -> noReturnType() + else -> returnType(typeString(it)) + } + } + blockBody("") + } + + return Fix(info.containingClass, "Add method", "Add method '${info.name}' to '${info.containingClass.name}'") { + uClass -> + val psiFactory = KtPsiFactory(uClass) + val function = psiFactory.createFunction(functionString.asString()) + val ktClassOrObject = uClass.asKtElement()!! + insertMembersAfter(null, ktClassOrObject, listOf(function), ktClassOrObject.declarations.lastOrNull()) + } + + } + + private fun createAddConstructorActions(info: MethodInsertionInfo.Constructor): List { + val constructorString = CallableBuilder(CONSTRUCTOR).apply { + modifier("") + typeParams() + name() + paramsFromInfo(info) + noReturnType() + blockBody("") + }.asString() + val primaryConstructor = info.containingClass.asKtElement()!!.primaryConstructor + + val addConstructorAction = if (primaryConstructor == null) + Fix(info.containingClass, + "Add method", + "Add primary constructor to '${info.containingClass.name}'", + { uClass -> + val psiFactory = KtPsiFactory(uClass) + val constructor = psiFactory.createSecondaryConstructor(constructorString) + val ktClass = uClass.asKtElement()!! + val newPrimaryConstructor = ktClass.createPrimaryConstructorIfAbsent() + newPrimaryConstructor.valueParameterList!!.replace(constructor.valueParameterList!!) + ShortenReferences.DEFAULT.process(newPrimaryConstructor) + }) + else Fix(info.containingClass, + "Add method", + "Add secondary constructor to '${info.containingClass.name}'", + { uClass -> + val psiFactory = KtPsiFactory(uClass) + val constructor = psiFactory.createSecondaryConstructor(constructorString) + val ktClassOrObject = uClass.asKtElement()!! + insertMembersAfter(null, ktClassOrObject, listOf(constructor), null) + }) + + val changePrimaryConstructorAction = run { + if (primaryConstructor == null) return@run null + QuickFixFactory.getInstance() + .createChangeMethodSignatureFromUsageFix( + LightClassUtil.getLightClassMethod(primaryConstructor)!!, + fakeParametersExpressions(info.parameters), + PsiSubstitutor.EMPTY, info.containingClass, false, 2 + ).takeIf { it.isAvailable(info.containingClass.project, null, info.containingClass.containingFile) } + } + + return listOf(changePrimaryConstructorAction, addConstructorAction).filterNotNull() + } + + private fun fakeParametersExpressions(parameters: List): Array = + when { + parameters.isEmpty() -> emptyArray() + else -> JavaPsiFacade.getElementFactory(parameters.first().project) + .createParameterList( + parameters.map { it.name }.toTypedArray(), + parameters.map { it.type }.toTypedArray() + ).parameters.map { FakeExpressionFromParameter(it) }.toTypedArray() + } + + 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 Fix(uClass: UClass, private val familyName: String, private val text: String, private val action: (uClass: UClass) -> Unit) : LocalQuickFixAndIntentionActionOnPsiElement(uClass) { + override fun getFamilyName(): String = familyName + + override fun getText(): String = text + + override fun invoke(project: Project, file: PsiFile, editor: Editor?, startElement: PsiElement, endElement: PsiElement) = + action(startElement as UClass) + } + +} + diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/CommonIntentionActionsTest.kt.172 b/idea/tests/org/jetbrains/kotlin/idea/quickfix/CommonIntentionActionsTest.kt.172 new file mode 100644 index 00000000000..b4e2b043f63 --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/CommonIntentionActionsTest.kt.172 @@ -0,0 +1,307 @@ +/* + * 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.codeInsight.intention.JvmCommonIntentionActionsFactory +import com.intellij.codeInsight.intention.MethodInsertionInfo +import com.intellij.lang.Language +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiModifier +import com.intellij.psi.PsiType +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.testFramework.fixtures.CodeInsightTestFixture +import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase +import org.jetbrains.uast.* +import org.junit.Assert + +class CommonIntentionActionsTest : LightPlatformCodeInsightFixtureTestCase() { + fun testMakeNotFinal() { + myFixture.configureByText("foo.kt", """ + class Foo { + fun bar(){} + } + """) + + myFixture.launchAction( + codeModifications + .createChangeModifierAction(atCaret(myFixture), PsiModifier.FINAL, false)!! + .ensureHasText("Make 'bar' open") + ) + myFixture.checkResult(""" + class Foo { + open fun bar(){} + } + """) + } + + fun testMakePrivate() { + myFixture.configureByText("foo.kt", """ + class Foo { + fun bar(){} + } + """) + + myFixture.launchAction( + codeModifications + .createChangeModifierAction(atCaret(myFixture), PsiModifier.PRIVATE, true)!! + .ensureHasText("Make 'Foo' private") + ) + myFixture.checkResult(""" + private class Foo { + fun bar(){} + } + """) + } + + fun testMakeNotPrivate() { + myFixture.configureByText("foo.kt", """ + private class Foo { + fun bar(){} + } + """.trim()) + + myFixture.launchAction( + codeModifications + .createChangeModifierAction(atCaret(myFixture), PsiModifier.PRIVATE, false)!! + .ensureHasText("Remove 'private' modifier") + ) + myFixture.checkResult(""" + class Foo { + fun bar(){} + } + """.trim(), true) + } + + fun testDontMakeFunInObjectsOpen() { + myFixture.configureByText("foo.kt", """ + object Foo { + fun bar(){} + } + """.trim()) + Assert.assertNull(codeModifications.createChangeModifierAction(atCaret(myFixture), PsiModifier.FINAL, false)) + } + + fun testAddVoidVoidMethod() { + myFixture.configureByText("foo.kt", """ + |class Foo { + | fun bar() {} + |} + """.trim().trimMargin()) + + myFixture.launchAction( + codeModifications.createAddCallableMemberActions( + MethodInsertionInfo.simpleMethodInfo(atCaret(myFixture), "baz", PsiModifier.PRIVATE, PsiType.VOID, emptyList()) + ).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( + codeModifications.createAddCallableMemberActions( + MethodInsertionInfo.simpleMethodInfo(atCaret(myFixture), "baz", PsiModifier.PUBLIC, PsiType.INT, makeParams(PsiType.INT)) + ).findWithText("Add method 'baz' to 'Foo'") + ) + myFixture.checkResult(""" + |class Foo { + | fun bar() {} + | fun baz(param0: Int): Int { + | + | } + |} + """.trim().trimMargin(), true) + } + + fun testAddIntPrimaryConstructor() { + myFixture.configureByText("foo.kt", """ + |class Foo { + |} + """.trim().trimMargin()) + + myFixture.launchAction( + codeModifications.createAddCallableMemberActions( + MethodInsertionInfo.constructorInfo(atCaret(myFixture), makeParams(PsiType.INT)) + ).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( + codeModifications.createAddCallableMemberActions( + MethodInsertionInfo.constructorInfo(atCaret(myFixture), makeParams(PsiType.INT)) + ).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( + codeModifications.createAddCallableMemberActions( + MethodInsertionInfo.constructorInfo(atCaret(myFixture), makeParams(PsiType.INT)) + ).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( + codeModifications.createAddCallableMemberActions( + MethodInsertionInfo.constructorInfo(atCaret(myFixture), makeParams()) + ).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( + codeModifications.createAddBeanPropertyActions( + atCaret(myFixture), + "baz", + PsiModifier.PUBLIC, + PsiType.getTypeByName("java.lang.String", project, GlobalSearchScope.allScope(project)), + true, + 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(codeModifications.createAddBeanPropertyActions( + atCaret(myFixture), "baz", PsiModifier.PUBLIC, PsiType.getTypeByName("java.lang.String", project, GlobalSearchScope.allScope(project)), true, true) + .findWithText("Add 'lateinit var' property 'baz' to 'Foo'")) + myFixture.checkResult(""" + |class Foo { + | lateinit var baz: String + | fun bar() {} + |} + """.trim().trimMargin(), true) + } + + fun testAddStringValProperty() { + myFixture.configureByText("foo.kt", """ + |class Foo { + | fun bar() {} + |} + """.trim().trimMargin()) + + myFixture.launchAction( + codeModifications.createAddBeanPropertyActions( + atCaret(myFixture), + "baz", + PsiModifier.PUBLIC, + PsiType.getTypeByName("java.lang.String", project, GlobalSearchScope.allScope(project)), + false, + true + ).findWithText("Add 'val' property 'baz' to 'Foo'") + ) + myFixture.checkResult(""" + |class Foo { + | val baz: String = TODO("initialize me") + | fun bar() {} + |} + """.trim().trimMargin(), 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 } + } + + @Suppress("UNCHECKED_CAST") + private fun atCaret(myFixture: CodeInsightTestFixture): T { + return myFixture.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 + + @Suppress("CAST_NEVER_SUCCEEDS") + private fun IntentionAction.ensureHasText(text: String): IntentionAction = + if (this.text == text) this else Assert.fail("intention with text '$text' was not found, only \"${this.text}\" available") as Nothing + + private val codeModifications: JvmCommonIntentionActionsFactory + get() = JvmCommonIntentionActionsFactory.forLanguage(Language.findLanguageByID("kotlin")!!)!! + +} +