diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt index de4fb3a2851..dfe8cfc0a91 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt @@ -35,6 +35,7 @@ import org.jetbrains.kotlin.idea.quickfix.createFromUsage.createTypeParameter.Cr import org.jetbrains.kotlin.idea.quickfix.createFromUsage.createVariable.CreateLocalVariableActionFactory import org.jetbrains.kotlin.idea.quickfix.createFromUsage.createVariable.CreateParameterByNamedArgumentActionFactory import org.jetbrains.kotlin.idea.quickfix.createFromUsage.createVariable.CreateParameterByRefActionFactory +import org.jetbrains.kotlin.idea.quickfix.expectactual.AddActualFix import org.jetbrains.kotlin.idea.quickfix.expectactual.CreateActualFix import org.jetbrains.kotlin.idea.quickfix.migration.MigrateExternalExtensionFix import org.jetbrains.kotlin.idea.quickfix.migration.MigrateTypeParameterListFix @@ -480,6 +481,8 @@ class QuickFixRegistrar : QuickFixContributor { OVERLOADS_WITHOUT_DEFAULT_ARGUMENTS.registerFactory(RemoveAnnotationFix.JvmOverloads) NO_ACTUAL_FOR_EXPECT.registerFactory(CreateActualFix) + NO_ACTUAL_CLASS_MEMBER_FOR_EXPECTED_CLASS.registerFactory(AddActualFix) + ACTUAL_MISSING.registerFactory(AddModifierFix.createFactory(KtTokens.ACTUAL_KEYWORD)) CAST_NEVER_SUCCEEDS.registerFactory(ReplacePrimitiveCastWithNumberConversionFix) diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/AddActualFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/AddActualFix.kt new file mode 100644 index 00000000000..7f602e71ac2 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/AddActualFix.kt @@ -0,0 +1,71 @@ +/* + * 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.expectactual + +import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.diagnostics.Diagnostic +import org.jetbrains.kotlin.diagnostics.DiagnosticFactory +import org.jetbrains.kotlin.diagnostics.Errors +import org.jetbrains.kotlin.idea.quickfix.KotlinQuickFixAction +import org.jetbrains.kotlin.idea.quickfix.KotlinSingleIntentionActionFactory +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtPsiFactory +import org.jetbrains.kotlin.psi.psiUtil.createSmartPointer +import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils + +class AddActualFix( + actualClassOrObject: KtClassOrObject, + expectedClassOrObject: KtClassOrObject +) : KotlinQuickFixAction(actualClassOrObject) { + + private val expectedClassPointer = expectedClassOrObject.createSmartPointer() + + override fun getFamilyName() = text + + override fun getText() = "Add missing actual members" + + override fun invoke(project: Project, editor: Editor?, file: KtFile) { + val element = element ?: return + val expectedClass = expectedClassPointer.element ?: return + val factory = KtPsiFactory(element) + val pureActualClass = factory.generateClassOrObjectByExpectedClass( + project, expectedClass, actualNeeded = true, existingDeclarations = element.declarations + ) + for (declaration in pureActualClass.declarations) { + element.addDeclaration(declaration) + } + val primaryConstructor = pureActualClass.primaryConstructor + if (element.primaryConstructor == null && primaryConstructor != null) { + element.addAfter(primaryConstructor, element.nameIdentifier) + } + } + + companion object : KotlinSingleIntentionActionFactory() { + override fun createAction(diagnostic: Diagnostic): IntentionAction? { + val incompatibleMap = DiagnosticFactory.cast(diagnostic, Errors.NO_ACTUAL_CLASS_MEMBER_FOR_EXPECTED_CLASS).b + val expectedClassDescriptor = incompatibleMap.firstOrNull()?.first?.containingDeclaration as? ClassDescriptor + ?: return null + val expectedClassOrObject = DescriptorToSourceUtils.descriptorToDeclaration(expectedClassDescriptor) as? KtClassOrObject + ?: return null + return (diagnostic.psiElement as? KtClassOrObject)?.let { AddActualFix(it, expectedClassOrObject) } + } + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/CreateActualFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/CreateActualFix.kt index 499ceab2010..2eab26de58d 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/CreateActualFix.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/CreateActualFix.kt @@ -156,7 +156,7 @@ class CreateActualClassFix( klass: KtClassOrObject, actualPlatform: MultiTargetPlatform.Specific ) : CreateActualFix(klass, actualPlatform, { project, element -> - generateClassOrObject(project, element, actualNeeded = true) + generateClassOrObjectByExpectedClass(project, element, actualNeeded = true) }) { override val elementType = run { @@ -206,15 +206,36 @@ private fun KtModifierListOwner.replaceExpectModifier(actualNeeded: Boolean) { } } -private fun KtPsiFactory.generateClassOrObject( +internal fun KtPsiFactory.generateClassOrObjectByExpectedClass( project: Project, expectedClass: KtClassOrObject, - actualNeeded: Boolean + actualNeeded: Boolean, + existingDeclarations: List = emptyList() ): KtClassOrObject { + fun KtDeclaration.exists() = + existingDeclarations.any { + name == it.name && this.javaClass == it.javaClass && when (this) { + is KtClassOrObject, is KtProperty, is KtEnumEntry -> true + is KtFunction -> { + it as KtFunction + valueParameters.size == it.valueParameters.size && + valueParameters.zip(it.valueParameters).all { (parameter, existingParameter) -> + parameter.name == existingParameter.name && + parameter.typeReference?.text == existingParameter.typeReference?.text + } + } + else -> true + } + } + val expectedText = expectedClass.text val actualClass = if (expectedClass is KtObjectDeclaration) createObject(expectedText) else createClass(expectedText) val isInterface = expectedClass is KtClass && expectedClass.isInterface() actualClass.declarations.forEach { + if (it.exists()) { + it.delete() + return@forEach + } when (it) { is KtEnumEntry -> return@forEach is KtClassOrObject -> it.delete() @@ -229,12 +250,12 @@ private fun KtPsiFactory.generateClassOrObject( } } - declLoop@ for (expectedDeclaration in expectedClass.declarations) { + declLoop@ for (expectedDeclaration in expectedClass.declarations.filter { !it.exists() }) { val descriptor = expectedDeclaration.toDescriptor() ?: continue val actualDeclaration: KtDeclaration = when (expectedDeclaration) { is KtClassOrObject -> if (expectedDeclaration !is KtEnumEntry) { - generateClassOrObject(project, expectedDeclaration, actualNeeded = true) + generateClassOrObjectByExpectedClass(project, expectedDeclaration, actualNeeded = true) } else { continue@declLoop diff --git a/idea/testData/multiModuleQuickFix/classFunction/header/header.kt b/idea/testData/multiModuleQuickFix/classFunction/header/header.kt new file mode 100644 index 00000000000..6f0c1a068a7 --- /dev/null +++ b/idea/testData/multiModuleQuickFix/classFunction/header/header.kt @@ -0,0 +1,5 @@ +// DISABLE-ERRORS + +expect class My { + fun foo(param: String): Int +} \ No newline at end of file diff --git a/idea/testData/multiModuleQuickFix/classFunction/header/header.kt.after b/idea/testData/multiModuleQuickFix/classFunction/header/header.kt.after new file mode 100644 index 00000000000..6f0c1a068a7 --- /dev/null +++ b/idea/testData/multiModuleQuickFix/classFunction/header/header.kt.after @@ -0,0 +1,5 @@ +// DISABLE-ERRORS + +expect class My { + fun foo(param: String): Int +} \ No newline at end of file diff --git a/idea/testData/multiModuleQuickFix/classFunction/jvm/My.kt b/idea/testData/multiModuleQuickFix/classFunction/jvm/My.kt new file mode 100644 index 00000000000..468248da9de --- /dev/null +++ b/idea/testData/multiModuleQuickFix/classFunction/jvm/My.kt @@ -0,0 +1,4 @@ +// "Add missing actual members" "true" +// DISABLE-ERRORS + +actual class My diff --git a/idea/testData/multiModuleQuickFix/classFunction/jvm/My.kt.after b/idea/testData/multiModuleQuickFix/classFunction/jvm/My.kt.after new file mode 100644 index 00000000000..d7055408b7c --- /dev/null +++ b/idea/testData/multiModuleQuickFix/classFunction/jvm/My.kt.after @@ -0,0 +1,8 @@ +// "Add missing actual members" "true" +// DISABLE-ERRORS + +actual class My { + actual fun foo(param: String): Int { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} diff --git a/idea/testData/multiModuleQuickFix/classOverloadedFunction/header/header.kt b/idea/testData/multiModuleQuickFix/classOverloadedFunction/header/header.kt new file mode 100644 index 00000000000..54edbdc16ea --- /dev/null +++ b/idea/testData/multiModuleQuickFix/classOverloadedFunction/header/header.kt @@ -0,0 +1,7 @@ +// DISABLE-ERRORS + +expect class My { + fun foo(param: String): Int + + fun foo(param: Int): Int +} \ No newline at end of file diff --git a/idea/testData/multiModuleQuickFix/classOverloadedFunction/header/header.kt.after b/idea/testData/multiModuleQuickFix/classOverloadedFunction/header/header.kt.after new file mode 100644 index 00000000000..54edbdc16ea --- /dev/null +++ b/idea/testData/multiModuleQuickFix/classOverloadedFunction/header/header.kt.after @@ -0,0 +1,7 @@ +// DISABLE-ERRORS + +expect class My { + fun foo(param: String): Int + + fun foo(param: Int): Int +} \ No newline at end of file diff --git a/idea/testData/multiModuleQuickFix/classOverloadedFunction/jvm/My.kt b/idea/testData/multiModuleQuickFix/classOverloadedFunction/jvm/My.kt new file mode 100644 index 00000000000..387ab63e693 --- /dev/null +++ b/idea/testData/multiModuleQuickFix/classOverloadedFunction/jvm/My.kt @@ -0,0 +1,6 @@ +// "Add missing actual members" "true" +// DISABLE-ERRORS + +actual class My { + fun foo(param: String) = 42 +} diff --git a/idea/testData/multiModuleQuickFix/classOverloadedFunction/jvm/My.kt.after b/idea/testData/multiModuleQuickFix/classOverloadedFunction/jvm/My.kt.after new file mode 100644 index 00000000000..7f27eefc60c --- /dev/null +++ b/idea/testData/multiModuleQuickFix/classOverloadedFunction/jvm/My.kt.after @@ -0,0 +1,9 @@ +// "Add missing actual members" "true" +// DISABLE-ERRORS + +actual class My { + fun foo(param: String) = 42 + actual fun foo(param: Int): Int { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} diff --git a/idea/testData/multiModuleQuickFix/classSomeProperties/header/header.kt b/idea/testData/multiModuleQuickFix/classSomeProperties/header/header.kt new file mode 100644 index 00000000000..19bd29624c2 --- /dev/null +++ b/idea/testData/multiModuleQuickFix/classSomeProperties/header/header.kt @@ -0,0 +1,11 @@ +// DISABLE-ERRORS + +expect class My { + fun foo(param: String): Int + + val x: String + + val pi: Double + + val correct: Boolean +} \ No newline at end of file diff --git a/idea/testData/multiModuleQuickFix/classSomeProperties/header/header.kt.after b/idea/testData/multiModuleQuickFix/classSomeProperties/header/header.kt.after new file mode 100644 index 00000000000..19bd29624c2 --- /dev/null +++ b/idea/testData/multiModuleQuickFix/classSomeProperties/header/header.kt.after @@ -0,0 +1,11 @@ +// DISABLE-ERRORS + +expect class My { + fun foo(param: String): Int + + val x: String + + val pi: Double + + val correct: Boolean +} \ No newline at end of file diff --git a/idea/testData/multiModuleQuickFix/classSomeProperties/jvm/My.kt b/idea/testData/multiModuleQuickFix/classSomeProperties/jvm/My.kt new file mode 100644 index 00000000000..d760647373f --- /dev/null +++ b/idea/testData/multiModuleQuickFix/classSomeProperties/jvm/My.kt @@ -0,0 +1,8 @@ +// "Add missing actual members" "true" +// DISABLE-ERRORS + +actual class My { + actual fun foo(param: String) = 42 + + actual val correct = true +} diff --git a/idea/testData/multiModuleQuickFix/classSomeProperties/jvm/My.kt.after b/idea/testData/multiModuleQuickFix/classSomeProperties/jvm/My.kt.after new file mode 100644 index 00000000000..fe0275be13e --- /dev/null +++ b/idea/testData/multiModuleQuickFix/classSomeProperties/jvm/My.kt.after @@ -0,0 +1,12 @@ +// "Add missing actual members" "true" +// DISABLE-ERRORS + +actual class My { + actual fun foo(param: String) = 42 + + actual val correct = true + actual val x: String + get() = TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + actual val pi: Double + get() = TODO("not implemented") //To change body of created functions use File | Settings | File Templates. +} diff --git a/idea/testData/multiModuleQuickFix/primaryConstructorAbsence/header/header.kt b/idea/testData/multiModuleQuickFix/primaryConstructorAbsence/header/header.kt new file mode 100644 index 00000000000..b7861f9c69f --- /dev/null +++ b/idea/testData/multiModuleQuickFix/primaryConstructorAbsence/header/header.kt @@ -0,0 +1,7 @@ +// DISABLE-ERRORS + +expect class WithPrimaryConstructor(x: Int, s: String) { + fun bar(x: String) + + val z: Double +} diff --git a/idea/testData/multiModuleQuickFix/primaryConstructorAbsence/header/header.kt.after b/idea/testData/multiModuleQuickFix/primaryConstructorAbsence/header/header.kt.after new file mode 100644 index 00000000000..b7861f9c69f --- /dev/null +++ b/idea/testData/multiModuleQuickFix/primaryConstructorAbsence/header/header.kt.after @@ -0,0 +1,7 @@ +// DISABLE-ERRORS + +expect class WithPrimaryConstructor(x: Int, s: String) { + fun bar(x: String) + + val z: Double +} diff --git a/idea/testData/multiModuleQuickFix/primaryConstructorAbsence/jvm/WithPrimaryConstructor.kt b/idea/testData/multiModuleQuickFix/primaryConstructorAbsence/jvm/WithPrimaryConstructor.kt new file mode 100644 index 00000000000..7d3bbd02422 --- /dev/null +++ b/idea/testData/multiModuleQuickFix/primaryConstructorAbsence/jvm/WithPrimaryConstructor.kt @@ -0,0 +1,8 @@ +// "Add missing actual members" "true" +// DISABLE-ERRORS + +actual class WithPrimaryConstructor { + fun bar(x: String) {} + + val z: Double = 3.14 +} diff --git a/idea/testData/multiModuleQuickFix/primaryConstructorAbsence/jvm/WithPrimaryConstructor.kt.after b/idea/testData/multiModuleQuickFix/primaryConstructorAbsence/jvm/WithPrimaryConstructor.kt.after new file mode 100644 index 00000000000..b74110cfaa2 --- /dev/null +++ b/idea/testData/multiModuleQuickFix/primaryConstructorAbsence/jvm/WithPrimaryConstructor.kt.after @@ -0,0 +1,8 @@ +// "Add missing actual members" "true" +// DISABLE-ERRORS + +actual class WithPrimaryConstructor actual constructor(x: Int, s: String) { + fun bar(x: String) {} + + val z: Double = 3.14 +} diff --git a/idea/testData/multiModuleQuickFix/secondaryConstructorAbsence/header/header.kt b/idea/testData/multiModuleQuickFix/secondaryConstructorAbsence/header/header.kt new file mode 100644 index 00000000000..7fba4f184cb --- /dev/null +++ b/idea/testData/multiModuleQuickFix/secondaryConstructorAbsence/header/header.kt @@ -0,0 +1,9 @@ +// DISABLE-ERRORS + +expect class WithSecondaryConstructor { + constructor(x: Int, s: String) + + fun bar(x: String) + + val z: Double +} diff --git a/idea/testData/multiModuleQuickFix/secondaryConstructorAbsence/header/header.kt.after b/idea/testData/multiModuleQuickFix/secondaryConstructorAbsence/header/header.kt.after new file mode 100644 index 00000000000..7fba4f184cb --- /dev/null +++ b/idea/testData/multiModuleQuickFix/secondaryConstructorAbsence/header/header.kt.after @@ -0,0 +1,9 @@ +// DISABLE-ERRORS + +expect class WithSecondaryConstructor { + constructor(x: Int, s: String) + + fun bar(x: String) + + val z: Double +} diff --git a/idea/testData/multiModuleQuickFix/secondaryConstructorAbsence/jvm/WithSecondaryConstructor.kt b/idea/testData/multiModuleQuickFix/secondaryConstructorAbsence/jvm/WithSecondaryConstructor.kt new file mode 100644 index 00000000000..6fccf8f2344 --- /dev/null +++ b/idea/testData/multiModuleQuickFix/secondaryConstructorAbsence/jvm/WithSecondaryConstructor.kt @@ -0,0 +1,8 @@ +// "Add missing actual members" "true" +// DISABLE-ERRORS + +actual class WithSecondaryConstructor { + fun bar(x: String) {} + + val z: Double = 3.14 +} diff --git a/idea/testData/multiModuleQuickFix/secondaryConstructorAbsence/jvm/WithSecondaryConstructor.kt.after b/idea/testData/multiModuleQuickFix/secondaryConstructorAbsence/jvm/WithSecondaryConstructor.kt.after new file mode 100644 index 00000000000..1f4ec70e7e6 --- /dev/null +++ b/idea/testData/multiModuleQuickFix/secondaryConstructorAbsence/jvm/WithSecondaryConstructor.kt.after @@ -0,0 +1,12 @@ +// "Add missing actual members" "true" +// DISABLE-ERRORS + +actual class WithSecondaryConstructor { + fun bar(x: String) {} + + val z: Double = 3.14 + + actual constructor(x: Int, s: String) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiModuleTest.kt b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiModuleTest.kt index 396856f1ca4..415e195b75c 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiModuleTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiModuleTest.kt @@ -59,6 +59,21 @@ class QuickFixMultiModuleTest : AbstractQuickFixMultiModuleTest() { doMultiPlatformTest() } + @Test + fun testClassFunction() { + doMultiPlatformTest() + } + + @Test + fun testClassOverloadedFunction() { + doMultiPlatformTest() + } + + @Test + fun testClassSomeProperties() { + doMultiPlatformTest() + } + @Test fun testDeprecatedHeader() { doMultiPlatformTest() @@ -94,6 +109,11 @@ class QuickFixMultiModuleTest : AbstractQuickFixMultiModuleTest() { doMultiPlatformTest() } + @Test + fun testNested() { + doMultiPlatformTest() + } + @Test fun testObject() { doMultiPlatformTest() @@ -125,12 +145,12 @@ class QuickFixMultiModuleTest : AbstractQuickFixMultiModuleTest() { } @Test - fun testNested() { + fun testPrimaryConstructor() { doMultiPlatformTest() } @Test - fun testPrimaryConstructor() { + fun testPrimaryConstructorAbsence() { doMultiPlatformTest() } @@ -144,6 +164,11 @@ class QuickFixMultiModuleTest : AbstractQuickFixMultiModuleTest() { doMultiPlatformTest(impls = "js" to TargetPlatformKind.JavaScript) } + @Test + fun testSecondaryConstructorAbsence() { + doMultiPlatformTest() + } + @Test fun testWithTest() { doMultiPlatformTest(expectName = "common", withTests = true)