Dataflow hint display initial implementation (IDEA-188211)

This commit is contained in:
Tagir Valeev
2018-03-14 13:19:49 +07:00
parent 9951bd829c
commit d62240fb5f
9 changed files with 338 additions and 26 deletions
@@ -61,6 +61,18 @@ public class CommonDataflow {
DfaFactMap map = this.myFacts.get(expression);
return map == null ? null : map.get(type);
}
/**
* Returns the fact map which represents all the facts known for given expression
*
* @param expression an expression to check
* @return the fact map which represents all the facts known for given expression; empty map if the expression was
* analyzed, but no particular facts were inferred; null if the expression was not analyzed.
*/
@Nullable
public DfaFactMap getAllFacts(PsiExpression expression) {
return this.myFacts.get(expression);
}
}
@Contract("null -> null")
@@ -22,7 +22,6 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Objects;
/**
* An immutable collection of facts which are known for some value. Each fact is identified by {@link DfaFactType} and fact value.
@@ -169,9 +168,16 @@ public final class DfaFactMap {
@SuppressWarnings("unchecked")
@Override
public String toString() {
return StreamEx.of(myMap.getKeys())
.map(key -> ((DfaFactType<Object>)key).toString(Objects.requireNonNull(myMap.get(key))))
.joining(", ");
return facts(DfaFactType::toString).joining(", ");
}
@SuppressWarnings("unchecked")
public <R> StreamEx<R> facts(FactMapper<R> mapper) {
return StreamEx.of(myMap.getKeys()).map(f -> {
DfaFactType<Object> key = (DfaFactType<Object>)f;
Object value = myMap.get(f);
return mapper.apply(key, value);
});
}
/**
@@ -188,4 +194,9 @@ public final class DfaFactMap {
private static <T> DfaFactMap updateMap(DfaFactMap map, DfaFactType<T> factType, DfaVariableValue value) {
return map.with(factType, factType.calcFromVariable(value));
}
@FunctionalInterface
public interface FactMapper<R> {
<T> R apply(DfaFactType<T> factType, T factValue);
}
}
@@ -19,6 +19,8 @@ import com.intellij.codeInspection.dataFlow.rangeSet.LongRangeSet;
import com.intellij.codeInspection.dataFlow.value.*;
import com.intellij.openapi.util.Key;
import com.intellij.psi.PsiModifierListOwner;
import com.intellij.psi.PsiPrimitiveType;
import com.intellij.psi.PsiType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -38,12 +40,20 @@ public abstract class DfaFactType<T> extends Key<T> {
/**
* This fact specifies whether the value can be null. The absence of the fact means that the nullability is unknown.
*/
public static final DfaFactType<Boolean> CAN_BE_NULL = new DfaFactType<Boolean>("Can be null") {
public static final DfaFactType<Boolean> CAN_BE_NULL = new DfaFactType<Boolean>("Nullability") {
@NotNull
@Override
String toString(@NotNull Boolean fact) {
public String toString(@NotNull Boolean fact) {
return fact ? "Nullable" : "NotNull";
}
@NotNull
@Override
public String getPresentationText(Boolean fact, PsiType type) {
if (type instanceof PsiPrimitiveType) return "";
return super.getPresentationText(fact, type);
}
@Nullable
@Override
Boolean intersectFacts(@NotNull Boolean left, @NotNull Boolean right) {
@@ -95,8 +105,9 @@ public abstract class DfaFactType<T> extends Key<T> {
*/
public static final DfaFactType<Boolean> OPTIONAL_PRESENCE = new DfaFactType<Boolean>("Optional presense") {
@NotNull
@Override
String toString(@NotNull Boolean fact) {
public String toString(@NotNull Boolean fact) {
return fact ? "present Optional" : "absent Optional";
}
};
@@ -152,12 +163,20 @@ public abstract class DfaFactType<T> extends Key<T> {
LongRangeSet intersection = left.intersect(right);
return intersection.isEmpty() ? null : intersection;
}
@NotNull
@Override
public String getPresentationText(LongRangeSet fact, PsiType type) {
LongRangeSet fromType = LongRangeSet.fromType(type);
if(fact.equals(fromType)) return "";
return fact.toString();
}
};
/**
* This fact represents a set of possible types of this value
* {@link TypeConstraint#EMPTY} value is equivalent to absent fact (not constrained)
*/
public static final DfaFactType<TypeConstraint> TYPE_CONSTRAINT = new DfaFactType<TypeConstraint>("Type") {
public static final DfaFactType<TypeConstraint> TYPE_CONSTRAINT = new DfaFactType<TypeConstraint>("Type constraints") {
@Override
boolean isSuper(@Nullable TypeConstraint superFact, @Nullable TypeConstraint subFact) {
return superFact == null || (subFact != null && superFact.isSuperStateOf(subFact));
@@ -187,14 +206,27 @@ public abstract class DfaFactType<T> extends Key<T> {
TypeConstraint unionFacts(@NotNull TypeConstraint left, @NotNull TypeConstraint right) {
return left.union(right);
}
@NotNull
@Override
public String getPresentationText(TypeConstraint fact, PsiType type) {
return fact.getPresentationText(type);
}
};
private final String myName;
private DfaFactType(String name) {
super("DfaFactType: " + name);
myName = name;
// Thread-safe as all DfaFactType instances are created only from DfaFactType class static initializer
ourFactTypes.add(this);
}
public String getName() {
return myName;
}
@Nullable
T fromDfaValue(DfaValue value) {
return value instanceof DfaFactMapValue ? ((DfaFactMapValue)value).get(this) : null;
@@ -238,10 +270,28 @@ public abstract class DfaFactType<T> extends Key<T> {
return left.equals(right) ? left : null;
}
String toString(@NotNull T fact) {
/**
* Produces a short suitable for debug output fact representation
* @param fact a fact to represent
* @return a string representation of the fact
*/
@NotNull
public String toString(@NotNull T fact) {
return fact.toString();
}
/**
* Produces a user-friendly presentation of the fact based on the fact itself and the type of the expression
* @param fact a fact to represent
* @param type an expression type
* @return a user-friendly string representation of the fact; empty string if the fact adds nothing to the expression type
* (e.g. fact is Range {0..65535} and type is 'char').
*/
@NotNull
public String getPresentationText(T fact, PsiType type) {
return toString(fact);
}
static List<DfaFactType<?>> getTypes() {
return Collections.unmodifiableList(ourFactTypes);
}
@@ -16,10 +16,7 @@
package com.intellij.codeInspection.dataFlow;
import com.intellij.codeInspection.dataFlow.value.DfaPsiType;
import com.intellij.psi.LambdaUtil;
import com.intellij.psi.PsiIntersectionType;
import com.intellij.psi.PsiPrimitiveType;
import com.intellij.psi.PsiType;
import com.intellij.psi.*;
import com.intellij.util.containers.ContainerUtil;
import one.util.streamex.EntryStream;
import one.util.streamex.StreamEx;
@@ -48,6 +45,21 @@ public final class TypeConstraint {
myNotInstanceofValues = notInstanceofValues;
}
@NotNull
public String getPresentationText(PsiType type) {
Set<DfaPsiType> instanceOfTypes = myInstanceofValues;
if (type instanceof PsiClassType) {
instanceOfTypes = StreamEx.of(instanceOfTypes)
.removeBy(DfaPsiType::getPsiType, ((PsiClassType)type).rawType())
.toSet();
}
return EntryStream.of("instanceof ", instanceOfTypes,
"not instanceof ", myNotInstanceofValues)
.removeValues(Set::isEmpty)
.mapKeyValue((prefix, set) -> StreamEx.of(set).map(DfaPsiType::toString).sorted().joining(", ", prefix, ""))
.joining("\n");
}
private static TypeConstraint create(@NotNull Set<DfaPsiType> instanceofValues, @NotNull Set<DfaPsiType> notInstanceofValues) {
if (instanceofValues.isEmpty() && notInstanceofValues.isEmpty()) {
return EMPTY;
@@ -222,9 +234,9 @@ public final class TypeConstraint {
public String toString() {
return EntryStream.of("instanceof ", myInstanceofValues,
"not instanceof ", myNotInstanceofValues)
.removeValues(Set::isEmpty)
.mapKeyValue((prefix, set) -> StreamEx.of(set).joining(",", prefix, ""))
.joining(" ");
.removeValues(Set::isEmpty)
.mapKeyValue((prefix, set) -> StreamEx.of(set).joining(", ", prefix, ""))
.joining(" ");
}
@Nullable
@@ -289,7 +289,7 @@ public abstract class LongRangeSet {
*/
private static ThreeState[] bits(long from, long to) {
ThreeState[] bits = new ThreeState[Long.SIZE];
Arrays.setAll(bits, idx -> ThreeState.NO);
Arrays.fill(bits, ThreeState.NO);
while (true) {
int fromBit = Long.numberOfLeadingZeros(from);
int toBit = Long.numberOfLeadingZeros(to);
@@ -308,6 +308,15 @@ public abstract class LongRangeSet {
return bits;
}
private static String formatNumber(long value) {
if (value == Long.MAX_VALUE) return "Long.MAX_VALUE";
if (value == Long.MAX_VALUE - 1) return "Long.MAX_VALUE-1";
if (value == Long.MIN_VALUE) return "Long.MIN_VALUE";
if (value == Integer.MAX_VALUE) return "Integer.MAX_VALUE";
if (value == Integer.MAX_VALUE - 1) return "Integer.MAX_VALUE-1";
if (value == Integer.MIN_VALUE) return "Integer.MIN_VALUE";
return String.valueOf(value);
}
/**
* Returns a stream of all values from this range. Be careful: could be huge
*
@@ -384,7 +393,7 @@ public abstract class LongRangeSet {
abstract long[] asRanges();
static String toString(long from, long to) {
return from == to ? String.valueOf(from) : from + (to - from == 1 ? ", " : "..") + to;
return formatNumber(from) + (from == to ? "" : (to - from == 1 ? ", " : "..") + formatNumber(to));
}
static long minValue(boolean isLong) {
@@ -673,7 +682,7 @@ public abstract class LongRangeSet {
@Override
public String toString() {
return "{" + myValue + "}";
return "{" + formatNumber(myValue) + "}";
}
}
@@ -15,9 +15,13 @@
*/
package com.intellij.codeInsight.hint;
import com.intellij.codeInspection.dataFlow.CommonDataflow;
import com.intellij.codeInspection.dataFlow.DfaFactMap;
import com.intellij.codeInspection.dataFlow.DfaFactType;
import com.intellij.lang.ExpressionTypeProvider;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiUtil;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@@ -57,4 +61,38 @@ public class JavaTypeProvider extends ExpressionTypeProvider<PsiExpression> {
}
return true;
}
@Override
public boolean hasAdvancedInformation() {
return true;
}
@NotNull
@Override
public String getAdvancedInformationHint(@NotNull PsiExpression expression) {
expression = PsiUtil.skipParenthesizedExprDown(expression);
if (expression == null) return "<unknown>";
CommonDataflow.DataflowResult result = CommonDataflow.getDataflowResult(expression);
DfaFactMap map = result == null ? null : result.getAllFacts(expression);
String basicTypeEscaped = getInformationHint(expression);
PsiType type = expression.getType();
String advancedTypeInfo = map == null ? "" : map.facts(new DfaFactMap.FactMapper<String>() {
@Override
public <T> String apply(DfaFactType<T> factType, T value) {
return formatFact(factType, value, type);
}
}).joining();
return advancedTypeInfo.isEmpty()
? basicTypeEscaped
: "<table>" + makeHtmlRow("Type", basicTypeEscaped) + advancedTypeInfo + "</table>";
}
private static <T> String formatFact(DfaFactType<T> factType, T value, PsiType type) {
String presentationText = factType.getPresentationText(value, type);
return presentationText.isEmpty() ? "" : makeHtmlRow(factType.getName(), StringUtil.escapeXml(presentationText));
}
private static String makeHtmlRow(String titleText, String contentHtml) {
return "<tr><td align='right' valign='top'><strong>" + StringUtil.escapeXml(titleText) + ":</strong></td><td>" + contentHtml + "</td></tr>";
}
}
@@ -0,0 +1,97 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hint;
import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.psi.*;
import com.intellij.testFramework.LightCodeInsightTestCase;
import org.intellij.lang.annotations.Language;
public class JavaTypeProviderTest extends LightCodeInsightTestCase {
public void testRangeHint() {
doTest(" void test(int x) {\n" +
" x = Math.abs(x);\n" +
" /*expression*/x\n" +
" }", "int",
"<table>" +
"<tr><td align='right' valign='top'><strong>Type:</strong></td>" +
"<td>int</td></tr>" +
"<tr><td align='right' valign='top'><strong>Range:</strong></td>" +
"<td>{Integer.MIN_VALUE, 0..Integer.MAX_VALUE}</td></tr>" +
"</table>");
}
public void testOptionalHint() {
doTest(" void test(java.util.Optional<String> t) {\n" +
" if(t.isPresent()) {\n" +
" /*expression*/t\n" +
" }\n" +
" }", "Optional&lt;String&gt;",
"<table>" +
"<tr><td align='right' valign='top'><strong>Type:</strong></td>" +
"<td>Optional&lt;String&gt;</td></tr>" +
"<tr><td align='right' valign='top'><strong>Nullability:</strong></td>" +
"<td>NotNull</td></tr>" +
"<tr><td align='right' valign='top'><strong>Optional presense:</strong></td>" +
"<td>present Optional</td></tr>" +
"</table>");
}
public void testTypeConstraint() {
doTest(" void x(Object a) {\n" +
" if(a instanceof String || a instanceof Number) {\n" +
" \n" +
" } else if(a instanceof CharSequence){\n" +
" /*expression*/a\n" +
" } else {\n" +
" a\n" +
" }\n" +
" }", "Object",
"<table>" +
"<tr><td align='right' valign='top'><strong>Type:</strong></td>" +
"<td>Object</td></tr>" +
"<tr><td align='right' valign='top'><strong>Nullability:</strong></td>" +
"<td>NotNull</td></tr>" +
"<tr><td align='right' valign='top'><strong>Type constraints:</strong></td>" +
"<td>instanceof CharSequence\n" +
"not instanceof Number, String</td></tr></table>");
}
public void testTypeConstraint2() {
doTest(" void x(Object a) {\n" +
" if(a instanceof String || a instanceof Number) {\n" +
" \n" +
" } else if(a instanceof CharSequence){\n" +
" \n" +
" } else {\n" +
" /*expression*/a\n" +
" }\n" +
" }\n", "Object",
"<table>" +
"<tr><td align='right' valign='top'><strong>Type:</strong></td>" +
"<td>Object</td></tr>" +
"<tr><td align='right' valign='top'><strong>Type constraints:</strong></td>" +
"<td>not instanceof CharSequence, Number</td></tr></table>");
}
private static void doTest(@Language(value = "JAVA", prefix = "@SuppressWarnings(\"all\")class X{", suffix = "}") String method,
@Language("HTML") String expectedHint,
@Language("HTML") String expectedAdvancedHint) {
PsiFile file =
PsiFileFactory.getInstance(getProject()).createFileFromText("X.java", JavaFileType.INSTANCE, "class X{" + method + "}");
PsiComment comment = SyntaxTraverser.psiTraverser(file).filter(PsiComment.class)
.filter(c -> c.textMatches("/*expression*/"))
.first();
assertNotNull("/*expression*/ comment not found", comment);
PsiElement sibling = comment.getNextSibling();
assertNotNull(sibling);
while (!(sibling instanceof PsiExpression) && sibling.getFirstChild() != null) {
sibling = sibling.getFirstChild();
}
assertTrue("Expression not found at: " + sibling.getText(), sibling instanceof PsiExpression);
PsiExpression expression = (PsiExpression)sibling;
JavaTypeProvider provider = new JavaTypeProvider();
assertEquals(expectedHint, provider.getInformationHint(expression));
assertTrue(provider.hasAdvancedInformation());
assertEquals(expectedAdvancedHint, provider.getAdvancedInformationHint(expression));
}
}
@@ -44,4 +44,26 @@ public abstract class ExpressionTypeProvider<T extends PsiElement> {
*/
@NotNull
public abstract List<T> getExpressionsAt(@NotNull PsiElement elementAt);
/**
* @return true if this type provider can provide more useful information (e.g. value range, nullability, etc.)
* on elements via {@link #getAdvancedInformationHint(PsiElement)}.
*/
public boolean hasAdvancedInformation() {
return false;
}
/**
* Returns HTML string containing advanced type information hint (e.g. nullability, values range, etc.)
*
* @param element an element to provide information about
* @return an advanced information hint. Should return the same result as {@link #getInformationHint(PsiElement)}
* if no additional information is available for given element.
* @throws UnsupportedOperationException if this provider does not provide any advanced information
* (in this case {@link #hasAdvancedInformation()} method must return false).
*/
@NotNull
public String getAdvancedInformationHint(@NotNull T element) {
throw new UnsupportedOperationException();
}
}
@@ -37,8 +37,11 @@ import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.JBIterable;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.awt.event.HierarchyEvent;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public class ShowExpressionTypeHandler implements CodeInsightActionHandler {
@@ -64,15 +67,12 @@ public class ShowExpressionTypeHandler implements CodeInsightActionHandler {
Pass<PsiElement> callback = new Pass<PsiElement>() {
@Override
public void pass(@NotNull PsiElement expression) {
ExpressionTypeProvider provider = ObjectUtils.assertNotNull(map.get(expression));
//noinspection unchecked
ExpressionTypeProvider<PsiElement> provider = ObjectUtils.assertNotNull(map.get(expression));
final String informationHint = provider.getInformationHint(expression);
TextRange range = expression.getTextRange();
editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset());
ApplicationManager.getApplication().invokeLater(() -> {
HintManager.getInstance().setRequestFocusForNextHint(myRequestFocus);
HintManager.getInstance().showInformationHint(editor, informationHint);
});
displayHint(new DisplayedTypeInfo(expression, provider, editor), informationHint);
}
};
if (map.isEmpty()) {
@@ -82,16 +82,33 @@ public class ShowExpressionTypeHandler implements CodeInsightActionHandler {
});
}
else if (map.size() == 1) {
callback.pass(ObjectUtils.assertNotNull(ContainerUtil.getFirstItem(map.keySet())));
Map.Entry<PsiElement, ExpressionTypeProvider> entry = map.entrySet().iterator().next();
PsiElement expression = entry.getKey();
ExpressionTypeProvider provider = entry.getValue();
DisplayedTypeInfo typeInfo = new DisplayedTypeInfo(expression, provider, editor);
if (typeInfo.isRepeating() && provider.hasAdvancedInformation()) {
//noinspection unchecked
String informationHint = provider.getAdvancedInformationHint(expression);
displayHint(typeInfo, informationHint);
} else {
callback.pass(expression);
}
}
else {
IntroduceTargetChooser.showChooser(
editor, ContainerUtil.newArrayList(map.keySet()), callback,
expression -> expression.getText()
PsiElement::getText
);
}
}
private void displayHint(@NotNull DisplayedTypeInfo typeInfo, String informationHint) {
ApplicationManager.getApplication().invokeLater(() -> {
HintManager.getInstance().setRequestFocusForNextHint(myRequestFocus);
typeInfo.showHint(informationHint);
});
}
@NotNull
public Map<PsiElement, ExpressionTypeProvider> getExpressions(@NotNull PsiFile file,
@NotNull Editor editor) {
@@ -130,5 +147,49 @@ public class ShowExpressionTypeHandler implements CodeInsightActionHandler {
language -> DumbService.getInstance(project).filterByDumbAwareness(LanguageExpressionTypes.INSTANCE.allForLanguage(language))).addAllTo(ContainerUtil.newLinkedHashSet());
}
static final class DisplayedTypeInfo {
private static volatile DisplayedTypeInfo ourCurrentInstance;
final @NotNull PsiElement myElement;
final @NotNull ExpressionTypeProvider<?> myProvider;
final @NotNull Editor myEditor;
DisplayedTypeInfo(@NotNull PsiElement element, @NotNull ExpressionTypeProvider<?> provider, @NotNull Editor editor) {
myElement = element;
myProvider = provider;
myEditor = editor;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DisplayedTypeInfo info = (DisplayedTypeInfo)o;
return Objects.equals(myElement, info.myElement) &&
Objects.equals(myProvider, info.myProvider) &&
Objects.equals(myEditor, info.myEditor);
}
/**
* @return true if the same hint (i.e. on the same PsiElement, with the same provider, in the same editor) is displayed currently.
*/
boolean isRepeating() {
return this.equals(ourCurrentInstance);
}
void showHint(String informationHint) {
JComponent label = HintUtil.createInformationLabel(informationHint);
setInstance(this);
label.addHierarchyListener(e -> {
if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0 && !label.isShowing()) {
setInstance(null);
}
});
HintManager.getInstance().showInformationHint(myEditor, label);
}
private static void setInstance(DisplayedTypeInfo typeInfo) {
ourCurrentInstance = typeInfo;
}
}
}