/* * 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; import com.intellij.codeInspection.dataFlow.rangeSet.LongRangeSet; import com.intellij.codeInspection.dataFlow.value.*; import com.intellij.openapi.progress.ProgressManager; 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; 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(); @Nullable List mergeByFacts(@NotNull List states) { MultiMap statesByFact = MultiMap.createLinked(); for (DfaMemoryStateImpl state : states) { ProgressManager.checkCanceled(); for (Fact fact : getFacts(state)) { statesByFact.putValue(fact, state); } } for (final Fact fact : statesByFact.keySet()) { if (statesByFact.get(fact).size() == states.size() || fact.myPositive) continue; Collection statesWithNegations = statesByFact.get(fact.getPositiveCounterpart()); if (statesWithNegations.isEmpty()) continue; ProgressManager.checkCanceled(); MultiMap, DfaMemoryStateImpl> statesByUnrelatedFacts1 = mapByUnrelatedFacts(fact, statesByFact.get(fact)); MultiMap, DfaMemoryStateImpl> statesByUnrelatedFacts2 = mapByUnrelatedFacts(fact, statesWithNegations); Replacements replacements = new Replacements(states); for (Set key : statesByUnrelatedFacts1.keySet()) { final Collection group1 = statesByUnrelatedFacts1.get(key); final Collection group2 = statesByUnrelatedFacts2.get(key); if (group1.isEmpty() || group2.isEmpty()) continue; final Collection group = ContainerUtil.newArrayList(ContainerUtil.concat(group1, group2)); final Set unknowns = getAllUnknownVariables(group); replacements.stripAndMerge(group, original -> { DfaMemoryStateImpl copy = withUnknownVariables(original, unknowns); fact.removeFromState(copy); if (fact.myType == FactType.equality) { restoreOtherInequalities(fact, group, copy); } return copy; }); } if (replacements.hasMerges()) return replacements.getMergeResult(); } return null; } @NotNull private MultiMap, DfaMemoryStateImpl> mapByUnrelatedFacts(@NotNull Fact fact, @NotNull Collection states1) { MultiMap, DfaMemoryStateImpl> statesByUnrelatedFacts1 = MultiMap.createLinked(); for (DfaMemoryStateImpl state : states1) { statesByUnrelatedFacts1.putValue(getUnrelatedFacts(fact, state), state); } return statesByUnrelatedFacts1; } @NotNull private LinkedHashSet getUnrelatedFacts(@NotNull final Fact fact, @NotNull DfaMemoryStateImpl state) { return new LinkedHashSet<>(ContainerUtil.filter(getFacts(state), another -> !fact.invalidatesFact(another))); } private void restoreOtherInequalities(@NotNull Fact removedFact, @NotNull Collection mergedGroup, @NotNull DfaMemoryStateImpl state) { Set inequalitiesToRestore = null; for (DfaMemoryStateImpl member : mergedGroup) { Set memberFacts = getFacts(member); if (memberFacts.contains(removedFact)) { Set otherInequalities = getOtherInequalities(removedFact, memberFacts, member); if (inequalitiesToRestore == null) { inequalitiesToRestore = otherInequalities; } else { inequalitiesToRestore.retainAll(otherInequalities); } } } if (inequalitiesToRestore != null) { DfaRelationValue.Factory relationFactory = state.getFactory().getRelationFactory(); for (DfaConstValue toRestore : inequalitiesToRestore) { state.applyCondition(relationFactory.createRelation(removedFact.myVar, toRestore, JavaTokenType.EQEQ, true)); } } } @NotNull private static Set getOtherInequalities(@NotNull Fact removedFact, @NotNull Set memberFacts, @NotNull DfaMemoryStateImpl state) { Set otherInequalities = ContainerUtil.newLinkedHashSet(); Set eqValues = ContainerUtil.newHashSet(state.getEquivalentValues((DfaValue)removedFact.myArg)); for (Fact candidate : memberFacts) { if (candidate.myType == FactType.equality && !candidate.myPositive && candidate.myVar == removedFact.myVar && !eqValues.contains((DfaValue)candidate.myArg) && candidate.myArg instanceof DfaConstValue) { otherInequalities.add((DfaConstValue)candidate.myArg); } } return otherInequalities; } @NotNull private static Set getAllUnknownVariables(@NotNull Collection complementary) { final Set toFlush = ContainerUtil.newLinkedHashSet(); for (DfaMemoryStateImpl removedState : complementary) { toFlush.addAll(removedState.getUnknownVariables()); } return toFlush; } @NotNull private static DfaMemoryStateImpl withUnknownVariables(@NotNull DfaMemoryStateImpl original, @NotNull Set toFlush) { DfaMemoryStateImpl copy = original.createCopy(); for (DfaVariableValue value : toFlush) { copy.doFlush(value, true); } return copy; } @Nullable List mergeByUnknowns(@NotNull List states) { MultiMap byHash = new MultiMap<>(); for (DfaMemoryStateImpl state : states) { ProgressManager.checkCanceled(); byHash.putValue(state.getPartialHashCode(false, true), state); } Replacements replacements = new Replacements(states); for (Integer key : byHash.keySet()) { Collection similarStates = byHash.get(key); if (similarStates.size() < 2) continue; for (final DfaMemoryStateImpl state1 : similarStates) { ProgressManager.checkCanceled(); List complementary = ContainerUtil.filter(similarStates, state2 -> state1.equalsByRelations(state2) && state1.equalsByVariableStates(state2)); if (mergeUnknowns(replacements, complementary)) break; } } return replacements.getMergeResult(); } @Nullable List mergeByNullability(List states) { MultiMap byHash = new MultiMap<>(); for (DfaMemoryStateImpl state : states) { ProgressManager.checkCanceled(); byHash.putValue(state.getPartialHashCode(false, false), state); } Replacements replacements = new Replacements(states); for (Integer key : byHash.keySet()) { Collection similarStates = byHash.get(key); if (similarStates.size() < 2) continue; groupLoop: for (final DfaMemoryStateImpl state1 : similarStates) { ProgressManager.checkCanceled(); for (final DfaVariableValue var : state1.getChangedVariables()) { if (state1.getVariableState(var).getNullability() != Nullness.NULLABLE) { continue; } List complementary = ContainerUtil.filter(similarStates, state2 -> state1.equalsByRelations(state2) && areEquivalentModuloVar(state1, state2, var) && areVarStatesEqualModuloNullability(state1, state2, var)); if (mergeUnknowns(replacements, complementary)) break groupLoop; } } } 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; final Set toFlush = getAllUnknownVariables(complementary); if (toFlush.isEmpty()) return false; return replacements.stripAndMerge(complementary, original -> withUnknownVariables(original, toFlush)); } private boolean areEquivalentModuloVar(@NotNull DfaMemoryStateImpl state1, @NotNull DfaMemoryStateImpl state2, @NotNull DfaVariableValue var) { DfaMemoryStateImpl copy1 = copyWithoutVar(state1, var); DfaMemoryStateImpl copy2 = copyWithoutVar(state2, var); return copy2.equalsByRelations(copy1) && copy2.equalsByVariableStates(copy1); } @NotNull private DfaMemoryStateImpl copyWithoutVar(@NotNull DfaMemoryStateImpl state, @NotNull DfaVariableValue var) { Map map = myCopyCache.computeIfAbsent(state, k -> ContainerUtil.newIdentityHashMap()); DfaMemoryStateImpl copy = map.get(var); if (copy == null) { copy = state.createCopy(); copy.flushVariable(var); map.put(var, copy); } return copy; } private static boolean areVarStatesEqualModuloNullability(@NotNull DfaMemoryStateImpl state1, @NotNull DfaMemoryStateImpl state2, @NotNull DfaVariableValue var) { return state1.getVariableState(var).withNullability(Nullness.UNKNOWN).equals(state2.getVariableState(var).withNullability(Nullness.UNKNOWN)); } @NotNull private Set getFacts(@NotNull DfaMemoryStateImpl state) { Set result = myFacts.get(state); if (result != null) { return result; } result = ContainerUtil.newLinkedHashSet(); for (EqClass eqClass : state.getNonTrivialEqClasses()) { DfaValue constant = eqClass.findConstant(true); List vars = eqClass.getVariables(false); for (DfaVariableValue var : vars) { if (constant != null) { result.add(Fact.createEqualityFact(var, constant, true)); } for (DfaVariableValue eqVar : vars) { if (var != eqVar) { result.add(Fact.createEqualityFact(var, eqVar, true)); } } } } for (UnorderedPair classPair : state.getDistinctClassPairs()) { List vars1 = classPair.first.getVariables(false); List vars2 = classPair.second.getVariables(false); LinkedHashSet firstSet = new LinkedHashSet<>(vars1); ContainerUtil.addIfNotNull(firstSet, classPair.first.findConstant(true)); LinkedHashSet secondSet = new LinkedHashSet<>(vars2); ContainerUtil.addIfNotNull(secondSet, classPair.second.findConstant(true)); for (DfaVariableValue var : vars1) { for (DfaValue value : secondSet) { result.add(new Fact(FactType.equality, var, false, value)); } } for (DfaVariableValue var : vars2) { for (DfaValue value : firstSet) { result.add(new Fact(FactType.equality, var, false, value)); } } } Map states = state.getVariableStates(); for (DfaVariableValue var : states.keySet()) { DfaVariableState variableState = states.get(var); for (DfaPsiType type : variableState.getInstanceofValues()) { result.add(new Fact(FactType.instanceOf, var, true, type)); } for (DfaPsiType type : variableState.getNotInstanceofValues()) { result.add(new Fact(FactType.instanceOf, var, false, type)); } } myFacts.put(state, result); return result; } private enum FactType { equality, instanceOf } private static class Fact { @NotNull final FactType myType; @NotNull private final DfaVariableValue myVar; private final boolean myPositive; @NotNull private final Object myArg; // DfaValue for equality fact, DfaPsiType for instanceOf fact private Fact(@NotNull FactType type, @NotNull DfaVariableValue var, boolean positive, @NotNull Object arg) { myType = type; myVar = var; myPositive = positive; myArg = arg; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Fact)) return false; Fact fact = (Fact)o; if (myPositive != fact.myPositive) return false; if (!myArg.equals(fact.myArg)) return false; if (myType != fact.myType) return false; if (!myVar.equals(fact.myVar)) return false; return true; } @Override public int hashCode() { int result = myType.hashCode(); result = 31 * result + myVar.hashCode(); result = 31 * result + (myPositive ? 1 : 0); result = 31 * result + myArg.hashCode(); return result; } @Override public String toString() { return myVar + " " + (myPositive ? "" : "!") + myType + " " + myArg; } @NotNull private static Fact createEqualityFact(@NotNull DfaVariableValue var, @NotNull DfaValue val, boolean equal) { if (val instanceof DfaVariableValue && val.getID() < var.getID()) { return new Fact(FactType.equality, (DfaVariableValue)val, equal, var); } return new Fact(FactType.equality, var, equal, val); } @NotNull private Fact getPositiveCounterpart() { return new Fact(myType, myVar, true, myArg); } boolean invalidatesFact(@NotNull Fact another) { if (another.myType != myType) return false; if (myType == FactType.equality) { return aboutSame(myVar, another.myVar) || aboutSame(myVar, another.myArg); } return aboutSame(myVar, another.myVar) && aboutSame(myArg, another.myArg); } private static boolean aboutSame(Object v1, Object v2) { return normalize(v1) == normalize(v2); } private static Object normalize(Object value) { if (value instanceof DfaVariableValue && ((DfaVariableValue)value).isNegated()) { return ((DfaVariableValue)value).createNegated(); } return value; } void removeFromState(@NotNull DfaMemoryStateImpl state) { DfaVariableState varState = state.getVariableState(myVar); if (myType == FactType.equality) { state.flushVariable(myVar); state.setVariableState(myVar, varState); } else { state.setVariableState(myVar, varState.withoutType((DfaPsiType)myArg)); } } } private static class Replacements { @NotNull private final List myAllStates; private final Set myRemovedStates = ContainerUtil.newIdentityTroveSet(); private final List myMerged = ContainerUtil.newArrayList(); private Replacements(@NotNull List allStates) { myAllStates = allStates; } private boolean hasMerges() { return !myMerged.isEmpty(); } @Nullable private List getMergeResult() { if (hasMerges()) { List result = ContainerUtil.newArrayList(myMerged); for (DfaMemoryStateImpl state : myAllStates) { if (!myRemovedStates.contains(state)) { result.add(state); } } return result; } return null; } private boolean stripAndMerge(@NotNull Collection group, @NotNull Function stripper) { if (group.size() <= 1) return false; MultiMap strippedToOriginals = MultiMap.create(); for (DfaMemoryStateImpl original : group) { strippedToOriginals.putValue(stripper.fun(original), original); } boolean hasMerges = false; for (Map.Entry> entry : strippedToOriginals.entrySet()) { Collection merged = entry.getValue(); if (merged.size() > 1) { myRemovedStates.addAll(merged); myMerged.add(entry.getKey()); hasMerges = true; } } return hasMerges; } } }