Method Hierarchy: Refactoring

- Do not rely on light methods and Java method hierarchy
- Support val/var parameters
- Support built-in member functions

 #KT-14346 Fixed
This commit is contained in:
Alexey Sedunov
2017-07-03 20:44:41 +03:00
parent 10941ae732
commit 53d12e6cf7
16 changed files with 316 additions and 39 deletions
@@ -18,7 +18,6 @@ package org.jetbrains.kotlin.asJava
import com.intellij.psi.*
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.IncorrectOperationException
import org.jetbrains.kotlin.asJava.classes.KtLightClass
import org.jetbrains.kotlin.asJava.classes.KtLightClassForFacade
import org.jetbrains.kotlin.asJava.elements.KtLightAnnotationForSourceEntry
@@ -30,6 +29,7 @@ import org.jetbrains.kotlin.load.java.propertyNameByGetMethodName
import org.jetbrains.kotlin.load.java.propertyNameBySetMethodName
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.platform.JavaToKotlinClassMap
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
@@ -37,6 +37,15 @@ import org.jetbrains.kotlin.psi.psiUtil.isExtensionDeclaration
fun KtClassOrObject.toLightClass(): KtLightClass? = LightClassGenerationSupport.getInstance(project).getLightClass(this)
fun KtClassOrObject.toLightClassWithBuiltinMapping(): PsiClass? {
toLightClass()?.let { return it }
val fqName = fqName ?: return null
val javaClassFqName = JavaToKotlinClassMap.mapKotlinToJava(fqName.toUnsafe())?.asSingleFqName() ?: return null
val searchScope = useScope as? GlobalSearchScope ?: return null
return JavaPsiFacade.getInstance(project).findClass(javaClassFqName.asString(), searchScope)
}
fun KtFile.findFacadeClass(): KtLightClass? {
return LightClassGenerationSupport.getInstance(project)
.getFacadeClassesInPackage(packageFqName, this.useScope as? GlobalSearchScope ?: GlobalSearchScope.projectScope(project))
@@ -22,13 +22,13 @@ import com.intellij.psi.PsiModifier
import com.intellij.psi.search.searches.ClassInheritorsSearch
import com.intellij.util.EmptyQuery
import com.intellij.util.Query
import org.jetbrains.kotlin.asJava.toLightClass
import org.jetbrains.kotlin.asJava.toLightClassWithBuiltinMapping
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.KtClassOrObject
fun HierarchySearchRequest<*>.searchInheritors(): Query<PsiClass> {
val psiClass: PsiClass = when (originalElement) {
is KtClassOrObject -> runReadAction { originalElement.toLightClass() }
is KtClassOrObject -> runReadAction { originalElement.toLightClassWithBuiltinMapping() }
is PsiClass -> originalElement
else -> null
} ?: return EmptyQuery.getEmptyQuery()
@@ -16,23 +16,42 @@
package org.jetbrains.kotlin.idea.hierarchy.overrides
import com.intellij.ide.hierarchy.HierarchyNodeDescriptor
import com.intellij.ide.hierarchy.HierarchyTreeStructure
import com.intellij.ide.hierarchy.JavaHierarchyUtil
import com.intellij.ide.hierarchy.MethodHierarchyBrowserBase
import com.intellij.ide.hierarchy.method.MethodHierarchyBrowser
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.project.Project
import com.intellij.psi.ElementDescriptionUtil
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiMethod
import com.intellij.ui.PopupHandler
import com.intellij.usageView.UsageViewLongNameLocation
import org.jetbrains.kotlin.asJava.getRepresentativeLightMethod
import org.jetbrains.kotlin.asJava.unwrapped
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.psi.KtCallableDeclaration
import org.jetbrains.kotlin.psi.KtDeclaration
import javax.swing.JPanel
import javax.swing.JTree
class KotlinOverrideHierarchyBrowser(
project: Project, baseElement: PsiElement
) : MethodHierarchyBrowser(project, baseElement.getRepresentativeLightMethod()) {
) : MethodHierarchyBrowserBase(project, baseElement) {
override fun createTrees(trees: MutableMap<String, JTree>) {
val actionManager = ActionManager.getInstance()
val tree = createTree(false)
val group = actionManager.getAction(IdeActions.GROUP_METHOD_HIERARCHY_POPUP) as ActionGroup
PopupHandler.installPopupHandler(tree, group, ActionPlaces.METHOD_HIERARCHY_VIEW_POPUP, actionManager)
BaseOnThisMethodAction().registerCustomShortcutSet(actionManager.getAction(IdeActions.ACTION_METHOD_HIERARCHY).shortcutSet, tree)
trees.put(MethodHierarchyBrowserBase.METHOD_TYPE, tree)
}
override fun createLegendPanel(): JPanel? =
MethodHierarchyBrowserBase.createStandardLegendPanel(
KotlinBundle.message("hierarchy.legend.member.is.defined.in.class"),
@@ -40,18 +59,15 @@ class KotlinOverrideHierarchyBrowser(
KotlinBundle.message("hierarchy.legend.member.should.be.defined")
)
override fun getElementFromDescriptor(descriptor: HierarchyNodeDescriptor) = descriptor.psiElement
override fun isApplicableElement(psiElement: PsiElement): Boolean =
psiElement.isOverrideHierarchyElement()
override fun createHierarchyTreeStructure(typeName: String, psiElement: PsiElement): HierarchyTreeStructure? =
if (typeName == MethodHierarchyBrowserBase.METHOD_TYPE) KotlinOverrideTreeStructure(myProject, psiElement) else null
if (typeName == MethodHierarchyBrowserBase.METHOD_TYPE) KotlinOverrideTreeStructure(myProject, psiElement as KtCallableDeclaration) else null
override fun getBaseMethod(): PsiMethod? {
val builder = myBuilders.get(myCurrentViewType)
if (builder == null) return null
return (builder.treeStructure as KotlinOverrideTreeStructure).javaTreeStructures.get(0).baseMethod
}
override fun getComparator() = JavaHierarchyUtil.getComparator(myProject)!!
override fun getContentDisplayName(typeName: String, element: PsiElement): String? {
val targetElement = element.unwrapped
@@ -0,0 +1,163 @@
/*
* Copyright 2010-2017 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.kotlin.idea.hierarchy.overrides
import com.intellij.icons.AllIcons
import com.intellij.ide.IdeBundle
import com.intellij.ide.hierarchy.HierarchyNodeDescriptor
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.roots.ui.util.CompositeAppearance
import com.intellij.openapi.util.Comparing
import com.intellij.openapi.util.Iconable
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiMember
import com.intellij.ui.LayeredIcon
import com.intellij.ui.RowIcon
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.idea.caches.resolve.getJavaMemberDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import org.jetbrains.kotlin.psi.psiUtil.createSmartPointer
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.descriptorUtil.parents
import org.jetbrains.kotlin.types.substitutions.getTypeSubstitutor
import org.jetbrains.kotlin.util.findCallableMemberBySignature
import java.awt.Font
import javax.swing.Icon
class KotlinOverrideHierarchyNodeDescriptor (
parentNode: HierarchyNodeDescriptor?,
klass: PsiElement,
baseElement: PsiElement
) : HierarchyNodeDescriptor(klass.project, parentNode, klass, parentNode == null) {
private val baseElement = baseElement.createSmartPointer()
private var rawIcon: Icon? = null
private var stateIcon: Icon? = null
private fun resolveToDescriptor(psiElement: PsiElement): DeclarationDescriptor? {
return when (psiElement) {
is KtNamedDeclaration -> psiElement.resolveToDescriptor()
is PsiMember -> psiElement.getJavaMemberDescriptor()
else -> null
}
}
private fun getBaseDescriptor() = baseElement.element?.let { resolveToDescriptor(it) } as? CallableMemberDescriptor
private fun getCurrentClassDescriptor() = psiElement?.let { resolveToDescriptor(it) } as? ClassDescriptor
private fun getCurrentDescriptor(): CallableMemberDescriptor? {
val classDescriptor = getCurrentClassDescriptor() ?: return null
val baseDescriptor = getBaseDescriptor() ?: return null
val baseClassDescriptor = baseDescriptor.containingDeclaration as? ClassDescriptor ?: return null
val substitutor = getTypeSubstitutor(baseClassDescriptor.defaultType, classDescriptor.defaultType) ?: return null
return classDescriptor.findCallableMemberBySignature(baseDescriptor.substitute(substitutor) as CallableMemberDescriptor)
}
internal fun calculateState(): Icon? {
val classDescriptor = getCurrentClassDescriptor() ?: return null
val callableDescriptor = getCurrentDescriptor() ?: return AllIcons.Hierarchy.MethodNotDefined
if (callableDescriptor.kind == CallableMemberDescriptor.Kind.DECLARATION) {
if (callableDescriptor.modality == Modality.ABSTRACT) return null
return AllIcons.Hierarchy.MethodDefined
}
val isAbstractClass = classDescriptor.modality == Modality.ABSTRACT
val hasBaseImplementation = DescriptorUtils.getAllOverriddenDeclarations(callableDescriptor).any { it.modality != Modality.ABSTRACT }
return if (isAbstractClass || hasBaseImplementation) AllIcons.Hierarchy.MethodNotDefined else AllIcons.Hierarchy.ShouldDefineMethod
}
override fun update(): Boolean {
var flags = Iconable.ICON_FLAG_VISIBILITY
if (isMarkReadOnly) {
flags = flags or Iconable.ICON_FLAG_READ_STATUS
}
var changes = super.update()
val classPsi = psiElement
val classDescriptor = getCurrentClassDescriptor()
if (classPsi == null || classDescriptor == null) {
val invalidPrefix = IdeBundle.message("node.hierarchy.invalid")
if (!myHighlightedText.text.startsWith(invalidPrefix)) {
myHighlightedText.beginning.addText(invalidPrefix, getInvalidPrefixAttributes())
}
return true
}
val newRawIcon = classPsi.getIcon(flags)
val newStateIcon = calculateState()
if (changes || newRawIcon !== rawIcon || newStateIcon !== stateIcon) {
changes = true
rawIcon = newRawIcon
stateIcon = newStateIcon
var newIcon = rawIcon
if (myIsBase) {
val icon = LayeredIcon(2)
icon.setIcon(newIcon, 0)
icon.setIcon(AllIcons.Hierarchy.Base, 1, -AllIcons.Hierarchy.Base.iconWidth / 2, 0)
newIcon = icon
}
if (stateIcon != null) {
newIcon = RowIcon(stateIcon, newIcon)
}
icon = newIcon
}
val oldText = myHighlightedText
myHighlightedText = CompositeAppearance()
var classNameAttributes: TextAttributes? = null
if (myColor != null) {
classNameAttributes = TextAttributes(myColor, null, null, null, Font.PLAIN)
}
with (myHighlightedText.ending) {
addText(classDescriptor.name.asString(), classNameAttributes)
classDescriptor.parents.forEach { parentDescriptor ->
when (parentDescriptor) {
is MemberDescriptor -> {
addText(" in ${parentDescriptor.name.asString()}", classNameAttributes)
if (parentDescriptor is FunctionDescriptor) {
addText("()", classNameAttributes)
}
}
is PackageFragmentDescriptor -> {
addText(" (${parentDescriptor.fqName.asString()})", HierarchyNodeDescriptor.getPackageNameAttributes())
return@forEach
}
}
}
}
myName = myHighlightedText.text
if (!Comparing.equal(myHighlightedText, oldText)) {
changes = true
}
return changes
}
}
@@ -23,10 +23,9 @@ import com.intellij.ide.hierarchy.MethodHierarchyBrowserBase
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiMethod
import org.jetbrains.kotlin.idea.hierarchy.getCurrentElement
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.KtCallableDeclaration
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.getParentOfTypesAndPredicate
class KotlinOverrideHierarchyProvider: HierarchyProvider {
@@ -44,8 +43,7 @@ class KotlinOverrideHierarchyProvider: HierarchyProvider {
}
private fun getOverrideHierarchyElement(element: PsiElement?): PsiElement?
= element?.getParentOfTypesAndPredicate() { it.isOverrideHierarchyElement() }
= element?.getParentOfTypesAndPredicate { it.isOverrideHierarchyElement() }
}
fun PsiElement.isOverrideHierarchyElement(): Boolean
= this is PsiMethod || this is KtNamedFunction || this is KtProperty
fun PsiElement.isOverrideHierarchyElement() = this is KtCallableDeclaration && containingClassOrObject != null
@@ -16,31 +16,35 @@
package org.jetbrains.kotlin.idea.hierarchy.overrides
import com.intellij.ide.hierarchy.HierarchyTreeStructure
import com.intellij.icons.AllIcons
import com.intellij.ide.hierarchy.HierarchyNodeDescriptor
import com.intellij.ide.hierarchy.HierarchyTreeStructure
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.ide.hierarchy.method.MethodHierarchyNodeDescriptor
import com.intellij.ide.hierarchy.method.MethodHierarchyTreeStructure
import com.intellij.util.ArrayUtil
import org.jetbrains.kotlin.asJava.toLightMethods
import com.intellij.util.containers.ContainerUtil
class KotlinOverrideTreeStructure(project: Project, val element: PsiElement) : HierarchyTreeStructure(project, null) {
val javaTreeStructures = element.toLightMethods().map { method -> MethodHierarchyTreeStructure(project, method) }
import org.jetbrains.kotlin.asJava.unwrapped
import org.jetbrains.kotlin.idea.search.declarationsSearch.HierarchySearchRequest
import org.jetbrains.kotlin.idea.search.declarationsSearch.searchInheritors
import org.jetbrains.kotlin.psi.KtCallableDeclaration
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.createSmartPointer
class KotlinOverrideTreeStructure(project: Project, declaration: KtCallableDeclaration) : HierarchyTreeStructure(project, null) {
init {
setBaseElement(javaTreeStructures.first().baseDescriptor!!)
setBaseElement(KotlinOverrideHierarchyNodeDescriptor(null, declaration.containingClassOrObject!!, declaration))
}
private val baseElement = declaration.createSmartPointer()
override fun buildChildren(nodeDescriptor: HierarchyNodeDescriptor): Array<Any> {
fun buildChildrenByTreeStructure(javaTreeStructure: MethodHierarchyTreeStructure): Array<Any> {
return javaTreeStructure.getChildElements(nodeDescriptor as MethodHierarchyNodeDescriptor) ?: ArrayUtil.EMPTY_OBJECT_ARRAY
}
return javaTreeStructures
.asSequence()
.map (::buildChildrenByTreeStructure)
.reduce { a, b -> ContainerUtil.union(a.toSet(), b.toSet()).toTypedArray() }
val baseElement = baseElement.element ?: return ArrayUtil.EMPTY_OBJECT_ARRAY
val psiElement = nodeDescriptor.psiElement ?: return ArrayUtil.EMPTY_OBJECT_ARRAY
val subclasses = HierarchySearchRequest(psiElement, psiElement.useScope, false).searchInheritors().findAll()
return subclasses
.mapNotNull {
val subclass = it.unwrapped ?: return@mapNotNull null
KotlinOverrideHierarchyNodeDescriptor(nodeDescriptor, subclass, baseElement)
}
.filter { it.calculateState() != AllIcons.Hierarchy.MethodNotDefined }
.toTypedArray()
}
}
@@ -0,0 +1,20 @@
<node text="List (kotlin.collections)" base="true">
<node text="UnmodifiableList in Collections (java.util)"/>
<node text="CheckedList in Collections (java.util)"/>
<node text="SynchronizedList in Collections (java.util)"/>
<node text="AbstractList (java.util)">
<node text="ArrayList in Arrays (java.util)"/>
<node text="EmptyList in Collections (java.util)"/>
<node text="SubList in ArrayList (java.util)"/>
<node text="AbstractSequentialList (java.util)"/>
<node text="CopiesList in Collections (java.util)"/>
<node text="SingletonList in Collections (java.util)"/>
<node text="ArrayList (java.util)"/>
</node>
<node text="ArrayList (java.util)"/>
<node text="AbstractList (kotlin.collections)">
<node text="SubList in AbstractList (kotlin.collections)"/>
<node text="ReversedListReadOnly (kotlin.collections)"/>
</node>
<node text="EmptyList (kotlin.collections)"/>
</node>
@@ -0,0 +1,3 @@
fun foo(list: List<String>){
list.<caret>get(1)
}
@@ -0,0 +1,12 @@
<node text="T ()" base="true">
<node text="B ()"/>
<node text="X ()"/>
<node text="Y ()">
<node text="A ()">
<node text="C ()"/>
</node>
<node text="Z ()">
<node text="D ()"/>
</node>
</node>
</node>
@@ -0,0 +1,3 @@
fun <caret>foo() {
}
@@ -0,0 +1 @@
Cannot apply action for element at caret
@@ -0,0 +1,5 @@
<node text="T ()" base="true">
<node text="B ()"/>
<node text="X ()"/>
<node text="Y ()"/>
</node>
@@ -0,0 +1,9 @@
open class T(open var <caret>foo: String = "")
open class X: T("") {
override var foo: String
get() = ""
set(value: String) {}
}
open class Y(override var foo: String = ""): T(foo)
@@ -0,0 +1,15 @@
class B extends T {
B() {
super("");
}
@Override
public String getFoo() {
return "";
}
@Override
public void setFoo(String value) {
}
}
@@ -41,6 +41,7 @@ import org.jetbrains.kotlin.idea.hierarchy.calls.KotlinCalleeTreeStructure;
import org.jetbrains.kotlin.idea.hierarchy.calls.KotlinCallerTreeStructure;
import org.jetbrains.kotlin.idea.hierarchy.overrides.KotlinOverrideTreeStructure;
import org.jetbrains.kotlin.idea.test.PluginTestCaseBase;
import org.jetbrains.kotlin.psi.KtCallableDeclaration;
import org.jetbrains.kotlin.psi.KtElement;
import java.io.File;
@@ -173,7 +174,7 @@ public abstract class AbstractHierarchyTest extends KotlinHierarchyViewTestBase
public HierarchyTreeStructure compute() {
return new KotlinOverrideTreeStructure(
getProject(),
getElementAtCaret(LanguageCallHierarchy.INSTANCE)
(KtCallableDeclaration) getElementAtCaret(LanguageMethodHierarchy.INSTANCE)
);
}
};
@@ -560,6 +560,12 @@ public class HierarchyTestGenerated extends AbstractHierarchyTest {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/hierarchy/overrides"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, false);
}
@TestMetadata("kotlinBuiltInMemberFunction")
public void testKotlinBuiltInMemberFunction() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/hierarchy/overrides/kotlinBuiltInMemberFunction/");
doOverrideHierarchyTest(fileName);
}
@TestMetadata("kotlinFunctionInClass")
public void testKotlinFunctionInClass() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/hierarchy/overrides/kotlinFunctionInClass/");
@@ -583,5 +589,17 @@ public class HierarchyTestGenerated extends AbstractHierarchyTest {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/hierarchy/overrides/kotlinPropertyInTrait/");
doOverrideHierarchyTest(fileName);
}
@TestMetadata("kotlinTopLevelFunction")
public void testKotlinTopLevelFunction() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/hierarchy/overrides/kotlinTopLevelFunction/");
doOverrideHierarchyTest(fileName);
}
@TestMetadata("kotlinVarParameter")
public void testKotlinVarParameter() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/hierarchy/overrides/kotlinVarParameter/");
doOverrideHierarchyTest(fileName);
}
}
}