mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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)
This commit is contained in:
+2
-2
@@ -241,7 +241,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 new DataflowResult(result);
|
||||
if (!(block instanceof PsiClass)) return visitor.myResult;
|
||||
DataflowResult dfr = visitor.myResult.copy();
|
||||
@@ -256,7 +256,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;
|
||||
|
||||
+12
@@ -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) {
|
||||
|
||||
+6
-5
@@ -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);
|
||||
}
|
||||
|
||||
+9
@@ -1643,6 +1643,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());
|
||||
}
|
||||
|
||||
+385
@@ -0,0 +1,385 @@
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,563 @@
|
||||
// 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.codeInsight.NullabilityAnnotationInfo;
|
||||
import com.intellij.codeInsight.NullableNotNullManager;
|
||||
import com.intellij.codeInspection.dataFlow.TrackingDfaMemoryState.MemoryStateChange;
|
||||
import com.intellij.codeInspection.dataFlow.TrackingDfaMemoryState.Relation;
|
||||
import com.intellij.codeInspection.dataFlow.instructions.*;
|
||||
import com.intellij.codeInspection.dataFlow.rangeSet.LongRangeSet;
|
||||
import com.intellij.codeInspection.dataFlow.value.DfaConstValue;
|
||||
import com.intellij.codeInspection.dataFlow.value.DfaRelationValue.RelationType;
|
||||
import com.intellij.codeInspection.dataFlow.value.DfaValue;
|
||||
import com.intellij.codeInspection.dataFlow.value.DfaVariableValue;
|
||||
import com.intellij.codeInspection.dataFlow.value.VariableDescriptor;
|
||||
import com.intellij.openapi.util.Pair;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.psi.*;
|
||||
import com.intellij.util.ArrayUtil;
|
||||
import com.intellij.util.ObjectUtils;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.siyeh.ig.psiutils.BoolUtils;
|
||||
import com.siyeh.ig.psiutils.ExpressionUtils;
|
||||
import one.util.streamex.StreamEx;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
public class TrackingRunner extends StandardDataFlowRunner {
|
||||
private final List<MemoryStateChange> myHistoryForContext = new ArrayList<>();
|
||||
private final PsiExpression myExpression;
|
||||
|
||||
public TrackingRunner(boolean unknownMembersAreNullable, @Nullable PsiElement context, PsiExpression expression) {
|
||||
super(unknownMembersAreNullable, context);
|
||||
myExpression = expression;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected DfaMemoryState createMemoryState() {
|
||||
return new TrackingDfaMemoryState(getFactory());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected DfaInstructionState[] acceptInstruction(@NotNull InstructionVisitor visitor, @NotNull DfaInstructionState instructionState) {
|
||||
Instruction instruction = instructionState.getInstruction();
|
||||
TrackingDfaMemoryState memState = (TrackingDfaMemoryState)instructionState.getMemoryState().createCopy();
|
||||
DfaInstructionState[] states = super.acceptInstruction(visitor, instructionState);
|
||||
for (DfaInstructionState state : states) {
|
||||
((TrackingDfaMemoryState)state.getMemoryState()).recordChange(instruction, memState);
|
||||
}
|
||||
if (instruction instanceof ExpressionPushingInstruction) {
|
||||
ExpressionPushingInstruction pushing = (ExpressionPushingInstruction)instruction;
|
||||
if (pushing.getExpression() == myExpression && pushing.getExpressionRange() == null) {
|
||||
for (DfaInstructionState state : states) {
|
||||
myHistoryForContext.addAll(((TrackingDfaMemoryState)state.getMemoryState()).getHistory());
|
||||
}
|
||||
}
|
||||
}
|
||||
return states;
|
||||
}
|
||||
|
||||
public static List<CauseItem> findProblemCause(boolean unknownAreNullables,
|
||||
boolean ignoreAssertions,
|
||||
PsiExpression expression,
|
||||
DfaProblemType type) {
|
||||
PsiElement body = DfaUtil.getDataflowContext(expression);
|
||||
if (body == null) return Collections.emptyList();
|
||||
TrackingRunner runner = new TrackingRunner(unknownAreNullables, body, expression);
|
||||
StandardInstructionVisitor visitor = new StandardInstructionVisitor();
|
||||
RunnerResult result = runner.analyzeMethodRecursively(body, visitor, ignoreAssertions);
|
||||
if (result != RunnerResult.OK) return Collections.emptyList();
|
||||
List<CauseItem> results = new ArrayList<>();
|
||||
for (MemoryStateChange history : runner.myHistoryForContext) {
|
||||
CauseItem root = findCauseChain(expression, history, type);
|
||||
if (!results.contains(root)) {
|
||||
results.add(root);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
public abstract static class DfaProblemType {
|
||||
public abstract String toString();
|
||||
}
|
||||
|
||||
public static class CauseItem {
|
||||
final @NotNull List<CauseItem> myChildren;
|
||||
final @NotNull DfaProblemType myProblem;
|
||||
final @Nullable PsiElement myTarget;
|
||||
|
||||
CauseItem(@NotNull String problem, @Nullable PsiElement target) {
|
||||
this(new CustomDfaProblemType(problem), target);
|
||||
}
|
||||
|
||||
CauseItem(@NotNull DfaProblemType problem, @Nullable PsiElement target) {
|
||||
myChildren = new ArrayList<>();
|
||||
myProblem = problem;
|
||||
myTarget = target;
|
||||
}
|
||||
|
||||
CauseItem(@NotNull String problem, @NotNull MemoryStateChange change) {
|
||||
this(new CustomDfaProblemType(problem), change);
|
||||
}
|
||||
|
||||
CauseItem(@NotNull DfaProblemType problem, @NotNull MemoryStateChange change) {
|
||||
this(problem, change.getExpression());
|
||||
}
|
||||
|
||||
void addChildren(CauseItem... causes) {
|
||||
ContainerUtil.addAllNotNull(myChildren, causes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
CauseItem item = (CauseItem)o;
|
||||
return myChildren.equals(item.myChildren) &&
|
||||
myProblem.toString().equals(item.myProblem.toString()) &&
|
||||
Objects.equals(myTarget, item.myTarget);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(myChildren, myProblem, myTarget);
|
||||
}
|
||||
|
||||
public String dump() {
|
||||
return dump(0);
|
||||
}
|
||||
|
||||
private String dump(int indent) {
|
||||
return StringUtil.repeat(" ", indent) + toString() + (myTarget == null ? "" : " (" + myTarget.getText() + ")") + "\n" +
|
||||
StreamEx.of(myChildren).map(child -> child.dump(indent + 1)).joining();
|
||||
}
|
||||
|
||||
public Stream<CauseItem> children() {
|
||||
return StreamEx.of(myChildren);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PsiElement getTarget() {
|
||||
return myTarget;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return myProblem.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static class ValueDfaProblemType extends DfaProblemType {
|
||||
final Object myValue;
|
||||
|
||||
public ValueDfaProblemType(Object value) {
|
||||
myValue = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Value is always " + myValue;
|
||||
}
|
||||
}
|
||||
|
||||
static class CustomDfaProblemType extends DfaProblemType {
|
||||
private final String myMessage;
|
||||
|
||||
CustomDfaProblemType(String message) {
|
||||
myMessage = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return myMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: 1. Find causes of other warnings:
|
||||
Cause for possible NPE
|
||||
Cause for AIOOBE
|
||||
Cause for "Contract always fails"
|
||||
Cause for possible CCE
|
||||
Cause for "modifying an immutable collection"
|
||||
Cause for "Collection is always empty" (separate inspection now)
|
||||
TODO: 2. Describe causes in more cases:
|
||||
Warning caused by contract
|
||||
Warning caused by CustomMethodHandler
|
||||
Warning caused by mismatched type constraints
|
||||
TODO: 3. Check how it works with
|
||||
Inliners (notably: Stream API)
|
||||
Ternary operators
|
||||
TODO: 4. Check for possible performance disasters (likely on some code patterns current algo might blow up)
|
||||
TODO: 5. Problem when interesting state doesn't reach the current condition, need to do something with this
|
||||
*/
|
||||
@NotNull
|
||||
private static CauseItem findCauseChain(PsiExpression expression, MemoryStateChange history, DfaProblemType type) {
|
||||
CauseItem root = new CauseItem(type, expression);
|
||||
if (history.getExpression() != expression) return root;
|
||||
if (type instanceof ValueDfaProblemType) {
|
||||
Object expectedValue = ((ValueDfaProblemType)type).myValue;
|
||||
CauseItem[] causes = findConstantValueCause(expression, history, expectedValue);
|
||||
root.addChildren(causes);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static CauseItem[] findConstantValueCause(PsiExpression expression, MemoryStateChange history, Object expectedValue) {
|
||||
Object constantExpressionValue = ExpressionUtils.computeConstantExpression(expression);
|
||||
DfaValue value = history.myTopOfStack;
|
||||
if (constantExpressionValue != null && constantExpressionValue.equals(expectedValue)) {
|
||||
return new CauseItem[]{new CauseItem("It's compile-time constant which evaluates to '" + value + "'", expression)};
|
||||
}
|
||||
if (value instanceof DfaConstValue) {
|
||||
Object constValue = ((DfaConstValue)value).getValue();
|
||||
if (Objects.equals(constValue, expectedValue) && constValue instanceof Boolean) {
|
||||
return findBooleanResultCauses(expression, history, ((Boolean)constValue).booleanValue());
|
||||
}
|
||||
}
|
||||
if (value instanceof DfaVariableValue) {
|
||||
MemoryStateChange change = history.findRelation(
|
||||
(DfaVariableValue)value, rel -> rel.myRelationType == RelationType.EQ && rel.myCounterpart instanceof DfaConstValue &&
|
||||
Objects.equals(expectedValue, ((DfaConstValue)rel.myCounterpart).getValue()));
|
||||
if (change != null) {
|
||||
PsiExpression varSourceExpression = change.getExpression();
|
||||
Instruction instruction = change.myInstruction;
|
||||
if (instruction instanceof AssignInstruction && change.myTopOfStack == value) {
|
||||
PsiExpression rValue = ((AssignInstruction)instruction).getRExpression();
|
||||
CauseItem item = new CauseItem("'" + value + "' was assigned", rValue);
|
||||
MemoryStateChange push = change.findExpressionPush(rValue);
|
||||
if (push != null) {
|
||||
item.addChildren(findConstantValueCause(rValue, push, expectedValue));
|
||||
}
|
||||
return new CauseItem[]{item};
|
||||
}
|
||||
else if (expectedValue instanceof Boolean && varSourceExpression != null) {
|
||||
return new CauseItem[]{new CauseItem("'" + value + " == "+expectedValue+"' was established from condition", varSourceExpression)};
|
||||
}
|
||||
}
|
||||
}
|
||||
return new CauseItem[0];
|
||||
}
|
||||
|
||||
private static CauseItem[] findBooleanResultCauses(PsiExpression expression,
|
||||
MemoryStateChange history,
|
||||
boolean value) {
|
||||
if (BoolUtils.isNegation(expression)) {
|
||||
PsiExpression negated = BoolUtils.getNegated(expression);
|
||||
if (negated != null) {
|
||||
MemoryStateChange negatedPush = history.findExpressionPush(negated);
|
||||
if (negatedPush != null) {
|
||||
CauseItem cause = new CauseItem("Value '" + negated.getText() + "' is always '" + !value + "'", negated);
|
||||
cause.addChildren(findConstantValueCause(negated, negatedPush, !value));
|
||||
return new CauseItem[]{cause};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (expression instanceof PsiBinaryExpression) {
|
||||
PsiBinaryExpression binOp = (PsiBinaryExpression)expression;
|
||||
RelationType relationType =
|
||||
RelationType.fromElementType(binOp.getOperationTokenType());
|
||||
if (relationType != null) {
|
||||
if (!value) {
|
||||
relationType = relationType.getNegated();
|
||||
}
|
||||
PsiExpression leftOperand = binOp.getLOperand();
|
||||
PsiExpression rightOperand = binOp.getROperand();
|
||||
MemoryStateChange leftChange = history.findExpressionPush(leftOperand);
|
||||
MemoryStateChange rightChange = history.findExpressionPush(rightOperand);
|
||||
if (leftChange != null && rightChange != null) {
|
||||
DfaValue leftValue = leftChange.myTopOfStack;
|
||||
DfaValue rightValue = rightChange.myTopOfStack;
|
||||
if (leftValue == rightValue &&
|
||||
(leftValue instanceof DfaVariableValue || leftValue instanceof DfaConstValue)) {
|
||||
return new CauseItem[]{new CauseItem("Comparison arguments are the same", binOp.getOperationSign())};
|
||||
}
|
||||
if (leftValue != rightValue && relationType.isInequality() &&
|
||||
leftValue instanceof DfaConstValue && rightValue instanceof DfaConstValue) {
|
||||
return new CauseItem[]{
|
||||
new CauseItem("Comparison arguments are different constants", binOp.getOperationSign())};
|
||||
}
|
||||
return findRelationCause(relationType, leftChange, rightChange);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new CauseItem[0];
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static CauseItem[] findRelationCause(RelationType relationType,
|
||||
MemoryStateChange leftChange,
|
||||
MemoryStateChange rightChange) {
|
||||
DfaValue leftValue = leftChange.myTopOfStack;
|
||||
DfaValue rightValue = rightChange.myTopOfStack;
|
||||
if (leftValue instanceof DfaVariableValue) {
|
||||
Relation relation = new Relation(relationType, rightValue);
|
||||
MemoryStateChange change = findRelationAddedChange(leftChange, (DfaVariableValue)leftValue, relation);
|
||||
if (change != null) {
|
||||
CauseItem cause = findRelationCause(change, (DfaVariableValue)leftValue, relation, rightChange);
|
||||
if (cause != null) {
|
||||
Instruction instruction = change.myInstruction;
|
||||
if (instruction instanceof AssignInstruction) {
|
||||
PsiExpression expression = ((AssignInstruction)instruction).getRExpression();
|
||||
MemoryStateChange assignmentChange = change.findExpressionPush(expression);
|
||||
if (assignmentChange != null) {
|
||||
DfaValue target = change.myTopOfStack;
|
||||
if (target == rightValue) {
|
||||
return ArrayUtil.prepend(cause, findRelationCause(relationType, leftChange, assignmentChange));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new CauseItem[]{cause};
|
||||
}
|
||||
}
|
||||
if (rightValue instanceof DfaVariableValue) {
|
||||
Relation relation = new Relation(
|
||||
Objects.requireNonNull(relationType.getFlipped()), leftValue);
|
||||
MemoryStateChange change = findRelationAddedChange(rightChange, (DfaVariableValue)rightValue, relation);
|
||||
if (change != null) {
|
||||
return new CauseItem[]{findRelationCause(change, (DfaVariableValue)rightValue, relation, leftChange)};
|
||||
}
|
||||
}
|
||||
Pair<MemoryStateChange, DfaNullability> leftNullability = leftChange.findFact(leftValue, DfaFactType.NULLABILITY);
|
||||
Pair<MemoryStateChange, DfaNullability> rightNullability = rightChange.findFact(rightValue, DfaFactType.NULLABILITY);
|
||||
if ((leftNullability.second == DfaNullability.NULL && rightNullability.second == DfaNullability.NOT_NULL) ||
|
||||
(rightNullability.second == DfaNullability.NULL && leftNullability.second == DfaNullability.NOT_NULL)) {
|
||||
return new CauseItem[]{findNullabilityCause(leftChange, leftNullability.first, leftNullability.second),
|
||||
findNullabilityCause(rightChange, rightNullability.first, rightNullability.second)};
|
||||
}
|
||||
|
||||
Pair<MemoryStateChange, LongRangeSet> leftRange = leftChange.findFact(leftValue, DfaFactType.RANGE);
|
||||
Pair<MemoryStateChange, LongRangeSet> rightRange = rightChange.findFact(rightValue, DfaFactType.RANGE);
|
||||
if (leftRange.second != null && rightRange.second != null) {
|
||||
LongRangeSet fromRelation = rightRange.second.fromRelation(relationType.getNegated());
|
||||
if (fromRelation != null && !fromRelation.intersects(leftRange.second)) {
|
||||
return new CauseItem[]{
|
||||
findRangeCause(leftChange, leftRange.first, leftRange.second, "Left operand range is %s"),
|
||||
findRangeCause(rightChange, rightRange.first, rightRange.second, "Right operand range is %s")};
|
||||
}
|
||||
}
|
||||
return new CauseItem[0];
|
||||
}
|
||||
|
||||
private static CauseItem findRelationCause(MemoryStateChange change,
|
||||
DfaVariableValue value,
|
||||
Relation relation, MemoryStateChange counterPartChange) {
|
||||
Instruction instruction = change.myInstruction;
|
||||
String condition = value + " " + relation;
|
||||
if (relation.myCounterpart instanceof DfaConstValue &&
|
||||
((DfaConstValue)relation.myCounterpart).getValue() == null && relation.myRelationType == RelationType.NE) {
|
||||
if (instruction instanceof CheckNotNullInstruction) {
|
||||
PsiExpression expression = ((CheckNotNullInstruction)instruction).getProblem().getDereferencedExpression();
|
||||
String text = expression == null ? value.toString() : expression.getText();
|
||||
return new CauseItem("'" + text + "' was dereferenced", expression);
|
||||
}
|
||||
if (instruction instanceof InstanceofInstruction) {
|
||||
PsiExpression expression = ((InstanceofInstruction)instruction).getExpression();
|
||||
return new CauseItem("The 'instanceof' check implies non-nullity", expression);
|
||||
}
|
||||
}
|
||||
if (instruction instanceof AssignInstruction) {
|
||||
DfaValue target = change.myTopOfStack;
|
||||
PsiExpression rValue = ((AssignInstruction)instruction).getRExpression();
|
||||
if (target == value) {
|
||||
CauseItem item = new CauseItem("'" + target + "' was assigned", rValue);
|
||||
MemoryStateChange rValuePush = change.findExpressionPush(rValue);
|
||||
if (rValuePush != null) {
|
||||
item.addChildren(findRelationCause(relation.myRelationType, rValuePush, counterPartChange));
|
||||
}
|
||||
return item;
|
||||
}
|
||||
if (target == relation.myCounterpart) {
|
||||
return new CauseItem("'" + target + "' was assigned", rValue);
|
||||
}
|
||||
}
|
||||
PsiExpression expression = change.getExpression();
|
||||
if (expression != null) {
|
||||
if (expression instanceof PsiBinaryExpression) {
|
||||
PsiExpression lOperand = ((PsiBinaryExpression)expression).getLOperand();
|
||||
PsiExpression rOperand = ((PsiBinaryExpression)expression).getROperand();
|
||||
MemoryStateChange leftPos = change.findExpressionPush(lOperand);
|
||||
MemoryStateChange rightPos = change.findExpressionPush(rOperand);
|
||||
if (leftPos != null && rightPos != null) {
|
||||
DfaValue leftValue = leftPos.myTopOfStack;
|
||||
DfaValue rightValue = rightPos.myTopOfStack;
|
||||
if (leftValue == value && rightValue == relation.myCounterpart ||
|
||||
rightValue == value && leftValue == relation.myCounterpart) {
|
||||
return new CauseItem(new CustomDfaProblemType(condition + " was checked before"), expression);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new CauseItem(new CustomDfaProblemType(condition + " is known from here"), expression);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static CauseItem findNullabilityCause(MemoryStateChange factUse, MemoryStateChange factDef, DfaNullability nullability) {
|
||||
PsiExpression expression = factUse.getExpression();
|
||||
if (factDef != null && expression != null) {
|
||||
PsiExpression defExpression = factDef.getExpression();
|
||||
if (defExpression != null) {
|
||||
return new CauseItem(
|
||||
new CustomDfaProblemType(expression.getText() + " is known to be '"+nullability.getPresentationName()+"' from here"),
|
||||
defExpression);
|
||||
}
|
||||
}
|
||||
if (expression instanceof PsiMethodCallExpression) {
|
||||
PsiMethodCallExpression call = (PsiMethodCallExpression)expression;
|
||||
PsiMethod method = call.resolveMethod();
|
||||
return fromMemberNullability(nullability, method, "Method", call.getMethodExpression().getReferenceNameElement());
|
||||
}
|
||||
if (expression instanceof PsiReferenceExpression) {
|
||||
PsiVariable variable = ObjectUtils.tryCast(((PsiReferenceExpression)expression).resolve(), PsiVariable.class);
|
||||
if (variable instanceof PsiField) {
|
||||
return fromMemberNullability(nullability, variable, "Field", ((PsiReferenceExpression)expression).getReferenceNameElement());
|
||||
}
|
||||
if (variable instanceof PsiParameter) {
|
||||
return fromMemberNullability(nullability, variable, "Parameter", ((PsiReferenceExpression)expression).getReferenceNameElement());
|
||||
}
|
||||
if (variable != null) {
|
||||
return fromMemberNullability(nullability, variable, "Variable", ((PsiReferenceExpression)expression).getReferenceNameElement());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static CauseItem fromMemberNullability(DfaNullability nullability,
|
||||
PsiModifierListOwner owner,
|
||||
String memberName, PsiElement anchor) {
|
||||
if (owner != null) {
|
||||
NullabilityAnnotationInfo info = NullableNotNullManager.getInstance(owner.getProject()).findEffectiveNullabilityInfo(owner);
|
||||
if (info != null && DfaNullability.fromNullability(info.getNullability()) == nullability) {
|
||||
String message;
|
||||
String name = ((PsiNamedElement)owner).getName();
|
||||
if (info.isInferred()) {
|
||||
message = memberName + " '" + name + "' was inferred to be '" + nullability.getPresentationName() + "'";
|
||||
}
|
||||
else if (info.isExternal()) {
|
||||
message = memberName + " '" + name + "' is externally annotated as '" + nullability.getPresentationName() + "'";
|
||||
}
|
||||
else if (info.isContainer()) {
|
||||
message = memberName + " '" + name + "' inherits container annotation, thus '" + nullability.getPresentationName() + "'";
|
||||
}
|
||||
else {
|
||||
message = memberName + " '" + name + "' is annotated as '" + nullability.getPresentationName() + "'";
|
||||
}
|
||||
if (owner.getContainingFile() == anchor.getContainingFile()) {
|
||||
anchor = owner.getNavigationElement();
|
||||
}
|
||||
return new CauseItem(message, anchor);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static CauseItem findRangeCause(MemoryStateChange factUse,
|
||||
MemoryStateChange factDef,
|
||||
LongRangeSet range,
|
||||
String template) {
|
||||
DfaValue value = factUse.myTopOfStack;
|
||||
if (value instanceof DfaVariableValue) {
|
||||
VariableDescriptor descriptor = ((DfaVariableValue)value).getDescriptor();
|
||||
if (descriptor instanceof SpecialField && range.equals(LongRangeSet.indexRange())) {
|
||||
switch (((SpecialField)descriptor)) {
|
||||
case ARRAY_LENGTH:
|
||||
return new CauseItem("Array length is always non-negative", factUse);
|
||||
case STRING_LENGTH:
|
||||
return new CauseItem("String length is always non-negative", factUse);
|
||||
case COLLECTION_SIZE:
|
||||
return new CauseItem("Collection size is always non-negative", factUse);
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
PsiExpression expression = factUse.getExpression();
|
||||
if (expression != null) {
|
||||
if (expression instanceof PsiLiteralExpression) {
|
||||
return null; // Literal range is quite evident
|
||||
}
|
||||
if (expression instanceof PsiMethodCallExpression) {
|
||||
PsiMethodCallExpression call = (PsiMethodCallExpression)expression;
|
||||
PsiMethod method = call.resolveMethod();
|
||||
if (method != null) {
|
||||
LongRangeSet fromAnnotation = LongRangeSet.fromPsiElement(method);
|
||||
if (fromAnnotation.equals(range)) {
|
||||
return new CauseItem("The range of '" + method.getName() + "' is specified by annotation as " + range,
|
||||
call.getMethodExpression().getReferenceNameElement());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (expression instanceof PsiBinaryExpression &&
|
||||
(PsiType.LONG.equals(expression.getType()) || PsiType.INT.equals(expression.getType()))) {
|
||||
boolean isLong = PsiType.LONG.equals(expression.getType());
|
||||
PsiBinaryExpression binOp = (PsiBinaryExpression)expression;
|
||||
PsiExpression left = binOp.getLOperand();
|
||||
PsiExpression right = binOp.getROperand();
|
||||
MemoryStateChange leftPush = factUse.findExpressionPush(left);
|
||||
MemoryStateChange rightPush = factUse.findExpressionPush(right);
|
||||
if (leftPush != null && rightPush != null) {
|
||||
DfaValue leftValue = leftPush.myTopOfStack;
|
||||
DfaValue rightValue = rightPush.myTopOfStack;
|
||||
Pair<MemoryStateChange, LongRangeSet> leftSet = leftPush.findFact(leftValue, DfaFactType.RANGE);
|
||||
Pair<MemoryStateChange, LongRangeSet> rightSet = rightPush.findFact(rightValue, DfaFactType.RANGE);
|
||||
LongRangeSet fromType = Objects.requireNonNull(LongRangeSet.fromType(expression.getType()));
|
||||
if (leftSet.second == null) {
|
||||
leftSet = Pair.create(null, fromType);
|
||||
}
|
||||
if (rightSet.second == null) {
|
||||
rightSet = Pair.create(null, fromType);
|
||||
}
|
||||
LongRangeSet result = leftSet.second.binOpFromToken(binOp.getOperationTokenType(), rightSet.second, isLong);
|
||||
if (range.equals(result)) {
|
||||
CauseItem cause = new CauseItem("Range of '" + binOp.getOperationSign().getText() + "' result is " + range, factUse);
|
||||
CauseItem leftCause = null, rightCause = null;
|
||||
if (!leftSet.second.equals(fromType)) {
|
||||
leftCause = findRangeCause(leftPush, leftSet.first, leftSet.second, "Left operand range is %s");
|
||||
}
|
||||
if (!rightSet.second.equals(fromType)) {
|
||||
rightCause = findRangeCause(rightPush, rightSet.first, rightSet.second, "Right operand range is %s");
|
||||
}
|
||||
cause.addChildren(leftCause, rightCause);
|
||||
return cause;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CauseItem item = new CauseItem(String.format(template, range.toString()), factUse);
|
||||
if (factDef != null) {
|
||||
PsiExpression defExpression = factDef.getExpression();
|
||||
if (defExpression != null) {
|
||||
item.addChildren(new CauseItem("Range is known from here", defExpression));
|
||||
}
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private static MemoryStateChange findRelationAddedChange(MemoryStateChange history, DfaVariableValue var, Relation relation) {
|
||||
List<Relation> subRelations;
|
||||
switch (relation.myRelationType) {
|
||||
case NE:
|
||||
subRelations = Arrays.asList(relation, new Relation(RelationType.GT, relation.myCounterpart),
|
||||
new Relation(RelationType.LT, relation.myCounterpart));
|
||||
break;
|
||||
case LE:
|
||||
subRelations = Arrays.asList(new Relation(RelationType.EQ, relation.myCounterpart),
|
||||
new Relation(RelationType.LT, relation.myCounterpart));
|
||||
break;
|
||||
case GE:
|
||||
subRelations = Arrays.asList(new Relation(RelationType.EQ, relation.myCounterpart),
|
||||
new Relation(RelationType.GT, relation.myCounterpart));
|
||||
break;
|
||||
default:
|
||||
subRelations = Collections.singletonList(relation);
|
||||
}
|
||||
return history.findRelation(var, subRelations::contains);
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
// 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()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
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>) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
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>) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
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>) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
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>) {}
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
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>) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
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>);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
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>) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
Value is always true (a == a)
|
||||
Comparison arguments are the same (==)
|
||||
*/
|
||||
class Test {
|
||||
void test(int a) {
|
||||
if (<selection>a == a</selection>) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
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>) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
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>) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
Value is always true (null == s)
|
||||
's' was assigned (null)
|
||||
*/
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
class Test {
|
||||
void test(@NotNull String s) {
|
||||
s = null;
|
||||
if (<selection>null == s</selection>) return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
Value is always false (s == null)
|
||||
s != null was checked before (null == s)
|
||||
*/
|
||||
class Test {
|
||||
void test(String s) {
|
||||
if (null == s) return;
|
||||
System.out.println(s.trim());
|
||||
if (<selection>s == null</selection>) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
Value is always false (s == null)
|
||||
The 'instanceof' check implies non-nullity (s instanceof String)
|
||||
*/
|
||||
class Test {
|
||||
void test(Object s) {
|
||||
if (s instanceof String) {
|
||||
if (<selection>s == null</selection>){
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
Value is always false (s == s1)
|
||||
's1' was assigned (null)
|
||||
s != null was checked before (null == s)
|
||||
*/
|
||||
class Test {
|
||||
void test(String s) {
|
||||
if (null == s) return;
|
||||
String s1 = null;
|
||||
if (<selection>s == s1</selection>) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
Value is always false (x * 2 == y * 2 + 1)
|
||||
Range of '*' result is {Integer.MIN_VALUE..Integer.MAX_VALUE-1}: even (x * 2)
|
||||
Range of '+' result is {-2147483647..Integer.MAX_VALUE}: odd (y * 2 + 1)
|
||||
Range of '*' result is {Integer.MIN_VALUE..Integer.MAX_VALUE-1}: even (y * 2)
|
||||
*/
|
||||
class Test {
|
||||
void test(int x, int y) {
|
||||
// May produce better result in future (keep only mod info (even/odd), remove range which is irrelevant here)
|
||||
if (<selection>x * 2 == y * 2 + 1</selection>) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
Value is always true (x <= y)
|
||||
x <= y was checked before (x > y)
|
||||
*/
|
||||
class Test {
|
||||
void test(int x, int y) {
|
||||
if (x > y) return;
|
||||
|
||||
if (<selection>x <= y</selection>) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
Value is always true (x < y)
|
||||
x < y was checked before (x > y)
|
||||
*/
|
||||
class Test {
|
||||
void test(int x, int y) {
|
||||
if (x > y) return;
|
||||
|
||||
if (x == y) { // explanation doesn't point here: not entirely correct, but hard to fix; postponed
|
||||
return;
|
||||
}
|
||||
|
||||
if (<selection>x < y</selection>) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
Value is always false (s == null)
|
||||
's' was dereferenced (s)
|
||||
*/
|
||||
class Test {
|
||||
void test(String s) {
|
||||
System.out.println(s.trim());
|
||||
if (<selection>s == null</selection>) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
Value is always false (s.length() < 0)
|
||||
String length is always non-negative (s.length())
|
||||
*/
|
||||
class Test {
|
||||
void test(String s) {
|
||||
if (<selection>s.length() < 0</selection>) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
// 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.java.codeInspection;
|
||||
|
||||
import com.intellij.JavaTestUtil;
|
||||
import com.intellij.codeInspection.dataFlow.CommonDataflow;
|
||||
import com.intellij.codeInspection.dataFlow.TrackingRunner;
|
||||
import com.intellij.openapi.editor.SelectionModel;
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.psi.*;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import com.intellij.rt.execution.junit.FileComparisonFailure;
|
||||
import com.intellij.testFramework.LightProjectDescriptor;
|
||||
import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase;
|
||||
import one.util.streamex.StreamEx;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class DataFlowInspectionTrackerTest extends LightCodeInsightFixtureTestCase {
|
||||
@Override
|
||||
protected String getTestDataPath() {
|
||||
return JavaTestUtil.getJavaTestDataPath() + "/inspection/dataFlow/tracker/";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected LightProjectDescriptor getProjectDescriptor() {
|
||||
return JAVA_8_ANNOTATED;
|
||||
}
|
||||
|
||||
protected void doTest() {
|
||||
String filePath = getTestName(false) + ".java";
|
||||
PsiFile file = myFixture.configureByFile(filePath);
|
||||
SelectionModel model = myFixture.getEditor().getSelectionModel();
|
||||
int start = model.getSelectionStart();
|
||||
int end = model.getSelectionEnd();
|
||||
if (start == end) {
|
||||
fail("No selection!");
|
||||
}
|
||||
TextRange range = new TextRange(start, end);
|
||||
PsiElement element = file.findElementAt(start);
|
||||
while (element != null && element != file &&
|
||||
(!range.equals(element.getTextRange()) || element.getParent().getTextRange().equals(range))) {
|
||||
element = element.getParent();
|
||||
}
|
||||
String selectedText = model.getSelectedText();
|
||||
assertNotNull("Failed to find element at selection: " + selectedText, element);
|
||||
assertTrue("Selected element is not an expression: " + selectedText, element instanceof PsiExpression);
|
||||
PsiExpression expression = (PsiExpression)element;
|
||||
CommonDataflow.DataflowResult result = CommonDataflow.getDataflowResult(expression);
|
||||
assertNotNull("No common dataflow result for expression: " + selectedText, result);
|
||||
Set<Object> values = result.getExpressionValues(expression);
|
||||
assertEquals("No single value for expression: "+selectedText, 1, values.size());
|
||||
Object singleValue = values.iterator().next();
|
||||
List<TrackingRunner.CauseItem> items = TrackingRunner.findProblemCause(
|
||||
true, false, expression, new TrackingRunner.ValueDfaProblemType(singleValue));
|
||||
String dump = StreamEx.of(items).map(TrackingRunner.CauseItem::dump).joining("\n----\n");
|
||||
PsiComment firstComment = PsiTreeUtil.findChildOfType(file, PsiComment.class);
|
||||
if (firstComment == null) {
|
||||
fail("Comment not found");
|
||||
}
|
||||
String text = firstComment.getText();
|
||||
String prefix = StringUtil.substringBefore(text, "\n") + "\n";
|
||||
String suffix = StringUtil.substringAfterLast(text, "\n");
|
||||
String actual = prefix + dump + suffix;
|
||||
if (!text.equals(actual)) {
|
||||
PsiElement fileCopy = file.copy();
|
||||
PsiTreeUtil.findChildOfType(fileCopy, PsiComment.class)
|
||||
.replace(JavaPsiFacade.getElementFactory(getProject()).createCommentFromText(actual, null));
|
||||
int diff = actual.length() - text.length();
|
||||
String actualFile = fileCopy.getText();
|
||||
String origPath = myFixture.getTestDataPath() + "/" + filePath;
|
||||
actualFile = actualFile.substring(0, start + diff) +
|
||||
"<selection>" +
|
||||
actualFile.substring(start + diff, end + diff) +
|
||||
"</selection>" +
|
||||
actualFile.substring(end + diff);
|
||||
String expectedFile;
|
||||
try {
|
||||
expectedFile = new String(Files.readAllBytes(Paths.get(origPath)), StandardCharsets.UTF_8);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
throw new FileComparisonFailure("Dump differs", expectedFile, actualFile, origPath);
|
||||
}
|
||||
assertEquals(text, actual);
|
||||
}
|
||||
|
||||
public void testDitto() { doTest(); }
|
||||
public void testConstants() { doTest(); }
|
||||
public void testSimpleDeref() { doTest(); }
|
||||
public void testNullWasChecked() { doTest(); }
|
||||
public void testNullWasCheckedNullAssigned() { doTest(); }
|
||||
public void testNullWasCheckedInstanceOf() { doTest(); }
|
||||
public void testNotNullAnnotated() { doTest(); }
|
||||
public void testNotNullParameter() { doTest(); }
|
||||
public void testNotNullParameterAssigned() { doTest(); }
|
||||
public void testStringLength() { doTest(); }
|
||||
public void testModOutOfRange() { doTest(); }
|
||||
public void testOddityCheck() { doTest(); }
|
||||
public void testRepeatingIntegerComparison() { doTest(); }
|
||||
public void testRepeatingIntegerComparisonThreeBranches() { doTest(); }
|
||||
public void testAssignmentChain() { doTest(); }
|
||||
public void testBooleanChecked() { doTest(); }
|
||||
public void testBooleanReassignedMultipleTimes() { doTest(); }
|
||||
public void testBooleanUnderNegation() { doTest(); }
|
||||
public void testArrayLength() { doTest(); }
|
||||
public void testArrayLengthCollectionSize() { doTest(); }
|
||||
}
|
||||
Reference in New Issue
Block a user