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

Hi Dennis!

Please excuse the late follow-up.

Thank you for sharing that last example. Unfortunately, it still seems incomplete since setId is not defined on BaseEntity nor on Identifiable. As entity is declared of type BaseEntity, the entity.setId call would not compile.

As it is difficult to diagnose the problem without the full method declarations on all involved types, could you share another code snippet with the full declarations?

Best regards,

Anton Haubner

Hey Anton,

I will create example directly here

Hi Dennis!

Thank you for providing this repository!

Unfortunately, I can not reproduce the FP with the repository.

I forked it and set up a scan on a public SonarQube Cloud (SQC) project to make it easier for us to align.
Here is the Bar.java file you created after an SQC scan. I also added a second example for a case where the S4449 rule does trigger in a True Positive (TP) case: link

The full fork of your code with all additional changes to demonstrate the TP case can be found here: link

Is it possible that the reproducer does not fully align with the original code producing the issue?
Alternatively, it could be that the issue only triggers for older versions of the Java scanner. May I ask which SonarQube edition and version you are using for your scans?

Best regards,

Anton