Create from usage: Support generation of Java methods from usages in Kotlin code

This commit is contained in:
Alexey Sedunov
2015-02-10 15:17:13 +03:00
parent 97b0f50d35
commit 34d8e1c08c
23 changed files with 300 additions and 25 deletions
@@ -37,7 +37,6 @@ import org.jetbrains.kotlin.resolve.scopes.JetScope
import org.jetbrains.kotlin.types.*
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.idea.util.ShortenReferences
import org.jetbrains.kotlin.idea.refactoring.JetNameSuggester
import kotlin.properties.Delegates
import java.util.LinkedHashSet
import java.util.Collections
@@ -46,13 +45,10 @@ import java.util.ArrayList
import java.util.Properties
import org.jetbrains.kotlin.idea.caches.resolve.analyzeFullyAndGetResult
import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils
import org.jetbrains.kotlin.idea.refactoring.EmptyValidator
import org.jetbrains.kotlin.idea.refactoring.CollectingValidator
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.lexer.JetTokens
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.idea.refactoring.isMultiLine
import org.jetbrains.kotlin.types.checker.JetTypeChecker
import com.intellij.psi.SmartPointerManager
import org.jetbrains.kotlin.descriptors.impl.SimpleFunctionDescriptorImpl
@@ -69,6 +65,18 @@ import org.jetbrains.kotlin.psi.psiUtil.siblings
import org.jetbrains.kotlin.idea.util.isUnit
import com.intellij.openapi.editor.ScrollType
import org.jetbrains.kotlin.psi.psiUtil.isAncestor
import org.jetbrains.kotlin.load.java.descriptors.JavaClassDescriptor
import com.intellij.psi.PsiClass
import com.intellij.codeInsight.daemon.impl.quickfix.CreateFromUsageUtils
import com.intellij.psi.PsiMethod
import com.intellij.openapi.fileEditor.OpenFileDescriptor
import com.intellij.psi.codeStyle.JavaCodeStyleManager
import com.intellij.psi.PsiModifier
import org.jetbrains.kotlin.resolve.scopes.ChainedScope
import org.jetbrains.kotlin.resolve.scopes.WritableScopeImpl
import org.jetbrains.kotlin.resolve.scopes.RedeclarationHandler
import org.jetbrains.kotlin.resolve.scopes.WritableScope
import org.jetbrains.kotlin.idea.refactoring.*
private val TYPE_PARAMETER_LIST_VARIABLE_NAME = "typeParameterList"
private val TEMPLATE_FROM_USAGE_FUNCTION_BODY = "New Kotlin Function Body.kt"
@@ -228,7 +236,10 @@ class CallableBuilder(val config: CallableBuilderConfiguration) {
receiverClassDescriptor =
placement.receiverTypeCandidate.theType.getConstructor().getDeclarationDescriptor() as? ClassDescriptor
val classDeclaration = receiverClassDescriptor?.let { DescriptorToSourceUtils.classDescriptorToDeclaration(it) }
containingElement = if (config.isExtension) config.currentFile else classDeclaration as JetElement
containingElement = when {
!config.isExtension && classDeclaration is JetElement -> classDeclaration
else -> config.currentFile
}
}
else -> throw IllegalArgumentException("Unexpected placement: $placement")
}
@@ -243,12 +254,7 @@ class CallableBuilder(val config: CallableBuilderConfiguration) {
containingFileEditor = config.currentEditor!!
}
val scope = if (config.isExtension || receiverClassDescriptor == null) {
currentFileModule.getPackage(config.currentFile.getPackageFqName())!!.getMemberScope()
}
else {
(receiverClassDescriptor as ClassDescriptorWithResolutionScopes).getScopeForMemberDeclarationResolution()
}
val scope = getDeclarationScope()
receiverTypeCandidate = receiverType?.let { TypeCandidate(it, scope) }
@@ -297,6 +303,41 @@ class CallableBuilder(val config: CallableBuilderConfiguration) {
mandatoryTypeParametersAsCandidates.forEach { it.render(typeParameterNameMap, fakeFunction) }
}
private fun getDeclarationScope(): JetScope {
if (config.isExtension || receiverClassDescriptor == null) {
return currentFileModule.getPackage(config.currentFile.getPackageFqName())!!.getMemberScope()
}
if (receiverClassDescriptor is ClassDescriptorWithResolutionScopes) {
return receiverClassDescriptor.getScopeForMemberDeclarationResolution()
}
assert (receiverClassDescriptor is JavaClassDescriptor, "Unexpected receiver class: ${receiverClassDescriptor}")
val typeParamScope = with(
WritableScopeImpl(
JetScope.Empty,
receiverClassDescriptor,
RedeclarationHandler.DO_NOTHING,
"Scope with type parameters for ${receiverClassDescriptor.getName()}"
)
) {
receiverClassDescriptor.getTypeConstructor().getParameters().forEach { addClassifierDescriptor(it) }
changeLockLevel(WritableScope.LockLevel.READING)
}
val projections = receiverClassDescriptor.getTypeConstructor().getParameters()
.map { TypeProjectionImpl(it.getDefaultType()) }
val memberScope = receiverClassDescriptor.getMemberScope(projections)
return ChainedScope(
receiverClassDescriptor,
"Classifier resolution scope: ${receiverClassDescriptor.getName()}",
typeParamScope,
memberScope
)
}
private fun collectSubstitutionsForReceiverTypeParameters(
receiverType: JetType?,
result: MutableMap<JetType, JetType>
@@ -715,6 +756,38 @@ class CallableBuilder(val config: CallableBuilderConfiguration) {
typeRef.replace(fullyQualifiedReceiverTypeRef)
}
private fun transformToJavaMemberIfApplicable(declaration: JetNamedDeclaration): Boolean {
if (config.isExtension || receiverClassDescriptor !is JavaClassDescriptor) return false
val targetClass = DescriptorToSourceUtils.classDescriptorToDeclaration(receiverClassDescriptor) as? PsiClass
if (targetClass == null || !targetClass.canRefactor()) return false
val project = declaration.getProject()
val newJavaMethod: PsiMethod = when (declaration) {
is JetNamedFunction -> {
val method = createJavaMethod(declaration, targetClass)
val modifierList = method.getModifierList()
modifierList.setModifierProperty(PsiModifier.STATIC, false)
modifierList.setModifierProperty(PsiModifier.FINAL, false)
method
}
else -> return false
}
declaration.delete()
JavaCodeStyleManager.getInstance(project).shortenClassReferences(newJavaMethod);
val descriptor = OpenFileDescriptor(project, targetClass.getContainingFile().getVirtualFile())
val targetEditor = FileEditorManager.getInstance(project).openTextEditor(descriptor, true)
CreateFromUsageUtils.setupEditor(newJavaMethod, targetEditor)
return true
}
private fun setupEditor(declaration: JetNamedDeclaration) {
val caretModel = containingFileEditor.getCaretModel()
@@ -812,12 +885,13 @@ class CallableBuilder(val config: CallableBuilderConfiguration) {
if (callElement != null) {
setupCallTypeArguments(callElement, expression?.currentTypeParameters ?: Collections.emptyList())
}
// change short type names to fully qualified ones (to be shortened below)
setupTypeReferencesForShortening(newDeclaration, elementsToShorten, parameterTypeExpressions)
}
setupEditor(newDeclaration)
if (!transformToJavaMemberIfApplicable(newDeclaration)) {
// change short type names to fully qualified ones (to be shortened below)
setupTypeReferencesForShortening(newDeclaration, elementsToShorten, parameterTypeExpressions)
setupEditor(newDeclaration)
}
onFinish()
}
@@ -75,9 +75,18 @@ import org.jetbrains.kotlin.psi.psiUtil.parents
import com.intellij.refactoring.util.RefactoringUIUtil
import com.intellij.psi.PsiMember
import org.jetbrains.kotlin.idea.caches.resolve.getJavaMemberDescriptor
import com.intellij.psi.PsiElementFactory
import com.intellij.psi.PsiModifier
import com.intellij.psi.PsiTypeParameterList
import com.intellij.refactoring.changeSignature.ChangeSignatureUtil
import com.intellij.psi.PsiModifierList
import org.jetbrains.kotlin.asJava.LightClassUtil
import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor
import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor.Kind
import org.jetbrains.kotlin.resolve.OverridingUtil
import com.intellij.lang.java.JavaLanguage
import com.intellij.psi.PsiClass
import com.intellij.codeInsight.daemon.impl.quickfix.CreateFromUsageUtils
fun <T: Any> PsiElement.getAndRemoveCopyableUserData(key: Key<T>): T? {
val data = getCopyableUserData(key)
@@ -410,12 +419,72 @@ public fun comparePossiblyOverridingDescriptors(currentDescriptor: DeclarationDe
}
public fun PsiElement.canRefactor(): Boolean {
return when (this) {
is PsiPackage ->
return when {
this is PsiPackage ->
getDirectories().any { it.canRefactor() }
is JetElement, is PsiDirectory ->
this is JetElement,
this is PsiMember && getLanguage() == JavaLanguage.INSTANCE,
this is PsiDirectory ->
isWritable() && ProjectRootsUtil.isInProjectSource(this)
else ->
false
}
}
private fun copyModifierListItems(from: PsiModifierList, to: PsiModifierList, withPsiModifiers: Boolean = true) {
if (withPsiModifiers) {
for (modifier in PsiModifier.MODIFIERS) {
if (from.hasExplicitModifier(modifier)) {
to.setModifierProperty(modifier, true)
}
}
}
for (annotation in from.getAnnotations()) {
to.addAnnotation(annotation.getQualifiedName())
}
}
public fun createJavaMethod(function: JetNamedFunction, targetClass: PsiClass): PsiMethod {
val template = LightClassUtil.getLightClassMethod(function)
?: throw AssertionError("Can't generate light method: ${JetPsiUtil.getElementTextWithContext(function)}")
val factory = PsiElementFactory.SERVICE.getInstance(template.getProject())
val method = targetClass.add(factory.createMethod(template.getName(), template.getReturnType())) as PsiMethod
copyModifierListItems(template.getModifierList(), method.getModifierList())
val templateTypeParamList = template.getTypeParameterList()
if (templateTypeParamList != null) {
val targetTypeParamList = method.addAfter(factory.createTypeParameterList(), method.getModifierList()) as PsiTypeParameterList
val newTypeParams = templateTypeParamList.getTypeParameters()
.map { factory.createTypeParameter(it.getName(), it.getExtendsList().getReferencedTypes()) }
ChangeSignatureUtil.synchronizeList(
targetTypeParamList,
newTypeParams,
{ it.getTypeParameters().toList() },
BooleanArray(newTypeParams.size())
)
}
val targetParamList = method.getParameterList()
val newParams = template.getParameterList().getParameters().map {
val param = factory.createParameter(it.getName(), it.getType())
copyModifierListItems(it.getModifierList(), param.getModifierList())
param
}
ChangeSignatureUtil.synchronizeList(
targetParamList,
newParams,
{ it.getParameters().toList() },
BooleanArray(newParams.size())
)
if (template.getModifierList().hasModifierProperty(PsiModifier.ABSTRACT) || targetClass.isInterface()) {
method.getBody().delete()
}
else {
CreateFromUsageUtils.setupMethodBody(method)
}
return method
}
@@ -1,5 +1,6 @@
// "Create class 'Foo'" "false"
// ACTION: Create extension function 'Foo'
// ACTION: Create function 'Foo'
// ACTION: Convert to expression body
// ERROR: Unresolved reference: Foo
@@ -0,0 +1,10 @@
// "Create extension function 'foo'" "true"
// ERROR: Unresolved reference: foo
fun test(): Int {
return A().foo()
}
fun A.foo(): Int {
throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
}
@@ -0,0 +1,6 @@
// "Create extension function 'foo'" "true"
// ERROR: Unresolved reference: foo
fun test(): Int {
return A().<caret>foo()
}
@@ -0,0 +1,10 @@
// "Create extension function 'foo'" "true"
// ERROR: Unresolved reference: foo
fun test(): Int? {
return A().foo(1, "2")
}
fun A.foo(i: Int, s: String): Int? {
throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
}
@@ -0,0 +1,6 @@
// "Create extension function 'foo'" "true"
// ERROR: Unresolved reference: foo
fun test(): Int? {
return A().<caret>foo(1, "2")
}
@@ -0,0 +1,8 @@
// "Create function 'foo'" "false"
// ACTION: Convert to expression body
// ACTION: Create extension function 'foo'
// ERROR: Unresolved reference: foo
fun test(): Int {
return A().<caret>foo()
}
@@ -0,0 +1,7 @@
import org.jetbrains.annotations.Nullable;
interface A {
@Nullable
<T, U> U foo(U u, T t);
}
@@ -0,0 +1,7 @@
// "Create function 'foo'" "true"
// ERROR: Unresolved reference: foo
fun test(a: A): Int? {
return a.foo<String, Int>(1, "2")
}
@@ -0,0 +1,6 @@
// "Create function 'foo'" "true"
// ERROR: Unresolved reference: foo
fun test(a: A): Int? {
return a.<caret>foo<String, Int>(1, "2")
}
@@ -0,0 +1,10 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class A {
@Nullable
public Integer foo(int i, @NotNull String s) {
return null;
}
}
@@ -1,10 +1,7 @@
// "Create extension function 'foo'" "true"
// "Create function 'foo'" "true"
// ERROR: Unresolved reference: foo
fun test(): Int {
fun test(): Int? {
return A().foo(1, "2")
}
fun A.foo(i: Int, s: String): Int {
throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
}
@@ -1,6 +1,6 @@
// "Create extension function 'foo'" "true"
// "Create function 'foo'" "true"
// ERROR: Unresolved reference: foo
fun test(): Int {
fun test(): Int? {
return A().<caret>foo(1, "2")
}
@@ -0,0 +1,6 @@
class A {
public <T, U> U foo(U u, T t) {
return null;
}
}
@@ -0,0 +1,7 @@
// "Create function 'foo'" "true"
// ERROR: Unresolved reference: foo
fun test(): Int {
return A().foo<String, Int>(1, "2")
}
@@ -0,0 +1,6 @@
// "Create function 'foo'" "true"
// ERROR: Unresolved reference: foo
fun test(): Int {
return A().<caret>foo<String, Int>(1, "2")
}
@@ -319,12 +319,42 @@ public class QuickFixMultiFileTestGenerated extends AbstractQuickFixMultiFileTes
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/createFromUsage/createFunction/call"), Pattern.compile("^(\\w+)\\.before\\.Main\\.kt$"), true);
}
@TestMetadata("extensionFunOnGroovyType.before.Main.kt")
public void testExtensionFunOnGroovyType() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/createFromUsage/createFunction/call/extensionFunOnGroovyType.before.Main.kt");
doTestWithExtraFile(fileName);
}
@TestMetadata("extensionFunOnJavaType.before.Main.kt")
public void testExtensionFunOnJavaType() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/createFromUsage/createFunction/call/extensionFunOnJavaType.before.Main.kt");
doTestWithExtraFile(fileName);
}
@TestMetadata("funOnGroovyType.before.Main.kt")
public void testFunOnGroovyType() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/createFromUsage/createFunction/call/funOnGroovyType.before.Main.kt");
doTestWithExtraFile(fileName);
}
@TestMetadata("funOnJavaInterface.before.Main.kt")
public void testFunOnJavaInterface() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/createFromUsage/createFunction/call/funOnJavaInterface.before.Main.kt");
doTestWithExtraFile(fileName);
}
@TestMetadata("funOnJavaType.before.Main.kt")
public void testFunOnJavaType() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/createFromUsage/createFunction/call/funOnJavaType.before.Main.kt");
doTestWithExtraFile(fileName);
}
@TestMetadata("genericFunOnJavaType.before.Main.kt")
public void testGenericFunOnJavaType() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/createFromUsage/createFunction/call/genericFunOnJavaType.before.Main.kt");
doTestWithExtraFile(fileName);
}
}
}