Add intentions to move property from/to primary constructor

#KT-4578 Fixed
This commit is contained in:
Vyacheslav Gerasimov
2017-03-18 23:15:46 +03:00
parent fef4c8ccd8
commit f60a7ffab0
32 changed files with 465 additions and 0 deletions
@@ -0,0 +1,3 @@
class TestClass(text: String) {
private val text = text
}
@@ -0,0 +1 @@
class TestClass(private val <caret>text: String)
@@ -0,0 +1,5 @@
<html>
<body>
This intention moves property from primary constructor to the class body.
</body>
</html>
@@ -0,0 +1,2 @@
class TestClass(private val text: String = "Lorem Ipsum") {
}
@@ -0,0 +1,3 @@
class TestClass {
private val text = "Lorem Ipsum"
}
@@ -0,0 +1,5 @@
<html>
<body>
This intention moves property to primary constructor.
</body>
</html>
+10
View File
@@ -1522,6 +1522,16 @@
<category>Kotlin</category>
</intentionAction>
<intentionAction>
<className>org.jetbrains.kotlin.idea.intentions.MovePropertyToClassBodyIntention</className>
<category>Kotlin</category>
</intentionAction>
<intentionAction>
<className>org.jetbrains.kotlin.idea.intentions.MovePropertyToConstructorIntention</className>
<category>Kotlin</category>
</intentionAction>
<localInspection implementationClass="org.jetbrains.kotlin.idea.intentions.ObjectLiteralToLambdaInspection"
displayName="Object literal can be converted to lambda"
groupName="Kotlin"
@@ -0,0 +1,100 @@
/*
* 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.openapi.editor.Editor
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.descriptors.annotations.AnnotationUseSiteTarget
import org.jetbrains.kotlin.descriptors.annotations.KotlinTarget
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.isPropertyParameter
import org.jetbrains.kotlin.resolve.AnnotationChecker
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
class MovePropertyToClassBodyIntention : SelfTargetingIntention<KtParameter>(KtParameter::class.java, "Move to class body") {
override fun isApplicableTo(element: KtParameter, caretOffset: Int): Boolean = element.isPropertyParameter()
override fun applyTo(element: KtParameter, editor: Editor?) {
val parentClass = PsiTreeUtil.getParentOfType(element, KtClass::class.java) ?: return
val propertyDeclaration = KtPsiFactory(element)
.createProperty("${element.valOrVarKeyword?.text} ${element.name} = ${element.name}")
parentClass.addDeclaration(propertyDeclaration).apply {
val propertyModifierList = element.modifierList?.copy() as? KtModifierList
propertyModifierList?.let { modifierList?.replace(it) ?: addBefore(it, firstChild) }
modifierList?.annotationEntries?.forEach {
if (!it.isAppliedToProperty()) {
it.delete()
}
else if (it.useSiteTarget?.getAnnotationUseSiteTarget() == AnnotationUseSiteTarget.PROPERTY) {
it.useSiteTarget?.removeWithColon()
}
}
}
element.valOrVarKeyword?.delete()
val parameterAnnotationsText = element.modifierList?.annotationEntries
?.filter { it.isAppliedToConstructorParameter() }
?.takeIf { it.isNotEmpty() }
?.joinToString(separator = " ") { it.textWithoutUseSite() }
if (parameterAnnotationsText != null) {
element.modifierList?.replace(KtPsiFactory(element).createModifierList(parameterAnnotationsText))
}
else {
element.modifierList?.delete()
}
}
fun KtAnnotationEntry.isAppliedToProperty(): Boolean {
useSiteTarget?.getAnnotationUseSiteTarget()?.let {
return it == AnnotationUseSiteTarget.FIELD
|| it == AnnotationUseSiteTarget.PROPERTY
|| it == AnnotationUseSiteTarget.PROPERTY_GETTER
|| it == AnnotationUseSiteTarget.PROPERTY_SETTER
|| it == AnnotationUseSiteTarget.SETTER_PARAMETER
}
return !isApplicableToConstructorParameter()
}
fun KtAnnotationEntry.isAppliedToConstructorParameter(): Boolean {
useSiteTarget?.getAnnotationUseSiteTarget()?.let {
return it == AnnotationUseSiteTarget.CONSTRUCTOR_PARAMETER
}
return isApplicableToConstructorParameter()
}
fun KtAnnotationEntry.isApplicableToConstructorParameter(): Boolean {
val context = analyze(BodyResolveMode.PARTIAL)
val descriptor = context[BindingContext.ANNOTATION, this] ?: return false
val applicableTargets = AnnotationChecker.applicableTargetSet(descriptor)
return applicableTargets.contains(KotlinTarget.VALUE_PARAMETER)
}
fun KtAnnotationEntry.textWithoutUseSite() = "@" + typeReference?.text.orEmpty() + valueArgumentList?.text.orEmpty()
fun KtAnnotationUseSiteTarget.removeWithColon() {
nextSibling?.delete() // ':' symbol after use site
delete()
}
}
@@ -0,0 +1,132 @@
/*
* 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.openapi.editor.Editor
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.descriptors.ParameterDescriptor
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.descriptors.annotations.KotlinTarget
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor
import org.jetbrains.kotlin.idea.core.ShortenReferences
import org.jetbrains.kotlin.idea.util.CommentSaver
import org.jetbrains.kotlin.lexer.KtModifierKeywordToken
import org.jetbrains.kotlin.lexer.KtTokens.LATEINIT_KEYWORD
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.AnnotationChecker
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.resolve.source.getPsi
import org.jetbrains.kotlin.types.KotlinType
class MovePropertyToConstructorIntention :
SelfTargetingIntention<KtProperty>(KtProperty::class.java, "Move to constructor") {
override fun isApplicableTo(element: KtProperty, caretOffset: Int): Boolean =
!element.isLocal
&& !element.hasDelegate()
&& element.getter == null
&& element.setter == null
&& !element.hasModifier(LATEINIT_KEYWORD)
override fun applyTo(element: KtProperty, editor: Editor?) {
val parentClass = PsiTreeUtil.getParentOfType(element, KtClass::class.java) ?: return
val factory = KtPsiFactory(element)
val primaryConstructor = parentClass.createPrimaryConstructorIfAbsent()
val constructorParameter = element.findConstructorParameter()
val commentSaver = CommentSaver(element)
val propertyAnnotationsText = element.modifierList?.annotationEntries?.joinToString(separator = " ") {
if (it.isApplicableToConstructorParameter()) {
it.getTextWithUseSiteIfMissing("property")
}
else {
it.text
}
}
if (constructorParameter != null) {
val parameterAnnotationsText =
constructorParameter.modifierList?.annotationEntries?.joinToString(separator = " ") { it.text }
val parameterText = buildString {
element.modifierList?.getModifiersText()?.let(this::append)
propertyAnnotationsText?.takeIf(String::isNotBlank)?.let { appendWithSpaceBefore(it) }
parameterAnnotationsText?.let { appendWithSpaceBefore(it) }
appendWithSpaceBefore(element.valOrVarKeyword.text)
element.name?.let { appendWithSpaceBefore(it) }
constructorParameter.typeReference?.text?.let { append(": $it") }
constructorParameter.defaultValue?.text?.let { append(" = $it") }
}
constructorParameter.replace(factory.createParameter(parameterText)).apply {
commentSaver.restore(this)
}
}
else {
val type = (element.resolveToDescriptor(BodyResolveMode.PARTIAL) as? PropertyDescriptor)?.type ?: return
val parameterText = buildString {
element.modifierList?.getModifiersText()?.let(this::append)
propertyAnnotationsText?.takeIf(String::isNotBlank)?.let { appendWithSpaceBefore(it) }
appendWithSpaceBefore(element.valOrVarKeyword.text)
element.name?.let { appendWithSpaceBefore(it) }
appendWithSpaceBefore(": ${type.fqNameSafeAsString()}")
element.initializer?.text?.let { append(" = $it") }
}
primaryConstructor.valueParameterList?.addParameter(factory.createParameter(parameterText))?.apply {
ShortenReferences.DEFAULT.process(this)
commentSaver.restore(this)
}
}
element.delete()
}
private fun KtProperty.findConstructorParameter(): KtParameter? {
val reference = initializer as? KtReferenceExpression ?: return null
val parameterDescriptor = reference.analyze(BodyResolveMode.PARTIAL)[BindingContext.REFERENCE_TARGET, reference]
as? ParameterDescriptor ?: return null
return parameterDescriptor.source.getPsi() as? KtParameter
}
fun KtAnnotationEntry.isApplicableToConstructorParameter(): Boolean {
val context = analyze(BodyResolveMode.PARTIAL)
val descriptor = context[BindingContext.ANNOTATION, this] ?: return false
val applicableTargets = AnnotationChecker.applicableTargetSet(descriptor)
return applicableTargets.contains(KotlinTarget.VALUE_PARAMETER)
}
private fun KtAnnotationEntry.getTextWithUseSiteIfMissing(useSite: String) =
if (useSiteTarget == null)
"@$useSite:${typeReference?.text.orEmpty()}${valueArgumentList?.text.orEmpty()}"
else
text
private fun KotlinType.fqNameSafeAsString() = constructor.declarationDescriptor?.fqNameSafe?.asString() ?: "<error type>"
private fun KtModifierList.getModifiersText() = getModifiers().joinToString(separator = " ") { it.text }
private fun KtModifierList.getModifiers(): List<PsiElement> =
node.getChildren(null).filter { it.elementType is KtModifierKeywordToken }.map { it.psi }
private fun StringBuilder.appendWithSpaceBefore(str: String) = append(" " + str)
}
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.intentions.MovePropertyToClassBodyIntention
@@ -0,0 +1,6 @@
annotation class Annotation1(val a: Int = 0)
annotation class Annotation2(val a: Int = 0)
annotation class Annotation3(val a: Int = 0)
class TestClass(private @param:Annotation1(42) @property:Annotation1(42) @field:Annotation2(42) @Annotation3(42) val <caret>text: String = "LoremIpsum")
@@ -0,0 +1,8 @@
annotation class Annotation1(val a: Int = 0)
annotation class Annotation2(val a: Int = 0)
annotation class Annotation3(val a: Int = 0)
class TestClass(@Annotation1(42) @Annotation3(42) text: String = "LoremIpsum") {
private @Annotation1(42) @field:Annotation2(42) val text = text
}
@@ -0,0 +1,5 @@
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class ParameterAnnotation(val a: Int = 0)
class TestClass(private @ParameterAnnotation(42) val <caret>text: String = "LoremIpsum", val flag: Boolean)
@@ -0,0 +1,7 @@
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class ParameterAnnotation(val a: Int = 0)
class TestClass(@ParameterAnnotation(42) text: String = "LoremIpsum", val flag: Boolean) {
private val text = text
}
@@ -0,0 +1,5 @@
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
annotation class PropertyAnnotation(val a: Int = 0)
class TestClass(private @PropertyAnnotation(42) val <caret>text: String = "LoremIpsum")
@@ -0,0 +1,7 @@
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
annotation class PropertyAnnotation(val a: Int = 0)
class TestClass(text: String = "LoremIpsum") {
private @PropertyAnnotation(42) val text = text
}
@@ -0,0 +1 @@
class TestClass(val <caret>text: String)
@@ -0,0 +1,3 @@
class TestClass(text: String) {
val text = text
}
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.intentions.MovePropertyToConstructorIntention
@@ -0,0 +1,8 @@
annotation class Annotation1(val a: Int = 0)
annotation class Annotation2(val a: Int = 0)
annotation class Annotation3(val a: Int = 0)
class TestClass(@Annotation1(42) @Annotation3(42) initialText: String = "LoremIpsum") {
private @Annotation1(42) @field:Annotation2(42) val <caret>text = initialText
}
@@ -0,0 +1,7 @@
annotation class Annotation1(val a: Int = 0)
annotation class Annotation2(val a: Int = 0)
annotation class Annotation3(val a: Int = 0)
class TestClass(private @property:Annotation1(42) @field:Annotation2(42) @Annotation1(42) @Annotation3(42) val text: String = "LoremIpsum") {
}
@@ -0,0 +1,6 @@
// IS_APPLICABLE: false
// WITH_RUNTIME
class TestClass {
private val <caret>text: String by lazy { "Lorem Ipsum" }
}
@@ -0,0 +1,8 @@
// IS_APPLICABLE: false
class TestClass {
val <caret>text: String
get() {
return ""
}
}
@@ -0,0 +1,5 @@
// IS_APPLICABLE: false
class TestClass {
private lateinit var <caret>text: String
}
@@ -0,0 +1,7 @@
// IS_APPLICABLE: false
class TestClass {
fun foo() {
var <caret>text = ""
}
}
@@ -0,0 +1,3 @@
class TestClass {
private val <caret>text = "Lorem Ipsum"
}
@@ -0,0 +1,2 @@
class TestClass(private val text: String = "Lorem Ipsum") {
}
@@ -0,0 +1,5 @@
annotation class SuperAnnotation
class TestClass(text: String) {
@SuperAnnotation val <caret>text = text
}
@@ -0,0 +1,4 @@
annotation class SuperAnnotation
class TestClass(@property:SuperAnnotation val text: String) {
}
@@ -0,0 +1,8 @@
annotation class Annotation1(val a: Int = 0)
annotation class Annotation2(val a: Int = 0)
annotation class Annotation3(val a: Int = 0)
class TestClass(@Annotation1(42) @Annotation3(42) initialText: String = "LoremIpsum") {
private @Annotation1(42) @field:Annotation2(42) val <caret>text = "dolor sit amet"
}
@@ -0,0 +1,7 @@
annotation class Annotation1(val a: Int = 0)
annotation class Annotation2(val a: Int = 0)
annotation class Annotation3(val a: Int = 0)
class TestClass(@Annotation1(42) @Annotation3(42) initialText: String = "LoremIpsum", private @property:Annotation1(42) @field:Annotation2(42) val text: String = "dolor sit amet") {
}
@@ -11179,6 +11179,96 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
}
}
@TestMetadata("idea/testData/intentions/movePropertyToClassBody")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class MovePropertyToClassBody extends AbstractIntentionTest {
public void testAllFilesPresentInMovePropertyToClassBody() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/movePropertyToClassBody"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("annotationWithUseSite.kt")
public void testAnnotationWithUseSite() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/movePropertyToClassBody/annotationWithUseSite.kt");
doTest(fileName);
}
@TestMetadata("parameterAnnotation.kt")
public void testParameterAnnotation() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/movePropertyToClassBody/parameterAnnotation.kt");
doTest(fileName);
}
@TestMetadata("propertyAnnotation.kt")
public void testPropertyAnnotation() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/movePropertyToClassBody/propertyAnnotation.kt");
doTest(fileName);
}
@TestMetadata("simple.kt")
public void testSimple() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/movePropertyToClassBody/simple.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/intentions/movePropertyToConstructor")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class MovePropertyToConstructor extends AbstractIntentionTest {
public void testAllFilesPresentInMovePropertyToConstructor() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/movePropertyToConstructor"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("annotationWithUseSite.kt")
public void testAnnotationWithUseSite() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/movePropertyToConstructor/annotationWithUseSite.kt");
doTest(fileName);
}
@TestMetadata("delegated.kt")
public void testDelegated() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/movePropertyToConstructor/delegated.kt");
doTest(fileName);
}
@TestMetadata("getter.kt")
public void testGetter() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/movePropertyToConstructor/getter.kt");
doTest(fileName);
}
@TestMetadata("lateinit.kt")
public void testLateinit() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/movePropertyToConstructor/lateinit.kt");
doTest(fileName);
}
@TestMetadata("local.kt")
public void testLocal() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/movePropertyToConstructor/local.kt");
doTest(fileName);
}
@TestMetadata("simple.kt")
public void testSimple() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/movePropertyToConstructor/simple.kt");
doTest(fileName);
}
@TestMetadata("simpleAnnotation.kt")
public void testSimpleAnnotation() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/movePropertyToConstructor/simpleAnnotation.kt");
doTest(fileName);
}
@TestMetadata("withoutMatchingParameter.kt")
public void testWithoutMatchingParameter() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/movePropertyToConstructor/withoutMatchingParameter.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/intentions/moveToCompanion")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)