[nullsafe] Fix a bug and specify behavior for multiparam version for @TrueOnNull and @FalseOnNull annotations

Summary:
Yay, the previous refactoring finally makes it possible to do some actual
changes to the code in `TypeCheck.ml`!

Changes in this diff:
1. Fixes the bug: TrueOnNull and FalseOnNull were working only for
static methods. Surpsingly nobody noticed that. It is because the first
argument for non-static method was `this`.
2. Behavior change: TrueOnNull/FalseOnNull were not working correctly
where there are several argumens. See the task attached for the example
of the legit usecase. Now the behavior is the following: if there are
several Nullable arguments infer nullability for all of them.

Reviewed By: skcho

Differential Revision: D19770219

fbshipit-source-id: 7dffe42cd
master
Mitya Lyubarskiy 5 years ago committed by Facebook Github Bot
parent 5bee0858c6
commit 067a545c0e

@ -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 ->

@ -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
}
}
}

@ -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.]

Loading…
Cancel
Save