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