diff --git a/compiler/frontend/src/org/jetbrains/kotlin/parsing/JetParserDefinition.java b/compiler/frontend/src/org/jetbrains/kotlin/parsing/JetParserDefinition.java index f03dbb9dd49..71c408f870d 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/parsing/JetParserDefinition.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/parsing/JetParserDefinition.java @@ -123,6 +123,10 @@ public class JetParserDefinition implements ParserDefinition { @Override public SpaceRequirements spaceExistanceTypeBetweenTokens(ASTNode astNode, ASTNode astNode1) { + IElementType rightTokenType = astNode1.getElementType(); + if (rightTokenType == JetTokens.GET_KEYWORD || rightTokenType == JetTokens.SET_KEYWORD) { + return SpaceRequirements.MUST_LINE_BREAK; + } return SpaceRequirements.MAY; } } diff --git a/idea/resources/intentionDescriptions/IntroduceBackingPropertyIntention/after.kt.template b/idea/resources/intentionDescriptions/IntroduceBackingPropertyIntention/after.kt.template new file mode 100644 index 00000000000..96534460ac3 --- /dev/null +++ b/idea/resources/intentionDescriptions/IntroduceBackingPropertyIntention/after.kt.template @@ -0,0 +1,8 @@ +class Foo { + private var _x = "" + var x: String + get() = _x + set(value) { + _x = value + } +} diff --git a/idea/resources/intentionDescriptions/IntroduceBackingPropertyIntention/before.kt.template b/idea/resources/intentionDescriptions/IntroduceBackingPropertyIntention/before.kt.template new file mode 100644 index 00000000000..959c40828dc --- /dev/null +++ b/idea/resources/intentionDescriptions/IntroduceBackingPropertyIntention/before.kt.template @@ -0,0 +1,3 @@ +class Foo { + var x = "" +} diff --git a/idea/resources/intentionDescriptions/IntroduceBackingPropertyIntention/description.html b/idea/resources/intentionDescriptions/IntroduceBackingPropertyIntention/description.html new file mode 100644 index 00000000000..ee39b27aa57 --- /dev/null +++ b/idea/resources/intentionDescriptions/IntroduceBackingPropertyIntention/description.html @@ -0,0 +1,5 @@ + + +This intention creates a backing property for the specified property. + + \ No newline at end of file diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 73595400b8a..d58a66d7675 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -1028,6 +1028,11 @@ Kotlin + + org.jetbrains.kotlin.idea.intentions.IntroduceBackingPropertyIntention + Kotlin + + (javaClass(), "Introduce backing property") { + override fun isApplicableTo(element: JetProperty, caretOffset: Int): Boolean { + if (!canIntroduceBackingProperty(element)) return false + var elementAtCaret = element.containingFile.findElementAt(caretOffset) + if (elementAtCaret is PsiWhiteSpace) { + elementAtCaret = element.containingFile.findElementAt(caretOffset - 1) + } + return elementAtCaret == element.nameIdentifier || elementAtCaret == element.valOrVarKeyword + } + + override fun applyTo(element: JetProperty, editor: Editor) { + introduceBackingProperty(element) + } + + companion object { + public fun canIntroduceBackingProperty(element: JetProperty): Boolean { + val name = element.name ?: return false + if (name.startsWith('_')) return false + + if (element.hasDelegate() || element.receiverTypeReference != null) return false + + val containingClass = element.getStrictParentOfType() ?: return false + return containingClass.declarations.none { it is JetProperty && it.name == "_" + name } + } + + fun introduceBackingProperty(element: JetProperty) { + createBackingProperty(element) + + val type = SpecifyTypeExplicitlyIntention.getTypeForDeclaration(element) + SpecifyTypeExplicitlyIntention.addTypeAnnotation(null, element, type) + + val getter = element.getter + if (getter == null) { + createGetter(element) + } + else { + replaceFieldReferences(getter, element.name!!) + } + + if (element.isVar) { + val setter = element.setter + if (setter == null) { + createSetter(element) + } + else { + replaceFieldReferences(setter, element.name!!) + } + } + + element.removeInitializer() + + replaceBackingFieldReferences(element) + } + + private fun createGetter(element: JetProperty) { + val body = "get() = _${element.name}" + val newGetter = JetPsiFactory(element).createProperty("val x $body").getter!! + element.add(newGetter) + } + + private fun createSetter(element: JetProperty) { + val body = "set(value) { _${element.name} = value }" + val newSetter = JetPsiFactory(element).createProperty("val x $body").setter!! + element.add(newSetter) + } + + private fun JetProperty.removeInitializer() { + val initializer = initializer + if (initializer != null) { + val eq = initializer.prevLeaf { it.node.elementType == JetTokens.EQ } + if (eq != null) { + initializer.parent.deleteChildRange(eq, initializer) + } + } + } + + private fun createBackingProperty(element: JetProperty) { + val backingPropertyText = StringBuilder { + append("private ") + append(element.valOrVarKeyword.text) + append(" _") + append(element.name) + val typeRef = element.typeReference + if (typeRef != null) { + append(": ") + append(typeRef.text) + } + + val initializer = element.initializer + if (initializer != null) { + append(" = ") + append(initializer.text) + } + }.toString() + + val backingProp = JetPsiFactory(element).createProperty(backingPropertyText) + element.parent.addBefore(backingProp, element) + } + + private fun replaceFieldReferences(element: JetElement, propertyName: String) { + val bindingContext = element.analyze() + element.acceptChildren(object : JetTreeVisitorVoid() { + override fun visitSimpleNameExpression(expression: JetSimpleNameExpression) { + val target = bindingContext.get(BindingContext.REFERENCE_TARGET, expression) + if (target is SyntheticFieldDescriptor) { + expression.replace(JetPsiFactory(element).createSimpleName("_$propertyName")) + } + } + }) + } + + private fun replaceBackingFieldReferences(prop: JetProperty) { + val containingClass = prop.getStrictParentOfType()!! + ReferencesSearch.search(prop, LocalSearchScope(containingClass)).forEach { + val element = it.element as? JetNameReferenceExpression + if (element != null && element.getReferencedNameElementType() == JetTokens.FIELD_IDENTIFIER) { + element.replace(JetPsiFactory(element).createSimpleName("_${prop.name}")) + } + } + } + } +} + +class IntroduceBackingPropertyFix(prop: JetProperty): JetIntentionAction(prop) { + override fun getText(): String = "Introduce backing property" + override fun getFamilyName(): String = getText() + + override fun invoke(project: Project, editor: Editor?, file: JetFile) { + IntroduceBackingPropertyIntention.introduceBackingProperty(element) + } + + companion object : JetSingleIntentionActionFactory() { + override fun createAction(diagnostic: Diagnostic): IntentionAction? { + val resolveResult = diagnostic.psiElement.reference?.resolve() + if (resolveResult is JetProperty && + IntroduceBackingPropertyIntention.canIntroduceBackingProperty(resolveResult)) { + return IntroduceBackingPropertyFix(resolveResult) + } + return null + } + } +} + diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/SpecifyTypeExplicitlyIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/SpecifyTypeExplicitlyIntention.kt index 7263b7757a1..584046d2c1d 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/SpecifyTypeExplicitlyIntention.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/SpecifyTypeExplicitlyIntention.kt @@ -22,7 +22,6 @@ import com.intellij.openapi.editor.Editor import com.intellij.psi.PsiDocumentManager import org.jetbrains.kotlin.builtins.KotlinBuiltIns import org.jetbrains.kotlin.descriptors.CallableDescriptor -import org.jetbrains.kotlin.diagnostics.Errors import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers import org.jetbrains.kotlin.idea.util.ShortenReferences @@ -33,7 +32,7 @@ import org.jetbrains.kotlin.resolve.DescriptorUtils import org.jetbrains.kotlin.types.ErrorUtils import org.jetbrains.kotlin.types.JetType import org.jetbrains.kotlin.types.TypeUtils -import java.util.ArrayList +import java.util.* public class SpecifyTypeExplicitlyIntention : JetSelfTargetingIntention(javaClass(), "Specify type explicitly"), LowPriorityAction { override fun isApplicableTo(element: JetCallableDeclaration, caretOffset: Int): Boolean { @@ -85,7 +84,7 @@ public class SpecifyTypeExplicitlyIntention : JetSelfTargetingIntentionx = "" + get() = $x + "!" + set(value) { $x = value + "!" } + + fun foo(): String { + return $x + } +} diff --git a/idea/testData/intentions/introduceBackingProperty/backingFieldRef.kt.after b/idea/testData/intentions/introduceBackingProperty/backingFieldRef.kt.after new file mode 100644 index 00000000000..0c0039698e4 --- /dev/null +++ b/idea/testData/intentions/introduceBackingProperty/backingFieldRef.kt.after @@ -0,0 +1,10 @@ +class Foo { + private var _x = "" + var x: String + get() = _x + "!" + set(value) { _x = value + "!" } + + fun foo(): String { + return _x + } +} diff --git a/idea/testData/intentions/introduceBackingProperty/simpleVal.kt b/idea/testData/intentions/introduceBackingProperty/simpleVal.kt new file mode 100644 index 00000000000..cb20184c3fe --- /dev/null +++ b/idea/testData/intentions/introduceBackingProperty/simpleVal.kt @@ -0,0 +1,3 @@ +class Foo { + val x = "" +} diff --git a/idea/testData/intentions/introduceBackingProperty/simpleVal.kt.after b/idea/testData/intentions/introduceBackingProperty/simpleVal.kt.after new file mode 100644 index 00000000000..0ba7432c705 --- /dev/null +++ b/idea/testData/intentions/introduceBackingProperty/simpleVal.kt.after @@ -0,0 +1,5 @@ +class Foo { + private val _x = "" + val x: String + get() = _x +} diff --git a/idea/testData/intentions/introduceBackingProperty/simpleVar.kt b/idea/testData/intentions/introduceBackingProperty/simpleVar.kt new file mode 100644 index 00000000000..8f5c84dc172 --- /dev/null +++ b/idea/testData/intentions/introduceBackingProperty/simpleVar.kt @@ -0,0 +1,3 @@ +class Foo { + var x = "" +} diff --git a/idea/testData/intentions/introduceBackingProperty/simpleVar.kt.after b/idea/testData/intentions/introduceBackingProperty/simpleVar.kt.after new file mode 100644 index 00000000000..c73aa83d421 --- /dev/null +++ b/idea/testData/intentions/introduceBackingProperty/simpleVar.kt.after @@ -0,0 +1,8 @@ +class Foo { + private var _x = "" + var x: String + get() = _x + set(value) { + _x = value + } +} diff --git a/idea/testData/intentions/introduceBackingProperty/valWithAccessor.kt b/idea/testData/intentions/introduceBackingProperty/valWithAccessor.kt new file mode 100644 index 00000000000..63d51a508fc --- /dev/null +++ b/idea/testData/intentions/introduceBackingProperty/valWithAccessor.kt @@ -0,0 +1,4 @@ +class Foo { + val x = "" + get() = field + "!" +} diff --git a/idea/testData/intentions/introduceBackingProperty/valWithAccessor.kt.after b/idea/testData/intentions/introduceBackingProperty/valWithAccessor.kt.after new file mode 100644 index 00000000000..588abd381e4 --- /dev/null +++ b/idea/testData/intentions/introduceBackingProperty/valWithAccessor.kt.after @@ -0,0 +1,5 @@ +class Foo { + private val _x = "" + val x: String + get() = _x + "!" +} diff --git a/idea/testData/intentions/introduceBackingProperty/varWithAccessor.kt b/idea/testData/intentions/introduceBackingProperty/varWithAccessor.kt new file mode 100644 index 00000000000..2b9ca2dd3bf --- /dev/null +++ b/idea/testData/intentions/introduceBackingProperty/varWithAccessor.kt @@ -0,0 +1,5 @@ +class Foo { + var x = "" + get() = field + "!" + set(value) { field = value + "!" } +} diff --git a/idea/testData/intentions/introduceBackingProperty/varWithAccessor.kt.after b/idea/testData/intentions/introduceBackingProperty/varWithAccessor.kt.after new file mode 100644 index 00000000000..606adbb751c --- /dev/null +++ b/idea/testData/intentions/introduceBackingProperty/varWithAccessor.kt.after @@ -0,0 +1,6 @@ +class Foo { + private var _x = "" + var x: String + get() = _x + "!" + set(value) { _x = value + "!" } +} diff --git a/idea/testData/quickfix/migration/backingFieldSyntax/introduceBackingField.kt b/idea/testData/quickfix/migration/backingFieldSyntax/introduceBackingField.kt new file mode 100644 index 00000000000..9531d94def6 --- /dev/null +++ b/idea/testData/quickfix/migration/backingFieldSyntax/introduceBackingField.kt @@ -0,0 +1,11 @@ +// "Introduce backing property" "true" + +class Foo { + var x = "" + get() = $x + "!" + set(value) { $x = value + "!" } + + fun foo(): String { + return $x + } +} diff --git a/idea/testData/quickfix/migration/backingFieldSyntax/introduceBackingField.kt.after b/idea/testData/quickfix/migration/backingFieldSyntax/introduceBackingField.kt.after new file mode 100644 index 00000000000..64c7fcb1bc4 --- /dev/null +++ b/idea/testData/quickfix/migration/backingFieldSyntax/introduceBackingField.kt.after @@ -0,0 +1,12 @@ +// "Introduce backing property" "true" + +class Foo { + private var _x = "" + var x: String + get() = _x + "!" + set(value) { _x = value + "!" } + + fun foo(): String { + return _x + } +} diff --git a/idea/testData/quickfix/override/typeMismatchOnOverride/cantChangeMultipleOverriddenPropertiesTypes.kt b/idea/testData/quickfix/override/typeMismatchOnOverride/cantChangeMultipleOverriddenPropertiesTypes.kt index 9ade4150f35..5e92b8cdb87 100644 --- a/idea/testData/quickfix/override/typeMismatchOnOverride/cantChangeMultipleOverriddenPropertiesTypes.kt +++ b/idea/testData/quickfix/override/typeMismatchOnOverride/cantChangeMultipleOverriddenPropertiesTypes.kt @@ -1,5 +1,6 @@ // "Change 'A.x' type to '(Int) -> Int'" "false" // ACTION: Change 'C.x' type to '(String) -> Int' +// ACTION: Introduce backing property // ERROR: Return type is '(kotlin.Int) → kotlin.Int', which is not a subtype of overridden
public abstract val x: (kotlin.String) → kotlin.Int defined in A interface A { val x: (String) -> Int diff --git a/idea/testData/quickfix/typeAddition/noAddErrorType.kt b/idea/testData/quickfix/typeAddition/noAddErrorType.kt index 642c383c0f9..13de5200f32 100644 --- a/idea/testData/quickfix/typeAddition/noAddErrorType.kt +++ b/idea/testData/quickfix/typeAddition/noAddErrorType.kt @@ -1,5 +1,6 @@ // "Specify type explicitly" "false" // ACTION: Convert property to function +// ACTION: Introduce backing property // ERROR: Unresolved reference: foo class A() { diff --git a/idea/testData/quickfix/typeMismatch/dontChangeOverriddenPropertyTypeToErrorType.kt b/idea/testData/quickfix/typeMismatch/dontChangeOverriddenPropertyTypeToErrorType.kt index 753ae956121..46bba04c05b 100644 --- a/idea/testData/quickfix/typeMismatch/dontChangeOverriddenPropertyTypeToErrorType.kt +++ b/idea/testData/quickfix/typeMismatch/dontChangeOverriddenPropertyTypeToErrorType.kt @@ -1,5 +1,6 @@ // "Change 'B.x' type to '(String) -> [ERROR : Ay]'" "false" // ACTION: Change 'A.x' type to '(Int) -> Int' +// ACTION: Introduce backing propertty // ERROR: Return type is '(kotlin.Int) → kotlin.Int', which is not a subtype of overridden
public abstract val x: (kotlin.String) → [ERROR : Ay] defined in A // ERROR: Unresolved reference: Ay interface A { diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java index 1b4283b0ead..c589899115b 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java @@ -5173,6 +5173,45 @@ public class IntentionTestGenerated extends AbstractIntentionTest { } } + @TestMetadata("idea/testData/intentions/introduceBackingProperty") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class IntroduceBackingProperty extends AbstractIntentionTest { + public void testAllFilesPresentInIntroduceBackingProperty() throws Exception { + JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/introduceBackingProperty"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true); + } + + @TestMetadata("backingFieldRef.kt") + public void testBackingFieldRef() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/introduceBackingProperty/backingFieldRef.kt"); + doTest(fileName); + } + + @TestMetadata("simpleVal.kt") + public void testSimpleVal() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/introduceBackingProperty/simpleVal.kt"); + doTest(fileName); + } + + @TestMetadata("simpleVar.kt") + public void testSimpleVar() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/introduceBackingProperty/simpleVar.kt"); + doTest(fileName); + } + + @TestMetadata("valWithAccessor.kt") + public void testValWithAccessor() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/introduceBackingProperty/valWithAccessor.kt"); + doTest(fileName); + } + + @TestMetadata("varWithAccessor.kt") + public void testVarWithAccessor() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/introduceBackingProperty/varWithAccessor.kt"); + doTest(fileName); + } + } + @TestMetadata("idea/testData/intentions/invertIfCondition") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java index c576996b70f..a58b13ce1e7 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java @@ -4045,6 +4045,12 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest { JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/migration/backingFieldSyntax"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true); } + @TestMetadata("introduceBackingField.kt") + public void testIntroduceBackingField() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/migration/backingFieldSyntax/introduceBackingField.kt"); + doTest(fileName); + } + @TestMetadata("usage.kt") public void testUsage() throws Exception { String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/migration/backingFieldSyntax/usage.kt");