Create Class from Usage: Support nested classes

This covers the case when original expression doesn't contains qualifier
Also for local or inner containing classes:
  - forbid nested objects
  - add 'inner' to nested class declaration

 #KT-16404 Fixed
This commit is contained in:
Alexey Sedunov
2017-07-21 14:09:43 +03:00
parent d30461afc6
commit 97a3d343f7
21 changed files with 252 additions and 94 deletions
@@ -413,6 +413,11 @@ class CallableBuilder(val config: CallableBuilderConfiguration) {
typeCandidates[typeInfo]?.forEach { it.render(typeParameterNameMap, fakeFunction) }
}
private fun isInsideInnerOrLocalClass(): Boolean {
val classOrObject = containingElement.getNonStrictParentOfType<KtClassOrObject>()
return classOrObject is KtClass && (classOrObject.isInner() || classOrObject.isLocal)
}
private fun createDeclarationSkeleton(): KtNamedDeclaration {
with (config) {
val assignmentToReplace =
@@ -490,14 +495,14 @@ class CallableBuilder(val config: CallableBuilderConfiguration) {
val safeName = name.quoteIfNeeded()
when (kind) {
ClassKind.ENUM_ENTRY -> {
val targetParent = targetParents.singleOrNull()
val targetParent = applicableParents.singleOrNull()
if (!(targetParent is KtClass && targetParent.isEnum())) throw AssertionError("Enum class expected: ${targetParent?.text}")
val hasParameters = targetParent.primaryConstructorParameters.isNotEmpty()
psiFactory.createEnumEntry("$safeName${if (hasParameters) "()" else " "}")
}
else -> {
val openMod = if (open) "open " else ""
val innerMod = if (inner) "inner " else ""
val innerMod = if (inner || isInsideInnerOrLocalClass()) "inner " else ""
val typeParamList = when (kind) {
ClassKind.PLAIN_CLASS, ClassKind.INTERFACE -> "<>"
else -> ""
@@ -53,7 +53,6 @@ object CreateClassFromCallWithConstructorCalleeActionFactory : CreateClassFromUs
val isAnnotation = element is KtAnnotationEntry
val callee = element.calleeExpression as? KtConstructorCalleeExpression ?: return null
val calleeRef = callee.constructorReferenceExpression ?: return null
val file = element.containingFile as? KtFile ?: return null
val typeRef = callee.typeReference ?: return null
val userType = typeRef.typeElement as? KtUserType ?: return null
@@ -62,7 +61,7 @@ object CreateClassFromCallWithConstructorCalleeActionFactory : CreateClassFromUs
val qualifier = userType.qualifier?.referenceExpression
val qualifierDescriptor = qualifier?.let { context[BindingContext.REFERENCE_TARGET, it] }
val targetParents = getTargetParentsByQualifier(file, qualifier != null, qualifierDescriptor).ifEmpty { return null }
val targetParents = getTargetParentsByQualifier(element, qualifier != null, qualifierDescriptor).ifEmpty { return null }
val anyType = module.builtIns.nullableAnyType
val valueArguments = element.valueArguments
@@ -42,9 +42,8 @@ object CreateClassFromConstructorCallActionFactory: CreateClassFromUsageFactory<
val inAnnotationEntry = diagnostic.psiElement.getNonStrictParentOfType<KtAnnotationEntry>() != null
val (context, moduleDescriptor) = element.analyzeFullyAndGetResult()
val file = element.containingFile as? KtFile ?: return emptyList()
val call = element.getCall(context) ?: return emptyList()
val targetParents = getTargetParentsByCall(call, file, context).ifEmpty { return emptyList() }
val targetParents = getTargetParentsByCall(call, context).ifEmpty { return emptyList() }
val classKind = if (inAnnotationEntry) ClassKind.ANNOTATION_CLASS else ClassKind.PLAIN_CLASS
val fullCallExpr = element.getQualifiedExpressionForSelectorOrThis()
@@ -72,12 +71,10 @@ object CreateClassFromConstructorCallActionFactory: CreateClassFromUsageFactory<
val fullCallExpr =
if (callParent is KtQualifiedExpression && callParent.selectorExpression == callExpr) callParent else callExpr
val file = fullCallExpr.containingFile as? KtFile ?: return null
val (context, moduleDescriptor) = callExpr.analyzeFullyAndGetResult()
val call = callExpr.getCall(context) ?: return null
val targetParents = getTargetParentsByCall(call, file, context).ifEmpty { return null }
val targetParents = getTargetParentsByCall(call, context).ifEmpty { return null }
val inner = isInnerClassExpected(call)
val valueArguments = callExpr.valueArguments
@@ -57,8 +57,6 @@ object CreateClassFromReferenceExpressionActionFactory : CreateClassFromUsageFac
}
}
val file = element.containingFile as? KtFile ?: return Collections.emptyList()
val name = element.getReferencedName()
val (context, moduleDescriptor) = element.analyzeFullyAndGetResult()
@@ -70,11 +68,8 @@ object CreateClassFromReferenceExpressionActionFactory : CreateClassFromUsageFac
val receiverSelector = (fullCallExpr as? KtQualifiedExpression)?.receiverExpression?.getQualifiedElementSelector() as? KtReferenceExpression
val qualifierDescriptor = receiverSelector?.let { context[BindingContext.REFERENCE_TARGET, it] }
val targetParents = getTargetParentsByQualifier(
element.containingKtFile,
receiverSelector != null,
qualifierDescriptor
).ifEmpty { return emptyList() }
val targetParents = getTargetParentsByQualifier(element, receiverSelector != null, qualifierDescriptor)
.ifEmpty { return emptyList() }
targetParents.forEach {
if (element.getCreatePackageFixIfApplicable(it) != null) return emptyList()
@@ -100,7 +95,7 @@ object CreateClassFromReferenceExpressionActionFactory : CreateClassFromUsageFac
if (fullCallExpr.getAssignmentByLHS() != null) return Collections.emptyList()
val call = element.getCall(context) ?: return Collections.emptyList()
val targetParents = getTargetParentsByCall(call, file, context).ifEmpty { return emptyList() }
val targetParents = getTargetParentsByCall(call, context).ifEmpty { return emptyList() }
if (isInnerClassExpected(call)) return Collections.emptyList()
val allKinds = Arrays.asList(ClassKind.OBJECT, ClassKind.ENUM_ENTRY)
@@ -119,8 +114,6 @@ object CreateClassFromReferenceExpressionActionFactory : CreateClassFromUsageFac
}
override fun extractFixData(element: KtSimpleNameExpression, diagnostic: Diagnostic): ClassInfo? {
val file = element.containingFile as? KtFile ?: return null
val name = element.getReferencedName()
val (context, moduleDescriptor) = element.analyzeFullyAndGetResult()
@@ -131,11 +124,8 @@ object CreateClassFromReferenceExpressionActionFactory : CreateClassFromUsageFac
val receiverSelector = (fullCallExpr as? KtQualifiedExpression)?.receiverExpression?.getQualifiedElementSelector() as? KtReferenceExpression
val qualifierDescriptor = receiverSelector?.let { context[BindingContext.REFERENCE_TARGET, it] }
val targetParents = getTargetParentsByQualifier(
element.containingKtFile,
receiverSelector != null,
qualifierDescriptor
).ifEmpty { return null }
val targetParents = getTargetParentsByQualifier(element, receiverSelector != null, qualifierDescriptor)
.ifEmpty { return null }
return ClassInfo(
name = name,
@@ -145,7 +135,7 @@ object CreateClassFromReferenceExpressionActionFactory : CreateClassFromUsageFac
}
val call = element.getCall(context) ?: return null
val targetParents = getTargetParentsByCall(call, file, context).ifEmpty { return null }
val targetParents = getTargetParentsByCall(call, context).ifEmpty { return null }
val expectedTypeInfo = fullCallExpr.guessTypeForClass(context, moduleDescriptor)?.toClassTypeInfo() ?: TypeInfo.Empty
@@ -77,13 +77,11 @@ object CreateClassFromTypeReferenceActionFactory : CreateClassFromUsageFactory<K
val name = element.referenceExpression?.getReferencedName() ?: return null
if (element.parent.parent is KtConstructorCalleeExpression) return null
val file = element.containingFile as? KtFile ?: return null
val (context, module) = element.analyzeAndGetResult()
val qualifier = element.qualifier?.referenceExpression
val qualifierDescriptor = qualifier?.let { context[BindingContext.REFERENCE_TARGET, it] }
val targetParents = getTargetParentsByQualifier(file, qualifier != null, qualifierDescriptor).ifEmpty { return null }
val targetParents = getTargetParentsByQualifier(element, qualifier != null, qualifierDescriptor).ifEmpty { return null }
val expectedUpperBound = getExpectedUpperBound(element, context)
val anyType = module.builtIns.anyType
@@ -23,10 +23,7 @@ import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiPackage
import com.intellij.psi.*
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.codeInsight.CodeInsightUtils
import org.jetbrains.kotlin.idea.quickfix.IntentionActionPriority
@@ -38,8 +35,10 @@ import org.jetbrains.kotlin.idea.refactoring.chooseContainerElementIfNecessary
import org.jetbrains.kotlin.idea.refactoring.getOrCreateKotlinFile
import org.jetbrains.kotlin.idea.util.application.executeCommand
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.psiUtil.allChildren
import java.util.*
enum class ClassKind(val keyword: String, val description: String) {
@@ -58,13 +57,20 @@ val ClassKind.actionPriority: IntentionActionPriority
data class ClassInfo(
val kind: ClassKind = ClassKind.DEFAULT,
val name: String,
val targetParents: List<PsiElement>,
private val targetParents: List<PsiElement>,
val expectedTypeInfo: TypeInfo,
val inner: Boolean = false,
val open: Boolean = false,
val typeArguments: List<TypeInfo> = Collections.emptyList(),
val parameterInfos: List<ParameterInfo> = Collections.emptyList()
)
) {
val applicableParents by lazy {
targetParents.filter {
if (kind == ClassKind.OBJECT && it is KtClass && (it.isInner() || it.isLocal)) return@filter false
true
}
}
}
open class CreateClassFromUsageFix<E : KtElement> protected constructor (
element: E,
@@ -76,7 +82,8 @@ open class CreateClassFromUsageFix<E : KtElement> protected constructor (
if (!super.isAvailable(project, editor, file)) return false
with(classInfo) {
if (kind == DEFAULT) return false
targetParents.forEach {
if (applicableParents.isEmpty()) return false
applicableParents.forEach {
if (it is PsiClass) {
if (kind == OBJECT || kind == ENUM_ENTRY) return false
if (it.isInterface && inner) return false
@@ -90,60 +97,75 @@ open class CreateClassFromUsageFix<E : KtElement> protected constructor (
override fun startInWriteAction() = false
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
fun createFileByPackage(psiPackage: PsiPackage): KtFile? {
val directories = psiPackage.directories.filter { it.canRefactor() }
assert (directories.isNotEmpty()) { "Package '${psiPackage.qualifiedName}' must be refactorable" }
val currentModule = ModuleUtilCore.findModuleForPsiElement(file)
val preferredDirectory =
directories.firstOrNull { ModuleUtilCore.findModuleForPsiElement(it) == currentModule }
?: directories.firstOrNull()
val targetDirectory = if (directories.size > 1 && !ApplicationManager.getApplication().isUnitTestMode) {
DirectoryChooserUtil.chooseDirectory(directories.toTypedArray(), preferredDirectory, project, HashMap())
}
else {
preferredDirectory
} ?: return null
val fileName = "${classInfo.name}.${KotlinFileType.INSTANCE.defaultExtension}"
val targetFile = getOrCreateKotlinFile(fileName, targetDirectory)
if (targetFile == null) {
val filePath = "${targetDirectory.virtualFile.path}/$fileName"
CodeInsightUtils.showErrorHint(
targetDirectory.project,
editor!!,
"File $filePath already exists but does not correspond to Kotlin file",
"Create file",
null
)
}
return targetFile
}
if (editor == null) return
with (classInfo) {
chooseContainerElementIfNecessary(targetParents, editor, "Choose class container", true, { it }) {
runWriteAction {
val targetParent =
when (it) {
is KtElement, is PsiClass -> it
is PsiPackage -> createFileByPackage(it)
else -> throw AssertionError("Unexpected element: " + it.text)
} ?: return@runWriteAction
val constructorInfo = PrimaryConstructorInfo(classInfo, expectedTypeInfo)
val builder = CallableBuilderConfiguration(
Collections.singletonList(constructorInfo),
element as KtElement,
file,
editor,
false,
kind == PLAIN_CLASS || kind == INTERFACE
).createBuilder()
builder.placement = CallablePlacement.NoReceiver(targetParent)
project.executeCommand(text) { builder.build() }
}
if (ApplicationManager.getApplication().isUnitTestMode) {
val targetParent = classInfo.applicableParents.firstOrNull {
it.allChildren.any { it is PsiComment && it.text == "// TARGET_PARENT:" }
} ?: classInfo.applicableParents.last()
return doInvoke(targetParent, editor, file)
}
chooseContainerElementIfNecessary(classInfo.applicableParents, editor, "Choose class container", true, { it }) {
doInvoke(it, editor, file)
}
}
private fun createFileByPackage(
psiPackage: PsiPackage,
editor: Editor,
originalFile: KtFile
): KtFile? {
val directories = psiPackage.directories.filter { it.canRefactor() }
assert (directories.isNotEmpty()) { "Package '${psiPackage.qualifiedName}' must be refactorable" }
val currentModule = ModuleUtilCore.findModuleForPsiElement(originalFile)
val preferredDirectory =
directories.firstOrNull { ModuleUtilCore.findModuleForPsiElement(it) == currentModule }
?: directories.firstOrNull()
val targetDirectory = if (directories.size > 1 && !ApplicationManager.getApplication().isUnitTestMode) {
DirectoryChooserUtil.chooseDirectory(directories.toTypedArray(), preferredDirectory, originalFile.project, HashMap())
}
else {
preferredDirectory
} ?: return null
val fileName = "${classInfo.name}.${KotlinFileType.INSTANCE.defaultExtension}"
val targetFile = getOrCreateKotlinFile(fileName, targetDirectory)
if (targetFile == null) {
val filePath = "${targetDirectory.virtualFile.path}/$fileName"
CodeInsightUtils.showErrorHint(
targetDirectory.project,
editor,
"File $filePath already exists but does not correspond to Kotlin file",
"Create file",
null
)
}
return targetFile
}
private fun doInvoke(selectedParent: PsiElement, editor: Editor, file: KtFile) {
runWriteAction {
with(classInfo) {
val targetParent =
when (selectedParent) {
is KtElement, is PsiClass -> selectedParent
is PsiPackage -> createFileByPackage(selectedParent, editor, file)
else -> throw AssertionError("Unexpected element: " + selectedParent.text)
} ?: return@runWriteAction
val constructorInfo = PrimaryConstructorInfo(classInfo, expectedTypeInfo)
val builder = CallableBuilderConfiguration(
Collections.singletonList(constructorInfo),
element as KtElement,
file,
editor,
false,
kind == PLAIN_CLASS || kind == INTERFACE
).createBuilder()
builder.placement = CallablePlacement.NoReceiver(targetParent)
file.project.executeCommand(text) { builder.build() }
}
}
}
@@ -33,6 +33,7 @@ import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.guessT
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.noSubstitutions
import org.jetbrains.kotlin.idea.refactoring.canRefactor
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.parents
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils
import org.jetbrains.kotlin.resolve.DescriptorUtils
@@ -51,14 +52,15 @@ internal fun String.checkClassName(): Boolean = isNotEmpty() && Character.isUppe
private fun String.checkPackageName(): Boolean = isNotEmpty() && Character.isLowerCase(first())
internal fun getTargetParentsByQualifier(
file: KtFile,
element: KtElement,
isQualified: Boolean,
qualifierDescriptor: DeclarationDescriptor?
): List<PsiElement> {
val file = element.containingKtFile
val project = file.project
val targetParents: List<PsiElement> = when {
!isQualified ->
listOf(file)
element.parents.filterIsInstance<KtClassOrObject>().toList() + file
qualifierDescriptor is ClassDescriptor ->
listOfNotNull(DescriptorToSourceUtilsIde.getAnyDeclaration(project, qualifierDescriptor))
qualifierDescriptor is PackageViewDescriptor ->
@@ -72,12 +74,13 @@ internal fun getTargetParentsByQualifier(
return targetParents.filter { it.canRefactor() }
}
internal fun getTargetParentsByCall(call: Call, file: KtFile, context: BindingContext): List<PsiElement> {
internal fun getTargetParentsByCall(call: Call, context: BindingContext): List<PsiElement> {
val callElement = call.callElement
val receiver = call.explicitReceiver
return when (receiver) {
null -> getTargetParentsByQualifier(file, false, null)
is Qualifier -> getTargetParentsByQualifier(file, true, context[BindingContext.REFERENCE_TARGET, receiver.referenceExpression])
is ReceiverValue -> getTargetParentsByQualifier(file, true, receiver.type.constructor.declarationDescriptor)
null -> getTargetParentsByQualifier(callElement, false, null)
is Qualifier -> getTargetParentsByQualifier(callElement, true, context[BindingContext.REFERENCE_TARGET, receiver.referenceExpression])
is ReceiverValue -> getTargetParentsByQualifier(callElement, true, receiver.type.constructor.declarationDescriptor)
else -> throw AssertionError("Unexpected receiver: $receiver")
}
}
@@ -40,7 +40,7 @@ object CreateTypeAliasFromTypeReferenceActionFactory : KotlinSingleIntentionActi
if (!element.languageVersionSettings.supportsFeature(LanguageFeature.TypeAliases)) return null
val classInfo = CreateClassFromTypeReferenceActionFactory.extractFixData(element, diagnostic) ?: return null
val targetParent = classInfo.targetParents.singleOrNull { it !is KtDeclaration && it !is PsiPackage } ?: return null
val targetParent = classInfo.applicableParents.singleOrNull { it !is KtDeclaration && it !is PsiPackage } ?: return null
val expectedType = getTypeConstraintInfo(element)?.upperBound
if (expectedType != null && expectedType.containsError()) return null
@@ -0,0 +1,7 @@
// "Create class 'Nested'" "true"
class A {
// TARGET_PARENT:
class B {
val a = <caret>Nested()
}
}
@@ -0,0 +1,11 @@
// "Create class 'Nested'" "true"
class A {
// TARGET_PARENT:
class B {
val a = Nested()
class Nested {
}
}
}
@@ -0,0 +1,7 @@
// "Create class 'Nested'" "true"
class A {
// TARGET_PARENT:
inner class B {
val a = <caret>Nested()
}
}
@@ -0,0 +1,11 @@
// "Create class 'Nested'" "true"
class A {
// TARGET_PARENT:
inner class B {
val a = Nested()
inner class Nested {
}
}
}
@@ -0,0 +1,7 @@
// "Create object 'Nested'" "true"
class A {
// TARGET_PARENT:
class B {
val a = <caret>Nested
}
}
@@ -0,0 +1,11 @@
// "Create object 'Nested'" "true"
class A {
// TARGET_PARENT:
class B {
val a = Nested
object Nested {
}
}
}
@@ -0,0 +1,7 @@
// "Create object 'Nested'" "true"
class A {
// TARGET_PARENT:
inner class B {
val a = <caret>Nested
}
}
@@ -0,0 +1,11 @@
// "Create object 'Nested'" "true"
class A {
// TARGET_PARENT:
inner class B {
val a = Nested
}
}
object Nested {
}
@@ -0,0 +1,7 @@
// "Create class 'Nested'" "true"
class A {
// TARGET_PARENT:
class B {
val a: <caret>Nested = Nested()
}
}
@@ -0,0 +1,11 @@
// "Create class 'Nested'" "true"
class A {
// TARGET_PARENT:
class B {
val a: Nested = Nested()
class Nested {
}
}
}
@@ -0,0 +1,7 @@
// "Create class 'Nested'" "true"
class A {
// TARGET_PARENT:
inner class B {
val a: <caret>Nested = Nested()
}
}
@@ -0,0 +1,11 @@
// "Create class 'Nested'" "true"
class A {
// TARGET_PARENT:
inner class B {
val a: Nested = Nested()
inner class Nested {
}
}
}
@@ -1647,6 +1647,18 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
doTest(fileName);
}
@TestMetadata("createNestedClass.kt")
public void testCreateNestedClass() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/createFromUsage/createClass/callExpression/createNestedClass.kt");
doTest(fileName);
}
@TestMetadata("createNestedClassInInner.kt")
public void testCreateNestedClassInInner() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/createFromUsage/createClass/callExpression/createNestedClassInInner.kt");
doTest(fileName);
}
@TestMetadata("expectedTypeBySuperFunction.kt")
public void testExpectedTypeBySuperFunction() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/createFromUsage/createClass/callExpression/expectedTypeBySuperFunction.kt");
@@ -1959,6 +1971,18 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
doTest(fileName);
}
@TestMetadata("createNestedObject.kt")
public void testCreateNestedObject() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/createFromUsage/createClass/referenceExpression/createNestedObject.kt");
doTest(fileName);
}
@TestMetadata("createNestedObjectInInner.kt")
public void testCreateNestedObjectInInner() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/createFromUsage/createClass/referenceExpression/createNestedObjectInInner.kt");
doTest(fileName);
}
@TestMetadata("enumByClassLiteral.kt")
public void testEnumByClassLiteral() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/createFromUsage/createClass/referenceExpression/enumByClassLiteral.kt");
@@ -2196,6 +2220,18 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
doTest(fileName);
}
@TestMetadata("createNestedClass.kt")
public void testCreateNestedClass() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/createFromUsage/createClass/typeReference/createNestedClass.kt");
doTest(fileName);
}
@TestMetadata("createNestedClassInInner.kt")
public void testCreateNestedClassInInner() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/createFromUsage/createClass/typeReference/createNestedClassInInner.kt");
doTest(fileName);
}
@TestMetadata("enumEntryNotQualifierNoTypeArgs.kt")
public void testEnumEntryNotQualifierNoTypeArgs() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/createFromUsage/createClass/typeReference/enumEntryNotQualifierNoTypeArgs.kt");