False Positive for java:S4449 in NullMarked Package

SonarQube rule java:S4449 (“Nullness of parameters should be guaranteed”) reports false positives when a method parameter is annotated with Nullable (Jakarta Validation or JSpecify) in a package that is marked as NullMarked (JSpecify).

Example

  public abstract class AbstractBaseEntity<I> implements BaseEntity<I> {
  @org.jspecify.annotations.Nullable`
  @jakarta.annotation.Nullable`
  private I id;
  public void setId( @jakarta.annotation.Nullable @org.jspecify.annotations.Nullable final I id ) {
      this.id = id;
   }

Class Bar

public foo() {
  entity.setId( null );
}

This setId raised always “Annotate the parameter with javax.annotation.Nullable in method setId declaration.”

hi @MelleD ,

I am able to reproduce the problem you are reporting with the following piece of code:

package org.example.reproducer;

interface BaseEntity<I> {
  void setId(I id);
}

abstract class AbstractBaseEntity<I> implements BaseEntity<I> {
  @org.jspecify.annotations.Nullable
  @jakarta.annotation.Nullable
  private I id;

  @Override
  public void setId(@jakarta.annotation.Nullable @org.jspecify.annotations.Nullable final I id) {
    this.id = id;
  }
}

public final class Bar {
  public void foo(BaseEntity<Long> entity) {
    entity.setId(null); // S4449 raised here
  }
}

(and this @NullMarked package-info.java:

@org.jspecify.annotations.NullMarked
package org.example.reproducer;

)

However, in this constellation, I would argue that the reported issue is a True Positive since the foo() method calls setId on the BaseEntity interface where the argument is implicitly @NonNull due to the package-level @NullMarked annotation.

Hence, the method contract indeed does not accept a null value, even if a specific overload does.

Conversely, if @Nullable annotations are added to the base method definition [1], or if the method is called on the AbstractBaseEntity sub-type [2], null does become an allowed value for the call, and the sonar analysis also no longer raises an issue for my reproducer.

[1]:

interface BaseEntity<I> {
  void setId(
    @org.jspecify.annotations.Nullable
    @jakarta.annotation.Nullable
    I id);
}

or [2]:

public void foo(AbstractBaseEntity<Long> entity) {
  entity.setId(null);
}

I am unsure how entity or BaseEntity have been defined in your example. Can you confirm whether the above reproducer matches your setup?

Best regards,

Anton

Hey Anton,

thanks for the reply

here are the interfaces there are no setId

public interface BaseEntity<I> extends Identifiable<I> {
}
import org.jspecify.annotations.Nullable;

public interface Identifiable<I> {
   @Nullable I getId();
}

the setId has also not Override annotation. So looks for me not as an false positive

Hi Dennis!

Thank you for sharing the base interfaces.

In your initial post you also included this piece of code to demonstrate where an issue is raised:

public foo() {
  entity.setId( null );
}

Here, could you also share the declaration of entity, and specifically its declared type? Is it AbstractBaseEntity<I>, or is it a sub-type?
If it is a sub-type, does it override setId?

Best regards,

Anton

No thats the complete example

public interface BaseEntity<I> extends Identifiable<I> {}

public interface Identifiable<I> {
   @Nullable I getId();
}


abstract class AbstractBaseEntity<I> implements BaseEntity<I> {
  *@org*.jspecify.annotations.Nullable
  *@jakarta*.annotation.Nullable
  private I id;

  
  public void setId(*@jakarta*.annotation.Nullable *@org*.jspecify.annotations.Nullable final I id) {
    this.id = id;
  }
}

public final class Bar {
  public void foo(BaseEntity<Long> entity) {
    entity.setId(null); *// S4449 raised here*
  }
}