diff --git a/infer/src/nullsafe/typeCheck.ml b/infer/src/nullsafe/typeCheck.ml index ebc5de616..40a5f5909 100644 --- a/infer/src/nullsafe/typeCheck.ml +++ b/infer/src/nullsafe/typeCheck.ml @@ -656,30 +656,40 @@ let normalize_cond_for_sil_prune idenv ~node cond = let rec check_condition_for_sil_prune tenv idenv calls_this find_canonical_duplicate loc curr_pname curr_pdesc curr_annotated_signature linereader typestate checks true_branch instr_ref ~nullsafe_mode ~original_node ~node c : TypeState.t = - (* check if the expression is coming from a call, and return the argument *) - let from_call filter_callee e : Exp.t option = - match e with + (* check if the expression is coming from a call, and return the arguments *) + let extract_arguments_from_call filter_callee expr = + match expr with | Exp.Var id -> ( match Errdesc.find_normal_variable_funcall node id with - | Some (Exp.Const (Const.Cfun pn), e1 :: _, _, _) when filter_callee pn -> - Some e1 + | Some (Exp.Const (Const.Cfun pn), arguments, _, _) when filter_callee pn -> + Some arguments | _ -> None ) | _ -> None in - (* check if the expression is coming from instanceof *) - let from_instanceof e : Exp.t option = from_call ComplexExpressions.procname_instanceof e in + (* check if the expression is coming from (`a` instanceof `b`), and returns `b`, if it is the case *) + let extract_argument_from_instanceof expr = + match extract_arguments_from_call ComplexExpressions.procname_instanceof expr with + | Some [argument; _] -> + Some argument + | Some _ -> + Logging.die Logging.InternalError "expected exactly two arguments in instanceOf expression" + | None -> + None + in (* check if the expression is coming from a procedure returning false on null *) - let from_is_false_on_null e : Exp.t option = - from_call (ComplexExpressions.procname_is_false_on_null tenv) e + let extract_arguments_from_call_to_false_on_null_func e = + extract_arguments_from_call (ComplexExpressions.procname_is_false_on_null tenv) e in (* check if the expression is coming from a procedure returning true on null *) - let from_is_true_on_null e : Exp.t option = - from_call (ComplexExpressions.procname_is_true_on_null tenv) e + let extract_arguments_from_call_to_true_on_null_func e = + extract_arguments_from_call (ComplexExpressions.procname_is_true_on_null tenv) e in (* check if the expression is coming from Map.containsKey *) - let from_containsKey e : Exp.t option = from_call ComplexExpressions.procname_containsKey e in + let is_from_containsKey expr = + extract_arguments_from_call ComplexExpressions.procname_containsKey expr |> Option.is_some + in (* Call to x.containsKey(e) returned `true`. It means that subsequent calls to `x.get(e)` should be inferred as non-nullables. We achieve this behavior by adding the result of a call to `x.get(e)` (in form of corresponding pvar) @@ -766,14 +776,18 @@ let rec check_condition_for_sil_prune tenv idenv calls_this find_canonical_dupli We've just ensured that [expr] == false. Update the typestate accordingly. *) - let handle_boolean_equal_false expr = - match from_is_true_on_null expr with - | Some argument -> - (* [expr] is false hence, according to true-on-null contract, [argument] can not not be null. - Hence we can infer its nullability as a non-null. + let handle_boolean_equal_false expr typestate = + match extract_arguments_from_call_to_true_on_null_func expr with + | Some arguments -> + (* [expr] is false hence, according to true-on-null contract, neither of [arguments] can be null. + (otherwise the result would have been true) + Hence we can infer their nullability as non-null. *) - set_original_pvar_to_nonnull_in_typestate ~with_cond_redundant_check:false argument - typestate + List.fold ~init:typestate + ~f:(fun accumulated_typestate argument -> + set_original_pvar_to_nonnull_in_typestate ~with_cond_redundant_check:false argument + accumulated_typestate ) + arguments | None -> typestate in @@ -782,22 +796,25 @@ let rec check_condition_for_sil_prune tenv idenv calls_this find_canonical_dupli Update the typestate accordingly. *) let handle_boolean_equal_true expr typestate = - match from_is_false_on_null expr with - | Some argument -> - (* [expr] is true hence, according to false-on-null contract, [argument] can not not be null. - Hence we can infer its nullability as a non-null. + match extract_arguments_from_call_to_false_on_null_func expr with + | Some arguments -> + (* [expr] is true hence, according to false-on-null contract, neither of [arguments] can be null. + (otherwise the result would have been false). + Hence we can infer their nullability as non-null. *) - set_original_pvar_to_nonnull_in_typestate ~with_cond_redundant_check:false argument - typestate + List.fold ~init:typestate + ~f:(fun accumulated_typestate argument -> + set_original_pvar_to_nonnull_in_typestate ~with_cond_redundant_check:false argument + accumulated_typestate ) + arguments | None -> ( - match from_instanceof expr with + match extract_argument_from_instanceof expr with | Some argument -> (* ([argument] instanceof [expr] == true) implies (expr != null) *) set_original_pvar_to_nonnull_in_typestate ~with_cond_redundant_check:false argument typestate | None -> - if Option.is_some (from_containsKey expr) then - handle_containsKey_returned_true expr typestate + if is_from_containsKey expr then handle_containsKey_returned_true expr typestate else typestate ) in (* Assuming [expr] is a non-primitive, this is the branch where, according to PRUNE semantics, @@ -815,7 +832,7 @@ let rec check_condition_for_sil_prune tenv idenv calls_this find_canonical_dupli `null` and `false`, hence the expression means either "some_bool == false" or "some_object == null" We don't currently have a logic for the latter case, but we do support the former *) - handle_boolean_equal_false expr + handle_boolean_equal_false expr typestate | Exp.BinOp (Binop.Ne, Exp.Const (Const.Cint i), expr) | Exp.BinOp (Binop.Ne, expr, Exp.Const (Const.Cint i)) when IntLit.iszero i -> diff --git a/infer/tests/codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java b/infer/tests/codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java index 10ae3f10b..77828901f 100644 --- a/infer/tests/codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java +++ b/infer/tests/codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java @@ -16,65 +16,202 @@ import javax.annotation.Nullable; /** Testing functionality related to @TrueOnNull and @FalseOnNull methods */ public class TrueFalseOnNull { - // Example of API that benefits from annotating with @TrueOnNull and @FalseOnNull - static class AnnotatedTextUtils { + static class StaticOneParam { + @TrueOnNull + static boolean trueOnNull(@Nullable Object o) { + return o == null ? true : o.toString() == "something"; + } + + @FalseOnNull + static boolean falseOnNull(@Nullable Object o) { + return o == null ? false : o.toString() == "something"; + } + + static boolean notAnnotated(@Nullable Object o) { + return o == null ? false : o.toString() == "something"; + } + } + + static class NonStaticOneParam { + private String compareTo = "something"; @TrueOnNull - static boolean isEmpty(@Nullable CharSequence s) { - return s == null || s.equals(""); + boolean trueOnNull(@Nullable Object o) { + return o == null ? true : o.toString() == compareTo; } @FalseOnNull - static boolean isNotEmpty(@Nullable CharSequence s) { - return s != null && s.length() > 0; + boolean falseOnNull(@Nullable Object o) { + return o == null ? false : o.toString() == compareTo; + } + + boolean notAnnotated(@Nullable Object o) { + return o == null ? false : o.toString() == compareTo; } } - // The same API, but not annotated - static class NotAnnotatedTextUtils { - static boolean isEmpty(@Nullable CharSequence s) { - return s == null || s.equals(""); + // @TrueOnNull and @FalseOnNull should expect true/false will be returned if ANY of input objects + // is null. + // In other words, they should infer that all input nullable objects are non-null in the + // corresponding branch. + static class NonStaticSeveralParams { + private String compareTo = "something"; + + @TrueOnNull + boolean trueOnNull( + @Nullable Object nullable1, int primitive, Object nonnull, @Nullable Object nullable2) { + if (nullable1 == null || nullable2 == null) { + return true; + } + return nonnull == compareTo; + } + + @FalseOnNull + boolean falseOnNull( + @Nullable Object nullable1, int primitive, Object nonnull, @Nullable Object nullable2) { + if (nullable1 == null || nullable2 == null) { + return false; + } + return nonnull == compareTo; } - static boolean isNotEmpty(@Nullable CharSequence s) { - return s != null && s.length() > 0; + boolean notAnnotated( + @Nullable Object nullable1, int primitive, Object nonnull, @Nullable Object nullable2) { + if (nullable1 == null || nullable2 == null) { + return false; + } + return nonnull == compareTo; } } - class Test { - void testTrueOnNull(@Nullable CharSequence s) { - // Explicitly annotated - if (!AnnotatedTextUtils.isEmpty(s)) { - s.toString(); // OK: if we are here, we know that s is not null + class TestStaticOneParam { + + void trueOnNullPositiveBranchIsBAD(@Nullable String s) { + if (StaticOneParam.trueOnNull(s)) { + s.toString(); } + } - // Not annotated - if (!NotAnnotatedTextUtils.isEmpty(s)) { - s.toString(); // BAD: the typecker does not know s can not be null + void trueOnNullNegativeBranchIsOK(@Nullable String s) { + if (!StaticOneParam.trueOnNull(s)) { + s.toString(); } + } - if (AnnotatedTextUtils.isEmpty(s)) { - s.toString(); // BAD: s can be null or an empty string + void falseOnNullPositiveBranchIsOK(@Nullable String s) { + if (StaticOneParam.falseOnNull(s)) { + s.toString(); } } - void testFalseOnNull(@Nullable CharSequence s) { - // Explicitly annotated - if (AnnotatedTextUtils.isNotEmpty(s)) { - s.toString(); // OK: if we are here, we know that `s` is not null + void falseOnNullNegativeBranchIsBAD(@Nullable String s) { + if (!StaticOneParam.falseOnNull(s)) { + s.toString(); + } + } + + void notAnnotatedPositiveBranchIsBAD(@Nullable String s) { + if (StaticOneParam.notAnnotated(s)) { + s.toString(); + } + } + + void notAnnotatedNegativeBranchIsBAD(@Nullable String s) { + if (!StaticOneParam.notAnnotated(s)) { + s.toString(); } + } + } + + class TestNonStaticOneParam { + private NonStaticOneParam object = new NonStaticOneParam(); - // Not annotated - if (NotAnnotatedTextUtils.isNotEmpty(s)) { - s.toString(); // BAD: the typecker does not know `s` can not be null + void trueOnNullPositiveBranchIsBAD(@Nullable String s) { + if (object.trueOnNull(s)) { + s.toString(); } + } + + void trueOnNullNegativeBranchIsOK(@Nullable String s) { + if (!object.trueOnNull(s)) { + s.toString(); + } + } - if (!AnnotatedTextUtils.isNotEmpty(s)) { - s.toString(); // BAD: `s` can be null or an empty string + void falseOnNullPositiveBranchIsOK(@Nullable String s) { + if (object.falseOnNull(s)) { + s.toString(); } } - void testModelledTrueOnNull(String s) { + void falseOnNullNegativeBranchIsBAD(@Nullable String s) { + if (!object.falseOnNull(s)) { + s.toString(); + } + } + + void notAnnotatedPositiveBranchIsBAD(@Nullable String s) { + if (object.notAnnotated(s)) { + s.toString(); + } + } + + void notAnnotatedNegativeBranchIsBAD(@Nullable String s) { + if (!object.notAnnotated(s)) { + s.toString(); + } + } + } + + class TestNonStaticSeveralParams { + private NonStaticSeveralParams object = new NonStaticSeveralParams(); + + void trueOnNullPositiveBranchIsBAD(@Nullable String s1, @Nullable String s2) { + if (object.trueOnNull(s1, 1, "123", s2)) { + s1.toString(); + s2.toString(); + } + } + + void trueOnNullNegativeBranchIsOK(@Nullable String s1, @Nullable String s2) { + if (!object.trueOnNull(s1, 1, "123", s2)) { + s1.toString(); + s2.toString(); + } + } + + void falseOnNullPositiveBranchIsOK(@Nullable String s1, @Nullable String s2) { + if (object.falseOnNull(s1, 1, "123", s2)) { + s1.toString(); + s2.toString(); + } + } + + void falseOnNullNegativeBranchIsBAD(@Nullable String s1, @Nullable String s2) { + if (!object.falseOnNull(s1, 1, "123", s2)) { + s1.toString(); + s2.toString(); + } + } + + void notAnnotatedPositiveBranchIsBAD(@Nullable String s1, @Nullable String s2) { + if (object.notAnnotated(s1, 1, "123", s2)) { + s1.toString(); + s2.toString(); + } + } + + void notAnnotatedNegativeBranchIsBAD(@Nullable String s1, @Nullable String s2) { + if (!object.notAnnotated(s1, 1, "123", s2)) { + s1.toString(); + s2.toString(); + } + } + } + + class TestModels { + + void testModelledTrueOnNull(@Nullable String s) { // TextUtils.isEmpty is modelled as TrueOnNull if (!TextUtils.isEmpty(s)) { s.toString(); // OK: if we are here, we know that `s` is not null @@ -84,19 +221,19 @@ public class TrueFalseOnNull { if (!Strings.isNullOrEmpty(s)) { s.toString(); // OK: if we are here, we know that `s` is not null } - - if (!NotAnnotatedTextUtils.isEmpty(s)) { - s.toString(); // BAD: the typechecker can not figure this out for not modelled class - } } + } + + // this is a common enough pattern to be tested separately + class EarlyReturn { void testEarlyReturn(@Nullable CharSequence s1, @Nullable CharSequence s2) { - if (AnnotatedTextUtils.isEmpty(s1) || NotAnnotatedTextUtils.isEmpty(s2)) { + if (StaticOneParam.trueOnNull(s1) || StaticOneParam.notAnnotated(s2)) { return; } s1.toString(); // OK: if `s1` was null, we would no rech this point - s2.toString(); // BAD: typechecker can not figure this for not annotated class + s2.toString(); // BAD: no reason for `s2` to become non-nullable } } } diff --git a/infer/tests/codetoanalyze/java/nullsafe-default/issues.exp b/infer/tests/codetoanalyze/java/nullsafe-default/issues.exp index 787a7d9f2..b7bd7ee3c 100644 --- a/infer/tests/codetoanalyze/java/nullsafe-default/issues.exp +++ b/infer/tests/codetoanalyze/java/nullsafe-default/issues.exp @@ -247,9 +247,22 @@ codetoanalyze/java/nullsafe-default/StrictModeForThirdParty.java, codetoanalyze. codetoanalyze/java/nullsafe-default/SwitchCase.java, codetoanalyze.java.nullsafe_default.SwitchCase.getNullableColor():codetoanalyze.java.nullsafe_default.Color, 0, ERADICATE_RETURN_OVER_ANNOTATED, no_bucket, WARNING, [Method `getNullableColor()` is annotated with `@Nullable` but never returns null.] codetoanalyze/java/nullsafe-default/SwitchCase.java, codetoanalyze.java.nullsafe_default.SwitchCase.switchOnNullIsBad():java.lang.String, 1, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [NullPointerException will be thrown at this line! `color` is `null` and is dereferenced via calling `ordinal()`: null constant at line 14] codetoanalyze/java/nullsafe-default/SwitchCase.java, codetoanalyze.java.nullsafe_default.SwitchCase.switchOnNullableIsBad():java.lang.String, 1, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`color` is nullable and is not locally checked for null when calling `ordinal()`: call to getNullableColor() at line 28] -codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$Test.testEarlyReturn(java.lang.CharSequence,java.lang.CharSequence):void, 5, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s2` is nullable and is not locally checked for null when calling `toString()`.] -codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$Test.testFalseOnNull(java.lang.CharSequence):void, 6, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s` is nullable and is not locally checked for null when calling `toString()`.] -codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$Test.testFalseOnNull(java.lang.CharSequence):void, 10, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s` is nullable and is not locally checked for null when calling `toString()`.] -codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$Test.testTrueOnNull(java.lang.CharSequence):void, 6, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s` is nullable and is not locally checked for null when calling `toString()`.] -codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$Test.testTrueOnNull(java.lang.CharSequence):void, 10, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s` is nullable and is not locally checked for null when calling `toString()`.] +codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$EarlyReturn.testEarlyReturn(java.lang.CharSequence,java.lang.CharSequence):void, 5, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s2` is nullable and is not locally checked for null when calling `toString()`.] +codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$TestModels.testModelledTrueOnNull(java.lang.String):void, 0, ERADICATE_PARAMETER_NOT_NULLABLE, no_bucket, WARNING, [`TextUtils.isEmpty(...)`: parameter #1 is declared non-nullable but the argument `s` is nullable.] +codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$TestNonStaticOneParam.falseOnNullNegativeBranchIsBAD(java.lang.String):void, 1, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s` is nullable and is not locally checked for null when calling `toString()`.] +codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$TestNonStaticOneParam.notAnnotatedNegativeBranchIsBAD(java.lang.String):void, 1, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s` is nullable and is not locally checked for null when calling `toString()`.] +codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$TestNonStaticOneParam.notAnnotatedPositiveBranchIsBAD(java.lang.String):void, 1, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s` is nullable and is not locally checked for null when calling `toString()`.] +codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$TestNonStaticOneParam.trueOnNullPositiveBranchIsBAD(java.lang.String):void, 1, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s` is nullable and is not locally checked for null when calling `toString()`.] +codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$TestNonStaticSeveralParams.falseOnNullNegativeBranchIsBAD(java.lang.String,java.lang.String):void, 1, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s1` is nullable and is not locally checked for null when calling `toString()`.] +codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$TestNonStaticSeveralParams.falseOnNullNegativeBranchIsBAD(java.lang.String,java.lang.String):void, 2, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s2` is nullable and is not locally checked for null when calling `toString()`.] +codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$TestNonStaticSeveralParams.notAnnotatedNegativeBranchIsBAD(java.lang.String,java.lang.String):void, 1, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s1` is nullable and is not locally checked for null when calling `toString()`.] +codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$TestNonStaticSeveralParams.notAnnotatedNegativeBranchIsBAD(java.lang.String,java.lang.String):void, 2, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s2` is nullable and is not locally checked for null when calling `toString()`.] +codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$TestNonStaticSeveralParams.notAnnotatedPositiveBranchIsBAD(java.lang.String,java.lang.String):void, 1, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s1` is nullable and is not locally checked for null when calling `toString()`.] +codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$TestNonStaticSeveralParams.notAnnotatedPositiveBranchIsBAD(java.lang.String,java.lang.String):void, 2, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s2` is nullable and is not locally checked for null when calling `toString()`.] +codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$TestNonStaticSeveralParams.trueOnNullPositiveBranchIsBAD(java.lang.String,java.lang.String):void, 1, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s1` is nullable and is not locally checked for null when calling `toString()`.] +codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$TestNonStaticSeveralParams.trueOnNullPositiveBranchIsBAD(java.lang.String,java.lang.String):void, 2, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s2` is nullable and is not locally checked for null when calling `toString()`.] +codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$TestStaticOneParam.falseOnNullNegativeBranchIsBAD(java.lang.String):void, 1, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s` is nullable and is not locally checked for null when calling `toString()`.] +codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$TestStaticOneParam.notAnnotatedNegativeBranchIsBAD(java.lang.String):void, 1, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s` is nullable and is not locally checked for null when calling `toString()`.] +codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$TestStaticOneParam.notAnnotatedPositiveBranchIsBAD(java.lang.String):void, 1, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s` is nullable and is not locally checked for null when calling `toString()`.] +codetoanalyze/java/nullsafe-default/TrueFalseOnNull.java, codetoanalyze.java.nullsafe_default.TrueFalseOnNull$TestStaticOneParam.trueOnNullPositiveBranchIsBAD(java.lang.String):void, 1, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`s` is nullable and is not locally checked for null when calling `toString()`.] codetoanalyze/java/nullsafe-default/third-party-test-code/some/test/pckg/ThirdPartyTestClass.java, some.test.pckg.ThirdPartyTestClass.returnSpecifiedAsNullable():java.lang.String, 0, ERADICATE_RETURN_OVER_ANNOTATED, no_bucket, WARNING, [Method `returnSpecifiedAsNullable()` is annotated with `@Nullable` but never returns null.]