Quick Fixes: Implement 'Initialize with constructor parameter' quick-fix

#KT-6604 Fixed
This commit is contained in:
Alexey Sedunov
2015-11-12 17:19:05 +03:00
parent 138370ce5b
commit ac8532ffce
26 changed files with 428 additions and 11 deletions
@@ -411,7 +411,6 @@ public fun KtStringTemplateExpression.isPlain() = entries.all { it is KtLiteralS
public val KtDeclaration.containingClassOrObject: KtClassOrObject?
get() = (parent as? KtClassBody)?.parent as? KtClassOrObject
public fun KtExpression.getOutermostParenthesizerOrThis(): KtExpression {
return (parentsWithSelf zip parents).firstOrNull {
val (element, parent) = it
@@ -183,3 +183,12 @@ public fun KtDeclaration.setVisibility(visibilityModifier: KtModifierKeywordToke
addModifier(visibilityModifier)
}
fun KtSecondaryConstructor.getOrCreateBody(): KtBlockExpression {
bodyExpression?.let { return it }
val delegationCall = getDelegationCall()
val anchor = if (delegationCall.isImplicit) valueParameterList else delegationCall
val newBody = KtPsiFactory(this).createEmptyBody()
return addAfter(newBody, anchor) as KtBlockExpression
}
@@ -26,20 +26,26 @@ import com.intellij.psi.PsiElement
import com.intellij.psi.search.searches.MethodReferencesSearch
import org.jetbrains.kotlin.asJava.toLightMethods
import org.jetbrains.kotlin.descriptors.ClassDescriptorWithResolutionScopes
import org.jetbrains.kotlin.descriptors.ConstructorDescriptor
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
import org.jetbrains.kotlin.idea.codeInsight.CodeInsightUtils
import org.jetbrains.kotlin.idea.core.CollectingNameValidator
import org.jetbrains.kotlin.idea.core.KotlinNameSuggester
import org.jetbrains.kotlin.idea.core.appendElement
import org.jetbrains.kotlin.idea.core.getOrCreateBody
import org.jetbrains.kotlin.idea.core.refactoring.runRefactoringWithPostprocessing
import org.jetbrains.kotlin.idea.refactoring.changeSignature.*
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.createSmartPointer
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.resolve.descriptorUtil.secondaryConstructors
import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter
import org.jetbrains.kotlin.resolve.source.getPsi
import java.util.*
object InitializePropertyQuickFixFactory : KotlinIntentionActionsFactory() {
@@ -79,9 +85,7 @@ object InitializePropertyQuickFixFactory : KotlinIntentionActionsFactory() {
}
}
override fun performSilently(affectedFunctions: Collection<PsiElement>): Boolean {
return affectedFunctions.flatMap { it.toLightMethods() }.all { MethodReferencesSearch.search(it).findFirst() == null }
}
override fun performSilently(affectedFunctions: Collection<PsiElement>) = noUsagesExist(affectedFunctions)
}
}
@@ -113,6 +117,83 @@ object InitializePropertyQuickFixFactory : KotlinIntentionActionsFactory() {
}
}
class InitializeWithConstructorParameter(property: KtProperty) : KotlinQuickFixAction<KtProperty>(property) {
override fun getText() = "Initialize with constructor parameter"
override fun getFamilyName() = text
private fun configureChangeSignature(propertyDescriptor: PropertyDescriptor): KotlinChangeSignatureConfiguration {
return object : KotlinChangeSignatureConfiguration {
override fun configure(originalDescriptor: KotlinMethodDescriptor): KotlinMethodDescriptor {
return originalDescriptor.modify {
val classDescriptor = propertyDescriptor.containingDeclaration as ClassDescriptorWithResolutionScopes
val constructorScope = classDescriptor.scopeForClassHeaderResolution
val validator = CollectingNameValidator(originalDescriptor.parameters.map { it.name }) { name ->
constructorScope.getContributedDescriptors(DescriptorKindFilter.VARIABLES, { it.asString() == name }).isEmpty()
}
val initializerText = CodeInsightUtils.defaultInitializer(propertyDescriptor.type) ?: "null"
val newParam = KotlinParameterInfo(
callableDescriptor = originalDescriptor.baseDescriptor,
name = KotlinNameSuggester.suggestNameByName(propertyDescriptor.name.asString(), validator),
type = propertyDescriptor.type,
defaultValueForCall = KtPsiFactory(element.project).createExpression(initializerText)
)
it.addParameter(newParam)
}
}
override fun performSilently(affectedFunctions: Collection<PsiElement>): Boolean = noUsagesExist(affectedFunctions)
}
}
// TODO: Allow processing of multiple functions in Change Signature so that Start/Finish Mark can be used here
private fun processConstructors(
project: Project,
propertyDescriptor: PropertyDescriptor,
descriptorsToProcess: Iterator<ConstructorDescriptor>,
visitedElements: MutableSet<PsiElement> = HashSet()
) {
if (!descriptorsToProcess.hasNext()) return
val descriptor = descriptorsToProcess.next()
val constructorPointer = descriptor.source.getPsi()?.createSmartPointer()
val config = configureChangeSignature(propertyDescriptor)
val changeSignature = { runChangeSignature(project, descriptor, config, element, text) }
changeSignature.runRefactoringWithPostprocessing(project, "refactoring.changeSignature") {
val constructorOrClass = constructorPointer?.element
val constructor = constructorOrClass as? KtConstructor<*> ?: (constructorOrClass as? KtClass)?.getPrimaryConstructor()
if (constructor == null || !visitedElements.add(constructor)) return@runRefactoringWithPostprocessing
constructor.getValueParameters().lastOrNull()?.let { newParam ->
val psiFactory = KtPsiFactory(project)
if (constructor is KtSecondaryConstructor) {
constructor.getOrCreateBody().appendElement(psiFactory.createExpression("this.${element.name} = ${newParam.name!!}"))
}
else {
element.setInitializer(psiFactory.createExpression(newParam.name!!))
}
}
processConstructors(project, propertyDescriptor, descriptorsToProcess)
}
}
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
val propertyDescriptor = element.resolveToDescriptorIfAny() as? PropertyDescriptor ?: return
val classDescriptor = propertyDescriptor.containingDeclaration as? ClassDescriptorWithResolutionScopes ?: return
val klass = element.containingClassOrObject ?: return
val constructorDescriptors = if (klass.hasExplicitPrimaryConstructor() || klass.getSecondaryConstructors().isEmpty()) {
listOf(classDescriptor.unsubstitutedPrimaryConstructor!!)
}
else {
classDescriptor.secondaryConstructors
}
processConstructors(project, propertyDescriptor, constructorDescriptors.iterator())
}
}
private fun noUsagesExist(affectedFunctions: Collection<PsiElement>): Boolean {
return affectedFunctions.flatMap { it.toLightMethods() }.all { MethodReferencesSearch.search(it).findFirst() == null }
}
override fun doCreateActions(diagnostic: Diagnostic): List<IntentionAction> {
val property = diagnostic.psiElement as? KtProperty ?: return emptyList()
if (property.receiverTypeReference != null) return emptyList()
@@ -123,10 +204,13 @@ object InitializePropertyQuickFixFactory : KotlinIntentionActionsFactory() {
(property.containingClassOrObject as? KtClass)?.let { klass ->
if (klass.isAnnotation() || klass.isInterface()) return@let
if (property.accessors.isNotEmpty()) return@let
if (klass.getSecondaryConstructors().any { !it.getDelegationCall().isCallToThis }) return@let
actions.add(MoveToConstructorParameters(property))
if (property.accessors.isNotEmpty() || klass.getSecondaryConstructors().any { !it.getDelegationCall().isCallToThis }) {
actions.add(InitializeWithConstructorParameter(property))
}
else {
actions.add(MoveToConstructorParameters(property))
}
}
return actions
@@ -0,0 +1,4 @@
// "class org.jetbrains.kotlin.idea.quickfix.InitializePropertyQuickFixFactory$InitializeWithConstructorParameter" "false"
fun test() {
<caret>val n: Int
}
@@ -0,0 +1,11 @@
// "Initialize with constructor parameter" "true"
open class A(n: Int) {
<caret>var n: Int
get() = 1
}
class B : A(0)
fun test() {
val a = A(0)
}
@@ -0,0 +1,11 @@
// "Initialize with constructor parameter" "true"
open class A(n: Int, n1: Int) {
var n: Int = n1
get() = 1
}
class B : A(0, 0)
fun test() {
val a = A(0, 0)
}
@@ -0,0 +1,12 @@
// "Initialize with constructor parameter" "true"
open class A {
<caret>val n: Int
constructor(n: Int)
}
class B : A(1)
fun test() {
val a = A(1)
}
@@ -0,0 +1,14 @@
// "Initialize with constructor parameter" "true"
open class A {
val n: Int
constructor(n: Int, n1: Int) {
this.n = n1
}
}
class B : A(1, 0)
fun test() {
val a = A(1, 0)
}
@@ -0,0 +1,11 @@
// "Initialize with constructor parameter" "true"
open class A {
<caret>var n: Int
get() = 1
}
class B : A()
fun test() {
val a = A()
}
@@ -0,0 +1,11 @@
// "Initialize with constructor parameter" "true"
open class A(n: Int) {
var n: Int = n
get() = 1
}
class B : A(0)
fun test() {
val a = A(0)
}
@@ -0,0 +1,14 @@
// "Initialize with constructor parameter" "true"
open class A(s: String) {
<caret>var n: Int
get() = 1
constructor(): this("")
constructor(a: Int): this("" + a)
}
class B : A("")
fun test() {
val a = A("")
}
@@ -0,0 +1,14 @@
// "Initialize with constructor parameter" "true"
open class A(s: String, n: Int) {
var n: Int = n
get() = 1
constructor(): this("", 0)
constructor(a: Int): this("" + a, 0)
}
class B : A("", 0)
fun test() {
val a = A("", 0)
}
@@ -0,0 +1,11 @@
// "Initialize with constructor parameter" "true"
open class A(s: String) {
<caret>var n: Int
get() = 1
}
class B : A("")
fun test() {
val a = A("")
}
@@ -0,0 +1,11 @@
// "Initialize with constructor parameter" "true"
open class A(s: String, n: Int) {
var n: Int = n
get() = 1
}
class B : A("", 0)
fun test() {
val a = A("", 0)
}
@@ -0,0 +1,19 @@
// "Initialize with constructor parameter" "true"
open class A {
<caret>val n: Int
constructor(s: String)
constructor(a: Int) {
val t = 1
}
}
class B : A("")
class C : A(1)
fun test() {
val a = A("")
val aa = A(1)
}
@@ -0,0 +1,22 @@
// "Initialize with constructor parameter" "true"
open class A {
val n: Int
constructor(s: String, n: Int) {
this.n = n
}
constructor(a: Int, n: Int) {
val t = 1
this.n = n
}
}
class B : A("", 0)
class C : A(1, 0)
fun test() {
val a = A("", 0)
val aa = A(1, 0)
}
@@ -0,0 +1,7 @@
// "class org.jetbrains.kotlin.idea.quickfix.InitializePropertyQuickFixFactory$InitializeWithConstructorParameter" "false"
// ACTION: Make internal
// ACTION: Make private
// ACTION: Make protected
interface A {
<caret>val n: Int
}
@@ -0,0 +1,9 @@
// "class org.jetbrains.kotlin.idea.quickfix.InitializePropertyQuickFixFactory$InitializeWithConstructorParameter" "false"
// ACTION: Initialize property 'n'
// ACTION: Make 'n' abstract
// ACTION: Make internal
// ACTION: Make private
// ERROR: Property must be initialized or be abstract
object A {
<caret>val n: Int
}
@@ -0,0 +1,11 @@
// "class org.jetbrains.kotlin.idea.quickfix.InitializePropertyQuickFixFactory$InitializeWithConstructorParameter" "false"
// ERROR: Property must be initialized or be abstract
open class A {
<caret>val n: Int
}
class B : A()
fun test() {
val a = A()
}
@@ -0,0 +1,14 @@
// "class org.jetbrains.kotlin.idea.quickfix.InitializePropertyQuickFixFactory$InitializeWithConstructorParameter" "false"
// ERROR: Property must be initialized or be abstract
open class A(s: String) {
<caret>val n: Int
constructor(): this("")
constructor(a: Int): this("" + a)
}
class B : A("")
fun test() {
val a = A("")
}
@@ -0,0 +1,11 @@
// "class org.jetbrains.kotlin.idea.quickfix.InitializePropertyQuickFixFactory$InitializeWithConstructorParameter" "false"
// ERROR: Property must be initialized or be abstract
open class A(s: String) {
<caret>val n: Int
}
class B : A("")
fun test() {
val a = A("")
}
@@ -0,0 +1,7 @@
// "class org.jetbrains.kotlin.idea.quickfix.InitializePropertyQuickFixFactory$InitializeWithConstructorParameter" "false"
// ACTION: Make internal
// ACTION: Make private
// ACTION: Make protected
class A {
<caret>val n: Int by lazy { 0 }
}
@@ -0,0 +1,6 @@
// "class org.jetbrains.kotlin.idea.quickfix.InitializePropertyQuickFixFactory$InitializeWithConstructorParameter" "false"
// ACTION: Initialize property 'n'
// ACTION: Make internal
// ACTION: Make private
// ERROR: Property must be initialized
<caret>val n: Int
@@ -4,6 +4,7 @@
// ACTION: Make internal
// ACTION: Make private
// ACTION: Make protected
// ACTION: Initialize with constructor parameter
// ERROR: Property must be initialized or be abstract
open class A(x: Int)
@@ -4,6 +4,7 @@
// ACTION: Make internal
// ACTION: Make private
// ACTION: Make protected
// ACTION: Initialize with constructor parameter
// ERROR: Property must be initialized or be abstract
open class A
@@ -4293,6 +4293,99 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
}
}
@TestMetadata("idea/testData/quickfix/initializeWithConstructorParameter")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class InitializeWithConstructorParameter extends AbstractQuickFixTest {
public void testAllFilesPresentInInitializeWithConstructorParameter() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/initializeWithConstructorParameter"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true);
}
@TestMetadata("localVar.kt")
public void testLocalVar() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/initializeWithConstructorParameter/localVar.kt");
doTest(fileName);
}
@TestMetadata("memberPropertyInClassNameClashInPrimaryConstructor.kt")
public void testMemberPropertyInClassNameClashInPrimaryConstructor() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/initializeWithConstructorParameter/memberPropertyInClassNameClashInPrimaryConstructor.kt");
doTest(fileName);
}
@TestMetadata("memberPropertyInClassNameClashInSecondaryConstructor.kt")
public void testMemberPropertyInClassNameClashInSecondaryConstructor() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/initializeWithConstructorParameter/memberPropertyInClassNameClashInSecondaryConstructor.kt");
doTest(fileName);
}
@TestMetadata("memberPropertyInClassNoConstructors.kt")
public void testMemberPropertyInClassNoConstructors() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/initializeWithConstructorParameter/memberPropertyInClassNoConstructors.kt");
doTest(fileName);
}
@TestMetadata("memberPropertyInClassPrimaryAndSecondaryConstructors.kt")
public void testMemberPropertyInClassPrimaryAndSecondaryConstructors() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/initializeWithConstructorParameter/memberPropertyInClassPrimaryAndSecondaryConstructors.kt");
doTest(fileName);
}
@TestMetadata("memberPropertyInClassPrimaryConstructorOnly.kt")
public void testMemberPropertyInClassPrimaryConstructorOnly() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/initializeWithConstructorParameter/memberPropertyInClassPrimaryConstructorOnly.kt");
doTest(fileName);
}
@TestMetadata("memberPropertyInClassSecondaryConstructorsOnly.kt")
public void testMemberPropertyInClassSecondaryConstructorsOnly() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/initializeWithConstructorParameter/memberPropertyInClassSecondaryConstructorsOnly.kt");
doTest(fileName);
}
@TestMetadata("memberPropertyInInterface.kt")
public void testMemberPropertyInInterface() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/initializeWithConstructorParameter/memberPropertyInInterface.kt");
doTest(fileName);
}
@TestMetadata("memberPropertyInObject.kt")
public void testMemberPropertyInObject() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/initializeWithConstructorParameter/memberPropertyInObject.kt");
doTest(fileName);
}
@TestMetadata("memberPropertyNoAccessorsInClassNoConstructors.kt")
public void testMemberPropertyNoAccessorsInClassNoConstructors() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/initializeWithConstructorParameter/memberPropertyNoAccessorsInClassNoConstructors.kt");
doTest(fileName);
}
@TestMetadata("memberPropertyNoAccessorsInClassPrimaryAndSecondaryConstructors.kt")
public void testMemberPropertyNoAccessorsInClassPrimaryAndSecondaryConstructors() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/initializeWithConstructorParameter/memberPropertyNoAccessorsInClassPrimaryAndSecondaryConstructors.kt");
doTest(fileName);
}
@TestMetadata("memberPropertyNoAccessorsInClassPrimaryConstructorOnly.kt")
public void testMemberPropertyNoAccessorsInClassPrimaryConstructorOnly() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/initializeWithConstructorParameter/memberPropertyNoAccessorsInClassPrimaryConstructorOnly.kt");
doTest(fileName);
}
@TestMetadata("memberPropertyWithDelegateRuntime.kt")
public void testMemberPropertyWithDelegateRuntime() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/initializeWithConstructorParameter/memberPropertyWithDelegateRuntime.kt");
doTest(fileName);
}
@TestMetadata("topLevelProperty.kt")
public void testTopLevelProperty() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/initializeWithConstructorParameter/topLevelProperty.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/quickfix/insertDelegationCall")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)