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.”
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
}
}
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?
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?
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*
}
}