KT-9839: intention introduced: convert secondary constructor to primary one #KT-9839 Fixed

This commit is contained in:
Mikhail Glukhikh
2016-09-29 16:47:35 +03:00
parent b90414ac21
commit f3fa779762
44 changed files with 555 additions and 0 deletions

View File

@@ -386,6 +386,10 @@ class KtPsiFactory(private val project: Project) {
return createClass("class A()").getPrimaryConstructor()!!
}
fun createPrimaryConstructor(modifiers: String?): KtPrimaryConstructor {
return modifiers?.let { createClass("class A $modifiers constructor()").getPrimaryConstructor() } ?: createPrimaryConstructor()
}
fun createConstructorKeyword(): PsiElement =
createClass("class A constructor()").getPrimaryConstructor()!!.getConstructorKeyword()!!

View File

@@ -0,0 +1,3 @@
class Foo(val x: Int = 4, y: Int) {
val z = y
}

View File

@@ -0,0 +1,9 @@
class Foo {
val z = y
val x: Int
constructor(x: Int = 4, y: Int) {
this.x = x
}
}

View File

@@ -0,0 +1,5 @@
<html>
<body>
This intention converts secondary constructor to primary one.
</body>
</html>

View File

@@ -1350,6 +1350,11 @@
<category>Kotlin</category>
</intentionAction>
<intentionAction>
<className>org.jetbrains.kotlin.idea.intentions.ConvertSecondaryConstructorToPrimaryIntention</className>
<category>Kotlin</category>
</intentionAction>
<localInspection implementationClass="org.jetbrains.kotlin.idea.intentions.ObjectLiteralToLambdaInspection"
displayName="Object literal can be converted to lambda"
groupName="Kotlin"

View File

@@ -0,0 +1,149 @@
/*
* Copyright 2010-2016 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.openapi.editor.Editor
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.analyzeFully
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils
class ConvertSecondaryConstructorToPrimaryIntention : SelfTargetingIntention<KtSecondaryConstructor>(
KtSecondaryConstructor::class.java,
"Convert to primary constructor"
) {
private tailrec fun ConstructorDescriptor.isReachableByDelegationFrom(
constructor: ConstructorDescriptor, context: BindingContext, visited: Set<ConstructorDescriptor> = emptySet()
): Boolean {
if (constructor == this) return true
if (constructor in visited) return false
val resolvedDelegationCall = context[BindingContext.CONSTRUCTOR_RESOLVED_DELEGATION_CALL, constructor] ?: return false
val delegationDescriptor = resolvedDelegationCall.candidateDescriptor
return isReachableByDelegationFrom(delegationDescriptor, context, visited + constructor)
}
override fun isApplicableTo(element: KtSecondaryConstructor, caretOffset: Int): Boolean {
val delegationCall = element.getDelegationCall()
if (delegationCall.isCallToThis) return false
val klass = element.containingClassOrObject ?: return false
if (klass.hasPrimaryConstructor()) return false
val context = klass.analyze()
val classDescriptor = context[BindingContext.CLASS, klass] ?: return false
val elementDescriptor = context[BindingContext.CONSTRUCTOR, element] ?: return false
for (constructorDescriptor in classDescriptor.constructors) {
if (constructorDescriptor == elementDescriptor) continue
if (!elementDescriptor.isReachableByDelegationFrom(constructorDescriptor, context)) return false
}
return true
}
private fun KtExpression.tryConvertToPropertyByParameterInitialization(
constructorDescriptor: ConstructorDescriptor, context: BindingContext
): Pair<ValueParameterDescriptor, PropertyDescriptor>? {
if (this !is KtBinaryExpression || operationToken != KtTokens.EQ) return null
val rightReference = right as? KtReferenceExpression ?: return null
val rightDescriptor = context[BindingContext.REFERENCE_TARGET, rightReference] as? ValueParameterDescriptor ?: return null
if (rightDescriptor.containingDeclaration != constructorDescriptor) return null
val left = left
val leftReference = when (left) {
is KtReferenceExpression ->
left
is KtDotQualifiedExpression ->
if (left.receiverExpression is KtThisExpression) left.selectorExpression as? KtReferenceExpression else null
else ->
null
}
val leftDescriptor = context[BindingContext.REFERENCE_TARGET, leftReference] as? PropertyDescriptor ?: return null
return rightDescriptor to leftDescriptor
}
override fun applyTo(element: KtSecondaryConstructor, editor: Editor?) {
val klass = element.containingClassOrObject as? KtClass ?: return
val context = klass.analyzeFully()
val constructorDescriptor = context[BindingContext.CONSTRUCTOR, element] ?: return
val factory = KtPsiFactory(klass)
val constructorInClass = klass.createPrimaryConstructorIfAbsent()
val constructor = factory.createPrimaryConstructor(element.modifierList?.text?.replace("\n", " "))
val parameterList = constructor.valueParameterList!!
val parameterToPropertyMap = mutableMapOf<ValueParameterDescriptor, PropertyDescriptor>()
val initializer = factory.createAnonymousInitializer() as? KtClassInitializer
for (statement in element.bodyExpression?.statements ?: emptyList()) {
val (rightDescriptor, leftDescriptor) = statement.tryConvertToPropertyByParameterInitialization(constructorDescriptor, context)
?: with (initializer) {
(initializer?.body as? KtBlockExpression)?.let {
it.addBefore(statement.copy(), it.rBrace)
it.addBefore(factory.createNewLine(), it.rBrace)
}
null to null
}
if (rightDescriptor == null || leftDescriptor == null) continue
parameterToPropertyMap[rightDescriptor] = leftDescriptor
}
for (parameter in element.valueParameters) {
val newParameter = factory.createParameter(parameter.text)
val parameterDescriptor = context[BindingContext.VALUE_PARAMETER, parameter]
val propertyDescriptor = parameterToPropertyMap[parameterDescriptor]
if (parameterDescriptor != null && propertyDescriptor != null) {
val property = DescriptorToSourceUtils.descriptorToDeclaration(propertyDescriptor) as? KtProperty
if (property != null) {
if (propertyDescriptor.name == parameterDescriptor.name && propertyDescriptor.type == parameterDescriptor.type) {
val valOrVar = if (property.isVar) factory.createVarKeyword() else factory.createValKeyword()
newParameter.addBefore(valOrVar, newParameter.nameIdentifier)
val propertyModifiers = property.modifierList?.text
if (propertyModifiers != null) {
val newModifiers = factory.createModifierList(propertyModifiers)
newParameter.addBefore(newModifiers, newParameter.valOrVarKeyword)
}
property.delete()
}
else {
property.initializer = factory.createSimpleName(parameterDescriptor.name.asString())
}
}
}
parameterList.addParameter(newParameter)
}
val delegationCall = element.getDelegationCall()
val argumentList = delegationCall.valueArgumentList
if (!delegationCall.isImplicit && argumentList != null) {
for (superTypeListEntry in klass.getSuperTypeListEntries()) {
val typeReference = superTypeListEntry.typeReference ?: continue
val type = context[BindingContext.TYPE, typeReference]
if ((type?.constructor?.declarationDescriptor as? ClassDescriptor)?.kind == ClassKind.CLASS) {
val superTypeCallEntry = factory.createSuperTypeCallEntry("${typeReference.text}${argumentList.text}")
superTypeListEntry.replace(superTypeCallEntry)
break
}
}
}
constructorInClass.replace(constructor)
element.delete()
if ((initializer?.body as? KtBlockExpression)?.statements?.isNotEmpty() ?: false) {
initializer?.let { klass.addDeclaration(it) }
}
}
}

View File

@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.intentions.ConvertSecondaryConstructorToPrimaryIntention

View File

@@ -0,0 +1,11 @@
class DefaultValueChain {
val x1: Int
val x3: Int
val x5: Int
<caret>constructor(x1: Int, x2: Int = x1, x3: Int = x2, x4: Int = x3, x5: Int = x4) {
this.x1 = x1
this.x3 = x3
this.x5 = x5
}
}

View File

@@ -0,0 +1,3 @@
class DefaultValueChain(val x1: Int, x2: Int = x1, val x3: Int = x2, x4: Int = x3, val x5: Int = x4) {
}

View File

@@ -0,0 +1,10 @@
class ConvertToInit {
fun foo() {}
fun bar() {}
constructor(<caret>) {
foo()
bar()
}
}

View File

@@ -0,0 +1,11 @@
class ConvertToInit() {
fun foo() {}
fun bar() {}
init {
foo()
bar()
}
}

View File

@@ -0,0 +1,15 @@
class InitAndParams {
constructor<caret>(x: Int, z: Int) {
this.y = x
w = foo(y)
this.v = w + z
}
val y: Int
val w: Int
fun foo(arg: Int) = arg
val v: Int
}

View File

@@ -0,0 +1,15 @@
class InitAndParams(x: Int, z: Int) {
val y: Int = x
val w: Int
fun foo(arg: Int) = arg
val v: Int
init {
w = foo(y)
this.v = w + z
}
}

View File

@@ -0,0 +1,7 @@
// IS_APPLICABLE: false
class NonReachableConstructor {
constructor<caret>(x: String)
constructor(x: Int)
}

View File

@@ -0,0 +1,11 @@
// IS_APPLICABLE: false
// ERROR: There's a cycle in the delegation calls chain
// ERROR: There's a cycle in the delegation calls chain
class NonReachableLoop {
constructor<caret>(x: String)
constructor(x: Int, y: Int): this(x + y)
constructor(x: Int): this(x, x)
}

View File

@@ -0,0 +1,7 @@
class Protected {
internal var s: String
protected <caret>constructor(s: String) {
this.s = s
}
}

View File

@@ -0,0 +1,3 @@
class Protected protected constructor(internal var s: String) {
}

View File

@@ -0,0 +1,3 @@
class Simple {
constructor<caret>()
}

View File

@@ -0,0 +1,2 @@
class Simple() {
}

View File

@@ -0,0 +1,7 @@
class UseParam {
constructor<caret>(x: Int) {
this.y = x
}
val y: Int
}

View File

@@ -0,0 +1,4 @@
class UseParam(x: Int) {
val y: Int = x
}

View File

@@ -0,0 +1,10 @@
// WITH_RUNTIME
class WithVarArg {
val x: List<String>
constructor(<caret>vararg zz: String) {
x = listOf(*zz)
}
}

View File

@@ -0,0 +1,11 @@
// WITH_RUNTIME
class WithVarArg(vararg zz: String) {
val x: List<String>
init {
x = listOf(*zz)
}
}

View File

@@ -0,0 +1,9 @@
// WITH_RUNTIME
class VarargVal {
val param: Array<out String>
constructor<caret>(vararg param: String) {
this.param = param
}
}

View File

@@ -0,0 +1,5 @@
// WITH_RUNTIME
class VarargVal(vararg val param: String) {
}

View File

@@ -0,0 +1,5 @@
abstract class Base(val x: String)
class Derived : Base {
constructor(x: String<caret>): super(x)
}

View File

@@ -0,0 +1,4 @@
abstract class Base(val x: String)
class Derived(x: String) : Base(x) {
}

View File

@@ -0,0 +1,14 @@
interface Interface
interface Another
abstract class Base
class Derived : Interface, Base, Another {
val x: String
constructor(x: String<caret>): super() {
this.x = x
}
}

View File

@@ -0,0 +1,9 @@
interface Interface
interface Another
abstract class Base
class Derived(val x: String) : Interface, Base(), Another {
}

View File

@@ -0,0 +1,12 @@
annotation class AnnParam
annotation class AnnProperty
abstract class WithComposedModifiers {
@AnnProperty
open val x: Array<out String>
constructor<caret>(@AnnParam vararg x: String) {
this.x = x
}
}

View File

@@ -0,0 +1,8 @@
annotation class AnnParam
annotation class AnnProperty
abstract class WithComposedModifiers(@AnnParam vararg @AnnProperty
open val x: String) {
}

View File

@@ -0,0 +1,7 @@
// IS_APPLICABLE: false
class WithDelegation {
constructor()
constructor<caret>(x: Int): this()
}

View File

@@ -0,0 +1,10 @@
class WithDifferentTypeProperty {
val x: Number
val y: String
constructor(x: Int, <caret>z: String) {
this.x = x
this.y = z
}
}

View File

@@ -0,0 +1,6 @@
class WithDifferentTypeProperty(x: Int, z: String) {
val x: Number = x
val y: String = z
}

View File

@@ -0,0 +1,6 @@
annotation class Ann
internal class WithModifiers {
@Ann
private constructor<caret>()
}

View File

@@ -0,0 +1,4 @@
annotation class Ann
internal class WithModifiers @Ann private constructor() {
}

View File

@@ -0,0 +1,3 @@
class WithParameters {
constructor(<caret>x: Int = 42, y: String = "Hello")
}

View File

@@ -0,0 +1,2 @@
class WithParameters(x: Int = 42, y: String = "Hello") {
}

View File

@@ -0,0 +1,6 @@
// IS_APPLICABLE: false
// ERROR: Primary constructor call expected
class WithPrimary() {
constructor<caret>(x: Int)
}

View File

@@ -0,0 +1,11 @@
class WithProperties {
val x: Int
val y: Int
private val z: Int
constructor(x: Int, y: Int = 7, z: Int = 13<caret>) {
this.x = x
this.y = y
this.z = z
}
}

View File

@@ -0,0 +1,3 @@
class WithProperties(val x: Int, val y: Int = 7, private val z: Int = 13) {
}

View File

@@ -1,5 +1,6 @@
// "Create secondary constructor" "false"
// ERROR: Too many arguments for public constructor Any() defined in kotlin.Any
// ACTION: Convert to primary constructor
// WITH_RUNTIME
interface T {

View File

@@ -1,5 +1,6 @@
// "Insert 'this()' call" "false"
// ACTION: Insert 'super()' call
// ACTION: Convert to primary constructor
// ERROR: Explicit 'this' or 'super' call is required. There is no constructor in superclass that can be called without arguments
open class B(val x: Int)

View File

@@ -4688,6 +4688,129 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
}
}
@TestMetadata("idea/testData/intentions/convertSecondaryConstructorToPrimary")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ConvertSecondaryConstructorToPrimary extends AbstractIntentionTest {
public void testAllFilesPresentInConvertSecondaryConstructorToPrimary() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/convertSecondaryConstructorToPrimary"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true);
}
@TestMetadata("defaultValueChain.kt")
public void testDefaultValueChain() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSecondaryConstructorToPrimary/defaultValueChain.kt");
doTest(fileName);
}
@TestMetadata("init.kt")
public void testInit() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSecondaryConstructorToPrimary/init.kt");
doTest(fileName);
}
@TestMetadata("initAndParams.kt")
public void testInitAndParams() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSecondaryConstructorToPrimary/initAndParams.kt");
doTest(fileName);
}
@TestMetadata("nonReachable.kt")
public void testNonReachable() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSecondaryConstructorToPrimary/nonReachable.kt");
doTest(fileName);
}
@TestMetadata("nonReachableLoop.kt")
public void testNonReachableLoop() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSecondaryConstructorToPrimary/nonReachableLoop.kt");
doTest(fileName);
}
@TestMetadata("protectedConstructor.kt")
public void testProtectedConstructor() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSecondaryConstructorToPrimary/protectedConstructor.kt");
doTest(fileName);
}
@TestMetadata("simple.kt")
public void testSimple() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSecondaryConstructorToPrimary/simple.kt");
doTest(fileName);
}
@TestMetadata("useParam.kt")
public void testUseParam() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSecondaryConstructorToPrimary/useParam.kt");
doTest(fileName);
}
@TestMetadata("varArg.kt")
public void testVarArg() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSecondaryConstructorToPrimary/varArg.kt");
doTest(fileName);
}
@TestMetadata("varargVal.kt")
public void testVarargVal() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSecondaryConstructorToPrimary/varargVal.kt");
doTest(fileName);
}
@TestMetadata("withBaseClass.kt")
public void testWithBaseClass() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSecondaryConstructorToPrimary/withBaseClass.kt");
doTest(fileName);
}
@TestMetadata("withBaseClassNoArgs.kt")
public void testWithBaseClassNoArgs() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSecondaryConstructorToPrimary/withBaseClassNoArgs.kt");
doTest(fileName);
}
@TestMetadata("withComposedModifiers.kt")
public void testWithComposedModifiers() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSecondaryConstructorToPrimary/withComposedModifiers.kt");
doTest(fileName);
}
@TestMetadata("withDelegation.kt")
public void testWithDelegation() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSecondaryConstructorToPrimary/withDelegation.kt");
doTest(fileName);
}
@TestMetadata("withDifferentTypeProperty.kt")
public void testWithDifferentTypeProperty() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSecondaryConstructorToPrimary/withDifferentTypeProperty.kt");
doTest(fileName);
}
@TestMetadata("withModifiers.kt")
public void testWithModifiers() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSecondaryConstructorToPrimary/withModifiers.kt");
doTest(fileName);
}
@TestMetadata("withParameters.kt")
public void testWithParameters() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSecondaryConstructorToPrimary/withParameters.kt");
doTest(fileName);
}
@TestMetadata("withPrimary.kt")
public void testWithPrimary() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSecondaryConstructorToPrimary/withPrimary.kt");
doTest(fileName);
}
@TestMetadata("withProperties.kt")
public void testWithProperties() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSecondaryConstructorToPrimary/withProperties.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/intentions/convertToBlockBody")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)