Commit 72a4fb13 authored by Tagir Valeev's avatar Tagir Valeev
Browse files

Dataflow warning explanation (IDEA-209947) first draft

Only for constant expressions now (condition is always true/false/null); explain only in some cases (see TODO list in TrackingRunner.java)
parent 1dc7f6b8
Branches unavailable Tags unavailable
Showing with 1282 additions and 7 deletions
+1282 -7
......@@ -236,7 +236,7 @@ public class CommonDataflow {
if (block == null) return null;
DataFlowRunner runner = new DataFlowRunner(false, block);
CommonDataflowVisitor visitor = new CommonDataflowVisitor();
RunnerResult result = runner.analyzeMethodRecursively(block, visitor);
RunnerResult result = runner.analyzeMethodRecursively(block, visitor, false);
if (result != RunnerResult.OK) return null;
if (!(block instanceof PsiClass)) return visitor.myResult;
DataflowResult dfr = visitor.myResult.copy();
......@@ -251,7 +251,7 @@ public class CommonDataflow {
} else {
initialStates = StreamEx.of(states).map(DfaMemoryState::createCopy).toList();
}
if(runner.analyzeBlockRecursively(body, initialStates, visitor) == RunnerResult.OK) {
if(runner.analyzeBlockRecursively(body, initialStates, visitor, false) == RunnerResult.OK) {
dfr = visitor.myResult.copy();
} else {
visitor.myResult = dfr;
......
......@@ -408,6 +408,9 @@ public class DataFlowInspectionBase extends AbstractBaseJavaLocalInspectionTool
InspectionsBundle.message("inspection.data.flow.turn.off.true.asserts.quickfix"), true));
}
}
if (reporter.isOnTheFly()) {
ContainerUtil.addIfNotNull(fixes, createExplainFix(ref, new TrackingRunner.ValueDfaProblemType(value)));
}
String valueText;
ProblemHighlightType type;
......@@ -751,12 +754,21 @@ public class DataFlowInspectionBase extends AbstractBaseJavaLocalInspectionTool
}
ContainerUtil.addIfNotNull(fixes, createReplaceWithNullCheckFix(psiAnchor, evaluatesToTrue));
}
if (reporter.isOnTheFly() && psiAnchor instanceof PsiExpression) {
ContainerUtil.addIfNotNull(fixes, createExplainFix(
(PsiExpression)psiAnchor, new TrackingRunner.ValueDfaProblemType(evaluatesToTrue)));
}
String message = InspectionsBundle.message(isAtRHSOfBooleanAnd(psiAnchor) ?
"dataflow.message.constant.condition.when.reached" :
"dataflow.message.constant.condition", Boolean.toString(evaluatesToTrue));
reporter.registerProblem(psiAnchor, message, fixes.toArray(LocalQuickFix.EMPTY_ARRAY));
}
@Nullable
protected LocalQuickFix createExplainFix(PsiExpression anchor, TrackingRunner.DfaProblemType problemType) {
return null;
}
private static boolean isCoveredBySurroundingFix(PsiElement anchor, boolean evaluatesToTrue) {
PsiElement parent = PsiUtil.skipParenthesizedExprUp(anchor.getParent());
if (parent instanceof PsiPolyadicExpression) {
......
......@@ -363,21 +363,22 @@ public class DataFlowRunner {
LOG.error(new RuntimeExceptionWithAttachments(e, attachments));
}
public RunnerResult analyzeMethodRecursively(@NotNull PsiElement block, StandardInstructionVisitor visitor) {
public RunnerResult analyzeMethodRecursively(@NotNull PsiElement block, StandardInstructionVisitor visitor, boolean ignoreAssertions) {
Collection<DfaMemoryState> states = createInitialStates(block, visitor, false);
if (states == null) return RunnerResult.NOT_APPLICABLE;
return analyzeBlockRecursively(block, states, visitor);
return analyzeBlockRecursively(block, states, visitor, ignoreAssertions);
}
public RunnerResult analyzeBlockRecursively(@NotNull PsiElement block,
Collection<? extends DfaMemoryState> states,
StandardInstructionVisitor visitor) {
RunnerResult result = analyzeMethod(block, visitor, false, states);
StandardInstructionVisitor visitor,
boolean ignoreAssertions) {
RunnerResult result = analyzeMethod(block, visitor, ignoreAssertions, states);
if (result != RunnerResult.OK) return result;
Ref<RunnerResult> ref = Ref.create(RunnerResult.OK);
forNestedClosures((closure, nestedStates) -> {
RunnerResult res = analyzeBlockRecursively(closure, nestedStates, visitor);
RunnerResult res = analyzeBlockRecursively(closure, nestedStates, visitor, ignoreAssertions);
if (res != RunnerResult.OK) {
ref.set(res);
}
......
......@@ -1627,6 +1627,15 @@ public class DfaMemoryStateImpl implements DfaMemoryState {
mergeStacks(other);
myCachedHash = null;
myCachedNonTrivialEqClasses = null;
afterMerge(other);
}
/**
* Custom logic to be implemented by subclasses
* @param other
*/
protected void afterMerge(DfaMemoryStateImpl other) {
}
private void mergeStacks(DfaMemoryStateImpl other) {
......
......@@ -564,6 +564,9 @@ class StateMerger {
for (Map.Entry<DfaMemoryStateImpl, Collection<DfaMemoryStateImpl>> entry : strippedToOriginals.entrySet()) {
Collection<DfaMemoryStateImpl> merged = entry.getValue();
if (merged.size() > 1) {
for (DfaMemoryStateImpl state : merged) {
entry.getKey().afterMerge(state);
}
myRemovedStates.addAll(merged);
myMerged.add(entry.getKey());
}
......
// Copyright 2000-2019 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.codeInspection.dataFlow;
import com.intellij.codeInspection.dataFlow.instructions.ConditionalGotoInstruction;
import com.intellij.codeInspection.dataFlow.instructions.ExpressionPushingInstruction;
import com.intellij.codeInspection.dataFlow.instructions.Instruction;
import com.intellij.codeInspection.dataFlow.value.*;
import com.intellij.codeInspection.dataFlow.value.DfaRelationValue.RelationType;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.PsiExpression;
import com.intellij.util.ObjectUtils;
import one.util.streamex.EntryStream;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Predicate;
public class TrackingDfaMemoryState extends DfaMemoryStateImpl {
private final List<MemoryStateChange> myHistory;
protected TrackingDfaMemoryState(DfaValueFactory factory) {
super(factory);
myHistory = new ArrayList<>(1);
myHistory.add(null);
}
protected TrackingDfaMemoryState(TrackingDfaMemoryState toCopy) {
super(toCopy);
myHistory = new ArrayList<>(toCopy.myHistory);
}
@NotNull
@Override
public TrackingDfaMemoryState createCopy() {
return new TrackingDfaMemoryState(this);
}
@Override
protected void afterMerge(DfaMemoryStateImpl other) {
super.afterMerge(other);
assert other instanceof TrackingDfaMemoryState;
for (MemoryStateChange change : ((TrackingDfaMemoryState)other).myHistory) {
if (!tryMerge(change)) {
myHistory.add(change);
}
}
}
private boolean tryMerge(MemoryStateChange change) {
for (ListIterator<MemoryStateChange> iterator = myHistory.listIterator(); iterator.hasNext(); ) {
MemoryStateChange myChange = iterator.next();
MemoryStateChange merge = myChange.tryMerge(change);
if (merge != null) {
iterator.set(merge);
return true;
}
}
return false;
}
private Map<DfaVariableValue, Set<Relation>> getRelations() {
Map<DfaVariableValue, Set<Relation>> result = new HashMap<>();
for (EqClass eqClass : getNonTrivialEqClasses()) {
DfaConstValue constant = eqClass.findConstant();
List<DfaVariableValue> vars = eqClass.getVariables(false);
for (DfaVariableValue var : vars) {
Set<Relation> set = result.computeIfAbsent(var, k -> new HashSet<>());
if (constant != null) {
set.add(new Relation(RelationType.EQ, constant));
}
for (DfaVariableValue eqVar : vars) {
if (eqVar != var) {
set.add(new Relation(RelationType.EQ, eqVar));
}
}
}
}
for (DistinctPairSet.DistinctPair classPair : getDistinctClassPairs()) {
EqClass first = classPair.getFirst();
EqClass second = classPair.getSecond();
RelationType plain = classPair.isOrdered() ? RelationType.LT : RelationType.NE;
RelationType flipped = Objects.requireNonNull(plain.getFlipped());
List<DfaVariableValue> firstVars = first.getVariables(false);
List<DfaVariableValue> secondVars = second.getVariables(false);
for (DfaVariableValue var1 : firstVars) {
for (DfaVariableValue var2 : secondVars) {
result.computeIfAbsent(var1, k -> new HashSet<>()).add(new Relation(plain, var2));
result.computeIfAbsent(var2, k -> new HashSet<>()).add(new Relation(flipped, var1));
}
}
DfaConstValue firstConst = first.findConstant();
if (firstConst != null) {
for (DfaVariableValue var2 : secondVars) {
result.computeIfAbsent(var2, k -> new HashSet<>()).add(new Relation(flipped, firstConst));
}
}
DfaConstValue secondConst = second.findConstant();
if (secondConst != null) {
for (DfaVariableValue var1 : firstVars) {
result.computeIfAbsent(var1, k -> new HashSet<>()).add(new Relation(plain, secondConst));
}
}
}
return result;
}
void recordChange(Instruction instruction, TrackingDfaMemoryState previous) {
Map<DfaVariableValue, Change> result = new HashMap<>();
Set<DfaVariableValue> varsToCheck = new HashSet<>();
previous.forVariableStates((value, state) -> varsToCheck.add(value));
forVariableStates((value, state) -> varsToCheck.add(value));
for (DfaVariableValue value : varsToCheck) {
DfaFactMap newMap = getVariableState(value).myFactMap;
DfaFactMap oldMap = previous.getVariableState(value).myFactMap;
if (!newMap.equals(oldMap)) {
DfaFactMap added = DfaFactMap.EMPTY;
DfaFactMap removed = DfaFactMap.EMPTY;
for (DfaFactType<?> type : DfaFactType.getTypes()) {
Object oldVal = oldMap.get(type);
Object newVal = newMap.get(type);
if (!Objects.equals(oldVal, newVal)) {
//noinspection unchecked
added = added.with((DfaFactType<Object>)type, newVal);
//noinspection unchecked
removed = removed.with((DfaFactType<Object>)type, oldVal);
}
}
result.put(value, new Change(Collections.emptySet(), Collections.emptySet(), removed, added));
}
}
Map<DfaVariableValue, Set<Relation>> oldRelations = previous.getRelations();
Map<DfaVariableValue, Set<Relation>> newRelations = getRelations();
varsToCheck.clear();
varsToCheck.addAll(oldRelations.keySet());
varsToCheck.addAll(newRelations.keySet());
for (DfaVariableValue value : varsToCheck) {
Set<Relation> oldValueRelations = oldRelations.getOrDefault(value, Collections.emptySet());
Set<Relation> newValueRelations = newRelations.getOrDefault(value, Collections.emptySet());
if (!oldValueRelations.equals(newValueRelations)) {
Set<Relation> added = new HashSet<>(newValueRelations);
added.removeAll(oldValueRelations);
Set<Relation> removed = new HashSet<>(oldValueRelations);
removed.removeAll(newValueRelations);
result.compute(
value, (v, change) -> change == null
? Change.create(removed, added, DfaFactMap.EMPTY, DfaFactMap.EMPTY)
: Change.create(removed, added, change.myRemovedFacts, change.myAddedFacts));
}
}
DfaValue value = isEmptyStack() ? DfaUnknownValue.getInstance() : peek();
myHistory.replaceAll(prev -> MemoryStateChange.create(prev, instruction, result, value));
}
List<MemoryStateChange> getHistory() {
return myHistory;
}
static class Relation {
final @NotNull RelationType myRelationType;
final @NotNull DfaValue myCounterpart;
Relation(@NotNull RelationType type, @NotNull DfaValue counterpart) {
myRelationType = type;
myCounterpart = counterpart;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Relation relation = (Relation)o;
return myRelationType == relation.myRelationType &&
myCounterpart.equals(relation.myCounterpart);
}
@Override
public int hashCode() {
return Objects.hash(myRelationType, myCounterpart);
}
@Override
public String toString() {
return myRelationType + " " + myCounterpart;
}
}
static class Change {
final Set<Relation> myRemovedRelations;
final Set<Relation> myAddedRelations;
final DfaFactMap myRemovedFacts;
final DfaFactMap myAddedFacts;
private Change(Set<Relation> removedRelations, Set<Relation> addedRelations, DfaFactMap removedFacts, DfaFactMap addedFacts) {
myRemovedRelations = removedRelations.isEmpty() ? Collections.emptySet() : removedRelations;
myAddedRelations = addedRelations.isEmpty() ? Collections.emptySet() : addedRelations;
myRemovedFacts = removedFacts;
myAddedFacts = addedFacts;
}
static Change create(Set<Relation> removedRelations, Set<Relation> addedRelations, DfaFactMap removedFacts, DfaFactMap addedFacts) {
if (removedRelations.isEmpty() && addedRelations.isEmpty() && removedFacts == DfaFactMap.EMPTY && addedFacts == DfaFactMap.EMPTY) {
return null;
}
return new Change(removedRelations, addedRelations, removedFacts, addedFacts);
}
@Override
public String toString() {
String removed = StreamEx.of(myRemovedRelations).map(Object::toString).append(myRemovedFacts.toString())
.without("").joining(", ");
String added = StreamEx.of(myAddedRelations).map(Object::toString).append(myAddedFacts.toString())
.without("").joining(", ");
return (removed.isEmpty() ? "" : "-{" + removed + "} ") + (added.isEmpty() ? "" : "+{" + added + "}");
}
}
static final class MemoryStateChange {
final @Nullable MemoryStateChange myPrevious;
final @NotNull Instruction myInstruction;
final @NotNull Map<DfaVariableValue, Change> myChanges;
final @NotNull DfaValue myTopOfStack;
private MemoryStateChange(@Nullable MemoryStateChange previous,
@NotNull Instruction instruction,
@NotNull Map<DfaVariableValue, Change> changes,
@NotNull DfaValue topOfStack) {
myPrevious = previous;
myInstruction = instruction;
myChanges = changes;
myTopOfStack = topOfStack;
}
@Contract("null -> null")
@Nullable
MemoryStateChange findExpressionPush(@Nullable PsiExpression expression) {
if (expression == null) return null;
return findChange(change -> change.getExpression() == expression);
}
MemoryStateChange findRelation(DfaVariableValue value, @NotNull Predicate<Relation> relationPredicate) {
return findChange(change -> {
Change varChange = change.myChanges.get(value);
return varChange != null && varChange.myAddedRelations.stream().anyMatch(relationPredicate);
});
}
@NotNull
<T> Pair<MemoryStateChange, T> findFact(DfaValue value, DfaFactType<T> type) {
if (value instanceof DfaVariableValue) {
for (MemoryStateChange change = this; change != null; change = change.myPrevious) {
Change varChange = change.myChanges.get(value);
if (varChange != null) {
T added = varChange.myAddedFacts.get(type);
if (added != null) {
return Pair.create(change, added);
}
if (varChange.myRemovedFacts.get(type) != null) {
return Pair.create(change, null);
}
}
}
return Pair.create(null, ((DfaVariableValue)value).getInherentFacts().get(type));
}
return Pair.create(null, type.fromDfaValue(value));
}
@Nullable
private MemoryStateChange findChange(@NotNull Predicate<MemoryStateChange> predicate) {
for (MemoryStateChange change = myPrevious; change != null; change = change.myPrevious) {
if (predicate.test(change)) {
return change;
}
}
return null;
}
@Nullable
PsiExpression getExpression() {
if (myInstruction instanceof ExpressionPushingInstruction &&
((ExpressionPushingInstruction)myInstruction).getExpressionRange() == null) {
return ((ExpressionPushingInstruction)myInstruction).getExpression();
}
if (myInstruction instanceof ConditionalGotoInstruction) {
return ObjectUtils.tryCast(((ConditionalGotoInstruction)myInstruction).getPsiAnchor(), PsiExpression.class);
}
return null;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MemoryStateChange change = (MemoryStateChange)o;
return myInstruction.equals(change.myInstruction) &&
myTopOfStack.equals(change.myTopOfStack) &&
myChanges.equals(change.myChanges) &&
Objects.equals(myPrevious, change.myPrevious);
}
@Override
public int hashCode() {
return Objects.hash(myPrevious, myInstruction, myChanges, myTopOfStack);
}
@Nullable
public MemoryStateChange tryMerge(MemoryStateChange change) {
MemoryStateChange[] thisFlat = flatten();
MemoryStateChange[] thatFlat = change.flatten();
MemoryStateChange result = null;
int thisIndex = 0, thatIndex = 0;
while (true) {
int curThis = thisIndex;
if (thisIndex == thisFlat.length && thatIndex == thatFlat.length) {
return result;
}
if (thisIndex == thisFlat.length || thatIndex == thatFlat.length) return null;
while (thisIndex < thisFlat.length) {
MemoryStateChange thisChange = thisFlat[thisIndex];
if (thisChange.myInstruction == thatFlat[thatIndex].myInstruction) break;
if (!thisChange.myChanges.isEmpty()) {
thisIndex = thisFlat.length;
break;
}
thisIndex++;
}
if (thisIndex == thisFlat.length) {
thisIndex = curThis;
while (thatIndex < thatFlat.length) {
MemoryStateChange thatChange = thatFlat[thatIndex];
if (thatChange.myInstruction == thisFlat[thisIndex].myInstruction) break;
if (!thatChange.myChanges.isEmpty()) return null;
thatIndex++;
}
if (thatIndex == thatFlat.length) return null;
}
MemoryStateChange thisChange = thisFlat[thisIndex];
MemoryStateChange thatChange = thatFlat[thatIndex];
if (thisChange == thatChange) {
result = thisChange;
} else {
assert thisChange.myInstruction == thatChange.myInstruction;
if (!thisChange.myChanges.equals(thatChange.myChanges)) return null;
result = create(result, thisChange.myInstruction, thisChange.myChanges, thisChange.myTopOfStack.unite(thatChange.myTopOfStack));
}
thisIndex++;
thatIndex++;
}
}
@Nullable
static MemoryStateChange create(@Nullable MemoryStateChange previous,
@NotNull Instruction instruction,
@NotNull Map<DfaVariableValue, Change> result,
@NotNull DfaValue value) {
if (result.isEmpty() && value == DfaUnknownValue.getInstance()) {
return previous;
}
return new MemoryStateChange(previous, instruction, result, value);
}
MemoryStateChange[] flatten() {
List<MemoryStateChange> changes = StreamEx.iterate(this, Objects::nonNull, change -> change.myPrevious).toList();
Collections.reverse(changes);
return changes.toArray(new MemoryStateChange[0]);
}
String dump() {
return StreamEx.of(flatten()).joining("\n");
}
@Override
public String toString() {
return myInstruction.getIndex() + " " + myInstruction + ": " + myTopOfStack +
(myChanges.isEmpty() ? "" :
"; Changes: " + EntryStream.of(myChanges).join(": ", "\n\t", "").joining());
}
}
}
......@@ -20,6 +20,7 @@ import com.intellij.codeInsight.daemon.impl.quickfix.DeleteSideEffectsAwareFix;
import com.intellij.codeInsight.daemon.impl.quickfix.SimplifyBooleanExpressionFix;
import com.intellij.codeInsight.daemon.impl.quickfix.UnwrapSwitchLabelFix;
import com.intellij.codeInspection.*;
import com.intellij.codeInspection.dataFlow.fix.FindDfaProblemCauseFix;
import com.intellij.codeInspection.dataFlow.fix.SurroundWithRequireNonNullFix;
import com.intellij.codeInspection.nullable.NullableStuffInspection;
import com.intellij.pom.java.LanguageLevel;
......@@ -76,6 +77,12 @@ public class DataFlowInspection extends DataFlowInspectionBase {
return WrapWithMutableCollectionFix.createFix(violation, onTheFly);
}
@Nullable
@Override
protected LocalQuickFix createExplainFix(PsiExpression anchor, TrackingRunner.DfaProblemType problemType) {
return new FindDfaProblemCauseFix(TREAT_UNKNOWN_MEMBERS_AS_NULLABLE, IGNORE_ASSERT_STATEMENTS, anchor, problemType);
}
@Nullable
@Override
protected LocalQuickFix createUnwrapSwitchLabelFix() {
......
// Copyright 2000-2019 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.codeInspection.dataFlow.fix;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.codeInsight.hint.HintManagerImpl;
import com.intellij.codeInsight.intention.LowPriorityAction;
import com.intellij.codeInsight.unwrap.ScopeHighlighter;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.codeInspection.dataFlow.TrackingRunner;
import com.intellij.ide.util.PsiNavigationSupport;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.ui.popup.JBPopupAdapter;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.LightweightWindowEvent;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.util.containers.ContainerUtil;
import one.util.streamex.EntryStream;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
public class FindDfaProblemCauseFix implements LocalQuickFix, LowPriorityAction {
private final boolean myUnknownMembersAsNullable;
private final boolean myIgnoreAssertStatements;
private final SmartPsiElementPointer<PsiExpression> myAnchor;
private final TrackingRunner.DfaProblemType myProblemType;
public FindDfaProblemCauseFix(boolean unknownMembersAsNullable,
boolean ignoreAssertStatements,
PsiExpression anchor,
TrackingRunner.DfaProblemType problemType) {
myUnknownMembersAsNullable = unknownMembersAsNullable;
myIgnoreAssertStatements = ignoreAssertStatements;
myAnchor = SmartPointerManager.createPointer(anchor);
myProblemType = problemType;
}
@Override
public boolean startInWriteAction() {
return false;
}
@Nls(capitalization = Nls.Capitalization.Sentence)
@NotNull
@Override
public String getFamilyName() {
return "Find cause";
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
PsiExpression element = myAnchor.getElement();
if (element == null) return;
Editor editor = FileEditorManager.getInstance(project).getSelectedTextEditor();
if (editor == null) return;
Document document = editor.getDocument();
PsiFile file = element.getContainingFile();
PsiFile topLevelFile = InjectedLanguageManager.getInstance(project).getTopLevelFile(file);
if (topLevelFile == null || document != topLevelFile.getViewProvider().getDocument()) return;
List<TrackingRunner.CauseItem> items =
TrackingRunner.findProblemCause(myUnknownMembersAsNullable, myIgnoreAssertStatements, element, myProblemType);
TrackingRunner.CauseItem root = ContainerUtil.getOnlyItem(items);
if (root == null) {
HintManagerImpl hintManager = (HintManagerImpl)HintManager.getInstance();
hintManager.showErrorHint(editor, "Unable to find the cause");
return;
}
class CauseWithDepth {
final int myDepth;
final TrackingRunner.CauseItem myCauseItem;
CauseWithDepth(int depth, TrackingRunner.CauseItem item) {
myDepth = depth;
myCauseItem = item;
}
@Override
public String toString() {
return StringUtil.repeat(" ", myDepth - 1) + myCauseItem;
}
}
List<CauseWithDepth> causes =
EntryStream.ofTree(root, (depth, c) -> c.children()).skip(1).mapKeyValue((d, i) -> new CauseWithDepth(d, i)).toList();
if (causes.isEmpty()) {
HintManagerImpl hintManager = (HintManagerImpl)HintManager.getInstance();
hintManager.showErrorHint(editor, "Unable to find the cause");
return;
}
if (causes.size() == 1) {
TrackingRunner.CauseItem item = causes.get(0).myCauseItem;
navigate(editor, file, item);
return;
}
AtomicReference<ScopeHighlighter> highlighter = new AtomicReference<>(new ScopeHighlighter(editor));
JBPopup popup = JBPopupFactory.getInstance().createPopupChooserBuilder(causes)
.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
.setAccessibleName(root.toString())
.setTitle(StringUtil.wordsToBeginFromUpperCase(root.toString()))
.setMovable(false)
.setResizable(false)
.setRequestFocus(true)
.setItemSelectedCallback((cause) -> {
ScopeHighlighter h = highlighter.get();
if (h == null) return;
h.dropHighlight();
if (cause == null) return;
PsiElement target = cause.myCauseItem.getTarget();
if (target == null || !target.isValid()) return;
TextRange range = target.getTextRange();
h.highlight(Pair.create(range, Collections.singletonList(range)));
})
.addListener(new JBPopupAdapter() {
@Override
public void onClosed(@NotNull LightweightWindowEvent event) {
highlighter.getAndSet(null).dropHighlight();
}
})
.setItemChosenCallback(cause -> navigate(editor, file, cause.myCauseItem))
.createPopup();
popup.showInBestPositionFor(editor);
}
private static void navigate(Editor editor, PsiFile file, TrackingRunner.CauseItem item) {
PsiElement target = item.getTarget();
if (target == null) return;
TextRange range = target.getTextRange();
PsiFile targetFile = target.getContainingFile();
assert targetFile == file;
PsiNavigationSupport.getInstance().createNavigatable(file.getProject(), targetFile.getVirtualFile(), range.getStartOffset())
.navigate(true);
HintManagerImpl hintManager = (HintManagerImpl)HintManager.getInstance();
hintManager.showInformationHint(editor, StringUtil.escapeXmlEntities(item.toString()));
}
}
/*
Value is always true (s.length > 0)
Left operand range is {1..Integer.MAX_VALUE} (s.length)
Range is known from here (s[0])
*/
class Test {
void test(String[] s) {
if (s[0].isEmpty()) return;
if (<selection>s.length > 0</selection>) {
}
}
}
\ No newline at end of file
/*
Value is always false (s.length == list.size())
Left operand range is {1..Integer.MAX_VALUE} (s.length)
Range is known from here (s[0])
Right operand range is {0} (list.size())
Range is known from here (list.isEmpty())
*/
import java.util.List;
class Test {
void test(String[] s, List<String> list) {
if (!s[0].isEmpty()) return;
if (!list.isEmpty()) return;
if (<selection>s.length == list.size()</selection>) {
}
}
}
\ No newline at end of file
/*
Value is always false (s == null)
's' was assigned (s1)
s1 != null was checked before (s1 == null)
*/
class Test {
void test(String s, String s1) {
if (s1 == null) return;
s = s1;
if (<selection>s == null</selection>) {
}
}
}
\ No newline at end of file
/*
Value is always true (b)
'b == true' was established from condition (!b)
*/
import org.jetbrains.annotations.NotNull;
class Test {
void test(boolean b) {
if (!b) return;
if(<selection>b</selection>) {}
}
}
\ No newline at end of file
/*
Value is always false (b4)
'b4' was assigned (b3)
'b3' was assigned (b2)
'b2' was assigned (b1)
'b1' was assigned (b)
'b == false' was established from condition (b)
*/
import org.jetbrains.annotations.NotNull;
class Test {
void test(boolean b) {
if (b) return;
boolean b1 = b;
boolean b2 = b1;
boolean b3 = b2;
boolean b4 = b3;
if (<selection>b4</selection>) {}
}
}
\ No newline at end of file
/*
Value is always false (b)
'b == false' was established from condition (!b)
*/
class Test {
void test(boolean b, int limit) {
for(int i=0; i<=limit; i++) {
if (!b) System.out.println(<selection>b</selection>);
}
}
}
\ No newline at end of file
/*
Value is always false (E.A == E.B)
Comparison arguments are different constants (==)
*/
class Test {
enum E {A, B, C}
void test() {
if (<selection>E.A == E.B</selection>) {
}
}
}
\ No newline at end of file
/*
Value is always true (a == a)
Comparison arguments are the same (==)
*/
class Test {
void test(int a) {
if (<selection>a == a</selection>) {}
}
}
\ No newline at end of file
/*
Value is always false (x % 10 == 15)
Range of '%' result is {-9..9} (x % 10)
*/
class Test {
void test(int x) {
if (<selection>x % 10 == 15</selection>) {
}
}
}
\ No newline at end of file
/*
Value is always false (s1 == null)
's1' was assigned (s.trim())
Method 'trim' is externally annotated as 'non-null' (trim)
*/
class Test {
void test(String s) {
String s1 = s.trim();
if (<selection>s1 == null</selection>) {
}
}
}
\ No newline at end of file
/*
Value is always false (null == s)
Parameter 's' is annotated as 'non-null' (@NotNull String s)
*/
import org.jetbrains.annotations.NotNull;
class Test {
void test(@NotNull String s) {
if (<selection>null == s</selection>) return;
}
}
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment