Skip to content

Commit 1131621

Browse files
committed
Add collecting-sorted-Stream-into-unordered-Map-or-Set.ql
1 parent d2f60e6 commit 1131621

File tree

1 file changed

+128
-0
lines changed

1 file changed

+128
-0
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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

Comments
 (0)