Skip to content

Conversation

@bdragan
Copy link
Contributor

@bdragan bdragan commented Jun 25, 2025

Closes #5107.

A null check is redundant if it is performed on a variable or method call that is known to be non-null within a @NullMarked scope.

Within a @NullMarked scope, types are non-null by default unless explicitly annotated with @Nullable.
Therefore, checking a variable or method return value (that isn't @Nullable) for nullness is unnecessary, as it should never be null.

Example:

import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

@NullMarked
class MyClass {
  void process(String definitelyNonNull) {
    // BUG: Diagnostic contains: RedundantNullCheck
    if (definitelyNonNull == null) {
      System.out.println("This will never happen");
    }
    // ...
  }

  String getString() {
    return "hello";
  }

  @Nullable String getNullableString() {
    return null;
  }

  void anotherMethod() {
    String s = getString();
    // BUG: Diagnostic contains: RedundantNullCheck
    if (s == null) {
      // s is known to be non-null because getString() is not @Nullable
      // and we are in a @NullMarked scope.
      System.out.println("Redundant check");
    }

    String nullableStr = getNullableString();
    if (nullableStr == null) { // This check is NOT redundant
      System.out.println("Nullable string might be null");
    }
  }
}

This check helps to clean up code and reduce clutter by removing unnecessary null checks, making the code easier to read and maintain.
It also reinforces the contract provided by @NullMarked and @Nullable annotations.

@google-cla
Copy link

google-cla bot commented Jun 25, 2025

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@bdragan bdragan changed the title 5107: Added RedundantNullCheck bug pattern Added RedundantNullCheck bug pattern Jun 27, 2025
Copy link
Member

@cpovirk cpovirk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for doing this, and I apologize again for not getting to it.

My suggested edits assume a few static imports:

import static com.google.errorprone.util.ASTHelpers.hasImplicitType;
import static com.google.errorprone.util.ASTHelpers.isConsideredFinal;
import static javax.lang.model.type.TypeKind.TYPEVAR;

Whatever special cases you're up for adding tests for are great, but I can't complain when you already have a bunch of tests and I've left you hanging so long.

return buildDescription(tree).build();
}

private static boolean isEffectivelyNullable(MethodSymbol methodSymbol, VisitorState state) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A totally optional thing that you could look into is handling of constructors: I think those end up in the MethodSymbol code path, and of course any call to a constructor produces a non-null value, regardless of annotations. Given that, you might well be able to return false if methodSymbol.isConstructor().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, done. That reminded me of literal initializers, which I now also covered.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hadn't considered how much more we could do here if we wanted. (Even without getting into true "dataflow," we could cover many of the basics that we have in NullnessPropagationTransfer.) The cases you've already covered strike me as the most important ones: It's easier to imagine that someone might wrongly conclude that foo() can return null than that i + 1 can produce null :) The next steps that come to mind are to cover expressions that "pass through" values, like the ternary operator or a cast. I would actively recommend not trying to squeeze that into this PR, since I fear that I will disappear again if I force enough rounds of review. But that's something else you could consider as a follow-up change or in your internal version if it feels valuable enough.

@cpovirk cpovirk self-assigned this Sep 22, 2025
copybara-service bot pushed a commit to google/guava that referenced this pull request Sep 22, 2025
…ck`](google/error-prone#5121).

Also, use that check as motivation to finally make `AbstractBiMap` more understandable to nullness checkers. While in the area, I also made `inverse` be `private`. That required changing a reference in a subclass in the file to use `super.inverse`. As I understand it from my experience in 856123e, a `private` field like `inverse` is [accessible](https://docs.oracle.com/javase/specs/jls/se24/html/jls-6.html#jls-6.6.1) but not inherited ([1](https://docs.oracle.com/javase/specs/jls/se24/html/jls-8.html#d5e13771), [2](https://docs.oracle.com/javase/specs/jls/se24/html/jls-8.html#jls-8.2)).

(And marginally simplify a couple `AbstractBiMap` subclasses by removing unnecessary type arguments.)

RELNOTES=n/a
PiperOrigin-RevId: 809750887
copybara-service bot pushed a commit to google/guava that referenced this pull request Sep 22, 2025
…ck`](google/error-prone#5121).

Also, use that check as motivation to finally make `AbstractBiMap` more understandable to nullness checkers. While in the area, I also made `inverse` be `private`. That required changing a reference in a subclass in the file to use `super.inverse`. As I understand it from my experience in 856123e, a `private` field like `inverse` is [accessible](https://docs.oracle.com/javase/specs/jls/se24/html/jls-6.html#jls-6.6.1) but not inherited ([1](https://docs.oracle.com/javase/specs/jls/se24/html/jls-8.html#d5e13771), [2](https://docs.oracle.com/javase/specs/jls/se24/html/jls-8.html#jls-8.2)).

(And marginally simplify a couple `AbstractBiMap` subclasses by removing unnecessary type arguments.)

RELNOTES=n/a
PiperOrigin-RevId: 810029934
@bdragan
Copy link
Contributor Author

bdragan commented Sep 23, 2025

@cpovirk Great, thank you, I incorporated and pushed your suggestions (with two additions - implicit vars which aren't parameters and literal initializers). Also added tests for the new use cases.

@bdragan bdragan requested a review from cpovirk September 23, 2025 08:23
@bdragan
Copy link
Contributor Author

bdragan commented Sep 23, 2025

@cpovirk Also we think that extending this with support for redundant Objects.requireNonNull calls would be beneficial, wdyt? If you agree I will also push it here - I already have a working solution for direct calls which are most common in practice, i.e. Objects.requireNonNull(x) (so method references are not covered i.e. Object::requireNonNull but that should be fine I guess).

@cpovirk
Copy link
Member

cpovirk commented Sep 23, 2025

@cpovirk Also we think that extending this with support for redundant Objects.requireNonNull calls would be beneficial, wdyt? If you agree I will also push it here - I already have a working solution for direct calls which are most common in practice, i.e. Objects.requireNonNull(x) (so method references are not covered i.e. Object::requireNonNull but that should be fine I guess).

That is a good question. I tend to have different views for different code:

In places like Guava, we actively want to make sure that we produce NPE immediately if users pass null where they shouldn't. This prepares us for the potential that the JVM performs similar checks automatically and unavoidably someday, but more immediately, it ensures that we aren't setting up a trap that will spring later in the program (like we would be doing if we were to let you put null into a collection that shouldn't support it). More often, the NPE does happen immediately "naturally," so there's no need to check. But we at least occasionally still check explicitly so that we can provide a nicer exception message (or so that we can support a weird environment that doesn't guarantee NPE, as we sometimes have to do).

For more typical code, I don't usually bother with defensive checks at all, since the damage from getting things "wrong" is more contained. There, I'd expect to see requireNonNull only for the "non-redundant" cases.

I would say that it's fine for you to do whatever suits you, but I will eventually end up running this by someone for internal review, so we may end up changing course then. We also always have the option of making the behavior configurable with a flag. That still leaves the question of which behavior to make the default, but it at least offers flexibility to users who have strong enough opinions.

Copy link
Member

@cpovirk cpovirk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again. I could approve this as it is now to move the import process along, but I'll hold off in case you want to try to add requireNonNull support or make other changes. In the meantime, I'll retest what you have on our depot to see if anything new (or anything that I Just missed) jumps out at me.

import javax.lang.model.element.ElementKind;

@BugPattern(
summary = "Explicit null check on a variable or method call that is not @Nullable within a @NullMarked scope.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got to wondering if someone might read the current text as saying that the null check has to be within a @NullMarked scope, as opposed to saying that the field or method needs to be so. Maybe we can avoid that by focusing on what we know instead of how we know it, as in the following...?

Suggested change
summary = "Explicit null check on a variable or method call that is not @Nullable within a @NullMarked scope.",
summary = "Explicit null check on an expression that is known not to be null.",

That said, when I tested this out on our codebase, I saw a number of cases in which the value could have been null but the annotations were wrong :) So I'm tempted to say something like:

Suggested change
summary = "Explicit null check on a variable or method call that is not @Nullable within a @NullMarked scope.",
summary = "Explicit null check on an expression that is non-null according to nullness annotations.",

But then that's not true for cases like literals and constructors... :)

Maybe this is ultimately best covered in the Markdown docs. But if you have more thoughts on how to phrase things here, maybe we can figure something out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, we should improve the wording here. Maybe just explicitly mention both (language semantics and annotations), something like:

Null check on an expression that is statically determined to be non-null according to language semantics or nullness annotations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe simply should instead of statically determined (which is redundant phrase in the context of static analysis itself):

Null check on an expression that should be non-null according to language semantics or nullness annotations.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I like your "statically determined" phrasing over "should": Even when we qualify it with "according to language semantics or nullness annotations," I worry that "should" could be misread as somehow related to what the code does if it encounters null. I'm not sure I'm phrasing that well :\ Like, if I write...

if (next == null) {
  throw new IllegalStateException();
}

...then clearly next "should" be non-null. And that's "should" in a different sense than the one we'd be talking about here.

I agree that "statically determined" is technically redundant, but I think it's worth it for the emphasis. I say that especially because I'm seeing a lot of cases in which the actual problem is that a type is missing a @Nullable annotation :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed; then for now I changed it (both in pattern summary and in md) to:

Null check on an expression that is statically determined to be non-null according to language semantics or nullness annotations.

Let's think and improve more if needed.

@@ -0,0 +1,52 @@
A null check is redundant if it is performed on a variable or method call that is known to be non-null within a `@NullMarked` scope.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning to what I was saying about above how the nullness information we have may or may not come from annotations and how those annotations may or may not be correct....

I'm not sure if we have a consistent style for how we phrase these, but here are some things I might say:

Some code checks for null when null is "impossible." In some cases, null is truly impossible according to the rules of the Java language, such as when testing the result of a constructor call. In other cases, null is impossible according to nullness annotations on a parameter or method that is being used: Either the type is annotated with @NonNull, or it has no annotation but appears in the scope of a @NullMarked annotation, which makes most unannotated types non-null.

An "impossible" null check may indicate that the null check is a bug. For example, maybe it should instead be checking whether the value is an empty collection or Optional. Alternatively, the check might merely have become unnecessary. Or, in some cases, the value can be null at runtime, and the real problem is that a method or parameter is missing a @Nullable annotation.

return NO_MATCH;
}

if ((varSymbol.getKind() == ElementKind.LOCAL_VARIABLE
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another case in which I'm going to ask something more a question for the future: Now that we restrict checking to final (and effectively final) VarSymbol declaration, would it make sense to also cover fields?

(This also reminds me that exception parameters are always non-null, at least if no one assigns back to them! But I say that mostly just to get it out of my system.)

Copy link
Member

@cpovirk cpovirk Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other thoughts on this proposal (in addition to the ones above on line 61), again with the motivation of capturing them for the future:

If we cover fields, we could identify additional opportunities (while also keeping all the current ones) by looking for @NonNull annotations on them and @NullMarked annotations on the enclosing elements. That would let us treat even more fields as non-null, including but not limited to non-final fields.

That said, we do sometimes see users who claim that a field is non-null when it's merely non-null after some initialization occurs. So it's possible that those users would be annoyed if we were to complain that their lazy-initialization code checks for the nullness of a "non-null" field. That, at least, is an issue only for non-final fields.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Err, sorry, you do cover fields; I'm just focused on the branch this is not responsible for them.

I will be out for a couple hours, but I'll try to reevaluate when I get back. Several of my comments need revisions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, let's see if I can evaluate the full logic instead of over-focusing on specific details and losing the bigger picture.

There are a few VarSymbol cases in which we could report that a value is never null:

  • for a field that matches either of the following conditions:
    • @NonNull (I'm not sure we check at the moment. That's fine)
    • a non-type-variable-typed, non-@Nullable field in the scope of @NullMarked
  • for a parameter that matches either of the "field" conditions and is not an implicitly typed lambda parameter
  • for a variable that matches both of the following conditions:
    • "considered final"
    • initialized to a !isEffectivelyNullable method or to a literal (other than null)
  • (probably also for a field that matches all the "variable" conditions, though that's another thing we don't check at the moment, and that's fine, too)

We should figure out whether that is even right. Whatever we settle on, and whatever part of that we implement now, it might be easiest to express with an isEffectivelyNullable(VarSymbol, ...) method. That would let us have a separate section for each case, from which we can return early, since we can't really do that with the logic inline in matchBinary. (If we have a separate method, that could also help with other possible future generalizations that we've discussed.)

Copy link
Contributor Author

@bdragan bdragan Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, all points are correct.

for a field that matches either of the following conditions:

  • @NonNull (I'm not sure we check at the moment. That's fine)

Good catch. I was focused mostly on @NullMarked scope (where @NonNull is redundant, at least when it comes to tools implementing null-marked spec), but since I already added support for it for methods out of @NullMarked scope, I changed it now for var symbols too.

probably also for a field that matches all the "variable" conditions

We have to think more when it comes to fields in some situations, including alignment with other tools, e.g. a field that is not marked as nullable in @NullMarked scope will not be considered nullable even if it is not considered final or is initialized with an isEffectivelyNullable method, so we'd need to take care of it during implementation. Hence I left the logic for fields to be annotation-driven only for now.

isEffectivelyNullable(VarSymbol, ...)

Good idea, I already did it for methods and the flow is easier to follow for them with that. Now I refactored the logic for var symbols that way too. 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another aspect about fields is that they are more affected by build-time or runtime code manipulation tools (i.e. like Lombok, dependency injection, bytecode weaving in runtime, etc) than local variables I guess, so the static analysis assumptions we make might not be suitable to all scenarios in practice.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that Error Prone conveniently blows up on Lombok code, at least :) But, yes, good point: Fields do get weird treatment from tools in a variety of situations. I also expect for humans to be less reliable at annotating their fields than their APIs simply because I expect them to focus on public members, while fields are usually private (aside from the occasional constants).

(I have some other distractions going on at the moment but should be able to get back to this tomorrow if I don't manage to today.)

Copy link
Member

@cpovirk cpovirk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, the results looked great, but I ended up with more thoughts anyway. I hope that it's just simplifications at this point.

if (NullnessUtils.isAlreadyAnnotatedNullable(varSymbol)) {
return NO_MATCH;
}
if (varSymbol.asType().getKind() == TYPEVAR) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I was just looking at this suggestion of mine again, and I think I was mistaken: We shouldn't need to check TYPEVAR for variables: I'm thinking that the only case in which we could ever produce a finding for a type-variable-typed variable is a call to a method that returns @NonNull T for some type-variable type T. In that case, we're already relying on isEffectivelyNullable(MethodSymbol, ...) to do the right thing for TYPEVAR.

So I think we can remove the TYPEVAR check here, leaving only the one below (the one for methods).

Copy link
Member

@cpovirk cpovirk Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, sorry again: I am mixed up, as I'll cover below. [edit: here]

return NO_MATCH;
}

if ((varSymbol.getKind() == ElementKind.LOCAL_VARIABLE
Copy link
Member

@cpovirk cpovirk Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other thoughts on this proposal (in addition to the ones above on line 61), again with the motivation of capturing them for the future:

If we cover fields, we could identify additional opportunities (while also keeping all the current ones) by looking for @NonNull annotations on them and @NullMarked annotations on the enclosing elements. That would let us treat even more fields as non-null, including but not limited to non-final fields.

That said, we do sometimes see users who claim that a field is non-null when it's merely non-null after some initialization occurs. So it's possible that those users would be annoyed if we were to complain that their lazy-initialization code checks for the nullness of a "non-null" field. That, at least, is an issue only for non-final fields.

@cpovirk
Copy link
Member

cpovirk commented Sep 24, 2025

I also wanted to write down somewhere how this differs from https://errorprone.info/bugpattern/ImpossibleNullComparison, at least as I understand it:

  • ImpossibleNullComparison does not trust annotations.
  • ImpossibleNullComparison does not handle literals.
  • ImpossibleNullComparison does not track fields after assignment.
  • ImpossibleNullComparison does look for operations like requireNonNull that this PR at least currently doesn't cover.
  • ImpossibleNullComparison has some suggested fixes, at least a couple of which (the ones for List/Collection and Optional) we could probably someday steal for here.
  • ImpossibleNullComparison handles case null in switch.
  • ImpossibleNullComparison handles specific, well-known APIs.

The long-term plan should likely be to merge these, probably offering configuration options and enhanced docs. But I'm hoping to be able to make the case that we shouldn't block on that.

@bdragan
Copy link
Contributor Author

bdragan commented Sep 24, 2025

@cpovirk Also we think that extending this with support for redundant Objects.requireNonNull calls would be beneficial, wdyt? If you agree I will also push it here - I already have a working solution for direct calls which are most common in practice, i.e. Objects.requireNonNull(x) (so method references are not covered i.e. Object::requireNonNull but that should be fine I guess).

That is a good question. I tend to have different views for different code:

In places like Guava, we actively want to make sure that we produce NPE immediately if users pass null where they shouldn't. This prepares us for the potential that the JVM performs similar checks automatically and unavoidably someday, but more immediately, it ensures that we aren't setting up a trap that will spring later in the program (like we would be doing if we were to let you put null into a collection that shouldn't support it). More often, the NPE does happen immediately "naturally," so there's no need to check. But we at least occasionally still check explicitly so that we can provide a nicer exception message (or so that we can support a weird environment that doesn't guarantee NPE, as we sometimes have to do).

For more typical code, I don't usually bother with defensive checks at all, since the damage from getting things "wrong" is more contained. There, I'd expect to see requireNonNull only for the "non-redundant" cases.

I would say that it's fine for you to do whatever suits you, but I will eventually end up running this by someone for internal review, so we may end up changing course then. We also always have the option of making the behavior configurable with a flag. That still leaves the question of which behavior to make the default, but it at least offers flexibility to users who have strong enough opinions.

Makes sense. I pushed it now too, it is optional and disabled by default (can be enabled with RedundantNullCheck:CheckRequireNonNull flag).

Copy link
Member

@cpovirk cpovirk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The results from running this over our depot look excellent—so many people who think that methods might return null when they actually throw an exception, for example, plus all the cases of missing @Nullable annotations that I'd mentioned before. I'm still not sure if we'll be able to justify turning it on for everyone, since some people have chosen to perform manual defensive checks on parameters, but I hope we can get there when we're finally able to focus more on nullness again!

I have run out of behavioral suggestions, so all that's left is some style-type things.

Comment on lines 108 to 116
VarSymbol varSymbol = nullCheck.varSymbolButUsuallyPreferBareIdentifier();
if (varSymbol != null) {
isRedundant = !isEffectivelyNullable(varSymbol, state);
} else {
MethodSymbol methodSymbol = nullCheck.methodSymbol();
if (methodSymbol != null) {
isRedundant = !isEffectivelyNullable(methodSymbol, state);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
VarSymbol varSymbol = nullCheck.varSymbolButUsuallyPreferBareIdentifier();
if (varSymbol != null) {
isRedundant = !isEffectivelyNullable(varSymbol, state);
} else {
MethodSymbol methodSymbol = nullCheck.methodSymbol();
if (methodSymbol != null) {
isRedundant = !isEffectivelyNullable(methodSymbol, state);
}
}
VarSymbol varSymbol = nullCheck.varSymbolButUsuallyPreferBareIdentifier();
MethodSymbol methodSymbol = nullCheck.methodSymbol();
boolean isRedundant = false;
if (varSymbol != null) {
isRedundant = !isEffectivelyNullable(varSymbol, state);
} else if (methodSymbol != null) {
isRedundant = !isEffectivelyNullable(methodSymbol, state);
}

It might be even better to go a little further and, instead of setting isRedundant, just return immediately, especially after the slightly simplification I'm suggesting to the return statement. You could merge the checks into lines like:

    if (varSymbol != null && !isEffectivelyNullable(varSymbol, state) {
      return describeMatch(tree);
    }

if (!OBJECTS_REQUIRE_NON_NULL.matches(tree, state)) {
return NO_MATCH;
}
if (tree.getArguments().isEmpty()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect we could safely ignore this possibility, since the code shouldn't compile and thus shouldn't ever get to Error Prone.

return true;
}

if (isLocalOrResourceVariable && varDecl != null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (isLocalOrResourceVariable && varDecl != null) {
if (isLocalOrResourceVariable) {

If varDecl is null, then we would have already thrown NPE during the call to hasImplicitType. We could move the check up, but my hope would be that a local/resource variable would always have a declaration, so I'm proposing to just remove it.

(If we did want to handle the null case, then we'd probably want to do something different so that we don't just fall through to the isInNullMarkedScope check at the end of the method.)

}

if (isRedundant) {
return buildDescription(tree).build();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return buildDescription(tree).build();
return describeMatch(tree);

Comment on lines 156 to 157
if (initializer.getKind() == Tree.Kind.METHOD_INVOCATION) {
MethodInvocationTree methodInvocation = (MethodInvocationTree) initializer;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (initializer.getKind() == Tree.Kind.METHOD_INVOCATION) {
MethodInvocationTree methodInvocation = (MethodInvocationTree) initializer;
if (initializer instanceof MethodInvocationTree methodInvocation) {

Comment on lines 88 to 92
if (symbol instanceof VarSymbol) {
isRedundant = !isEffectivelyNullable((VarSymbol) symbol, state);
} else if (symbol instanceof MethodSymbol) {
isRedundant = !isEffectivelyNullable((MethodSymbol) symbol, state);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (symbol instanceof VarSymbol) {
isRedundant = !isEffectivelyNullable((VarSymbol) symbol, state);
} else if (symbol instanceof MethodSymbol) {
isRedundant = !isEffectivelyNullable((MethodSymbol) symbol, state);
}
if (symbol instanceof VarSymbol varSymbol) {
isRedundant = !isEffectivelyNullable(varSymbol, state);
} else if (symbol instanceof MethodSymbol methodSymbol) {
isRedundant = !isEffectivelyNullable(methodSymbol, state);
}

private final boolean checkRequireNonNull;

@Inject
public RedundantNullCheck(ErrorProneFlags flags) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public RedundantNullCheck(ErrorProneFlags flags) {
RedundantNullCheck(ErrorProneFlags flags) {

Just to follow our internal style of reducing the visibility of constructors that are going to be called only through reflection.

}

if (isRedundant) {
return buildDescription(tree).build();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return buildDescription(tree).build();
return describeMatch(tree);

@bdragan
Copy link
Contributor Author

bdragan commented Sep 25, 2025

The results from running this over our depot look excellent—so many people who think that methods might return null when they actually throw an exception, for example, plus all the cases of missing @Nullable annotations that I'd mentioned before. I'm still not sure if we'll be able to justify turning it on for everyone, since some people have chosen to perform manual defensive checks on parameters, but I hope we can get there when we're finally able to focus more on nullness again!

I have run out of behavioral suggestions, so all that's left is some style-type things.

Great, I have pushed the style-type changes you suggested, all valid points, thanks!

Copy link
Member

@cpovirk cpovirk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, let's see if my approval accomplishes anything in this repo. I can also keep an eye out for the internal review and see what I can do there.

cpovirk added a commit to jspecify/jdk that referenced this pull request Sep 26, 2025
Most of these came to my attention during the testing of
[RedundantNullCheck](google/error-prone#5121).

Notes:

- For `BufferedInputStream` and `FilterInputStream`, see
  #128.
- For `URL`, the docs for `getAuthority()` don't mention `null`, but
  it's easy enough to demonstrate that `null` is a possible return
  value:
  ```java
  var u = new URL("mailto:foo@bar.com");
  System.out.println("authority " + u.getAuthority());
  ```
- `AlgorithmParameters.toString()` is gross but documented as such, as
  eamonnmcmanus points out.
- `Matcher.group()` is unfortunate: At one point, it could not return
  `null`, but that changed, as documented since
  [JDK-8274663](https://bugs.openjdk.org/browse/JDK-8274663). The change
  is a consequence of
  [`usePattern`](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/regex/Matcher.html#usePattern(java.util.regex.Pattern)),
  which was added all the way back in Java 5. I am a little tempted to
  lie here and leave the type as non-null.
- In `Component`, I also removed an annotation on the package-private
  helper method used by `getName()`.
- In `ResultSet`, `getBlob` and `getObject` have no documentatino about
  `null` returns. Gemini [wrote me some code that demonstrates the
  possible null returns](https://g.co/gemini/share/776a7669ae47).
- Also in `ResultSet`, I simplified some type-parameter declarations.
  This ensures that the `Class<T>` parameter has its type argument in
  bounds, and it makes no difference to the result type, which is always
  nullable, regardless of the type argument.

Fixes #128
copybara-service bot pushed a commit that referenced this pull request Sep 26, 2025
Closes #5107.

A null check is redundant if it is performed on a variable or method call that is known to be non-null within a `@NullMarked` scope.

Within a `@NullMarked` scope, types are non-null by default unless explicitly annotated with `@Nullable`.
Therefore, checking a variable or method return value (that isn't `@Nullable`) for nullness is unnecessary, as it should never be null.

Example:

```java
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

@NullMarked
class MyClass {
  void process(String definitelyNonNull) {
    // BUG: Diagnostic contains: RedundantNullCheck
    if (definitelyNonNull == null) {
      System.out.println("This will never happen");
    }
    // ...
  }

  String getString() {
    return "hello";
  }

  @nullable String getNullableString() {
    return null;
  }

  void anotherMethod() {
    String s = getString();
    // BUG: Diagnostic contains: RedundantNullCheck
    if (s == null) {
      // s is known to be non-null because getString() is not @nullable
      // and we are in a @NullMarked scope.
      System.out.println("Redundant check");
    }

    String nullableStr = getNullableString();
    if (nullableStr == null) { // This check is NOT redundant
      System.out.println("Nullable string might be null");
    }
  }
}
```

This check helps to clean up code and reduce clutter by removing unnecessary null checks, making the code easier to read and maintain.
It also reinforces the contract provided by `@NullMarked` and `@Nullable` annotations.

Fixes #5121

FUTURE_COPYBARA_INTEGRATE_REVIEW=#5121 from bdragan:5107-redundant_null_check f935970
PiperOrigin-RevId: 811786926
cpovirk added a commit to jspecify/jdk that referenced this pull request Sep 29, 2025
Add some missing `@Nullable` annotations.

Most of these came to my attention during the testing of
[RedundantNullCheck](google/error-prone#5121).

Notes:

- For `BufferedInputStream` and `FilterInputStream`, see
  #128.
- For `URL`, the docs for `getAuthority()` don't mention `null`, but
  it's easy enough to demonstrate that `null` is a possible return
  value:
  ```java
  var u = new URL("mailto:foo@bar.com");
  System.out.println("authority " + u.getAuthority());
  ```
- `AlgorithmParameters.toString()` is gross but documented as such, as
  eamonnmcmanus points out.
- `Matcher.group()` is unfortunate: At one point, it could not return
  `null`, but that changed, as documented since
  [JDK-8274663](https://bugs.openjdk.org/browse/JDK-8274663). The change
  is a consequence of
  [`usePattern`](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/regex/Matcher.html#usePattern(java.util.regex.Pattern)),
  which was added all the way back in Java 5. I am a little tempted to
  lie here and leave the type as non-null.
- In `Component`, I also removed an annotation on the package-private
  helper method used by `getName()`.
- In `ResultSet`, `getBlob` and `getObject` have no documentation about
  `null` returns. Gemini [wrote me some code that demonstrates the
  possible null returns](https://g.co/gemini/share/776a7669ae47).
- Also in `ResultSet`, I simplified some type-parameter declarations.
  This ensures that the `Class<T>` parameter has its type argument in
  bounds, and it makes no difference to the result type, which is always
  nullable, regardless of the type argument.

Fixes #128
copybara-service bot pushed a commit to google/guava that referenced this pull request Nov 10, 2025
…llCheck`](https://errorprone.info/bugpattern/RedundantNullCheck).

`RedundantNullCheck` was from google/error-prone#5121. I had already taken some of its suggestions in 125fd02, but I'd been on the fence about this one.

What pushed me over the edge was that my 34f3253 causes failures in nullness checking. I'm sure I could find another way to resolve those, but this approach probably makes more sense, anyway: If we imagine that nullness is enforced as in Kotlin (or possibly someday Java), then the call to `checkEntryNotNull` would immediately throw a `NullPointerException`, possibly with no message, instead of allowing the method to execute far enough to produce our custom `NullPointerException`, which includes both the key and the value.

(I think there were a handful of other parameters like this one on which I also had declined to add `@Nullable`. I'm not revisiting that right now, but we could someday.)

RELNOTES=n/a
PiperOrigin-RevId: 830554169
copybara-service bot pushed a commit to google/guava that referenced this pull request Nov 10, 2025
…dundantNullCheck`](https://errorprone.info/bugpattern/RedundantNullCheck).

`RedundantNullCheck` was from google/error-prone#5121. I had already taken some of its suggestions in 125fd02, but I'd been on the fence about these ones.

What pushed me over the edge was that my 34f3253 causes failures in nullness checking. I'm sure I could find another way to resolve those, but this approach probably makes more sense, anyway: If we imagine that nullness is enforced as in Kotlin (or possibly someday Java), then the call to `checkEntryNotNull` would immediately throw a `NullPointerException`, possibly with no message, instead of allowing the method to execute far enough to produce our custom `NullPointerException`, which includes both the key and the value.

(I think there were a handful of other parameters like this one on which I also had declined to add `@Nullable`. I'm not revisiting that right now, but we could someday.)

RELNOTES=n/a
PiperOrigin-RevId: 830554169
copybara-service bot pushed a commit to google/guava that referenced this pull request Nov 10, 2025
…dundantNullCheck`](https://errorprone.info/bugpattern/RedundantNullCheck).

`RedundantNullCheck` was from google/error-prone#5121. I had already taken some of its suggestions in 125fd02, but I'd been on the fence about these ones.

What pushed me over the edge was that my 34f3253 causes failures in nullness checking. I'm sure I could find another way to resolve those, but this approach probably makes more sense, anyway: If we imagine that nullness is enforced as in Kotlin (or possibly someday Java), then the call to `checkEntryNotNull` would immediately throw a `NullPointerException`, possibly with no message, instead of allowing the method to execute far enough to produce our custom `NullPointerException`, which includes both the key and the value.

(I think there were a handful of other parameters like this one on which I also had declined to add `@Nullable`. I'm not revisiting that right now, but we could someday.)

RELNOTES=n/a
PiperOrigin-RevId: 830586416
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UnnecessaryCheckNotNull doesn't work with @NullMarked

2 participants