From e27b0199fc8eea90e1f5f086c007cc6556fdabe0 Mon Sep 17 00:00:00 2001 From: Tagir Valeev Date: Thu, 16 Mar 2017 13:20:32 +0700 Subject: [PATCH] CustomMethodHandlers: support Math.max, Math.min, Math.abs; LongRangeSet: useless methods removed; more tests --- .../dataFlow/CustomMethodHandlers.java | 39 +++++- .../dataFlow/rangeSet/LongRangeSet.java | 124 ++++++++++++++---- .../fixture/LongRangeKnownMethods.java | 62 ++++++++- .../dataFlow/rangeSet/LongRangeSetTest.java | 72 ++++++---- 4 files changed, 244 insertions(+), 53 deletions(-) diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/CustomMethodHandlers.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/CustomMethodHandlers.java index d03566459399..c5a3295229a7 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/CustomMethodHandlers.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/CustomMethodHandlers.java @@ -28,9 +28,9 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import static com.intellij.psi.CommonClassNames.JAVA_LANG_STRING; +import static com.intellij.psi.CommonClassNames.*; import static com.intellij.psi.JavaTokenType.*; -import static com.siyeh.ig.callMatcher.CallMatcher.instanceCall; +import static com.siyeh.ig.callMatcher.CallMatcher.*; /** * @author Tagir Valeev @@ -52,7 +52,21 @@ public class CustomMethodHandlers { .register(instanceCall(JAVA_LANG_STRING, "startsWith").parameterCount(1), (qualifier, args, memState, factory) -> stringStartsEnds(qualifier, args, memState, factory, false)) .register(instanceCall(JAVA_LANG_STRING, "endsWith").parameterCount(1), - (qualifier, args, memState, factory) -> stringStartsEnds(qualifier, args, memState, factory, true)); + (qualifier, args, memState, factory) -> stringStartsEnds(qualifier, args, memState, factory, true)) + .register(anyOf(staticCall(JAVA_LANG_MATH, "max").parameterTypes("int", "int"), + staticCall(JAVA_LANG_MATH, "max").parameterTypes("long", "long"), + staticCall(JAVA_LANG_INTEGER, "max").parameterTypes("int", "int"), + staticCall(JAVA_LANG_LONG, "max").parameterTypes("long", "long")), + (qualifier, args, memState, factory) -> mathMinMax(args, memState, factory, true)) + .register(anyOf(staticCall(JAVA_LANG_MATH, "min").parameterTypes("int", "int"), + staticCall(JAVA_LANG_MATH, "min").parameterTypes("long", "long"), + staticCall(JAVA_LANG_INTEGER, "min").parameterTypes("int", "int"), + staticCall(JAVA_LANG_LONG, "min").parameterTypes("long", "long")), + (qualifier, args, memState, factory) -> mathMinMax(args, memState, factory, false)) + .register(staticCall(JAVA_LANG_MATH, "abs").parameterTypes("int"), + (qualifier, args, memState, factory) -> mathAbs(args, memState, factory, false)) + .register(staticCall(JAVA_LANG_MATH, "abs").parameterTypes("long"), + (qualifier, args, memState, factory) -> mathAbs(args, memState, factory, true)); public static CustomMethodHandler find(PsiMethodCallExpression call) { return CUSTOM_METHOD_HANDLERS.mapFirst(call); @@ -116,6 +130,25 @@ public class CustomMethodHandlers { return applyCondition(memState, trueRelation, factory.getBoolean(true), falseRelation, factory.getBoolean(false)); } + private static List mathMinMax(DfaValue[] args, DfaMemoryState memState, DfaValueFactory factory, boolean max) { + if(args == null || args.length != 2) return Collections.emptyList(); + LongRangeSet first = memState.getRange(args[0]); + LongRangeSet second = memState.getRange(args[1]); + if (first == null || second == null || first.isEmpty() || second.isEmpty()) return Collections.emptyList(); + LongRangeSet domain = max ? LongRangeSet.range(Math.max(first.min(), second.min()), Long.MAX_VALUE) + : LongRangeSet.range(Long.MIN_VALUE, Math.min(first.max(), second.max())); + LongRangeSet result = first.union(second).intersect(domain); + return singleResult(memState, factory.getRangeFactory().create(result)); + } + + private static List mathAbs(DfaValue[] args, DfaMemoryState memState, DfaValueFactory factory, boolean isLong) { + DfaValue arg = ArrayUtil.getFirstElement(args); + if(arg == null) return Collections.emptyList(); + LongRangeSet range = memState.getRange(arg); + if (range == null) return Collections.emptyList(); + return singleResult(memState, factory.getRangeFactory().create(range.abs(isLong))); + } + private static List singleResult(DfaMemoryState state, DfaValue value) { state.push(value); return Collections.singletonList(state); 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 index ed9ca5fb7d03..1412c72dc587 100644 --- 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 @@ -23,6 +23,8 @@ import org.jetbrains.annotations.Nullable; import java.util.Arrays; import java.util.NoSuchElementException; +import java.util.stream.IntStream; +import java.util.stream.LongStream; /** * An immutable set of long values optimized for small number of ranges. @@ -40,27 +42,7 @@ public abstract class LongRangeSet { */ 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) { + public LongRangeSet without(long value) { return subtract(point(value)); } @@ -144,8 +126,8 @@ public abstract class LongRangeSet { } if (JavaTokenType.NE.equals(relation)) { long min = min(); - if (min == max()) return Range.LONG_RANGE.subtract(this); - return Range.LONG_RANGE; + if (min == max()) return all().without(min); + return all(); } if (JavaTokenType.GT.equals(relation)) { long min = min(); @@ -164,6 +146,22 @@ public abstract class LongRangeSet { return null; } + /** + * Returns a range which represents all the possible values after applying {@link Math#abs(int)} or {@link Math#abs(long)} + * to the values from this set + * + * @param isLong whether {@link Math#abs(long)} is applied + * @return a new range + */ + public abstract LongRangeSet abs(boolean isLong); + + /** + * Returns a stream of all values from this range. Be careful: could be huge + * + * @return a new stream + */ + public abstract LongStream stream(); + /** * @return an empty set */ @@ -171,6 +169,13 @@ public abstract class LongRangeSet { return Empty.EMPTY; } + /** + * @return a set containing all possible long values + */ + public static LongRangeSet all() { + return Range.LONG_RANGE; + } + /** * Creates a set containing single given value * @@ -215,6 +220,10 @@ public abstract class LongRangeSet { return from == to ? String.valueOf(from) : from + (to - from == 1 ? ", " : "..") + to; } + static long minValue(boolean isLong) { + return isLong ? Long.MIN_VALUE : Integer.MIN_VALUE; + } + /** * @return LongRangeSet describing possible array or string indices (from 0 to Integer.MAX_VALUE) */ @@ -248,7 +257,7 @@ public abstract class LongRangeSet { return Range.INT_RANGE; } if (type.equals(PsiType.LONG)) { - return Range.LONG_RANGE; + return all(); } } return null; @@ -309,6 +318,16 @@ public abstract class LongRangeSet { return other.isEmpty(); } + @Override + public LongRangeSet abs(boolean isLong) { + return this; + } + + @Override + public LongStream stream() { + return LongStream.empty(); + } + @Override long[] asRanges() { return new long[0]; @@ -372,6 +391,16 @@ public abstract class LongRangeSet { return other.isEmpty() || equals(other); } + @Override + public LongRangeSet abs(boolean isLong) { + return myValue >= 0 || myValue == minValue(isLong) ? this : point(-myValue); + } + + @Override + public LongStream stream() { + return LongStream.of(myValue); + } + @Override long[] asRanges() { return new long[] {myValue, myValue}; @@ -509,6 +538,35 @@ public abstract class LongRangeSet { return other.isEmpty() || other.min() >= myFrom && other.max() <= myTo; } + @Override + public LongRangeSet abs(boolean isLong) { + if (myFrom >= 0) return this; + long minValue = minValue(isLong); + long low = myFrom, hi = myTo; + if (low <= minValue) { + low = minValue + 1; + } + if (myTo <= 0) { + hi = -low; + low = -myTo; + } + else { + hi = Math.max(-low, hi); + low = 0; + } + if (myFrom <= minValue) { + return new RangeSet(new long[]{minValue, minValue, low, hi}); + } + else { + return new Range(low, hi); + } + } + + @Override + public LongStream stream() { + return LongStream.rangeClosed(myFrom, myTo); + } + @Override long[] asRanges() { return new long[] {myFrom, myTo}; @@ -572,7 +630,7 @@ public abstract class LongRangeSet { if (other instanceof Point || other instanceof Range) { return other.intersect(this); } - return subtract(Range.LONG_RANGE.subtract(other)); + return subtract(all().subtract(other)); } @Override @@ -626,6 +684,22 @@ public abstract class LongRangeSet { return other.subtract(this).isEmpty(); } + @Override + public LongRangeSet abs(boolean isLong) { + LongRangeSet result = all(); + for (int i = 0; i < myRanges.length; i += 2) { + result = result.subtract(range(myRanges[i], myRanges[i + 1]).abs(isLong)); + } + return all().subtract(result); + } + + @Override + public LongStream stream() { + return IntStream.range(0, myRanges.length / 2) + .mapToObj(idx -> LongStream.rangeClosed(myRanges[idx * 2], myRanges[idx * 2 + 1])) + .reduce(LongStream::concat).orElseGet(LongStream::empty); + } + @Override long[] asRanges() { return myRanges; diff --git a/java/java-tests/testData/inspection/dataFlow/fixture/LongRangeKnownMethods.java b/java/java-tests/testData/inspection/dataFlow/fixture/LongRangeKnownMethods.java index e309d9f2be2c..a804edd5986e 100644 --- a/java/java-tests/testData/inspection/dataFlow/fixture/LongRangeKnownMethods.java +++ b/java/java-tests/testData/inspection/dataFlow/fixture/LongRangeKnownMethods.java @@ -80,8 +80,8 @@ public class LongRangeKnownMethods { } void testEqualsIgnoreCase(String s) { - if(!s.equalsIgnoreCase("xyz") || !s.isEmpty()) { - System.out.println("Always"); + if(s.equalsIgnoreCase("xyz") && s.isEmpty()) { + System.out.println("Never"); } } @@ -91,4 +91,62 @@ public class LongRangeKnownMethods { System.out.println("Impossible"); } } + + void testMax(int x) { + x = Math.max(x, 0); + if (x > -1) { + System.out.println("Always"); + } + if (x > 0) { + System.out.println("Not always"); + } + } + + void testMin(long x, long y) { + if (x < 10 && y > 10) { + y = Long.min(x, y); + if (y > 20) { + System.out.println("Impossible"); + } + } + if (y > 20) { + System.out.println("Possible"); + } + } + + void testMinMax(List rows) { + int start = Integer.MAX_VALUE; + int end = -1; + + for (int i = 0; i < rows.size(); i++) { + String row = rows.get(i); + if (!row.isEmpty()) { + start = Math.min(start, i); + end = Math.max(end, i); + } + } + + if(end >= 0 && start < Integer.MAX_VALUE) { + System.out.println("Ok"); + } + } + + void testAbs(long x, int y) { + x = Math.abs(x); + y = Math.abs(y); + if (x == Long.MIN_VALUE) { + System.out.println("possible"); + } + if (x == Long.MIN_VALUE + 1) { + System.out.println("impossible"); + } + if (x == Integer.MIN_VALUE) { + System.out.println("impossible"); + } + if (y == Integer.MIN_VALUE) { + System.out.println("possible"); + } else if(y < 0) { + System.out.println("impossible"); + } + } } 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 index 7cadd5ddc874..3febd3291820 100644 --- a/java/java-tests/testSrc/com/intellij/codeInspection/dataFlow/rangeSet/LongRangeSetTest.java +++ b/java/java-tests/testSrc/com/intellij/codeInspection/dataFlow/rangeSet/LongRangeSetTest.java @@ -73,41 +73,36 @@ public class LongRangeSetTest { 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()); + assertEquals("{0..20}", range(0, 20).subtract(range(30, Long.MAX_VALUE)).toString()); + assertEquals("{0..19}", range(0, 20).subtract(range(20, Long.MAX_VALUE)).toString()); + assertEquals("{0..18}", range(0, 20).subtract(range(19, Long.MAX_VALUE)).toString()); + assertEquals("{0}", range(0, 20).subtract(range(1, Long.MAX_VALUE)).toString()); + assertTrue(range(0, 20).subtract(range(0, Long.MAX_VALUE)).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(indexRange(), fromType(PsiType.INT).gt(-1)); - assertTrue(fullRange.subtract(fullRange).isEmpty()); - - assertEquals(point(10), fullRange.eq(10)); - assertTrue(range(30, 50).eq(10).isEmpty()); + assertEquals("{-9223372036854775808}", all().subtract(range(Long.MIN_VALUE + 1, Long.MAX_VALUE)).toString()); + assertEquals("{9223372036854775807}", all().subtract(range(Long.MIN_VALUE, Long.MAX_VALUE - 1)).toString()); + assertTrue(all().subtract(range(Long.MIN_VALUE, Long.MAX_VALUE)).isEmpty()); + assertEquals(indexRange(), fromType(PsiType.INT).subtract(range(Long.MIN_VALUE, (long)-1))); + assertTrue(all().subtract(all()).isEmpty()); } @Test public void testSets() { - assertEquals("{0..9, 11..20}", range(0, 20).ne(10).toString()); + assertEquals("{0..9, 11..20}", range(0, 20).without(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..9, 12..20}", range(0, 20).without(10).without(11).toString()); + assertEquals("{0..9, 12..14, 16..20}", range(0, 20).without(10).without(11).without(15).toString()); + assertEquals("{0, 4..20}", range(0, 20).without(3).without(2).without(1).toString()); + assertEquals("{4..20}", range(0, 20).without(3).without(2).without(1).without(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); + LongRangeSet first = fromType(PsiType.CHAR).without(45); + LongRangeSet second = + fromType(PsiType.CHAR).without(32).without(40).without(44).without(45).without(46).without(58).without(59).without(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()); @@ -207,6 +202,18 @@ public class LongRangeSetTest { } assertEquals(message, intersection, right.subtract(fromType(PsiType.LONG).subtract(left))); assertEquals(message, intersection, left.subtract(fromType(PsiType.LONG).subtract(right))); + intersection.stream().limit(1000).forEach(e -> { + assertTrue(left.contains(e)); + assertTrue(right.contains(e)); + }); + lDiff.stream().limit(1000).forEach(e -> { + assertTrue(left.contains(e)); + assertFalse(right.contains(e)); + }); + rDiff.stream().limit(1000).forEach(e -> { + assertFalse(left.contains(e)); + assertTrue(right.contains(e)); + }); switch (r.nextInt(3)) { case 0: data[idx] = lDiff; @@ -242,4 +249,23 @@ public class LongRangeSetTest { assertEquals(fromType(PsiType.LONG), range(100, 200).fromRelation(JavaTokenType.NE)); assertEquals("{-9223372036854775808..99, 101..9223372036854775807}", point(100).fromRelation(JavaTokenType.NE).toString()); } + + @Test + public void testAbs() { + assertTrue(empty().abs(true).isEmpty()); + assertEquals(point(Long.MAX_VALUE), point(Long.MIN_VALUE + 1).abs(true)); + assertEquals(point(Long.MIN_VALUE), point(Long.MIN_VALUE).abs(true)); + assertEquals(point(Integer.MIN_VALUE), point(Integer.MIN_VALUE).abs(false)); + assertEquals(point(Integer.MAX_VALUE + 1L), point(Integer.MIN_VALUE).abs(true)); + assertEquals(range(100, 200), range(100, 200).abs(true)); + assertEquals(range(0, 200), range(-1, 200).abs(true)); + assertEquals(range(0, 200), range(-200, 200).abs(false)); + assertEquals(range(0, 201), range(-201, 200).abs(false)); + assertEquals(range(0, Long.MAX_VALUE).union(point(Long.MIN_VALUE)), all().abs(true)); + assertEquals(range(100, Integer.MAX_VALUE).union(point(Integer.MIN_VALUE)), range(Integer.MIN_VALUE, -100).abs(false)); + assertEquals(range(100, Integer.MAX_VALUE + 1L), range(Integer.MIN_VALUE, -100).abs(true)); + LongRangeSet set = range(-900, 1000).subtract(range(-800, -600)).subtract(range(-300, 100)).subtract(range(500, 700)); + assertEquals("{-900..-801, -599..-301, 101..499, 701..1000}", set.toString()); + assertEquals("{101..599, 701..1000}", set.abs(false).toString()); + } } \ No newline at end of file