diff --git a/idea/src/org/jetbrains/kotlin/idea/slicer/HackedSliceLeafValueClassNode.java b/idea/src/org/jetbrains/kotlin/idea/slicer/HackedSliceLeafValueClassNode.java new file mode 100644 index 00000000000..3ea8f9bfe11 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/slicer/HackedSliceLeafValueClassNode.java @@ -0,0 +1,56 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.slicer; + +import com.intellij.openapi.project.Project; +import com.intellij.slicer.JavaSliceUsage; +import com.intellij.slicer.SliceLeafValueRootNode; +import com.intellij.slicer.SliceNode; +import com.intellij.slicer.SliceUsageCellRendererBase; +import com.intellij.ui.SimpleTextAttributes; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.util.ArrayList; + +class HackedSliceLeafValueClassNode extends SliceLeafValueRootNode { + private final String myClassName; + + HackedSliceLeafValueClassNode(@NotNull Project project, @NotNull SliceNode root, @NotNull String className) { + super(project, + root, + JavaSliceUsage.createRootUsage(root.getValue().getElement(), root.getValue().params), + new ArrayList<>()); + myClassName = className; + } + + @Override + public boolean canNavigate() { + return false; + } + + @Override + public boolean canNavigateToSource() { + return false; + } + + @Override + public void customizeCellRenderer(@NotNull SliceUsageCellRendererBase renderer, + @NotNull JTree tree, + Object value, + boolean selected, + boolean expanded, + boolean leaf, + int row, + boolean hasFocus) { + renderer.append(myClassName, SimpleTextAttributes.DARK_TEXT); + } + + @Override + public String getNodeText() { + return myClassName; + } +} diff --git a/idea/src/org/jetbrains/kotlin/idea/slicer/HackedSliceNullnessAnalyzerBase.java b/idea/src/org/jetbrains/kotlin/idea/slicer/HackedSliceNullnessAnalyzerBase.java new file mode 100644 index 00000000000..ae28f445b5e --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/slicer/HackedSliceNullnessAnalyzerBase.java @@ -0,0 +1,230 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ +package org.jetbrains.kotlin.idea.slicer; + +import com.intellij.codeInsight.Nullability; +import com.intellij.codeInsight.PsiEquivalenceUtil; +import com.intellij.ide.util.treeView.AbstractTreeNode; +import com.intellij.ide.util.treeView.AbstractTreeStructure; +import com.intellij.openapi.application.ReadAction; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.util.Ref; +import com.intellij.psi.PsiElement; +import com.intellij.slicer.*; +import com.intellij.util.WalkingState; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.FactoryMap; +import gnu.trove.THashSet; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +public abstract class HackedSliceNullnessAnalyzerBase { + @NotNull + private final SliceLeafEquality myLeafEquality; + + @NotNull + private final SliceLanguageSupportProvider myProvider; + + public HackedSliceNullnessAnalyzerBase(@NotNull SliceLeafEquality leafEquality, + @NotNull SliceLanguageSupportProvider provider) { + myLeafEquality = leafEquality; + myProvider = provider; + } + + private void groupByNullness(NullAnalysisResult result, SliceRootNode oldRoot, final Map map) { + SliceRootNode root = createNewTree(result, oldRoot, map); + + SliceUsage rootUsage = oldRoot.getCachedChildren().get(0).getValue(); + SliceManager.getInstance(root.getProject()).createToolWindow(true, root, true, SliceManager.getElementDescription(null, rootUsage.getElement(), " Grouped by Nullness") ); + } + + @NotNull + public SliceRootNode createNewTree(NullAnalysisResult result, SliceRootNode oldRoot, final Map map) { + SliceRootNode root = oldRoot.copy(); + assert oldRoot.getCachedChildren().size() == 1; + SliceNode oldRootStart = oldRoot.getCachedChildren().get(0); + root.setChanged(); + root.targetEqualUsages.clear(); + + List children = new ArrayList<>(); + ContainerUtil.addIfNotNull(children, createValueRootNode(result, oldRoot, map, root, oldRootStart, "Null Values", NullAnalysisResult.NULLS)); + ContainerUtil.addIfNotNull(children, createValueRootNode(result, oldRoot, map, root, oldRootStart, "NotNull Values", NullAnalysisResult.NOT_NULLS)); + ContainerUtil.addIfNotNull(children, createValueRootNode(result, oldRoot, map, root, oldRootStart, "Other Values", NullAnalysisResult.UNKNOWNS)); + root.setChildren(children); + return root; + } + + private HackedSliceLeafValueClassNode createValueRootNode( + NullAnalysisResult result, + SliceRootNode oldRoot, + final Map map, + SliceRootNode root, + SliceNode oldRootStart, + String nodeName, + final int group) { + Collection groupedByValue = result.groupedByValue[group]; + if (groupedByValue.isEmpty()) { + return null; + } + HackedSliceLeafValueClassNode + valueRoot = new HackedSliceLeafValueClassNode(root.getProject(), root, nodeName); + + Set uniqueValues = new THashSet<>(groupedByValue, myLeafEquality); + for (final PsiElement expression : uniqueValues) { + SliceNode newRoot = SliceLeafAnalyzer.filterTree(oldRootStart, oldNode -> { + if (oldNode.getDuplicate() != null) { + return null; + } + + for (PsiElement nullSuspect : group(oldNode, map, group)) { + if (PsiEquivalenceUtil.areElementsEquivalent(nullSuspect, expression)) { + return oldNode.copy(); + } + } + return null; + }, (node, children) -> { + if (!children.isEmpty()) return true; + PsiElement element = node.getValue().getElement(); + if (element == null) return false; + return PsiEquivalenceUtil.areElementsEquivalent(element, expression); // leaf can be there only if it's filtering expression + }); + valueRoot.myCachedChildren.add( + new SliceLeafValueRootNode(root.getProject(), + valueRoot, + myProvider.createRootUsage(expression, oldRoot.getValue().params), + Collections.singletonList(newRoot)) + ); + } + return valueRoot; + } + + public void startAnalyzeNullness(@NotNull AbstractTreeStructure treeStructure, @NotNull Runnable finish) { + final SliceRootNode root = (SliceRootNode)treeStructure.getRootElement(); + final Ref leafExpressions = Ref.create(null); + final Map map = createMap(); + + String encouragementPiece = " (may very well take the whole day)"; + ProgressManager.getInstance().run(new Task.Backgroundable( + root.getProject(), "Expanding All Nodes..." + encouragementPiece, true) { + @Override + public void run(@NotNull final ProgressIndicator indicator) { + NullAnalysisResult l = calcNullableLeaves(root, treeStructure, map); + leafExpressions.set(l); + } + + @Override + public void onCancel() { + finish.run(); + } + + @Override + public void onSuccess() { + try { + NullAnalysisResult leaves = leafExpressions.get(); + if (leaves == null) return; //cancelled + + groupByNullness(leaves, root, map); + } + finally { + finish.run(); + } + } + }); + } + + public static Map createMap() { + return FactoryMap.createMap(k->new NullAnalysisResult(), ContainerUtil::newIdentityTroveMap); + } + + private static NullAnalysisResult node(@NotNull SliceNode node, @NotNull Map nulls) { + return nulls.get(node); + } + private static Collection group(@NotNull SliceNode node, @NotNull Map nulls, int group) { + return nulls.get(node).groupedByValue[group]; + } + + @NotNull + public NullAnalysisResult calcNullableLeaves(@NotNull final SliceNode root, + @NotNull AbstractTreeStructure treeStructure, + @NotNull final Map map) { + final SliceLeafAnalyzer.SliceNodeGuide guide = new SliceLeafAnalyzer.SliceNodeGuide(treeStructure); + WalkingState walkingState = new WalkingState(guide) { + @Override + public void visit(@NotNull final SliceNode element) { + element.calculateDupNode(); + node(element, map).clear(); + SliceNode duplicate = element.getDuplicate(); + if (duplicate != null) { + node(element, map).add(node(duplicate, map)); + } + else { + final PsiElement value = ReadAction.compute(() -> element.getValue().getElement()); + boolean canBeLeaf = element.getValue().canBeLeaf(); + Nullability nullability = canBeLeaf ? ReadAction.compute(() -> checkNullability(value)) : Nullability.UNKNOWN; + if (nullability == Nullability.NULLABLE) { + group(element, map, NullAnalysisResult.NULLS).add(value); + } + else if (nullability == Nullability.NOT_NULL) { + group(element, map, NullAnalysisResult.NOT_NULLS).add(value); + } + else { + Collection children = ReadAction.compute(element::getChildren); + if (children.isEmpty()) { + group(element, map, NullAnalysisResult.UNKNOWNS).add(value); + } + super.visit(element); + } + } + } + + @Override + public void elementFinished(@NotNull SliceNode element) { + SliceNode parent = guide.getParent(element); + if (parent != null) { + node(parent, map).add(node(element, map)); + } + } + }; + walkingState.visit(root); + + return node(root, map); + } + + /** + * Implementors must override this method; default implementation just throws UnsupportedOperationException. + * + * @param element element to find nullability for + * @return element nullability + */ + @NotNull + protected Nullability checkNullability(final PsiElement element) { + throw new UnsupportedOperationException(); + } + + public static class NullAnalysisResult { + static final int NULLS = 0; + static final int NOT_NULLS = 1; + static final int UNKNOWNS = 2; + final Collection[] groupedByValue = new Collection[] {new THashSet(),new THashSet(),new THashSet()}; + + public void clear() { + for (Collection elements : groupedByValue) { + elements.clear(); + } + } + + private void add(NullAnalysisResult duplicate) { + for (int i = 0; i < groupedByValue.length; i++) { + Collection elements = groupedByValue[i]; + Collection other = duplicate.groupedByValue[i]; + elements.addAll(other); + } + } + } +} + diff --git a/idea/src/org/jetbrains/kotlin/idea/slicer/KotlinSliceProvider.kt b/idea/src/org/jetbrains/kotlin/idea/slicer/KotlinSliceProvider.kt index 1bfea9556e9..e4fcf5ffe4a 100644 --- a/idea/src/org/jetbrains/kotlin/idea/slicer/KotlinSliceProvider.kt +++ b/idea/src/org/jetbrains/kotlin/idea/slicer/KotlinSliceProvider.kt @@ -50,8 +50,8 @@ class KotlinSliceProvider : SliceLanguageSupportProvider, SliceUsageTransformer val leafAnalyzer by lazy { SliceLeafAnalyzer(LEAF_ELEMENT_EQUALITY, this) } - val nullnessAnalyzer: SliceNullnessAnalyzerBase by lazy { - object : SliceNullnessAnalyzerBase(LEAF_ELEMENT_EQUALITY, this) { + val nullnessAnalyzer: HackedSliceNullnessAnalyzerBase by lazy { + object : HackedSliceNullnessAnalyzerBase(LEAF_ELEMENT_EQUALITY, this) { override fun checkNullability(element: PsiElement?): Nullability { val types = when (element) { is KtCallableDeclaration -> listOfNotNull((element.resolveToDescriptorIfAny() as? CallableDescriptor)?.returnType) diff --git a/idea/testData/slicer/inflow/inlineFunctionManyCalls.nullnessGroups.txt b/idea/testData/slicer/inflow/inlineFunctionManyCalls.nullnessGroups.txt index dddb38b2bf8..f6ec68e1a53 100644 --- a/idea/testData/slicer/inflow/inlineFunctionManyCalls.nullnessGroups.txt +++ b/idea/testData/slicer/inflow/inlineFunctionManyCalls.nullnessGroups.txt @@ -1,11 +1,28 @@ [NotNull Values] -7 with(123) { +7 with(123) { 3 fun Any.extensionFun() { 7 [LAMBDA CALLS] with(123) { -19 [LAMBDA CALLS] withNoInline(1) { +44 (INLINE CALL with) [LAMBDA CALLS] inline fun with(receiver: T, block: T.() -> R): R { +45 (INLINE CALL with) val result = receiver.block() +44 (INLINE CALL with) inline fun with(receiver: T, block: T.() -> R): R { +7 with(123) { 12 this.extensionFun() 3 fun Any.extensionFun() { 12 this.extensionFun() +19 withNoInline(1) { +3 fun Any.extensionFun() { +19 [LAMBDA CALLS] withNoInline(1) { +53 [LAMBDA CALLS] fun withNoInline(receiver: T, block: T.() -> R): R { +54 val result = receiver.block() +53 fun withNoInline(receiver: T, block: T.() -> R): R { +19 withNoInline(1) { +23 withNoInline(2) { +3 fun Any.extensionFun() { +19 [LAMBDA CALLS] withNoInline(1) { +53 [LAMBDA CALLS] fun withNoInline(receiver: T, block: T.() -> R): R { +54 val result = receiver.block() +53 fun withNoInline(receiver: T, block: T.() -> R): R { +23 withNoInline(2) { 36 it.extensionFun() 3 fun Any.extensionFun() { 28 it.extensionFun() diff --git a/idea/testData/slicer/inflow/onFunctionReceiverType.nullnessGroups.txt b/idea/testData/slicer/inflow/onFunctionReceiverType.nullnessGroups.txt index 5e2a6b5c985..82765b28b4b 100644 --- a/idea/testData/slicer/inflow/onFunctionReceiverType.nullnessGroups.txt +++ b/idea/testData/slicer/inflow/onFunctionReceiverType.nullnessGroups.txt @@ -5,9 +5,13 @@ 14 1.extensionFun() 8 fun Any.extensionFun() { 14 1.extensionFun() -18 with(123) { +18 with(123) { 8 fun Any.extensionFun() { 18 [LAMBDA CALLS] with(123) { +27 (INLINE CALL with) [LAMBDA CALLS] inline fun with(receiver: T, block: T.() -> R): R { +28 (INLINE CALL with) return receiver.block() +27 (INLINE CALL with) inline fun with(receiver: T, block: T.() -> R): R { +18 with(123) { 24 "A".foo() 8 fun Any.extensionFun() { 11 fun String.foo() { diff --git a/idea/tests/org/jetbrains/kotlin/idea/slicer/AbstractSlicerNullnessGroupingTest.kt b/idea/tests/org/jetbrains/kotlin/idea/slicer/AbstractSlicerNullnessGroupingTest.kt index b80af7bb606..8273e50ea88 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/slicer/AbstractSlicerNullnessGroupingTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/slicer/AbstractSlicerNullnessGroupingTest.kt @@ -5,7 +5,6 @@ package org.jetbrains.kotlin.idea.slicer -import com.intellij.slicer.SliceNullnessAnalyzerBase import com.intellij.slicer.SliceRootNode import org.jetbrains.kotlin.test.KotlinTestUtils import java.io.File @@ -14,7 +13,7 @@ abstract class AbstractSlicerNullnessGroupingTest : AbstractSlicerTest() { override fun doTest(path: String, sliceProvider: KotlinSliceProvider, rootNode: SliceRootNode) { val treeStructure = TestSliceTreeStructure(rootNode) val analyzer = sliceProvider.nullnessAnalyzer - val nullnessByNode = SliceNullnessAnalyzerBase.createMap() + val nullnessByNode = HackedSliceNullnessAnalyzerBase.createMap() val nullness = analyzer.calcNullableLeaves(rootNode, treeStructure, nullnessByNode) val newRootNode = analyzer.createNewTree(nullness, rootNode, nullnessByNode) val renderedForest = buildString { diff --git a/idea/tests/org/jetbrains/kotlin/idea/slicer/SlicerTestUtil.kt b/idea/tests/org/jetbrains/kotlin/idea/slicer/SlicerTestUtil.kt index 7e2ea3c72ed..1d5647de516 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/slicer/SlicerTestUtil.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/slicer/SlicerTestUtil.kt @@ -38,7 +38,7 @@ internal fun buildTreeRepresentation(rootNode: SliceNode): String { return text } - fun SliceNode.isSliceLeafValueClassNode() = javaClass.name == "com.intellij.slicer.SliceLeafValueClassNode" + fun SliceNode.isSliceLeafValueClassNode() = this is HackedSliceLeafValueClassNode fun process(node: SliceNode, indent: Int): String { val usage = node.element!!.value