| 
 | 1 | +/**  | 
 | 2 | + * Finds incorrect usage of the `Comparator` factory methods `nullsFirst` and `nullsLast` where the  | 
 | 3 | + * desired behavior is apparently to handle nullable 'sort keys'.  | 
 | 4 | + *  | 
 | 5 | + * For example, consider this class:  | 
 | 6 | + * ```java  | 
 | 7 | + * class MyClass {  | 
 | 8 | + *     public Integer getI() {  | 
 | 9 | + *         return 2;  | 
 | 10 | + *     }  | 
 | 11 | + *  | 
 | 12 | + *     public String getNullableS() {  | 
 | 13 | + *         return null;  | 
 | 14 | + *     }  | 
 | 15 | + * }  | 
 | 16 | + * ```  | 
 | 17 | + *  | 
 | 18 | + * An incorrect usage of `nullsFirst` might look like this:  | 
 | 19 | + * ```java  | 
 | 20 | + * Comparator<MyClass> comp = Comparator.comparing(MyClass::getI)  | 
 | 21 | + *     .thenComparing(Comparator.nullsFirst(Comparator.comparing(MyClass::getNullableS)));  | 
 | 22 | + * ```  | 
 | 23 | + *  | 
 | 24 | + * The problem is that `nullsFirst` here applies to the compared `MyClass`, not the nullable `String`.  | 
 | 25 | + * So this will cause a `NullPointerException`.  | 
 | 26 | + *  | 
 | 27 | + * Instead one of the factory methods with separate 'sort key extractor' should be used, for example:  | 
 | 28 | + * ```java  | 
 | 29 | + * Comparator<MyClass> comp = Comparator.comparing(MyClass::getI)  | 
 | 30 | + *     // First extracts the 'sort key', and then applies `nullsFirst` on it  | 
 | 31 | + *     .thenComparing(MyClass::getNullableS, Comparator.nullsFirst(Comparator.naturalOrder()));  | 
 | 32 | + * ```  | 
 | 33 | + *  | 
 | 34 | + * @id TODO  | 
 | 35 | + * @kind problem  | 
 | 36 | + */  | 
 | 37 | + | 
 | 38 | +import java  | 
 | 39 | + | 
 | 40 | +class ComparatorMethod extends Method {  | 
 | 41 | +  ComparatorMethod() {  | 
 | 42 | +    getSourceDeclaration().getDeclaringType().hasQualifiedName("java.util", "Comparator")  | 
 | 43 | +  }  | 
 | 44 | +}  | 
 | 45 | + | 
 | 46 | +from MethodAccess nullsHandlingCall, ComparatorMethod nullsHandlingMethod  | 
 | 47 | +where  | 
 | 48 | +  // Check for cases where `nullsFirst` / `nullsLast` is part of a comparator chain, and therefore the  | 
 | 49 | +  // intention was likely to handle null for a 'key' and not the object itself (otherwise the complete  | 
 | 50 | +  // chain would have been wrapped with `nulls...`)  | 
 | 51 | +  (  | 
 | 52 | +    // `comp.thenComparing(nulls)`  | 
 | 53 | +    exists(MethodAccess thenComparingCall |  | 
 | 54 | +      thenComparingCall.getMethod().(ComparatorMethod).getSignature() =  | 
 | 55 | +        "thenComparing(java.util.Comparator)" and  | 
 | 56 | +      thenComparingCall.getArgument(0) = nullsHandlingCall  | 
 | 57 | +    )  | 
 | 58 | +    or  | 
 | 59 | +    // `nulls.thenComparing(...)`  | 
 | 60 | +    exists(MethodAccess thenComparingCall |  | 
 | 61 | +      thenComparingCall.getMethod().(ComparatorMethod).getName().matches("thenComparing%") and  | 
 | 62 | +      thenComparingCall.getQualifier() = nullsHandlingCall  | 
 | 63 | +    )  | 
 | 64 | +  ) and  | 
 | 65 | +  nullsHandlingMethod = nullsHandlingCall.getMethod() and  | 
 | 66 | +  nullsHandlingMethod.hasName(["nullsFirst", "nullsLast"])  | 
 | 67 | +select nullsHandlingCall, "Potential incorrect usage of `" + nullsHandlingMethod.getName() + "`"  | 
0 commit comments