From 97cdb4cdd514a317749972895cfbe14eb0254f83 Mon Sep 17 00:00:00 2001 From: Tagir Valeev Date: Mon, 13 Mar 2017 15:59:52 +0700 Subject: [PATCH] IDEA-169503 Integral range tracking for variables inside dataflow analysis --- .../dataFlow/DataFlowRunner.java | 19 +- .../dataFlow/DfaInstructionState.java | 6 +- .../dataFlow/DfaMemoryState.java | 4 + .../dataFlow/DfaMemoryStateImpl.java | 105 ++- .../dataFlow/DfaVariableState.java | 82 ++- .../NullParameterConstraintChecker.java | 4 +- .../dataFlow/StandardInstructionVisitor.java | 120 ++-- .../codeInspection/dataFlow/StateMerger.java | 62 +- .../dataFlow/ValuableDataFlowRunner.java | 19 +- .../dataFlow/rangeSet/LongRangeSet.java | 638 ++++++++++++++++++ .../dataFlow/value/DfaExpressionFactory.java | 6 +- .../dataFlow/value/DfaRangeValue.java | 70 ++ .../dataFlow/value/DfaRelationValue.java | 20 +- .../dataFlow/value/DfaValueFactory.java | 9 +- .../dataFlow/fixture/LongRangeBasics.java | 91 +++ .../fixture/LongRangeKnownMethods.java | 18 + .../dataFlow/fixture/LongRangeLoop.java | 36 + .../DataFlowInspectionTestSuite.java | 3 +- .../DataFlowRangeAnalysisTest.java | 63 ++ .../dataFlow/rangeSet/LongRangeSetTest.java | 245 +++++++ .../siyeh/ig/psiutils/ComparisonUtils.java | 11 + 21 files changed, 1533 insertions(+), 98 deletions(-) create mode 100644 java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/rangeSet/LongRangeSet.java create mode 100644 java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/value/DfaRangeValue.java create mode 100644 java/java-tests/testData/inspection/dataFlow/fixture/LongRangeBasics.java create mode 100644 java/java-tests/testData/inspection/dataFlow/fixture/LongRangeKnownMethods.java create mode 100644 java/java-tests/testData/inspection/dataFlow/fixture/LongRangeLoop.java create mode 100644 java/java-tests/testSrc/com/intellij/codeInspection/DataFlowRangeAnalysisTest.java create mode 100644 java/java-tests/testSrc/com/intellij/codeInspection/dataFlow/rangeSet/LongRangeSetTest.java diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DataFlowRunner.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DataFlowRunner.java index 3f8943eb0cbf..239981bebd1e 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DataFlowRunner.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DataFlowRunner.java @@ -177,7 +177,7 @@ public class DataFlowRunner { if (instruction instanceof BranchingInstruction) { BranchingInstruction branching = (BranchingInstruction)instruction; Collection processed = processedStates.get(branching); - if (processed.contains(instructionState.getMemoryState())) { + if (containsState(processed, instructionState)) { continue; } if (processed.size() > MAX_STATES_PER_BRANCH) { @@ -198,8 +198,8 @@ public class DataFlowRunner { handleStepOutOfLoop(instruction, nextInstruction, loopNumber, processedStates, incomingStates, states, after, queue); if (nextInstruction instanceof BranchingInstruction) { BranchingInstruction branching = (BranchingInstruction)nextInstruction; - if (processedStates.get(branching).contains(state.getMemoryState()) || - incomingStates.get(branching).contains(state.getMemoryState())) { + if (containsState(processedStates.get(branching), state) || + containsState(incomingStates.get(branching), state)) { continue; } if (loopNumber[branching.getIndex()] != 0) { @@ -221,6 +221,19 @@ public class DataFlowRunner { } } + private static boolean containsState(Collection processed, + DfaInstructionState instructionState) { + if (processed.contains(instructionState.getMemoryState())) { + return true; + } + for (DfaMemoryState state : processed) { + if (((DfaMemoryStateImpl)state).isSuperStateOf((DfaMemoryStateImpl)instructionState.getMemoryState())) { + return true; + } + } + return false; + } + private void handleStepOutOfLoop(@NotNull final Instruction prevInstruction, @NotNull Instruction nextInstruction, @NotNull final int[] loopNumber, diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaInstructionState.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaInstructionState.java index 9a06ed0a4f15..fd963326c9d4 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaInstructionState.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaInstructionState.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2015 JetBrains s.r.o. + * Copyright 2000-2017 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,6 @@ package com.intellij.codeInspection.dataFlow; import com.intellij.codeInspection.dataFlow.instructions.Instruction; import com.intellij.openapi.util.Pair; -import com.intellij.util.Function; import com.intellij.util.Processor; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.MultiMap; @@ -125,7 +124,8 @@ class StateQueue { StateMerger merger = new StateMerger(); while (true) { - List nextStates = merger.mergeByFacts(group); + List nextStates = merger.mergeByRanges(group); + if (nextStates == null) nextStates = merger.mergeByFacts(group); if (nextStates == null) nextStates = merger.mergeByNullability(group); if (nextStates == null) nextStates = merger.mergeByUnknowns(group); if (nextStates == null) break; diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaMemoryState.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaMemoryState.java index 33997bbfdf07..31a919304405 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaMemoryState.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaMemoryState.java @@ -15,6 +15,7 @@ */ package com.intellij.codeInspection.dataFlow; +import com.intellij.codeInspection.dataFlow.rangeSet.LongRangeSet; import com.intellij.codeInspection.dataFlow.value.DfaConstValue; import com.intellij.codeInspection.dataFlow.value.DfaRelationValue; import com.intellij.codeInspection.dataFlow.value.DfaValue; @@ -53,6 +54,9 @@ public interface DfaMemoryState { ThreeState checkOptional(DfaValue value); + @Nullable + LongRangeSet getRange(DfaValue value); + void flushFields(); void flushVariable(DfaVariableValue variable); diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaMemoryStateImpl.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaMemoryStateImpl.java index ac6d3f44d4d7..9525224c6941 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaMemoryStateImpl.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaMemoryStateImpl.java @@ -24,6 +24,7 @@ */ package com.intellij.codeInspection.dataFlow; +import com.intellij.codeInspection.dataFlow.rangeSet.LongRangeSet; import com.intellij.codeInspection.dataFlow.value.*; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.util.Pair; @@ -40,6 +41,7 @@ import com.intellij.util.ThreeState; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.Stack; import gnu.trove.*; +import one.util.streamex.StreamEx; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -207,12 +209,9 @@ public class DfaMemoryStateImpl implements DfaMemoryState { if (!myDistinctClasses.isEmpty()) { result.append("\n distincts: "); - List distincts = new ArrayList<>(); - for (UnorderedPair pair : getDistinctClassPairs()) { - distincts.add("{" + pair.first + ", " + pair.second + "}"); - } - Collections.sort(distincts); - result.append(StringUtil.join(distincts, " ")); + String distincts = + StreamEx.of(getDistinctClassPairs()).map(pair -> "{" + pair.first + ", " + pair.second + "}").sorted().joining(" "); + result.append(distincts); } if (!myStack.isEmpty()) { @@ -361,6 +360,28 @@ public class DfaMemoryStateImpl implements DfaMemoryState { myIdToEqClassesIndices.remove(id); } + /** + * Returns true if current state describes all possible concrete program states described by {@code that} state. + * + * @param that a sub-state candidate + * @return true if current state is a super-state of the supplied state. + */ + public boolean isSuperStateOf(DfaMemoryStateImpl that) { + if (!equalsSuperficially(that) || + !equalsByUnknownVariables(that) || + !getNonTrivialEqClasses().equals(that.getNonTrivialEqClasses()) || + !that.getDistinctClassPairs().containsAll(getDistinctClassPairs())) { + return false; + } + if(myVariableStates.size() != that.myVariableStates.size()) return false; + for (Map.Entry entry : myVariableStates.entrySet()) { + DfaVariableState thisState = entry.getValue(); + DfaVariableState thatState = that.myVariableStates.get(entry.getKey()); + if(Objects.equals(thisState, thatState)) continue; + if(thatState == null || thisState == null || !thisState.isSuperStateOf(thatState)) return false; + } + return true; + } private static boolean canBeInRelation(@NotNull DfaValue dfaValue) { DfaValue unwrapped = unwrap(dfaValue); @@ -640,6 +661,27 @@ public class DfaMemoryStateImpl implements DfaMemoryState { } } + void setRange(DfaVariableValue target, LongRangeSet range) { + if (!isUnknownState(target)) { + setVariableState(target, getVariableState(target).withRange(range)); + } + } + + public boolean applyRange(LongRangeSet range, DfaVariableValue target) { + if (!isUnknownState(target) && range != null) { + DfaVariableState state = getVariableState(target); + LongRangeSet oldRange = state.getRange(); + if (oldRange == null) { + oldRange = LongRangeSet.fromType(target.getVariableType()); + if (oldRange == null) return true; + } + LongRangeSet newRange = oldRange.intersect(range); + if (newRange.isEmpty()) return false; + setVariableState(target, state.withRange(newRange)); + } + return true; + } + static DfaValue unwrap(DfaValue value) { if (value instanceof DfaBoxedValue) { return ((DfaBoxedValue)value).getWrappedValue(); @@ -683,6 +725,25 @@ public class DfaMemoryStateImpl implements DfaMemoryState { if (dfaLeft instanceof DfaUnknownValue || dfaRight instanceof DfaUnknownValue) return true; boolean isNegated = dfaRelation.isNegated(); + + if (dfaLeft instanceof DfaVariableValue) { + LongRangeSet right = getRange(dfaRight); + if (right != null) { + if (!applyRange(right.fromRelation(dfaRelation.getComparisonOperation()), (DfaVariableValue)dfaLeft)) { + return false; + } + } + } + if (dfaRight instanceof DfaVariableValue) { + LongRangeSet left = getRange(dfaLeft); + if (left != null) { + if (!applyRange(left.fromRelation(DfaRelationValue.getSymmetricOperation(dfaRelation.getComparisonOperation())), + (DfaVariableValue)dfaRight)) { + return false; + } + } + } + if (dfaLeft instanceof DfaTypeValue && ((DfaTypeValue)dfaLeft).isNotNull() && dfaRight == myFactory.getConstFactory().getNull()) { return isNegated; } @@ -946,6 +1007,38 @@ public class DfaMemoryStateImpl implements DfaMemoryState { return value instanceof DfaOptionalValue ? ThreeState.fromBoolean(((DfaOptionalValue)value).isPresent()) : ThreeState.UNSURE; } + /** + * Returns range of possible values for given DfaValue if possible + * + * @param value value to get the range from + * @return possible range or null if range is not known/non-applicable. Empty range indicates that no exact value is possible + * for given DfaValue (likely impossible code path). + */ + @Nullable + @Override + public LongRangeSet getRange(DfaValue value) { + if (value instanceof DfaVariableValue) { + DfaVariableValue var = (DfaVariableValue)value; + DfaVariableState state = getVariableState(var); + LongRangeSet range = state.getRange(); + if (range == null) { + DfaConstValue constValue = getConstantValue(var); + if (constValue != null) { + return LongRangeSet.fromConstant(constValue.getValue()); + } + return LongRangeSet.fromType(var.getVariableType()); + } + return range; + } + if (value instanceof DfaRangeValue) { + return ((DfaRangeValue)value).getValue(); + } + if (value instanceof DfaConstValue) { + return LongRangeSet.fromConstant(((DfaConstValue)value).getValue()); + } + return null; + } + @Nullable private DfaRelationValue compareToNull(DfaValue dfaVar, boolean negated) { DfaConstValue dfaNull = myFactory.getConstFactory().getNull(); diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaVariableState.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaVariableState.java index e61e10abf342..407b5e985a02 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaVariableState.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaVariableState.java @@ -24,44 +24,69 @@ */ package com.intellij.codeInspection.dataFlow; +import com.intellij.codeInspection.dataFlow.rangeSet.LongRangeSet; import com.intellij.codeInspection.dataFlow.value.DfaPsiType; import com.intellij.codeInspection.dataFlow.value.DfaTypeValue; import com.intellij.codeInspection.dataFlow.value.DfaValue; import com.intellij.codeInspection.dataFlow.value.DfaVariableValue; import com.intellij.openapi.util.text.StringUtil; -import com.intellij.psi.PsiPrimitiveType; +import com.intellij.psi.*; import com.intellij.util.ThreeState; import com.intellij.util.containers.ContainerUtil; +import com.siyeh.ig.psiutils.MethodUtils; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; class DfaVariableState { @NotNull final Set myInstanceofValues; @NotNull final Set myNotInstanceofValues; @NotNull final Nullness myNullability; @NotNull final ThreeState myOptionalPresence; + @Nullable final LongRangeSet myRange; private final int myHash; DfaVariableState(@NotNull DfaVariableValue dfaVar) { - this(Collections.emptySet(), Collections.emptySet(), dfaVar.getInherentNullability(), ThreeState.UNSURE); + this(Collections.emptySet(), Collections.emptySet(), dfaVar.getInherentNullability(), ThreeState.UNSURE, getInitialRange(dfaVar)); + } + + public boolean isSuperStateOf(DfaVariableState that) { + if(!myNotInstanceofValues.equals(that.myNotInstanceofValues)) return false; + if(!myInstanceofValues.equals(that.myNotInstanceofValues)) return false; + if(!myNullability.equals(that.myNullability)) return false; + if(!myOptionalPresence.equals(that.myOptionalPresence)) return false; + if(Objects.equals(myRange, that.myRange)) return true; + return myRange != null && that.myRange != null && myRange.contains(that.myRange); + } + + private static LongRangeSet getInitialRange(DfaVariableValue var) { + DfaVariableValue qualifier = var.getQualifier(); + if(qualifier != null) { + PsiModifierListOwner owner = var.getPsiVariable(); + boolean stringLength = owner instanceof PsiMethod && + MethodUtils.methodMatches((PsiMethod)owner, CommonClassNames.JAVA_LANG_STRING, PsiType.INT, "length"); + boolean arrayLength = + owner instanceof PsiField && "length".equals(((PsiField)owner).getName()) && qualifier.getVariableType() instanceof PsiArrayType; + if(stringLength || arrayLength) { + return LongRangeSet.indexRange(); + } + } + return LongRangeSet.fromType(var.getVariableType()); } DfaVariableState(@NotNull Set instanceofValues, - @NotNull Set notInstanceofValues, - @NotNull Nullness nullability, - @NotNull ThreeState optionalPresence) { + @NotNull Set notInstanceofValues, + @NotNull Nullness nullability, + @NotNull ThreeState optionalPresence, + @Nullable LongRangeSet range) { myInstanceofValues = instanceofValues; myNotInstanceofValues = notInstanceofValues; myNullability = nullability; myOptionalPresence = optionalPresence; - myHash = ((myInstanceofValues.hashCode() * 31 + myNotInstanceofValues.hashCode()) * 31 + myNullability.hashCode()) * 31 + - myOptionalPresence.hashCode(); + myRange = range; + myHash = Objects.hash(myInstanceofValues, myNotInstanceofValues, myNullability, myOptionalPresence, range); } public boolean isNullable() { @@ -101,7 +126,7 @@ class DfaVariableState { HashSet newInstanceof = ContainerUtil.newHashSet(myInstanceofValues); newInstanceof.removeAll(moreGeneric); newInstanceof.add(dfaType.getDfaType()); - result = createCopy(newInstanceof, myNotInstanceofValues, result.myNullability, myOptionalPresence); + result = createCopy(newInstanceof, myNotInstanceofValues, result.myNullability, myOptionalPresence, myRange); return result; } @@ -129,7 +154,7 @@ class DfaVariableState { HashSet newNotInstanceof = ContainerUtil.newHashSet(myNotInstanceofValues); newNotInstanceof.removeAll(moreSpecific); newNotInstanceof.add(dfaType.getDfaType()); - return createCopy(myInstanceofValues, newNotInstanceof, myNullability, myOptionalPresence); + return createCopy(myInstanceofValues, newNotInstanceof, myNullability, myOptionalPresence, myRange); } @NotNull @@ -137,12 +162,12 @@ class DfaVariableState { if (myInstanceofValues.contains(type)) { HashSet newInstanceof = ContainerUtil.newHashSet(myInstanceofValues); newInstanceof.remove(type); - return createCopy(newInstanceof, myNotInstanceofValues, myNullability, myOptionalPresence); + return createCopy(newInstanceof, myNotInstanceofValues, myNullability, myOptionalPresence, myRange); } if (myNotInstanceofValues.contains(type)) { HashSet newNotInstanceof = ContainerUtil.newHashSet(myNotInstanceofValues); newNotInstanceof.remove(type); - return createCopy(myInstanceofValues, newNotInstanceof, myNullability, myOptionalPresence); + return createCopy(myInstanceofValues, newNotInstanceof, myNullability, myOptionalPresence, myRange); } return this; } @@ -159,14 +184,17 @@ class DfaVariableState { myNullability == aState.myNullability && myOptionalPresence == aState.myOptionalPresence && myInstanceofValues.equals(aState.myInstanceofValues) && - myNotInstanceofValues.equals(aState.myNotInstanceofValues); + myNotInstanceofValues.equals(aState.myNotInstanceofValues) && + Objects.equals(myRange, aState.myRange); } @NotNull protected DfaVariableState createCopy(@NotNull Set instanceofValues, @NotNull Set notInstanceofValues, - @NotNull Nullness nullability, ThreeState optionalPresent) { - return new DfaVariableState(instanceofValues, notInstanceofValues, nullability, optionalPresent); + @NotNull Nullness nullability, + ThreeState optionalPresent, + LongRangeSet range) { + return new DfaVariableState(instanceofValues, notInstanceofValues, nullability, optionalPresent, range); } public String toString() { @@ -184,6 +212,9 @@ class DfaVariableState { if (myOptionalPresence != ThreeState.UNSURE) { buf.append(myOptionalPresence == ThreeState.YES ? " Optional with value" : " empty Optional"); } + if (myRange != null) { + buf.append(" ").append(myRange); + } return buf.toString(); } @@ -198,7 +229,7 @@ class DfaVariableState { @NotNull DfaVariableState withNullability(@NotNull Nullness nullness) { - return myNullability == nullness ? this : createCopy(myInstanceofValues, myNotInstanceofValues, nullness, myOptionalPresence); + return myNullability == nullness ? this : createCopy(myInstanceofValues, myNotInstanceofValues, nullness, myOptionalPresence, myRange); } @NotNull @@ -209,10 +240,16 @@ class DfaVariableState { DfaVariableState withOptionalPresense(final boolean presense) { ThreeState optionalPresent = ThreeState.fromBoolean(presense); return myOptionalPresence != optionalPresent - ? createCopy(myInstanceofValues, myNotInstanceofValues, myNullability, optionalPresent) + ? createCopy(myInstanceofValues, myNotInstanceofValues, myNullability, optionalPresent, myRange) : this; } + DfaVariableState withRange(@Nullable LongRangeSet range) { + return Objects.equals(range, myRange) + ? this + : createCopy(myInstanceofValues, myNotInstanceofValues, myNullability, myOptionalPresence, range); + } + @NotNull public DfaVariableState withValue(DfaValue value) { return this; @@ -234,4 +271,9 @@ class DfaVariableState { public ThreeState getOptionalPresense() { return myOptionalPresence; } + + @Nullable + public LongRangeSet getRange() { + return myRange; + } } diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/NullParameterConstraintChecker.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/NullParameterConstraintChecker.java index 00a28620bb0f..76392bb11707 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/NullParameterConstraintChecker.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/NullParameterConstraintChecker.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2016 JetBrains s.r.o. + * Copyright 2000-2017 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -130,7 +130,7 @@ class NullParameterConstraintChecker extends DataFlowRunner { super(factory); for (PsiParameter parameter : myPossiblyViolatedParameters) { setVariableState(getFactory().getVarFactory().createVariableValue(parameter, false), - new DfaVariableState(Collections.emptySet(), Collections.emptySet(), Nullness.NULLABLE, ThreeState.UNSURE)); + new DfaVariableState(Collections.emptySet(), Collections.emptySet(), Nullness.NULLABLE, ThreeState.UNSURE, null)); } } diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/StandardInstructionVisitor.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/StandardInstructionVisitor.java index 9b4e2984cdec..9e20e02ba97d 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/StandardInstructionVisitor.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/StandardInstructionVisitor.java @@ -16,6 +16,7 @@ package com.intellij.codeInspection.dataFlow; import com.intellij.codeInspection.dataFlow.instructions.*; +import com.intellij.codeInspection.dataFlow.rangeSet.LongRangeSet; import com.intellij.codeInspection.dataFlow.value.*; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Pair; @@ -29,6 +30,9 @@ import com.intellij.util.ThreeState; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.FactoryMap; import com.intellij.util.containers.MultiMap; +import com.siyeh.ig.callMatcher.CallMapper; +import com.siyeh.ig.callMatcher.CallMatcher; +import com.siyeh.ig.psiutils.ComparisonUtils; import com.siyeh.ig.psiutils.TypeUtils; import gnu.trove.THashSet; import org.jetbrains.annotations.NotNull; @@ -42,11 +46,18 @@ import static com.intellij.psi.JavaTokenType.*; * @author peter */ public class StandardInstructionVisitor extends InstructionVisitor { + private static final Logger LOG = Logger.getInstance("#com.intellij.codeInspection.dataFlow.StandardInstructionVisitor"); + private static final Object ANY_VALUE = new Object(); + private static final Set OPTIONAL_METHOD_NAMES = ContainerUtil.set("isPresent", "of", "ofNullable", "fromNullable", "empty", "absent", "or", "orElseGet", "ifPresent", "map", "flatMap", "filter", "transform"); - private static final Logger LOG = Logger.getInstance("#com.intellij.codeInspection.dataFlow.StandardInstructionVisitor"); - private static final Object ANY_VALUE = new Object(); + private static final CallMapper KNOWN_METHOD_RANGES = new CallMapper() + .register(CallMatcher.instanceCall(CommonClassNames.JAVA_LANG_STRING, "indexOf", "lastIndexOf"), + LongRangeSet.range(-1, Integer.MAX_VALUE)) + .register(CallMatcher.instanceCall("java.time.LocalDateTime", "getHour"), LongRangeSet.range(0, 23)) + .register(CallMatcher.instanceCall("java.time.LocalDateTime", "getMinute", "getSecond"), LongRangeSet.range(0, 59)); + private final Set myReachable = new THashSet<>(); private final Set myCanBeNullInInstanceof = new THashSet<>(); private final MultiMap myPossibleVariableValues = MultiMap.createSet(); @@ -451,6 +462,17 @@ public class StandardInstructionVisitor extends InstructionVisitor { } return factory.createTypeValue(type, nullability); } + DfaRangeValue rangeValue = factory.getRangeFactory().create(type); + if (rangeValue != null) { + PsiCall call = instruction.getCallExpression(); + if (call instanceof PsiMethodCallExpression) { + LongRangeSet range = KNOWN_METHOD_RANGES.mapFirst((PsiMethodCallExpression)call); + if (range != null) { + return rangeValue.intersect(range); + } + } + return rangeValue; + } return DfaUnknownValue.getInstance(); } @@ -476,6 +498,9 @@ public class StandardInstructionVisitor extends InstructionVisitor { final IElementType opSign = instruction.getOperationSign(); if (opSign != null) { DfaInstructionState[] states = handleConstantComparison(instruction, runner, memState, dfaRight, dfaLeft, opSign); + if (states == null) { + states = handleRangeComparison(instruction, runner, memState, dfaRight, dfaLeft, opSign); + } if (states == null) { states = handleRelationBinop(instruction, runner, memState, dfaRight, dfaLeft); } @@ -557,6 +582,27 @@ public class StandardInstructionVisitor extends InstructionVisitor { myUsefulInstanceofs.add(instruction); } + @Nullable + private static DfaInstructionState[] handleRangeComparison(BinopInstruction instruction, + DataFlowRunner runner, + DfaMemoryState state, + DfaValue right, + DfaValue left, IElementType sign) { + LongRangeSet leftRange = state.getRange(left); + if (leftRange == null) return null; + LongRangeSet rightRange = state.getRange(right); + if (rightRange == null) return null; + LongRangeSet constraint = rightRange.fromRelation(sign); + if (constraint != null && !constraint.intersects(leftRange)) { + return alwaysFalse(instruction, runner, state); + } + LongRangeSet revConstraint = rightRange.fromRelation(ComparisonUtils.getNegatedComparisonTokenType(sign)); + if (revConstraint != null && !revConstraint.intersects(leftRange)) { + return alwaysTrue(instruction, runner, state); + } + return null; + } + @Nullable private static DfaInstructionState[] handleConstantComparison(BinopInstruction instruction, DataFlowRunner runner, @@ -608,26 +654,11 @@ public class StandardInstructionVisitor extends InstructionVisitor { DfaMemoryState memState, DfaVariableValue var, IElementType opSign, Number comparedWith) { - Object knownValue = getKnownNumberValue(memState, var); + Number knownValue = getKnownNumberValue(memState, var); if (knownValue != null) { - return checkComparisonWithKnownValue(instruction, runner, memState, opSign, (Number)knownValue, comparedWith); + return checkComparisonWithKnownValue(instruction, runner, memState, opSign, knownValue, comparedWith); } - - PsiType varType = var.getVariableType(); - if (!(varType instanceof PsiPrimitiveType)) return null; - - if (PsiType.FLOAT.equals(varType) || PsiType.DOUBLE.equals(varType)) return null; - - double minValue = PsiType.BYTE.equals(varType) ? Byte.MIN_VALUE : PsiType.SHORT.equals(varType) - ? Short.MIN_VALUE : PsiType.INT.equals(varType) - ? Integer.MIN_VALUE : PsiType.CHAR.equals(varType) ? Character.MIN_VALUE : - Long.MIN_VALUE; - double maxValue = PsiType.BYTE.equals(varType) ? Byte.MAX_VALUE : PsiType.SHORT.equals(varType) - ? Short.MAX_VALUE : PsiType.INT.equals(varType) - ? Integer.MAX_VALUE : PsiType.CHAR.equals(varType) ? Character.MAX_VALUE : - Long.MAX_VALUE; - - return checkComparisonWithKnownRange(instruction, runner, memState, opSign, comparedWith, minValue, maxValue); + return null; } @Nullable @@ -642,7 +673,28 @@ public class StandardInstructionVisitor extends InstructionVisitor { IElementType opSign, Number leftValue, Number rightValue) { - return checkComparisonWithKnownRange(instruction, runner, memState, opSign, rightValue, leftValue, leftValue); + int cmp = compare(leftValue, rightValue); + Boolean result = null; + if (cmp < 0 || cmp > 0) { + if(opSign == EQEQ) result = false; + else if (opSign == NE) result = true; + } + if (opSign == LT) { + result = cmp < 0; + } + else if (opSign == GT) { + result = cmp > 0; + } + else if (opSign == LE) { + result = cmp <= 0; + } + else if (opSign == GE) { + result = cmp >= 0; + } + if (result == null) { + return null; + } + return result ? alwaysTrue(instruction, runner, memState) : alwaysFalse(instruction, runner, memState); } private static int compare(Number a, Number b) { @@ -653,32 +705,6 @@ public class StandardInstructionVisitor extends InstructionVisitor { return Double.compare(a.doubleValue(), b.doubleValue()); } - @Nullable - private static DfaInstructionState[] checkComparisonWithKnownRange(BinopInstruction instruction, - DataFlowRunner runner, - DfaMemoryState memState, - IElementType opSign, - Number comparedWith, - Number rangeMin, - Number rangeMax) { - if (compare(comparedWith, rangeMin) < 0 || compare(comparedWith, rangeMax) > 0) { - if (opSign == EQEQ) return alwaysFalse(instruction, runner, memState); - if (opSign == NE) return alwaysTrue(instruction, runner, memState); - } - - if (opSign == LT && compare(comparedWith, rangeMin) <= 0) return alwaysFalse(instruction, runner, memState); - if (opSign == LT && compare(comparedWith, rangeMax) > 0) return alwaysTrue(instruction, runner, memState); - if (opSign == LE && compare(comparedWith, rangeMax) >= 0) return alwaysTrue(instruction, runner, memState); - if (opSign == LE && compare(comparedWith, rangeMin) < 0) return alwaysFalse(instruction, runner, memState); - - if (opSign == GT && compare(comparedWith, rangeMax) >= 0) return alwaysFalse(instruction, runner, memState); - if (opSign == GT && compare(comparedWith, rangeMin) < 0) return alwaysTrue(instruction, runner, memState); - if (opSign == GE && compare(comparedWith, rangeMin) <= 0) return alwaysTrue(instruction, runner, memState); - if (opSign == GE && compare(comparedWith, rangeMax) > 0) return alwaysFalse(instruction, runner, memState); - - return null; - } - private static DfaInstructionState[] alwaysFalse(BinopInstruction instruction, DataFlowRunner runner, DfaMemoryState memState) { memState.push(runner.getFactory().getConstFactory().getFalse()); instruction.setFalseReachable(); diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/StateMerger.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/StateMerger.java index 4ad30e86b454..776bdae8f97f 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/StateMerger.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/StateMerger.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2015 JetBrains s.r.o. + * Copyright 2000-2017 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,13 +15,14 @@ */ package com.intellij.codeInspection.dataFlow; +import com.intellij.codeInspection.dataFlow.rangeSet.LongRangeSet; import com.intellij.codeInspection.dataFlow.value.*; import com.intellij.openapi.progress.ProgressManager; -import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.UnorderedPair; import com.intellij.psi.JavaTokenType; import com.intellij.util.Function; import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.HashMap; import com.intellij.util.containers.MultiMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -32,6 +33,7 @@ import java.util.*; * @author peter */ class StateMerger { + public static final int MAX_RANGE_STATES = 100; private final Map> myFacts = ContainerUtil.newIdentityHashMap(); private final Map> myCopyCache = ContainerUtil.newIdentityHashMap(); @@ -203,6 +205,57 @@ class StateMerger { return replacements.getMergeResult(); } + @Nullable + List mergeByRanges(List states) { + // If the same variable has different range A and B in different memState and range A contains range B + // then range A is replaced with range B + Map> ranges = new LinkedHashMap<>(); + for (DfaMemoryStateImpl state : states) { + Map variableStates = state.getVariableStates(); + for (Map.Entry entry : variableStates.entrySet()) { + LongRangeSet range = entry.getValue().getRange(); + if (range != null) { + ranges.computeIfAbsent(entry.getKey(), k -> new HashMap<>()).put(range, range); + } + } + } + boolean changed = false; + for (Map map : ranges.values()) { + for (Map.Entry entry : map.entrySet()) { + for(LongRangeSet candidate : map.values()) { + if(!entry.getValue().equals(candidate) && candidate.contains(entry.getValue())) { + entry.setValue(candidate); + changed = true; + } + } + } + } + if(changed) { + changed = false; + for (DfaMemoryStateImpl state : states) { + for (Map.Entry> entry : ranges.entrySet()) { + DfaVariableState variableState = state.getVariableState(entry.getKey()); + LongRangeSet range = variableState.getRange(); + LongRangeSet boundingRange = entry.getValue().get(range); + if (boundingRange != null && !boundingRange.equals(range)) { + state.setRange(entry.getKey(), boundingRange); + changed = true; + } + } + } + if(changed) { + return new ArrayList<>(new LinkedHashSet<>(states)); + } + } + if (states.size() <= MAX_RANGE_STATES || ranges.isEmpty()) return null; + // If there are too many states, try to drop range information from some variable + DfaVariableValue lastVar = Collections.max(ranges.keySet(), Comparator.comparing(DfaVariableValue::getID)); + for (DfaMemoryStateImpl state : states) { + state.setRange(lastVar, null); + } + return new ArrayList<>(new HashSet<>(states)); + } + private static boolean mergeUnknowns(@NotNull Replacements replacements, @NotNull List complementary) { if (complementary.size() < 2) return false; @@ -220,10 +273,7 @@ class StateMerger { @NotNull private DfaMemoryStateImpl copyWithoutVar(@NotNull DfaMemoryStateImpl state, @NotNull DfaVariableValue var) { - Map map = myCopyCache.get(state); - if (map == null) { - myCopyCache.put(state, map = ContainerUtil.newIdentityHashMap()); - } + Map map = myCopyCache.computeIfAbsent(state, k -> ContainerUtil.newIdentityHashMap()); DfaMemoryStateImpl copy = map.get(var); if (copy == null) { copy = state.createCopy(); diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/ValuableDataFlowRunner.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/ValuableDataFlowRunner.java index addf17873a3c..73b7f0cb897f 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/ValuableDataFlowRunner.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/ValuableDataFlowRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2015 JetBrains s.r.o. + * Copyright 2000-2017 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package com.intellij.codeInspection.dataFlow; +import com.intellij.codeInspection.dataFlow.rangeSet.LongRangeSet; import com.intellij.codeInspection.dataFlow.value.DfaPsiType; import com.intellij.codeInspection.dataFlow.value.DfaValue; import com.intellij.codeInspection.dataFlow.value.DfaValueFactory; @@ -78,8 +79,10 @@ class ValuableDataFlowRunner extends DataFlowRunner { private ValuableDfaVariableState(Set instanceofValues, Set notInstanceofValues, Nullness nullability, DfaValue value, - @NotNull FList concatenation, ThreeState optionalPresence) { - super(instanceofValues, notInstanceofValues, nullability, optionalPresence); + @NotNull FList concatenation, + ThreeState optionalPresence, + LongRangeSet range) { + super(instanceofValues, notInstanceofValues, nullability, optionalPresence, range); myValue = value; myConcatenation = concatenation; } @@ -89,8 +92,10 @@ class ValuableDataFlowRunner extends DataFlowRunner { protected DfaVariableState createCopy(@NotNull Set instanceofValues, @NotNull Set notInstanceofValues, @NotNull Nullness nullability, - ThreeState optionalPresence) { - return new ValuableDfaVariableState(instanceofValues, notInstanceofValues, nullability, myValue, myConcatenation, optionalPresence); + ThreeState optionalPresence, + LongRangeSet range) { + return new ValuableDfaVariableState(instanceofValues, notInstanceofValues, nullability, myValue, myConcatenation, optionalPresence, + range); } @NotNull @@ -98,13 +103,13 @@ class ValuableDataFlowRunner extends DataFlowRunner { public DfaVariableState withValue(@Nullable final DfaValue value) { if (value == myValue) return this; return new ValuableDfaVariableState(myInstanceofValues, myNotInstanceofValues, myNullability, value, myConcatenation, - myOptionalPresence); + myOptionalPresence, myRange); } ValuableDfaVariableState withExpression(@NotNull final FList concatenation) { if (concatenation == myConcatenation) return this; return new ValuableDfaVariableState(myInstanceofValues, myNotInstanceofValues, myNullability, myValue, concatenation, - myOptionalPresence); + myOptionalPresence, myRange); } @Override diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/rangeSet/LongRangeSet.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/rangeSet/LongRangeSet.java new file mode 100644 index 000000000000..19c28e2eab50 --- /dev/null +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/rangeSet/LongRangeSet.java @@ -0,0 +1,638 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.codeInspection.dataFlow.rangeSet; + +import com.intellij.psi.JavaTokenType; +import com.intellij.psi.PsiPrimitiveType; +import com.intellij.psi.PsiType; +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.NoSuchElementException; + +/** + * An immutable set of long values optimized for small number of ranges. + * + * @author Tagir Valeev + */ +public abstract class LongRangeSet { + LongRangeSet() {} + + /** + * Subtracts given set from the current + * + * @param other set to subtract + * @return a new set + */ + public abstract LongRangeSet subtract(LongRangeSet other); + + public LongRangeSet gt(long value) { + return subtract(range(Long.MIN_VALUE, value)); + } + + public LongRangeSet ge(long value) { + return value == Long.MIN_VALUE ? this : subtract(range(Long.MIN_VALUE, value - 1)); + } + + public LongRangeSet lt(long value) { + return subtract(range(value, Long.MAX_VALUE)); + } + + public LongRangeSet le(long value) { + return value == Long.MAX_VALUE ? this : subtract(range(value + 1, Long.MAX_VALUE)); + } + + public LongRangeSet eq(long value) { + return contains(value) ? point(value) : Empty.EMPTY; + } + + public LongRangeSet ne(long value) { + return subtract(point(value)); + } + + /** + * @return true if set is empty + */ + public boolean isEmpty() { + return this == Empty.EMPTY; + } + + /** + * Intersects current set with other + * + * @param other other set to intersect with + * @return a new set + */ + public abstract LongRangeSet intersect(LongRangeSet other); + + /** + * @return a minimal value contained in the set + * @throws NoSuchElementException if set is empty + */ + public abstract long min(); + + /** + * @return a maximal value contained in the set + * @throws NoSuchElementException if set is empty + */ + public abstract long max(); + + /** + * Checks if current set and other set have at least one common element + * + * @param other other set to check whether intersection exists + * @return true if this set intersects other set + */ + public abstract boolean intersects(LongRangeSet other); + + /** + * Checks whether current set contains given value + * + * @param value value to find + * @return true if current set contains given value + */ + public abstract boolean contains(long value); + + /** + * Checks whether current set contains all the values from other set + * + * @param other a sub-set candidate + * @return true if current set contains all the values from other + */ + public abstract boolean contains(LongRangeSet other); + + /** + * Creates a new set which contains all possible values satisfying given predicate regarding the current set. + *

+ * E.g. if current set is {0..10} and relation is "GT", then result will be {1..Long.MAX_VALUE} (values which can be greater than + * some value from the current set) + * + * @param relation relation to be applied to current set (JavaTokenType.EQEQ/NE/GT/GE/LT/LE) + * @return new set or null if relation is unsupported + */ + public LongRangeSet fromRelation(IElementType relation) { + if (isEmpty()) return null; + if (JavaTokenType.EQEQ.equals(relation)) { + return this; + } + if (JavaTokenType.NE.equals(relation)) { + long min = min(); + if (min == max()) return Range.LONG_RANGE.subtract(this); + return Range.LONG_RANGE; + } + if (JavaTokenType.GT.equals(relation)) { + long min = min(); + return min == Long.MAX_VALUE ? empty() : range(min + 1, Long.MAX_VALUE); + } + if (JavaTokenType.GE.equals(relation)) { + return range(min(), Long.MAX_VALUE); + } + if (JavaTokenType.LE.equals(relation)) { + return range(Long.MIN_VALUE, max()); + } + if (JavaTokenType.LT.equals(relation)) { + long max = max(); + return max == Long.MIN_VALUE ? empty() : range(Long.MIN_VALUE, max - 1); + } + return null; + } + + /** + * @return an empty set + */ + public static LongRangeSet empty() { + return Empty.EMPTY; + } + + /** + * Creates a set containing single given value + * + * @param value a value to be included into the set + * @return a new set + */ + public static LongRangeSet point(long value) { + return new Point(value); + } + + /** + * Creates a set containing single value which is equivalent to supplied boxed constant (if its type is supported) + * + * @param val constant to create a set from + * @return new LongRangeSet or null if constant type is unsupported + */ + @Nullable + public static LongRangeSet fromConstant(Object val) { + if (val instanceof Byte || val instanceof Short || val instanceof Integer || val instanceof Long) { + return point(((Number)val).longValue()); + } + else if (val instanceof Character) { + return point(((Character)val).charValue()); + } + return null; + } + + /** + * Creates a new set which contains all the numbers between from (inclusive) and to (inclusive) + * + * @param from lower bound + * @param to upper bound (must be greater or equal to {@code from}) + * @return a new LongRangeSet + */ + public static LongRangeSet range(long from, long to) { + return from == to ? new Point(from) : new Range(from, to); + } + + abstract long[] asRanges(); + + static String toString(long from, long to) { + return from == to ? String.valueOf(from) : from + (to - from == 1 ? ", " : "..") + to; + } + + /** + * @return LongRangeSet describing possible array or string indices (from 0 to Integer.MAX_VALUE) + */ + public static LongRangeSet indexRange() { + return Range.INDEX_RANGE; + } + + /** + * Creates a range for given type (for primitives and boxed: values range) + * + * @param type type to create a range for + * @return a range or null if type is not supported + */ + @Nullable + public static LongRangeSet fromType(PsiType type) { + if (type == null) { + return null; + } + type = PsiPrimitiveType.getOptionallyUnboxedType(type); + if (type != null) { + if (type.equals(PsiType.BYTE)) { + return Range.BYTE_RANGE; + } + if (type.equals(PsiType.CHAR)) { + return Range.CHAR_RANGE; + } + if (type.equals(PsiType.SHORT)) { + return Range.SHORT_RANGE; + } + if (type.equals(PsiType.INT)) { + return Range.INT_RANGE; + } + if (type.equals(PsiType.LONG)) { + return Range.LONG_RANGE; + } + } + return null; + } + + static LongRangeSet fromRanges(long[] ranges, int bound) { + if (bound == 0) { + return Empty.EMPTY; + } + else if (bound == 2) { + return range(ranges[0], ranges[1]); + } + else { + return new RangeSet(Arrays.copyOfRange(ranges, 0, bound)); + } + } + + static final class Empty extends LongRangeSet { + static final LongRangeSet EMPTY = new Empty(); + + @Override + public LongRangeSet subtract(LongRangeSet other) { + return this; + } + + @Override + public LongRangeSet intersect(LongRangeSet other) { + return this; + } + + @Override + public long min() { + throw new NoSuchElementException(); + } + + @Override + public long max() { + throw new NoSuchElementException(); + } + + @Override + public boolean intersects(LongRangeSet other) { + return false; + } + + @Override + public boolean contains(long value) { + return false; + } + + @Override + public boolean contains(LongRangeSet other) { + return other.isEmpty(); + } + + @Override + long[] asRanges() { + return new long[0]; + } + + @Override + public int hashCode() { + return 2154231; + } + + @Override + public boolean equals(Object obj) { + return obj == this; + } + + @Override + public String toString() { + return "{}"; + } + } + + static final class Point extends LongRangeSet { + final long myValue; + + Point(long value) { + myValue = value; + } + + @Override + public LongRangeSet subtract(LongRangeSet other) { + return other.contains(myValue) ? Empty.EMPTY : this; + } + + @Override + public LongRangeSet intersect(LongRangeSet other) { + return other.contains(myValue) ? this : Empty.EMPTY; + } + + @Override + public long min() { + return myValue; + } + + @Override + public long max() { + return myValue; + } + + @Override + public boolean intersects(LongRangeSet other) { + return other.contains(myValue); + } + + @Override + public boolean contains(long value) { + return myValue == value; + } + + @Override + public boolean contains(LongRangeSet other) { + return other.isEmpty() || equals(other); + } + + @Override + long[] asRanges() { + return new long[] {myValue, myValue}; + } + + @Override + public int hashCode() { + return Long.hashCode(myValue); + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + return o != null && o instanceof Point && myValue == ((Point)o).myValue; + } + + @Override + public String toString() { + return "{" + myValue + "}"; + } + } + + static final class Range extends LongRangeSet { + static final Range BYTE_RANGE = new Range(Byte.MIN_VALUE, Byte.MAX_VALUE); + static final Range CHAR_RANGE = new Range(Character.MIN_VALUE, Character.MAX_VALUE); + static final Range SHORT_RANGE = new Range(Short.MIN_VALUE, Short.MAX_VALUE); + static final Range INT_RANGE = new Range(Integer.MIN_VALUE, Integer.MAX_VALUE); + static final Range LONG_RANGE = new Range(Long.MIN_VALUE, Long.MAX_VALUE); + static final Range INDEX_RANGE = new Range(0, Integer.MAX_VALUE); + + final long myFrom; // inclusive + final long myTo; // inclusive + + Range(long from, long to) { + if (to <= from) { // to == from => must be Point + throw new IllegalArgumentException(to + "<=" + from); + } + myFrom = from; + myTo = to; + } + + @Override + public LongRangeSet subtract(LongRangeSet other) { + if (other.isEmpty()) return this; + if (other == this) return Empty.EMPTY; + if (other instanceof Point) { + long value = ((Point)other).myValue; + if (value < myFrom || value > myTo) return this; + if (value == myFrom) return range(myFrom + 1, myTo); + if (value == myTo) return range(myFrom, myTo - 1); + return new RangeSet(new long[]{myFrom, value - 1, value + 1, myTo}); + } + if (other instanceof Range) { + long from = ((Range)other).myFrom; + long to = ((Range)other).myTo; + if (to < myFrom || from > myTo) return this; + if (from <= myFrom && to >= myTo) return Empty.EMPTY; + if (from > myFrom && to < myTo) { + return new RangeSet(new long[]{myFrom, from - 1, to + 1, myTo}); + } + if (from <= myFrom) { + return range(to + 1, myTo); + } + if (to >= myTo) { + return range(myFrom, from - 1); + } + throw new InternalError("Impossible: " + this + ":" + other); + } + long[] ranges = ((RangeSet)other).myRanges; + LongRangeSet result = this; + for (int i = 0; i < ranges.length; i += 2) { + result = result.subtract(range(ranges[i], ranges[i + 1])); + if (result.isEmpty()) return result; + } + return result; + } + + @Override + public LongRangeSet intersect(LongRangeSet other) { + if (other == this) return this; + if (other.isEmpty()) return other; + if (other instanceof Point) { + return other.intersect(this); + } + if (other instanceof Range) { + long from = ((Range)other).myFrom; + long to = ((Range)other).myTo; + if (from <= myFrom && to >= myTo) return this; + if (from >= myFrom && to <= myTo) return other; + if (from < myFrom) { + from = myFrom; + } + if (to > myTo) { + to = myTo; + } + return from <= to ? range(from, to) : Empty.EMPTY; + } + long[] ranges = ((RangeSet)other).myRanges; + long[] result = new long[ranges.length]; + int index = 0; + for (int i = 0; i < ranges.length; i += 2) { + long[] res = intersect(range(ranges[i], ranges[i + 1])).asRanges(); + System.arraycopy(res, 0, result, index, res.length); + index += res.length; + } + return fromRanges(result, index); + } + + @Override + public long min() { + return myFrom; + } + + @Override + public long max() { + return myTo; + } + + @Override + public boolean intersects(LongRangeSet other) { + if (other.isEmpty()) return false; + if (other instanceof RangeSet) { + return other.intersects(this); + } + return myTo >= other.min() && myFrom <= other.max(); + } + + @Override + public boolean contains(long value) { + return myFrom <= value && myTo >= value; + } + + @Override + public boolean contains(LongRangeSet other) { + return other.isEmpty() || other.min() >= myFrom && other.max() <= myTo; + } + + @Override + long[] asRanges() { + return new long[] {myFrom, myTo}; + } + + @Override + public int hashCode() { + return Long.hashCode(myFrom) * 1337 + Long.hashCode(myTo); + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + return o != null && o instanceof Range && myFrom == ((Range)o).myFrom && myTo == ((Range)o).myTo; + } + + @Override + public String toString() { + return "{" + toString(myFrom, myTo) + "}"; + } + } + + static final class RangeSet extends LongRangeSet { + final long[] myRanges; + + RangeSet(long[] ranges) { + if (ranges.length < 4 || ranges.length % 2 != 0) { + // 0 ranges = Empty; 1 range = Range + throw new IllegalArgumentException("Bad length: " + ranges.length + " " + Arrays.toString(ranges)); + } + for (int i = 0; i < ranges.length; i += 2) { + if (ranges[i + 1] < ranges[i]) { + throw new IllegalArgumentException("Bad sub-range #" + (i / 2) + " " + Arrays.toString(ranges)); + } + if (i > 0 && (ranges[i - 1] == Long.MAX_VALUE || 1 + ranges[i - 1] > ranges[i])) { + throw new IllegalArgumentException("Bad sub-ranges #" + (i / 2 - 1) + " and #" + (i / 2) + " " + Arrays.toString(ranges)); + } + } + myRanges = ranges; + } + + @Override + public LongRangeSet subtract(LongRangeSet other) { + if (other.isEmpty()) return this; + if (other == this) return Empty.EMPTY; + long[] result = new long[myRanges.length + other.asRanges().length]; + int index = 0; + for (int i = 0; i < myRanges.length; i += 2) { + LongRangeSet res = range(myRanges[i], myRanges[i + 1]).subtract(other); + long[] ranges = res.asRanges(); + System.arraycopy(ranges, 0, result, index, ranges.length); + index += ranges.length; + } + return fromRanges(result, index); + } + + @Override + public LongRangeSet intersect(LongRangeSet other) { + if (other == this) return this; + if (other.isEmpty()) return other; + if (other instanceof Point || other instanceof Range) { + return other.intersect(this); + } + return subtract(Range.LONG_RANGE.subtract(other)); + } + + @Override + public long min() { + return myRanges[0]; + } + + @Override + public long max() { + return myRanges[myRanges.length - 1]; + } + + @Override + public boolean intersects(LongRangeSet other) { + if (other.isEmpty()) return false; + if (other instanceof Point) { + return contains(((Point)other).myValue); + } + long[] otherRanges = other.asRanges(); + int a = 0, b = 0; + while (true) { + long aFrom = myRanges[a]; + long aTo = myRanges[a + 1]; + long bFrom = otherRanges[b]; + long bTo = otherRanges[b + 1]; + if (aFrom <= bTo && bFrom <= aTo) return true; + if (aFrom > bTo) { + b += 2; + if (b >= otherRanges.length) return false; + } + else { + a += 2; + if (a >= myRanges.length) return false; + } + } + } + + @Override + public boolean contains(long value) { + for (int i = 0; i < myRanges.length; i += 2) { + if (value >= myRanges[i] && value <= myRanges[i + 1]) { + return true; + } + } + return false; + } + + @Override + public boolean contains(LongRangeSet other) { + if (other.isEmpty() || other == this) return true; + return other.subtract(this).isEmpty(); + } + + @Override + long[] asRanges() { + return myRanges; + } + + @Override + public int hashCode() { + return Arrays.hashCode(myRanges); + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + return o != null && o instanceof RangeSet && Arrays.equals(myRanges, ((RangeSet)o).myRanges); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("{"); + for (int i = 0; i < myRanges.length; i += 2) { + if (i > 0) sb.append(", "); + sb.append(LongRangeSet.toString(myRanges[i], myRanges[i + 1])); + } + sb.append("}"); + return sb.toString(); + } + } +} diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/value/DfaExpressionFactory.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/value/DfaExpressionFactory.java index 5960f87b6933..731c30ed1c05 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/value/DfaExpressionFactory.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/value/DfaExpressionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2015 JetBrains s.r.o. + * Copyright 2000-2017 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import com.intellij.psi.util.PropertyUtil; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.util.PsiUtil; import com.intellij.util.containers.ContainerUtil; +import com.siyeh.ig.psiutils.MethodUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -174,6 +175,9 @@ public class DfaExpressionFactory { return method; } } + if (MethodUtils.methodMatches(method, CommonClassNames.JAVA_LANG_STRING, PsiType.INT, "length")) { + return method; + } if (AnnotationUtil.findAnnotation(method.getContainingClass(), "javax.annotation.concurrent.Immutable") != null) { return method; } diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/value/DfaRangeValue.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/value/DfaRangeValue.java new file mode 100644 index 000000000000..47825a92cf30 --- /dev/null +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/value/DfaRangeValue.java @@ -0,0 +1,70 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.codeInspection.dataFlow.value; + +import com.intellij.codeInspection.dataFlow.rangeSet.LongRangeSet; +import com.intellij.psi.PsiType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Tagir Valeev + */ +public class DfaRangeValue extends DfaValue { + private final LongRangeSet myValue; + + DfaRangeValue(DfaValueFactory factory, @NotNull LongRangeSet value) { + super(factory); + myValue = value; + } + + public DfaRangeValue intersect(LongRangeSet value) { + return myFactory.getRangeFactory().create(myValue.intersect(value)); + } + + public LongRangeSet getValue() { + return myValue; + } + + public static class Factory { + private Map myValues = new HashMap<>(); + private DfaValueFactory myFactory; + + Factory(DfaValueFactory factory) { + myFactory = factory; + } + + /** + * Any value of given type (if type is supported) + * + * @param type type to create a range-value from + * @return DfaRangeValue representing range of given type + */ + @Nullable + public DfaRangeValue create(PsiType type) { + LongRangeSet domain = LongRangeSet.fromType(type); + return domain == null ? null : create(domain); + } + + @NotNull + public DfaRangeValue create(LongRangeSet value) { + return myValues.computeIfAbsent(value, val -> new DfaRangeValue(myFactory, val)); + } + } +} diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/value/DfaRelationValue.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/value/DfaRelationValue.java index cb2d4f1689ce..c77b320603ec 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/value/DfaRelationValue.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/value/DfaRelationValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2009 JetBrains s.r.o. + * Copyright 2000-2017 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import com.intellij.psi.tree.IElementType; import com.intellij.util.containers.HashMap; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; @@ -168,6 +169,23 @@ public class DfaRelationValue extends DfaValue { return myRelation == EQEQ && myIsNegated || myRelation == GT && !myIsNegated || myRelation == GE && myIsNegated; } + /** + * @return comparison operation (GT, GE, LE, LT, EQEQ, NE) if this relation represents comparison, null otherwise + */ + @Nullable + public IElementType getComparisonOperation() { + if(myRelation == GT) { + return myIsNegated ? LE : GT; + } + if(myRelation == GE) { + return myIsNegated ? LT : GE; + } + if(myRelation == EQEQ) { + return myIsNegated ? NE : EQEQ; + } + return null; + } + public boolean isInstanceOf() { return myRelation == INSTANCEOF_KEYWORD; } diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/value/DfaValueFactory.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/value/DfaValueFactory.java index dc5ccaf45280..830eb753ab2d 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/value/DfaValueFactory.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/value/DfaValueFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2015 JetBrains s.r.o. + * Copyright 2000-2017 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,6 +60,7 @@ public class DfaValueFactory { myRelationFactory = new DfaRelationValue.Factory(this); myExpressionFactory = new DfaExpressionFactory(this); myOptionalFactory = new DfaOptionalValue.Factory(this); + myRangeFactory = new DfaRangeValue.Factory(this); } public boolean isHonorFieldInitializers() { @@ -137,6 +138,7 @@ public class DfaValueFactory { private final DfaRelationValue.Factory myRelationFactory; private final DfaExpressionFactory myExpressionFactory; private final DfaOptionalValue.Factory myOptionalFactory; + private final DfaRangeValue.Factory myRangeFactory; @NotNull public DfaVariableValue.Factory getVarFactory() { @@ -166,4 +168,9 @@ public class DfaValueFactory { public DfaOptionalValue.Factory getOptionalFactory() { return myOptionalFactory; } + + @NotNull + public DfaRangeValue.Factory getRangeFactory() { + return myRangeFactory; + } } diff --git a/java/java-tests/testData/inspection/dataFlow/fixture/LongRangeBasics.java b/java/java-tests/testData/inspection/dataFlow/fixture/LongRangeBasics.java new file mode 100644 index 000000000000..774ef7470f41 --- /dev/null +++ b/java/java-tests/testData/inspection/dataFlow/fixture/LongRangeBasics.java @@ -0,0 +1,91 @@ +public class LongRangeBasics { + void testSwitch(int i) { + switch (i) { + case 0: + System.out.println("0"); + break; + case 1: + System.out.println("1"); + return; + case 2: + System.out.println("2"); + return; + default: + System.out.println("default"); + break; + } + if(i == 0) { + System.out.println("ouch"); + } + // i > 0 and i < 3 means (i == 1 || i == 2); in both cases we already returned + if(i > 0 && i < 3) { + System.out.println("oops"); + } + } + + void test(int i) { + if(i > 5) { + if(i < 0) { + System.out.println("Hello"); + } + } + } + + void test2(char c) { + int i = c; + if(i > 0x10000) { + System.out.println("Hello"); + } + } + + void test3(String s) { + int i = s.charAt(0); + if(i > 0x10000) { + System.out.println("Hello"); + } + } + + void test4(String s) { + if(s.charAt(0) < 0x10000) { + System.out.println("Hello"); + } + } + + void test1(int i, int j) { + if(i > 0 && j > i) { + // j > i which is > 0 means that j >= 2 + if(j == 1) { + if(i < 0) { + System.out.println("oops"); + } + } + } + } + + void testLength(String s) { + if(s.length() < 2) { + if(s.length() > 4) { + System.out.println("Never"); + } + if(s.length() == 1) { + System.out.println("One"); + } else if(s.length() == 0) { + System.out.println("Empty"); + } + } + if(s.length() < 0) { + System.out.println("Never"); + } + } + + void testArrayLength(int[] arr) { + if (arr.length > 0) { + System.out.println("Ok"); + } else if(arr.length == 0) { + System.out.println("Empty"); + } + if (arr.length < 0) { + System.out.println("Impossible"); + } + } +} diff --git a/java/java-tests/testData/inspection/dataFlow/fixture/LongRangeKnownMethods.java b/java/java-tests/testData/inspection/dataFlow/fixture/LongRangeKnownMethods.java new file mode 100644 index 000000000000..52ed4c792620 --- /dev/null +++ b/java/java-tests/testData/inspection/dataFlow/fixture/LongRangeKnownMethods.java @@ -0,0 +1,18 @@ +import java.time.LocalDateTime; + +public class LongRangeKnownMethods { + void testIndexOf(String s) { + int idx = s.indexOf("xyz"); + if(idx >= 0) { + System.out.println("Found"); + } else if(idx == -1) { + System.out.println("Not found"); + } + } + + void testLocalDateTime(LocalDateTime ldt) { + if(ldt.getHour() == 24) System.out.println(1); + if(ldt.getMinute() >= 0) System.out.println(2); + if(ldt.getSecond() >= 60) System.out.println(3); + } +} diff --git a/java/java-tests/testData/inspection/dataFlow/fixture/LongRangeLoop.java b/java/java-tests/testData/inspection/dataFlow/fixture/LongRangeLoop.java new file mode 100644 index 000000000000..0cadb272e057 --- /dev/null +++ b/java/java-tests/testData/inspection/dataFlow/fixture/LongRangeLoop.java @@ -0,0 +1,36 @@ +import java.util.Collection; + +public class LongRangeLoop { + public static int min(int[] values) { + int min = Integer.MAX_VALUE; + for (int value : values) { + if (value < min) min = value; + } + if (min > 0 && min < -10) { + System.out.println("Invalid result"); + } + return min; + } + + public static void boundedForLoop() { + for(int i=0; i<10; i++) { + if(i == 20) { + System.out.println("Oops"); + } + } + } + + public void testVariablesAreInDeclarationOrder(String file, Collection vars) throws Exception { + int previousIndex = -1; + for (String each : vars) { + int index = file.indexOf(each); + if(index <= previousIndex) { + throw new AssertionError(); + } + previousIndex = index; + if(index == -1) { + System.out.println("Impossible"); + } + } + } +} diff --git a/java/java-tests/testSrc/com/intellij/codeInspection/DataFlowInspectionTestSuite.java b/java/java-tests/testSrc/com/intellij/codeInspection/DataFlowInspectionTestSuite.java index 63519db6474a..9195edca4d6b 100644 --- a/java/java-tests/testSrc/com/intellij/codeInspection/DataFlowInspectionTestSuite.java +++ b/java/java-tests/testSrc/com/intellij/codeInspection/DataFlowInspectionTestSuite.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2013 JetBrains s.r.o. + * Copyright 2000-2017 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ public class DataFlowInspectionTestSuite { suite.addTestSuite(DataFlowInspectionAncientTest.class); suite.addTestSuite(ContractCheckTest.class); suite.addTestSuite(HardcodedContractsTest.class); + suite.addTestSuite(DataFlowRangeAnalysisTest.class); suite.addTestSuite(ContractInferenceFromSourceTest.class); suite.addTestSuite(NullityInferenceFromSourceTestCase.DfaInferenceTest.class); diff --git a/java/java-tests/testSrc/com/intellij/codeInspection/DataFlowRangeAnalysisTest.java b/java/java-tests/testSrc/com/intellij/codeInspection/DataFlowRangeAnalysisTest.java new file mode 100644 index 000000000000..38f09686fe44 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/codeInspection/DataFlowRangeAnalysisTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.codeInspection; + +import com.intellij.JavaTestUtil; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.testFramework.IdeaTestUtil; +import com.intellij.testFramework.LightProjectDescriptor; +import com.intellij.testFramework.PsiTestUtil; +import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor; +import org.jetbrains.annotations.NotNull; + +/** + * @author Tagir Valeev + */ +public class DataFlowRangeAnalysisTest extends DataFlowInspectionTestCase { + private static final DefaultLightProjectDescriptor PROJECT_DESCRIPTOR = new DefaultLightProjectDescriptor() { + @Override + public Sdk getSdk() { + return PsiTestUtil.addJdkAnnotations(IdeaTestUtil.getMockJdk18()); + } + }; + + @NotNull + @Override + protected LightProjectDescriptor getProjectDescriptor() { + return PROJECT_DESCRIPTOR; + } + + @Override + protected String getTestDataPath() { + return JavaTestUtil.getJavaTestDataPath() + "/inspection/dataFlow/fixture/"; + } + + public void testLongRangeBasics() { doTest(); } + + public void testLongRangeLoop() { doTest(); } + + public void testLongRangeKnownMethods() { + myFixture.addClass("package java.time;\n" + + "\n" + + "public interface LocalDateTime {\n" + + " int getHour();\n" + + " int getMinute();\n" + + " int getSecond();\n" + + "}"); + doTest(); + } + +} diff --git a/java/java-tests/testSrc/com/intellij/codeInspection/dataFlow/rangeSet/LongRangeSetTest.java b/java/java-tests/testSrc/com/intellij/codeInspection/dataFlow/rangeSet/LongRangeSetTest.java new file mode 100644 index 000000000000..288c782ad8e8 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/codeInspection/dataFlow/rangeSet/LongRangeSetTest.java @@ -0,0 +1,245 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.codeInspection.dataFlow.rangeSet; + +import com.intellij.psi.JavaTokenType; +import com.intellij.psi.PsiType; +import com.intellij.util.containers.HashMap; +import org.junit.Test; + +import java.util.Random; + +import static com.intellij.codeInspection.dataFlow.rangeSet.LongRangeSet.*; +import static org.junit.Assert.*; + +/** + * @author Tagir Valeev + */ +public class LongRangeSetTest { + @Test + public void testToString() { + assertEquals("{}", LongRangeSet.empty().toString()); + assertEquals("{10}", point(10).toString()); + assertEquals("{10}", range(10, 10).toString()); + assertEquals("{10, 11}", range(10, 11).toString()); + assertEquals("{10..100}", range(10, 100).toString()); + } + + @Test + public void testFromType() { + assertNull(LongRangeSet.fromType(PsiType.FLOAT)); + assertNull(LongRangeSet.fromType(PsiType.NULL)); + assertEquals("{-128..127}", LongRangeSet.fromType(PsiType.BYTE).toString()); + assertEquals("{0..65535}", LongRangeSet.fromType(PsiType.CHAR).toString()); + assertEquals("{-32768..32767}", LongRangeSet.fromType(PsiType.SHORT).toString()); + assertEquals("{-2147483648..2147483647}", LongRangeSet.fromType(PsiType.INT).toString()); + assertEquals("{0..2147483647}", LongRangeSet.indexRange().toString()); + assertEquals("{-9223372036854775808..9223372036854775807}", LongRangeSet.fromType(PsiType.LONG).toString()); + } + + @Test + public void testEquals() { + assertEquals(LongRangeSet.empty(), LongRangeSet.empty()); + assertEquals(point(10), point(10)); + assertNotEquals(point(10), point(11)); + assertEquals(point(10), range(10, 10)); + assertNotEquals(point(10), range(10, 11)); + assertEquals(range(10, 11), range(10, 11)); + assertNotEquals(range(10, 11), range(10, 12)); + } + + @Test + public void testDiff() { + assertEquals(LongRangeSet.empty(), LongRangeSet.empty().subtract(point(10))); + assertEquals(point(10), point(10).subtract(LongRangeSet.empty())); + assertEquals(point(10), point(10).subtract(point(11))); + assertEquals(LongRangeSet.empty(), point(10).subtract(point(10))); + assertEquals(point(10), point(10).subtract(range(15, 20))); + assertEquals(point(10), point(10).subtract(range(-10, -5))); + assertTrue(point(10).subtract(range(10, 20)).isEmpty()); + assertTrue(point(10).subtract(range(-10, 20)).isEmpty()); + assertTrue(point(10).subtract(range(-10, 10)).isEmpty()); + + assertEquals("{0..20}", range(0, 20).lt(30).toString()); + assertEquals("{0..19}", range(0, 20).lt(20).toString()); + assertEquals("{0..18}", range(0, 20).lt(19).toString()); + assertEquals("{0}", range(0, 20).lt(1).toString()); + assertTrue(range(0, 20).lt(0).isEmpty()); + + LongRangeSet fullRange = range(Long.MIN_VALUE, Long.MAX_VALUE); + assertEquals("{-9223372036854775808}", fullRange.le(Long.MIN_VALUE).toString()); + assertEquals(fullRange, fullRange.le(Long.MAX_VALUE)); + assertEquals("{9223372036854775807}", fullRange.ge(Long.MAX_VALUE).toString()); + assertEquals(fullRange, fullRange.ge(Long.MIN_VALUE)); + assertTrue(fullRange.gt(Long.MAX_VALUE).isEmpty()); + assertEquals(LongRangeSet.indexRange(), LongRangeSet.fromType(PsiType.INT).gt(-1)); + assertTrue(fullRange.subtract(fullRange).isEmpty()); + + assertEquals(point(10), fullRange.eq(10)); + assertTrue(range(30, 50).eq(10).isEmpty()); + } + + @Test + public void testSets() { + assertEquals("{0..9, 11..20}", range(0, 20).ne(10).toString()); + assertEquals("{0, 20}", range(0, 20).subtract(range(1, 19)).toString()); + assertEquals("{0, 1, 19, 20}", range(0, 20).subtract(range(2, 18)).toString()); + + assertEquals("{0..9, 12..20}", range(0, 20).ne(10).ne(11).toString()); + assertEquals("{0..9, 12..14, 16..20}", range(0, 20).ne(10).ne(11).ne(15).toString()); + assertEquals("{0, 4..20}", range(0, 20).ne(3).ne(2).ne(1).toString()); + assertEquals("{4..20}", range(0, 20).ne(3).ne(2).ne(1).ne(0).toString()); + + assertEquals("{0..2, 5..15, 19, 20}", + range(0, 20).subtract(range(3, 18).subtract(range(5, 15))).toString()); + + LongRangeSet first = fromType(PsiType.CHAR).ne(45); + LongRangeSet second = fromType(PsiType.CHAR).ne(32).ne(40).ne(44).ne(45).ne(46).ne(58).ne(59).ne(61); + assertEquals("{0..44, 46..65535}", first.toString()); + assertEquals("{0..31, 33..39, 41..43, 47..57, 60, 62..65535}", second.toString()); + assertEquals("{32, 40, 44, 46, 58, 59, 61}", first.subtract(second).toString()); + } + + @Test + public void testHash() { + HashMap map = new HashMap<>(); + map.put(LongRangeSet.empty(), "empty"); + map.put(point(10), "10"); + map.put(range(10, 10), "10-10"); + map.put(range(10, 11), "10-11"); + map.put(range(10, 12), "10-12"); + LongRangeSet longNotChar = LongRangeSet.fromType(PsiType.LONG).subtract(LongRangeSet.fromType(PsiType.CHAR)); + map.put(longNotChar, "longNotChar"); + + assertEquals("empty", map.get(LongRangeSet.empty())); + assertEquals("10-10", map.get(point(10))); + assertEquals("10-11", map.get(range(10, 11))); + assertEquals("10-12", map.get(range(10, 12))); + assertNull(map.get(range(11, 11))); + assertEquals("longNotChar", map.get(LongRangeSet.fromType(PsiType.LONG).subtract(LongRangeSet.fromType(PsiType.CHAR)))); + } + + @Test + public void testIntersects() { + assertFalse(LongRangeSet.empty().intersects(LongRangeSet.fromType(PsiType.LONG))); + assertTrue(point(Long.MIN_VALUE).intersects(LongRangeSet.fromType(PsiType.LONG))); + assertFalse(point(10).intersects(point(11))); + assertTrue(point(10).intersects(point(10))); + + assertTrue(range(10, 100).intersects(point(10))); + assertTrue(range(10, 100).intersects(point(100))); + assertFalse(range(10, 100).intersects(point(101))); + assertFalse(range(10, 100).intersects(point(9))); + + LongRangeSet range1020 = range(10, 20); + assertTrue(range1020.intersects(range1020)); + assertTrue(range1020.intersects(range(10, 30))); + assertTrue(range1020.intersects(range(20, 30))); + assertTrue(range1020.intersects(range(0, 30))); + assertTrue(range1020.intersects(range(0, 10))); + assertTrue(range1020.intersects(range(0, 20))); + + assertFalse(range1020.intersects(range(0, 9))); + assertFalse(range1020.intersects(range(21, 30))); + + LongRangeSet rangeSet = range1020.subtract(range(12, 13)).subtract(range(17, 18)); + assertFalse(rangeSet.intersects(point(12))); + assertFalse(point(12).intersects(rangeSet)); + assertFalse(rangeSet.intersects(LongRangeSet.empty())); + assertFalse(rangeSet.intersects(range(12, 13))); + assertFalse(range(12, 13).intersects(rangeSet)); + assertFalse(rangeSet.intersects(range(0, 9))); + assertFalse(rangeSet.intersects(range(21, 30))); + assertTrue(rangeSet.intersects(rangeSet)); + assertTrue(rangeSet.intersects(range1020)); + assertTrue(rangeSet.intersects(point(11))); + + LongRangeSet rangeSet2 = range1020.subtract(rangeSet); + assertEquals("{12, 13, 17, 18}", rangeSet2.toString()); + assertFalse(rangeSet.intersects(rangeSet2)); + } + + @Test + public void testIntersect() { + assertEquals("{0..100}", range(0, 100).intersect(range(0, 100)).toString()); + assertEquals("{100}", range(0, 100).intersect(range(100, 200)).toString()); + assertTrue(range(0, 100).intersect(range(101, 200)).isEmpty()); + assertTrue(point(100).intersect(point(200)).isEmpty()); + assertFalse(point(100).intersect(range(99, 101)).isEmpty()); + + LongRangeSet rangeSet = range(-1000, 1000).subtract(range(100, 500)).subtract(range(-500, -100)); + assertEquals("{-1000..-501, -99..99, 501..1000}", rangeSet.toString()); + assertEquals(point(99), rangeSet.intersect(point(99))); + assertTrue(rangeSet.intersect(point(100)).isEmpty()); + assertEquals("{0..99, 501..1000}", rangeSet.intersect(LongRangeSet.indexRange()).toString()); + } + + @Test + public void testIntersectSubtractRandomized() { + Random r = new Random(1); + LongRangeSet[] data = r.ints(1000, 0, 1000) + .mapToObj(x -> range(x, x + r.nextInt((x % 20) * 100 + 1))).toArray(LongRangeSet[]::new); + for (int i = 0; i < 2000; i++) { + int idx = r.nextInt(data.length); + LongRangeSet left = data[idx]; + LongRangeSet right = data[r.nextInt(data.length)]; + LongRangeSet lDiff = left.subtract(right); + LongRangeSet rDiff = right.subtract(left); + LongRangeSet intersection = left.intersect(right); + String message = left + " & " + right + " = " + intersection; + assertEquals(message, intersection, right.intersect(left)); + if (!intersection.isEmpty()) { + assertTrue(message, intersection.min() >= Math.max(left.min(), right.min())); + assertTrue(message, intersection.max() <= Math.min(left.max(), right.max())); + } + assertEquals(message, intersection, right.subtract(LongRangeSet.fromType(PsiType.LONG).subtract(left))); + assertEquals(message, intersection, left.subtract(LongRangeSet.fromType(PsiType.LONG).subtract(right))); + switch (r.nextInt(3)) { + case 0: + data[idx] = lDiff; + break; + case 1: + data[idx] = rDiff; + break; + case 2: + data[idx] = intersection; + break; + } + } + } + + @Test + public void testFromConstant() { + assertEquals("{0}", LongRangeSet.fromConstant(0).toString()); + assertEquals("{0}", LongRangeSet.fromConstant(0L).toString()); + assertEquals("{1}", LongRangeSet.fromConstant((byte)1).toString()); + assertEquals("{97}", LongRangeSet.fromConstant('a').toString()); + assertNull(LongRangeSet.fromConstant(null)); + assertNull(LongRangeSet.fromConstant(1.0)); + } + + @Test + public void testFromRelation() { + assertEquals(range(101, Long.MAX_VALUE), range(100, 200).fromRelation(JavaTokenType.GT)); + assertEquals(range(100, Long.MAX_VALUE), range(100, 200).fromRelation(JavaTokenType.GE)); + assertEquals(range(Long.MIN_VALUE, 199), range(100, 200).fromRelation(JavaTokenType.LT)); + assertEquals(range(Long.MIN_VALUE, 200), range(100, 200).fromRelation(JavaTokenType.LE)); + assertEquals(range(100, 200), range(100, 200).fromRelation(JavaTokenType.EQEQ)); + assertNull(range(100, 200).fromRelation(JavaTokenType.EQ)); + assertEquals(fromType(PsiType.LONG), range(100, 200).fromRelation(JavaTokenType.NE)); + assertEquals("{-9223372036854775808..99, 101..9223372036854775807}", point(100).fromRelation(JavaTokenType.NE).toString()); + } +} \ No newline at end of file diff --git a/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/ComparisonUtils.java b/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/ComparisonUtils.java index 6729225116bd..7889d1665c8f 100644 --- a/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/ComparisonUtils.java +++ b/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/ComparisonUtils.java @@ -89,6 +89,17 @@ public class ComparisonUtils { return s_invertedComparisons.get(tokenType); } + @Nullable + public static IElementType getNegatedComparisonTokenType(IElementType tokenType) { + if(JavaTokenType.EQEQ.equals(tokenType)) return JavaTokenType.NE; + if(JavaTokenType.NE.equals(tokenType)) return JavaTokenType.EQEQ; + if(JavaTokenType.LT.equals(tokenType)) return JavaTokenType.GE; + if(JavaTokenType.LE.equals(tokenType)) return JavaTokenType.GT; + if(JavaTokenType.GT.equals(tokenType)) return JavaTokenType.LE; + if(JavaTokenType.GE.equals(tokenType)) return JavaTokenType.LT; + return null; + } + @Contract("null, _, _ -> false") public static boolean isNullComparison(PsiExpression expression, @NotNull PsiVariable variable, boolean equal) { return variable.equals(ExpressionUtils.getVariableFromNullComparison(expression, equal));