Introduce Type Alias: Support type constructor extraction

This commit is contained in:
Alexey Sedunov
2016-07-19 18:18:24 +03:00
parent b5828a5fa1
commit 0769d47f4b
8 changed files with 85 additions and 22 deletions
@@ -466,4 +466,9 @@ fun KtElement.containingClass(): KtClass? = getStrictParentOfType<KtClass>()
fun KtClassOrObject.findPropertyByName(name: String): KtNamedDeclaration? {
return declarations.firstOrNull { it is KtProperty && it.name == name } as KtNamedDeclaration?
?: getPrimaryConstructorParameters().firstOrNull { it.hasValOrVar() && it.name == name }
}
fun isTypeConstructorReference(e: PsiElement): Boolean {
val parent = e.parent
return parent is KtUserType && parent.referenceExpression == e
}
@@ -31,6 +31,7 @@ import org.jetbrains.kotlin.descriptors.ClassKind;
import org.jetbrains.kotlin.idea.caches.resolve.ResolutionUtils;
import org.jetbrains.kotlin.lexer.KtTokens;
import org.jetbrains.kotlin.psi.*;
import org.jetbrains.kotlin.psi.psiUtil.KtPsiUtilKt;
import org.jetbrains.kotlin.resolve.BindingContext;
import org.jetbrains.kotlin.resolve.scopes.receivers.ClassQualifier;
import org.jetbrains.kotlin.resolve.scopes.receivers.Qualifier;
@@ -57,12 +58,18 @@ public class CodeInsightUtils {
break;
case TYPE_ELEMENT: elementClass = KtTypeElement.class;
break;
case TYPE_CONSTRUCTOR: elementClass = KtSimpleNameExpression.class;
break;
default: throw new IllegalArgumentException(elementKind.name());
}
PsiElement element = findElementOfClassAtRange(file, startOffset, endOffset, elementClass);
if (elementKind == ElementKind.TYPE_ELEMENT) return element;
if (elementKind == ElementKind.TYPE_CONSTRUCTOR) {
return element != null && KtPsiUtilKt.isTypeConstructorReference(element) ? element : null;
}
if (element instanceof KtScriptInitializer) {
element = ((KtScriptInitializer) element).getBody();
}
@@ -106,7 +113,8 @@ public class CodeInsightUtils {
public enum ElementKind {
EXPRESSION,
TYPE_ELEMENT
TYPE_ELEMENT,
TYPE_CONSTRUCTOR
}
@NotNull
@@ -145,7 +153,8 @@ public class CodeInsightUtils {
for (PsiElement element : array) {
boolean correctType = kind == ElementKind.EXPRESSION && element instanceof KtExpression
|| kind == ElementKind.TYPE_ELEMENT && element instanceof KtTypeElement;
|| kind == ElementKind.TYPE_ELEMENT && element instanceof KtTypeElement
|| kind == ElementKind.TYPE_CONSTRUCTOR && KtPsiUtilKt.isTypeConstructorReference(element);
if (!(correctType
|| element.getNode().getElementType() == KtTokens.SEMICOLON
|| element instanceof PsiWhiteSpace
@@ -24,6 +24,7 @@ import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.refactoring.RefactoringActionHandler
import org.jetbrains.kotlin.idea.codeInsight.CodeInsightUtils.ElementKind.TYPE_CONSTRUCTOR
import org.jetbrains.kotlin.idea.codeInsight.CodeInsightUtils.ElementKind.TYPE_ELEMENT
import org.jetbrains.kotlin.idea.refactoring.checkConflictsInteractively
import org.jetbrains.kotlin.idea.refactoring.getExtractionContainers
@@ -34,8 +35,11 @@ import org.jetbrains.kotlin.idea.refactoring.introduce.selectElementsWithTargetS
import org.jetbrains.kotlin.idea.refactoring.introduce.showErrorHint
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
import org.jetbrains.kotlin.psi.KtTypeAlias
import org.jetbrains.kotlin.psi.KtTypeElement
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.psi.psiUtil.isTypeConstructorReference
object KotlinIntroduceTypeAliasHandler : RefactoringActionHandler {
@JvmField
@@ -47,7 +51,7 @@ object KotlinIntroduceTypeAliasHandler : RefactoringActionHandler {
editor,
file,
"Select target code block",
listOf(TYPE_ELEMENT),
listOf(TYPE_ELEMENT, TYPE_CONSTRUCTOR),
{ elements, parent -> parent.getExtractionContainers(strict = true, includeAll = true) },
continuation
)
@@ -69,9 +73,19 @@ object KotlinIntroduceTypeAliasHandler : RefactoringActionHandler {
targetSibling: PsiElement,
descriptorSubstitutor: ((IntroduceTypeAliasDescriptor) -> IntroduceTypeAliasDescriptor)? = null
) {
val typeElement = elements.singleOrNull() as? KtTypeElement
?: return showErrorHint(project, editor, "No type to refactor", REFACTORING_NAME)
val introduceData = IntroduceTypeAliasData(typeElement, targetSibling)
val elementToExtract = elements.singleOrNull()
val errorMessage = when (elementToExtract) {
is KtSimpleNameExpression -> if (!isTypeConstructorReference(elementToExtract)) "Type reference is expected" else null
!is KtTypeElement -> "No type to refactor"
else -> null
}
if (errorMessage != null) return showErrorHint(project, editor, errorMessage, REFACTORING_NAME)
val introduceData = when (elementToExtract) {
is KtTypeElement -> IntroduceTypeAliasData(elementToExtract, targetSibling)
else -> IntroduceTypeAliasData(elementToExtract!!.getStrictParentOfType<KtTypeElement>()!!, targetSibling, true)
}
val analysisResult = introduceData.analyze()
when (analysisResult) {
is IntroduceTypeAliasAnalysisResult.Error -> {
@@ -34,7 +34,8 @@ internal var KtTypeReference.resolveInfo : TypeReferenceInfo? by CopyableUserDat
class IntroduceTypeAliasData(
val originalType: KtTypeElement,
val targetSibling: PsiElement
val targetSibling: PsiElement,
val extractTypeConstructor: Boolean = false
) : Disposable {
val resolutionFacade = originalType.getResolutionFacade()
val bindingContext = resolutionFacade.analyze(originalType, BodyResolveMode.PARTIAL)
@@ -58,42 +58,48 @@ fun IntroduceTypeAliasData.analyze(): IntroduceTypeAliasAnalysisResult {
val dummyVar = psiFactory.createProperty("val a: Int").apply {
typeReference!!.replace(originalType.parent as? KtTypeReference ?: psiFactory.createType(originalType))
}
val newReferences = dummyVar.typeReference!!.collectDescendantsOfType<KtTypeReference> { it.resolveInfo != null }
val newTypeReference = dummyVar.typeReference!!
val newReferences = newTypeReference.collectDescendantsOfType<KtTypeReference> { it.resolveInfo != null }
val newContext = dummyVar.analyzeInContext(targetScope, contextExpression)
val project = originalType.project
val unifier = KotlinPsiUnifier.DEFAULT
val groupedBrokenReferences = LinkedMultiMap<TypeReferenceInfo, TypeReferenceInfo>()
val groupedReferencesToExtract = LinkedMultiMap<TypeReferenceInfo, TypeReferenceInfo>()
val forcedCandidates = if (extractTypeConstructor) newTypeReference.typeElement!!.typeArgumentsAsTypes else emptyList()
for (newReference in newReferences) {
val resolveInfo = newReference.resolveInfo!!
val originalDescriptor = resolveInfo.type.constructor.declarationDescriptor
val newDescriptor = newContext[BindingContext.TYPE, newReference]?.constructor?.declarationDescriptor
if (compareDescriptors(project, originalDescriptor, newDescriptor)) continue
if (newReference !in forcedCandidates) {
val originalDescriptor = resolveInfo.type.constructor.declarationDescriptor
val newDescriptor = newContext[BindingContext.TYPE, newReference]?.constructor?.declarationDescriptor
if (compareDescriptors(project, originalDescriptor, newDescriptor)) continue
}
val equivalenceRepresentative = groupedBrokenReferences
val equivalenceRepresentative = groupedReferencesToExtract
.keySet()
.firstOrNull { unifier.unify(it.reference, resolveInfo.reference).matched }
if (equivalenceRepresentative != null) {
groupedBrokenReferences.putValue(equivalenceRepresentative, resolveInfo)
groupedReferencesToExtract.putValue(equivalenceRepresentative, resolveInfo)
}
else {
groupedBrokenReferences.putValue(resolveInfo, resolveInfo)
groupedReferencesToExtract.putValue(resolveInfo, resolveInfo)
}
val brokenReferenceInfoIterator = groupedBrokenReferences.values().iterator()
while (brokenReferenceInfoIterator.hasNext()) {
val brokenReferenceInfo = brokenReferenceInfoIterator.next()
if (resolveInfo.reference.isAncestor(brokenReferenceInfo.reference, true)) {
brokenReferenceInfoIterator.remove()
val referencesToExtractIterator = groupedReferencesToExtract.values().iterator()
while (referencesToExtractIterator.hasNext()) {
val referenceToExtract = referencesToExtractIterator.next()
if (resolveInfo.reference.isAncestor(referenceToExtract.reference, true)) {
referencesToExtractIterator.remove()
}
}
}
val typeParameterNameValidator = CollectingNameValidator()
val brokenReferences = groupedBrokenReferences.keySet().filter { groupedBrokenReferences[it].isNotEmpty() }
val brokenReferences = groupedReferencesToExtract.keySet().filter { groupedReferencesToExtract[it].isNotEmpty() }
val typeParameterNames = KotlinNameSuggester.suggestNamesForTypeParameters(brokenReferences.size, typeParameterNameValidator)
val typeParameters = (typeParameterNames zip brokenReferences).map { TypeParameter(it.first, groupedBrokenReferences[it.second]) }
val typeParameters = (typeParameterNames zip brokenReferences).map { TypeParameter(it.first, groupedReferencesToExtract[it.second]) }
if (typeParameters.any { it.typeReferenceInfos.any { it.reference.typeElement == originalType } }) {
return IntroduceTypeAliasAnalysisResult.Error("Type alias cannot refer to types which aren't accessible in the scope where it's defined")
@@ -0,0 +1,10 @@
// NAME: S
class A<X, Y>
class B<T>
// SIBLING:
fun foo(a: <selection>A</selection><(Int) -> B<Int>, B<String>?>) {
}
@@ -0,0 +1,12 @@
// NAME: S
class A<X, Y>
class B<T>
typealias S<T, U> = A<T, U>
// SIBLING:
fun foo(a: S<(Int) -> B<Int>, B<String>?>) {
}
@@ -4084,6 +4084,12 @@ public class ExtractionTestGenerated extends AbstractExtractionTest {
doIntroduceTypeAliasTest(fileName);
}
@TestMetadata("typeConstructor.kt")
public void testTypeConstructor() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceTypeAlias/typeConstructor.kt");
doIntroduceTypeAliasTest(fileName);
}
@TestMetadata("typesExtractedWithFunctionalType.kt")
public void testTypesExtractedWithFunctionalType() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceTypeAlias/typesExtractedWithFunctionalType.kt");