|
| 1 | +/** |
| 2 | + * Finds redundant usage of `Stream.sorted()` where a subsequent collection step does |
| 3 | + * not preserve order, for example `collect(Collectors.toSet())`. |
| 4 | + * |
| 5 | + * Either remove the redundant `sorted()` call or use a `Collectors` factory method which |
| 6 | + * preserves the element order, for example by providing a `Set` factory which creates |
| 7 | + * a `LinkedHashSet`. |
| 8 | + * |
| 9 | + * This query might produce false positives when intermediate stream steps have side-effects, |
| 10 | + * and the sorting is needed for these intermediate steps. However, note that side-effects in |
| 11 | + * intermediate steps are generally discouraged, see the |
| 12 | + * [documentation](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/stream/package-summary.html#SideEffects). |
| 13 | + * |
| 14 | + * This query is based on IntelliJ IDEA's 'RedundantStreamOptionalCall' warning. |
| 15 | + * |
| 16 | + * @kind problem |
| 17 | + */ |
| 18 | + |
| 19 | +import java |
| 20 | + |
| 21 | +class StreamType extends RefType { |
| 22 | + StreamType() { |
| 23 | + getSourceDeclaration().getASourceSupertype*().hasQualifiedName("java.util.stream", [ |
| 24 | + "Stream", |
| 25 | + "DoubleStream", |
| 26 | + "IntStream", |
| 27 | + "LongStream", |
| 28 | + ]) |
| 29 | + } |
| 30 | +} |
| 31 | + |
| 32 | +class SortedMethod extends Method { |
| 33 | + SortedMethod() { |
| 34 | + getDeclaringType() instanceof StreamType |
| 35 | + and hasName("sorted") |
| 36 | + } |
| 37 | +} |
| 38 | + |
| 39 | +class CollectMethod extends Method { |
| 40 | + CollectMethod() { |
| 41 | + getDeclaringType() instanceof StreamType |
| 42 | + and hasName("collect") |
| 43 | + } |
| 44 | +} |
| 45 | + |
| 46 | +class PotentiallyOrderIgnoringCollectorsMethod extends Method { |
| 47 | + PotentiallyOrderIgnoringCollectorsMethod() { |
| 48 | + getDeclaringType().hasQualifiedName("java.util.stream", "Collectors") |
| 49 | + // Only consider methods which always or depending on the arguments might ignore the order; |
| 50 | + // ignore methods which always preserve order such as `groupingBy` (because it returns `Map<..., List<...>>`) |
| 51 | + and hasName([ |
| 52 | + "toCollection", |
| 53 | + "toConcurrentMap", |
| 54 | + "toMap", |
| 55 | + "toSet", |
| 56 | + "toUnmodifiableMap", |
| 57 | + "toUnmodifiableSet", |
| 58 | + ]) |
| 59 | + } |
| 60 | +} |
| 61 | + |
| 62 | +/** |
| 63 | + * `Collectors` method which always ignores order, regardless of provided arguments (if any). |
| 64 | + */ |
| 65 | +class OrderIgnoringCollectorsMethod extends PotentiallyOrderIgnoringCollectorsMethod { |
| 66 | + OrderIgnoringCollectorsMethod() { |
| 67 | + // Ignore overload with `mapFactory` parameter |
| 68 | + hasName(["toConcurrentMap", "toMap"]) and getNumberOfParameters() <= 3 |
| 69 | + or |
| 70 | + hasName(["toSet", "toUnmodifiableMap", "toUnmodifiableSet"]) |
| 71 | + } |
| 72 | +} |
| 73 | + |
| 74 | +/** |
| 75 | + * Intermediate stream method which depends on the order of elements, without violating best |
| 76 | + * practices, e.g. taking a stateful filter predicate. |
| 77 | + */ |
| 78 | +class OrderDependentMethod extends Method { |
| 79 | + OrderDependentMethod() { |
| 80 | + getDeclaringType() instanceof StreamType |
| 81 | + and hasName([ |
| 82 | + "limit", |
| 83 | + "skip", |
| 84 | + ]) |
| 85 | + } |
| 86 | +} |
| 87 | + |
| 88 | +class UnorderedCollectionType extends RefType { |
| 89 | + UnorderedCollectionType() { |
| 90 | + hasQualifiedName("java.util", [ |
| 91 | + // In theory the actual instance could already be LinkedHashSet or LinkedHashMap, which preserve the order, |
| 92 | + // but assume that this is unlikely if type is already explicitly HashSet or HashMap instead of Set or Map |
| 93 | + "HashSet", "HashMap", |
| 94 | + ]) |
| 95 | + or hasQualifiedName("java.util.concurrent", ["ConcurrentMap", "ConcurrentHashMap"]) |
| 96 | + } |
| 97 | +} |
| 98 | + |
| 99 | +from MethodAccess sortedCall, MethodAccess collectCall, MethodAccess collectorsCall |
| 100 | +where |
| 101 | + sortedCall.getMethod() instanceof SortedMethod |
| 102 | + and collectCall.getMethod() instanceof CollectMethod |
| 103 | + and collectCall.getQualifier+() = sortedCall |
| 104 | + and collectCall.getArgument(0) = collectorsCall |
| 105 | + and collectorsCall.getMethod() instanceof PotentiallyOrderIgnoringCollectorsMethod |
| 106 | + // And seems to collect into collection type which does not preserve order |
| 107 | + and ( |
| 108 | + collectorsCall.getMethod() instanceof OrderIgnoringCollectorsMethod |
| 109 | + // Or other relevant Collectors method for which user provided arguments to create |
| 110 | + // an unordered collection; don't check supertype here because that would erroneously |
| 111 | + // match LinkedHashSet and LinkedHashMap then whose supertypes are HashSet and HashMap |
| 112 | + or collectCall.getType().(RefType).getSourceDeclaration() instanceof UnorderedCollectionType |
| 113 | + ) |
| 114 | + // And there is no order dependent call in between which justifies usage of `sorted` |
| 115 | + and not exists(MethodAccess orderDependentCall | |
| 116 | + orderDependentCall.getMethod() instanceof OrderDependentMethod |
| 117 | + and orderDependentCall.getQualifier+() = sortedCall |
| 118 | + and collectCall.getQualifier+() = orderDependentCall |
| 119 | + ) |
| 120 | + // And all chained calls return a Stream; this excludes cases where for example in between |
| 121 | + // the elements are collected to a List and a new Stream is created |
| 122 | + and forall(MethodAccess chainedCall | |
| 123 | + chainedCall.getQualifier+() = sortedCall |
| 124 | + and collectCall.getQualifier+() = chainedCall |
| 125 | + | |
| 126 | + chainedCall.getType() instanceof StreamType |
| 127 | + ) |
| 128 | +select sortedCall, "Sorting has no effect because $@ elements are collected without preserving order", collectorsCall, "here" |
0 commit comments