Create from usage: Support generation of Java methods from usages in Kotlin code
This commit is contained in:
+89
-15
@@ -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
@@ -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
|
||||
|
||||
|
||||
+10
@@ -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.
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// "Create extension function 'foo'" "true"
|
||||
// ERROR: Unresolved reference: foo
|
||||
|
||||
fun test(): Int {
|
||||
return A().<caret>foo()
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
class A {
|
||||
|
||||
}
|
||||
+10
@@ -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.
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// "Create extension function 'foo'" "true"
|
||||
// ERROR: Unresolved reference: foo
|
||||
|
||||
fun test(): Int? {
|
||||
return A().<caret>foo(1, "2")
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
class A {
|
||||
|
||||
}
|
||||
+8
@@ -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()
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
class A {
|
||||
|
||||
}
|
||||
+7
@@ -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")
|
||||
}
|
||||
|
||||
+6
@@ -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")
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
interface A {
|
||||
|
||||
}
|
||||
+10
@@ -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.
|
||||
}
|
||||
|
||||
+2
-2
@@ -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")
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
class A {
|
||||
|
||||
public <T, U> U foo(U u, T t) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// "Create function 'foo'" "true"
|
||||
// ERROR: Unresolved reference: foo
|
||||
|
||||
fun test(): Int {
|
||||
return A().foo<String, Int>(1, "2")
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// "Create function 'foo'" "true"
|
||||
// ERROR: Unresolved reference: foo
|
||||
|
||||
fun test(): Int {
|
||||
return A().<caret>foo<String, Int>(1, "2")
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
class A {
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user