Introduce "add missing actual members" quick-fix #KT-18449 Fixed

This commit is contained in:
Mikhail Glukhikh
2017-10-03 16:21:15 +03:00
committed by Mikhail Glukhikh
parent 1b0421d27b
commit 739b21f519
24 changed files with 288 additions and 7 deletions
@@ -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)
@@ -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<KtClassOrObject>(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) }
}
}
}
@@ -156,7 +156,7 @@ class CreateActualClassFix(
klass: KtClassOrObject,
actualPlatform: MultiTargetPlatform.Specific
) : CreateActualFix<KtClassOrObject>(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<KtDeclaration> = 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
@@ -0,0 +1,5 @@
// DISABLE-ERRORS
expect class My {
fun foo(param: String): Int
}
@@ -0,0 +1,5 @@
// DISABLE-ERRORS
expect class My {
fun foo(param: String): Int
}
@@ -0,0 +1,4 @@
// "Add missing actual members" "true"
// DISABLE-ERRORS
actual class <caret>My
@@ -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.
}
}
@@ -0,0 +1,7 @@
// DISABLE-ERRORS
expect class My {
fun foo(param: String): Int
fun foo(param: Int): Int
}
@@ -0,0 +1,7 @@
// DISABLE-ERRORS
expect class My {
fun foo(param: String): Int
fun foo(param: Int): Int
}
@@ -0,0 +1,6 @@
// "Add missing actual members" "true"
// DISABLE-ERRORS
actual class <caret>My {
fun foo(param: String) = 42
}
@@ -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.
}
}
@@ -0,0 +1,11 @@
// DISABLE-ERRORS
expect class My {
fun foo(param: String): Int
val x: String
val pi: Double
val correct: Boolean
}
@@ -0,0 +1,11 @@
// DISABLE-ERRORS
expect class My {
fun foo(param: String): Int
val x: String
val pi: Double
val correct: Boolean
}
@@ -0,0 +1,8 @@
// "Add missing actual members" "true"
// DISABLE-ERRORS
actual class <caret>My {
actual fun foo(param: String) = 42
actual val correct = true
}
@@ -0,0 +1,12 @@
// "Add missing actual members" "true"
// DISABLE-ERRORS
actual class <caret>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.
}
@@ -0,0 +1,7 @@
// DISABLE-ERRORS
expect class WithPrimaryConstructor(x: Int, s: String) {
fun bar(x: String)
val z: Double
}
@@ -0,0 +1,7 @@
// DISABLE-ERRORS
expect class WithPrimaryConstructor(x: Int, s: String) {
fun bar(x: String)
val z: Double
}
@@ -0,0 +1,8 @@
// "Add missing actual members" "true"
// DISABLE-ERRORS
actual class <caret>WithPrimaryConstructor {
fun bar(x: String) {}
val z: Double = 3.14
}
@@ -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
}
@@ -0,0 +1,9 @@
// DISABLE-ERRORS
expect class WithSecondaryConstructor {
constructor(x: Int, s: String)
fun bar(x: String)
val z: Double
}
@@ -0,0 +1,9 @@
// DISABLE-ERRORS
expect class WithSecondaryConstructor {
constructor(x: Int, s: String)
fun bar(x: String)
val z: Double
}
@@ -0,0 +1,8 @@
// "Add missing actual members" "true"
// DISABLE-ERRORS
actual class <caret>WithSecondaryConstructor {
fun bar(x: String) {}
val z: Double = 3.14
}
@@ -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.
}
}
@@ -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)