Hacked SliceNullnessAnalyzerBase from IDEA to take into account SliceUsage.isLeaf() and give reasonable and stable results for Kotlin

This commit is contained in:
Valentin Kipyatkov
2020-04-12 22:10:07 +03:00
parent e4d9801388
commit 83ef8def4d
7 changed files with 314 additions and 8 deletions
@@ -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;
}
}
@@ -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<SliceNode, NullAnalysisResult> 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<SliceNode, NullAnalysisResult> map) {
SliceRootNode root = oldRoot.copy();
assert oldRoot.getCachedChildren().size() == 1;
SliceNode oldRootStart = oldRoot.getCachedChildren().get(0);
root.setChanged();
root.targetEqualUsages.clear();
List<HackedSliceLeafValueClassNode> 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<SliceNode, NullAnalysisResult> map,
SliceRootNode root,
SliceNode oldRootStart,
String nodeName,
final int group) {
Collection<PsiElement> groupedByValue = result.groupedByValue[group];
if (groupedByValue.isEmpty()) {
return null;
}
HackedSliceLeafValueClassNode
valueRoot = new HackedSliceLeafValueClassNode(root.getProject(), root, nodeName);
Set<PsiElement> 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<NullAnalysisResult> leafExpressions = Ref.create(null);
final Map<SliceNode, NullAnalysisResult> 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<SliceNode, NullAnalysisResult> createMap() {
return FactoryMap.createMap(k->new NullAnalysisResult(), ContainerUtil::newIdentityTroveMap);
}
private static NullAnalysisResult node(@NotNull SliceNode node, @NotNull Map<SliceNode, NullAnalysisResult> nulls) {
return nulls.get(node);
}
private static Collection<PsiElement> group(@NotNull SliceNode node, @NotNull Map<SliceNode, NullAnalysisResult> nulls, int group) {
return nulls.get(node).groupedByValue[group];
}
@NotNull
public NullAnalysisResult calcNullableLeaves(@NotNull final SliceNode root,
@NotNull AbstractTreeStructure treeStructure,
@NotNull final Map<SliceNode, NullAnalysisResult> map) {
final SliceLeafAnalyzer.SliceNodeGuide guide = new SliceLeafAnalyzer.SliceNodeGuide(treeStructure);
WalkingState<SliceNode> walkingState = new WalkingState<SliceNode>(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<? extends AbstractTreeNode> 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<PsiElement>[] groupedByValue = new Collection[] {new THashSet<PsiElement>(),new THashSet<PsiElement>(),new THashSet<PsiElement>()};
public void clear() {
for (Collection<PsiElement> elements : groupedByValue) {
elements.clear();
}
}
private void add(NullAnalysisResult duplicate) {
for (int i = 0; i < groupedByValue.length; i++) {
Collection<PsiElement> elements = groupedByValue[i];
Collection<PsiElement> other = duplicate.groupedByValue[i];
elements.addAll(other);
}
}
}
}
@@ -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)
@@ -1,11 +1,28 @@
[NotNull Values]
7 with(123) <bold>{</bold>
7 with(<bold>123</bold>) {
3 fun <bold>Any</bold>.extensionFun() {
7 [LAMBDA CALLS] with(123) <bold>{</bold>
19 [LAMBDA CALLS] withNoInline(1) <bold>{</bold>
44 (INLINE CALL with) [LAMBDA CALLS] inline fun <T, R> with(receiver: T, <bold>block: T.() -> R</bold>): R {
45 (INLINE CALL with) val result = <bold>receiver</bold>.block()
44 (INLINE CALL with) inline fun <T, R> with(<bold>receiver: T</bold>, block: T.() -> R): R {
7 with(<bold>123</bold>) {
12 <bold>this</bold>.extensionFun()
3 fun <bold>Any</bold>.extensionFun() {
12 <bold>this</bold>.extensionFun()
19 withNoInline(<bold>1</bold>) {
3 fun <bold>Any</bold>.extensionFun() {
19 [LAMBDA CALLS] withNoInline(1) <bold>{</bold>
53 [LAMBDA CALLS] fun <T, R> withNoInline(receiver: T, <bold>block: T.() -> R</bold>): R {
54 val result = <bold>receiver</bold>.block()
53 fun <T, R> withNoInline(<bold>receiver: T</bold>, block: T.() -> R): R {
19 withNoInline(<bold>1</bold>) {
23 withNoInline(<bold>2</bold>) {
3 fun <bold>Any</bold>.extensionFun() {
19 [LAMBDA CALLS] withNoInline(1) <bold>{</bold>
53 [LAMBDA CALLS] fun <T, R> withNoInline(receiver: T, <bold>block: T.() -> R</bold>): R {
54 val result = <bold>receiver</bold>.block()
53 fun <T, R> withNoInline(<bold>receiver: T</bold>, block: T.() -> R): R {
23 withNoInline(<bold>2</bold>) {
36 <bold>it</bold>.extensionFun()
3 fun <bold>Any</bold>.extensionFun() {
28 <bold>it</bold>.extensionFun()
@@ -5,9 +5,13 @@
14 <bold>1</bold>.extensionFun()
8 fun <bold>Any</bold>.extensionFun() {
14 <bold>1</bold>.extensionFun()
18 with(123) <bold>{</bold>
18 with(<bold>123</bold>) {
8 fun <bold>Any</bold>.extensionFun() {
18 [LAMBDA CALLS] with(123) <bold>{</bold>
27 (INLINE CALL with) [LAMBDA CALLS] inline fun <T, R> with(receiver: T, <bold>block: T.() -> R</bold>): R {
28 (INLINE CALL with) return <bold>receiver</bold>.block()
27 (INLINE CALL with) inline fun <T, R> with(<bold>receiver: T</bold>, block: T.() -> R): R {
18 with(<bold>123</bold>) {
24 <bold>"A"</bold>.foo()
8 fun <bold>Any</bold>.extensionFun() {
11 fun <bold>String</bold>.foo() {
@@ -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 {
@@ -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