Supported converting member properties to extensions

Additionally, we now generate <selection>throw UnsupportedOperationException()</selection> when a missing body is added
This commit is contained in:
Andrey Breslav
2013-08-27 13:22:15 +04:00
parent c02f0a7d04
commit 3002ac96f0
29 changed files with 304 additions and 42 deletions
@@ -0,0 +1,30 @@
/*
* Copyright 2010-2013 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.jet.lang.psi;
import org.jetbrains.annotations.Nullable;
public interface JetCallableDeclaration extends JetNamedDeclaration, JetTypeParameterListOwner {
@Nullable
JetParameterList getValueParameterList();
@Nullable
JetTypeReference getReceiverTypeRef();
@Nullable
JetTypeReference getReturnTypeRef();
}
@@ -16,17 +16,7 @@
package org.jetbrains.jet.lang.psi;
import org.jetbrains.annotations.Nullable;
public interface JetFunction extends JetTypeParameterListOwner, JetDeclarationWithBody {
@Nullable
JetParameterList getValueParameterList();
@Nullable
JetTypeReference getReceiverTypeRef();
@Nullable
JetTypeReference getReturnTypeRef();
public interface JetFunction extends JetDeclarationWithBody, JetCallableDeclaration {
boolean isLocal();
}
@@ -39,7 +39,8 @@ import static org.jetbrains.jet.JetNodeTypes.PROPERTY_ACCESSOR;
import static org.jetbrains.jet.JetNodeTypes.PROPERTY_DELEGATE;
import static org.jetbrains.jet.lexer.JetTokens.*;
public class JetProperty extends JetTypeParameterListOwnerStub<PsiJetPropertyStub> implements JetVariableDeclaration {
public class JetProperty extends JetTypeParameterListOwnerStub<PsiJetPropertyStub> implements JetVariableDeclaration,
JetCallableDeclaration {
public JetProperty(@NotNull ASTNode node) {
super(node);
}
@@ -98,6 +99,13 @@ public class JetProperty extends JetTypeParameterListOwnerStub<PsiJetPropertyStu
} else return super.getUseScope();
}
@Nullable
@Override
public JetParameterList getValueParameterList() {
return null;
}
@Override
@Nullable
public JetTypeReference getReceiverTypeRef() {
ASTNode node = getNode().getFirstChildNode();
@@ -114,6 +122,12 @@ public class JetProperty extends JetTypeParameterListOwnerStub<PsiJetPropertyStu
return null;
}
@Nullable
@Override
public JetTypeReference getReturnTypeRef() {
return getTypeRef();
}
@Override
@Nullable
public JetTypeReference getTypeRef() {
@@ -167,7 +167,7 @@ public class JetPsiFactory {
return createDeclaration(project, text, JetProperty.class);
}
private static <T> T createDeclaration(Project project, String text, Class<T> clazz) {
public static <T> T createDeclaration(Project project, String text, Class<T> clazz) {
JetFile file = createFile(project, text);
List<JetDeclaration> dcls = file.getDeclarations();
assert dcls.size() == 1 : dcls.size() + " declarations in " + text;
@@ -16,9 +16,11 @@
package org.jetbrains.jet.plugin.intentions.declarations;
import com.intellij.codeInsight.CodeInsightUtilCore;
import com.intellij.codeInsight.intention.impl.BaseIntentionAction;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
@@ -28,9 +30,9 @@ import com.intellij.util.Function;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jet.lang.descriptors.ClassDescriptor;
import org.jetbrains.jet.lang.descriptors.DeclarationDescriptor;
import org.jetbrains.jet.lang.descriptors.SimpleFunctionDescriptor;
import org.jetbrains.jet.lang.psi.*;
import org.jetbrains.jet.lang.resolve.BindingContext;
import org.jetbrains.jet.lexer.JetToken;
@@ -40,8 +42,13 @@ import org.jetbrains.jet.plugin.caches.resolve.KotlinCacheManagerUtil;
import java.util.List;
import static org.jetbrains.jet.lang.resolve.BindingContext.DECLARATION_TO_DESCRIPTOR;
public class ConvertMemberToExtension extends BaseIntentionAction {
public static final String CARET_ANCHOR = "____CARET_ANCHOR____";
public static final String THROW_UNSUPPORTED_OPERATION_EXCEPTION = " throw UnsupportedOperationException()";
@NotNull
@Override
public String getText() {
@@ -58,32 +65,31 @@ public class ConvertMemberToExtension extends BaseIntentionAction {
public boolean isAvailable(
@NotNull Project project, Editor editor, PsiFile file
) {
JetNamedFunction function = getTarget(editor, file);
return function != null
&& function.getParent() instanceof JetClassBody
&& function.getParent().getParent() instanceof JetClass
&& function.getReceiverTypeRef() == null;
JetCallableDeclaration declaration = getTarget(editor, file);
if (declaration instanceof JetProperty) {
if (((JetProperty) declaration).getInitializer() != null) return false;
}
return declaration != null
&& declaration.getParent() instanceof JetClassBody
&& declaration.getParent().getParent() instanceof JetClass
&& declaration.getReceiverTypeRef() == null;
}
private static JetNamedFunction getTarget(Editor editor, PsiFile file) {
private static JetCallableDeclaration getTarget(Editor editor, PsiFile file) {
PsiElement element = file.findElementAt(editor.getCaretModel().getOffset());
return PsiTreeUtil.getParentOfType(element, JetNamedFunction.class, false, JetExpression.class);
return PsiTreeUtil.getParentOfType(element, JetCallableDeclaration.class, false, JetExpression.class);
}
@Override
public void invoke(
@NotNull Project project, Editor editor, PsiFile file
) throws IncorrectOperationException {
JetNamedFunction member = getTarget(editor, file);
JetCallableDeclaration member = getTarget(editor, file);
assert member != null : "Must be checked by isAvailable";
BindingContext bindingContext = KotlinCacheManagerUtil.getDeclarationsBindingContext(member);
SimpleFunctionDescriptor memberDescriptor = bindingContext.get(BindingContext.FUNCTION, member);
DeclarationDescriptor memberDescriptor = bindingContext.get(DECLARATION_TO_DESCRIPTOR, member);
if (memberDescriptor == null) return;
assert memberDescriptor.getReceiverParameter() == null : "Must have checked that there's no receiver earlier\n" +
"Descriptor: " + memberDescriptor + "\n" +
"Declaration: " + member.getText();
DeclarationDescriptor containingClass = memberDescriptor.getContainingDeclaration();
assert containingClass instanceof ClassDescriptor : "Members must be contained in classes: \n" +
@@ -98,23 +104,47 @@ public class ConvertMemberToExtension extends BaseIntentionAction {
JetParameterList valueParameterList = member.getValueParameterList();
JetTypeReference returnTypeRef = member.getReturnTypeRef();
String receiverAndTheRest = receiver +
name +
(valueParameterList == null ? "" : valueParameterList.getText()) +
(returnTypeRef != null ? ": " + returnTypeRef.getText() : "") +
textOfBody(member);
String extensionText = modifiers(member) + "fun " + typeParameters(member) + receiverAndTheRest;
String extensionText = modifiers(member) +
memberType(member) + " " +
typeParameters(member) +
receiver +
name +
(valueParameterList == null ? "" : valueParameterList.getText()) +
(returnTypeRef != null ? ": " + returnTypeRef.getText() : "") +
body(member);
JetNamedFunction extension = JetPsiFactory.createFunction(project, extensionText);
JetDeclaration extension = JetPsiFactory.createDeclaration(project, extensionText, JetDeclaration.class);
file.addAfter(extension, outermostParent);
PsiElement added = file.addAfter(extension, outermostParent);
file.addAfter(JetPsiFactory.createNewLine(project), outermostParent);
file.addAfter(JetPsiFactory.createNewLine(project), outermostParent);
member.delete();
CodeInsightUtilCore.forcePsiPostprocessAndRestoreElement(added);
int caretAnchor = added.getText().indexOf(CARET_ANCHOR);
if (caretAnchor >= 0) {
int caretOffset = added.getTextRange().getStartOffset() + caretAnchor;
JetSimpleNameExpression anchor = PsiTreeUtil.findElementOfClassAtOffset(file, caretOffset, JetSimpleNameExpression.class, false);
if (anchor != null && CARET_ANCHOR.equals(anchor.getReferencedName())) {
JetExpression throwException = JetPsiFactory.createExpression(project, THROW_UNSUPPORTED_OPERATION_EXCEPTION);
PsiElement replaced = anchor.replace(throwException);
TextRange range = replaced.getTextRange();
editor.getCaretModel().moveToOffset(range.getStartOffset());
editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset());
}
}
}
private static String modifiers(JetNamedFunction member) {
private static String memberType(JetCallableDeclaration member) {
if (member instanceof JetFunction) {
return "fun";
}
return ((JetProperty) member).getValOrVarNode().getText();
}
private static String modifiers(JetCallableDeclaration member) {
JetModifierList modifierList = member.getModifierList();
if (modifierList == null) return "";
for (IElementType modifierType : JetTokens.VISIBILITY_MODIFIERS.getTypes()) {
@@ -126,7 +156,7 @@ public class ConvertMemberToExtension extends BaseIntentionAction {
return "";
}
private static String typeParameters(JetNamedFunction member) {
private static String typeParameters(JetCallableDeclaration member) {
PsiElement classElement = member.getParent().getParent();
assert classElement instanceof JetClass : "Must be checked in isAvailable: " + classElement.getText();
@@ -142,11 +172,37 @@ public class ConvertMemberToExtension extends BaseIntentionAction {
}, ", ") + "> ";
}
private static String textOfBody(JetNamedFunction member) {
JetExpression bodyExpression = member.getBodyExpression();
if (bodyExpression == null) return "{}";
if (!member.hasBlockBody()) return " = " + bodyExpression.getText();
return bodyExpression.getText();
private static String body(JetCallableDeclaration member) {
if (member instanceof JetProperty) {
JetProperty property = (JetProperty) member;
return "\n" + getter(property) + "\n" + setter(property, !synthesizeBody(property.getGetter()));
}
else if (member instanceof JetFunction) {
JetFunction function = (JetFunction) member;
JetExpression bodyExpression = function.getBodyExpression();
if (bodyExpression == null) return "{" + CARET_ANCHOR + "}";
if (!function.hasBlockBody()) return " = " + bodyExpression.getText();
return bodyExpression.getText();
}
else {
return "";
}
}
private static String getter(JetProperty property) {
JetPropertyAccessor getter = property.getGetter();
if (synthesizeBody(getter)) return "get() = " + CARET_ANCHOR;
return getter.getText();
}
private static String setter(JetProperty property, boolean allowCaretAnchor) {
if (!property.isVar()) return "";
JetPropertyAccessor setter = property.getSetter();
if (synthesizeBody(setter)) return "set(value) {" + (allowCaretAnchor ? CARET_ANCHOR : THROW_UNSUPPORTED_OPERATION_EXCEPTION) + "}";
return setter.getText();
}
private static boolean synthesizeBody(@Nullable JetPropertyAccessor getter) {
return getter == null || getter.getBodyExpression() == null;
}
}
@@ -2,4 +2,5 @@ abstract class Owner {
}
fun Owner.f() {
<caret><selection>throw UnsupportedOperationException()</selection>
}
@@ -0,0 +1,3 @@
abstract class Owner {
abstract val <caret>p: Int
}
@@ -0,0 +1,5 @@
abstract class Owner {
}
val Owner.p: Int
get() = <caret><selection>throw UnsupportedOperationException()</selection>
@@ -0,0 +1,3 @@
class Owner {
fun <caret>() {}
}
@@ -0,0 +1,5 @@
class Owner {
}
fun Owner.() {
}
@@ -0,0 +1,3 @@
class Owner<T> {
val <R> <caret>p: R
}
@@ -0,0 +1,5 @@
class Owner<T> {
}
val <T, R> Owner<T>.p: R
get() = <caret><selection>throw UnsupportedOperationException()</selection>
@@ -0,0 +1,4 @@
class Owner {
val <caret>p: Int
get
}
@@ -0,0 +1,5 @@
class Owner {
}
val Owner.p: Int
get() = <caret><selection>throw UnsupportedOperationException()</selection>
@@ -0,0 +1,4 @@
class Owner {
val <caret>p: Int
get() = 1
}
@@ -0,0 +1,5 @@
class Owner {
}
val Owner.p: Int
get() = 1
@@ -0,0 +1,4 @@
class Owner {
val <caret>p: Int
get() { return 1}
}
@@ -0,0 +1,7 @@
class Owner {
}
val Owner.p: Int
get() {
return 1
}
@@ -0,0 +1,5 @@
class Owner {
var <caret>p: Int
get
set
}
@@ -0,0 +1,8 @@
class Owner {
}
var Owner.p: Int
get() = <caret><selection>throw UnsupportedOperationException()</selection>
set(value) {
throw UnsupportedOperationException()
}
@@ -0,0 +1,5 @@
class Owner {
var <caret>p: Int
get() = 1
set
}
@@ -0,0 +1,8 @@
class Owner {
}
var Owner.p: Int
get() = 1
set(value) {
<caret><selection>throw UnsupportedOperationException()</selection>
}
@@ -0,0 +1,4 @@
class Owner {
var <caret>p: Int
get {return 1}
}
@@ -0,0 +1,10 @@
class Owner {
}
var Owner.p: Int
get {
return 1
}
set(value) {
<caret><selection>throw UnsupportedOperationException()</selection>
}
@@ -0,0 +1,5 @@
class Owner {
var <caret>p: Int
get() = 1
set(v) {}
}
@@ -0,0 +1,7 @@
class Owner {
}
var Owner.p: Int
get() = 1
set(v) {
}
@@ -0,0 +1,4 @@
class Owner {
var <caret>p: Int
set(v) {}
}
@@ -0,0 +1,7 @@
class Owner {
}
var Owner.p: Int
get() = <caret><selection>throw UnsupportedOperationException()</selection>
set(v) {
}
@@ -798,6 +798,11 @@ public class CodeTransformationsTestGenerated extends AbstractCodeTransformation
doTestConvertMemberToExtension("idea/testData/intentions/declarations/convertMemberToExtension/abstract.kt");
}
@TestMetadata("abstractProperty.kt")
public void testAbstractProperty() throws Exception {
doTestConvertMemberToExtension("idea/testData/intentions/declarations/convertMemberToExtension/abstractProperty.kt");
}
public void testAllFilesPresentInConvertMemberToExtension() throws Exception {
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), "org.jetbrains.jet.generators.tests.GenerateTests", new File("idea/testData/intentions/declarations/convertMemberToExtension"), Pattern.compile("^(.+)\\.kt$"), true);
}
@@ -812,6 +817,11 @@ public class CodeTransformationsTestGenerated extends AbstractCodeTransformation
doTestConvertMemberToExtension("idea/testData/intentions/declarations/convertMemberToExtension/extension.kt");
}
@TestMetadata("funcitonNoName.kt")
public void testFuncitonNoName() throws Exception {
doTestConvertMemberToExtension("idea/testData/intentions/declarations/convertMemberToExtension/funcitonNoName.kt");
}
@TestMetadata("functionType.kt")
public void testFunctionType() throws Exception {
doTestConvertMemberToExtension("idea/testData/intentions/declarations/convertMemberToExtension/functionType.kt");
@@ -842,6 +852,11 @@ public class CodeTransformationsTestGenerated extends AbstractCodeTransformation
doTestConvertMemberToExtension("idea/testData/intentions/declarations/convertMemberToExtension/genericFunParamAfterName.kt");
}
@TestMetadata("genericProperty.kt")
public void testGenericProperty() throws Exception {
doTestConvertMemberToExtension("idea/testData/intentions/declarations/convertMemberToExtension/genericProperty.kt");
}
@TestMetadata("inFunctionBody.kt")
public void testInFunctionBody() throws Exception {
doTestConvertMemberToExtension("idea/testData/intentions/declarations/convertMemberToExtension/inFunctionBody.kt");
@@ -912,6 +927,46 @@ public class CodeTransformationsTestGenerated extends AbstractCodeTransformation
doTestConvertMemberToExtension("idea/testData/intentions/declarations/convertMemberToExtension/unknownType.kt");
}
@TestMetadata("valWithDefaultGetter.kt")
public void testValWithDefaultGetter() throws Exception {
doTestConvertMemberToExtension("idea/testData/intentions/declarations/convertMemberToExtension/valWithDefaultGetter.kt");
}
@TestMetadata("valWithGetter.kt")
public void testValWithGetter() throws Exception {
doTestConvertMemberToExtension("idea/testData/intentions/declarations/convertMemberToExtension/valWithGetter.kt");
}
@TestMetadata("valWithGetterBlockBody.kt")
public void testValWithGetterBlockBody() throws Exception {
doTestConvertMemberToExtension("idea/testData/intentions/declarations/convertMemberToExtension/valWithGetterBlockBody.kt");
}
@TestMetadata("varWithDefaultGetterAndSetter.kt")
public void testVarWithDefaultGetterAndSetter() throws Exception {
doTestConvertMemberToExtension("idea/testData/intentions/declarations/convertMemberToExtension/varWithDefaultGetterAndSetter.kt");
}
@TestMetadata("varWithDefaultSetter.kt")
public void testVarWithDefaultSetter() throws Exception {
doTestConvertMemberToExtension("idea/testData/intentions/declarations/convertMemberToExtension/varWithDefaultSetter.kt");
}
@TestMetadata("varWithGetter.kt")
public void testVarWithGetter() throws Exception {
doTestConvertMemberToExtension("idea/testData/intentions/declarations/convertMemberToExtension/varWithGetter.kt");
}
@TestMetadata("varWithGetterAndSetter.kt")
public void testVarWithGetterAndSetter() throws Exception {
doTestConvertMemberToExtension("idea/testData/intentions/declarations/convertMemberToExtension/varWithGetterAndSetter.kt");
}
@TestMetadata("varWithSetter.kt")
public void testVarWithSetter() throws Exception {
doTestConvertMemberToExtension("idea/testData/intentions/declarations/convertMemberToExtension/varWithSetter.kt");
}
@TestMetadata("withType.kt")
public void testWithType() throws Exception {
doTestConvertMemberToExtension("idea/testData/intentions/declarations/convertMemberToExtension/withType.kt");