[java-inspection] IDEA-382441 jspecify inspection doesn't highlight assignments

GitOrigin-RevId: 29497c955c64040544a3bf4af0af0edd08763201
This commit is contained in:
Mikhail Pyltsin
2026-01-13 12:40:21 +00:00
committed by intellij-monorepo-bot
parent 901cea64df
commit 453ca78ef1
11 changed files with 166 additions and 27 deletions
@@ -132,7 +132,14 @@ annotate.overridden.methods.parameters.nonnull=Annotate overriding method parame
annotate.overridden.methods.parameters.nullable=Annotate overriding method parameters as nullable
anonymous.ref.loc.can.be.replaced.with.0=Anonymous #ref can be replaced with {0}
anonymous.ref.loc.can.be.replaced.with.lambda=Anonymous #ref can be replaced with lambda
assigning.a.collection.of.nullable.elements=Assigning a collection of nullable elements into a collection of non-null elements
assigning.a.collection.of.nullable.elements=\
<html><body>\
Assigning a class with nullable type arguments when a class with not-null type arguments is expected{0}\
</body></html>
assigning.a.collection.of.notnull.elements=\
<html><body>\
Assigning a class with not-null type arguments when a class with nullable type arguments is expected{0}\
</body></html>
returning.a.class.with.nullable.arguments=\
<html><body>\
Returning a class with nullable type arguments when a class with not-null type arguments is expected{0}\
@@ -87,6 +87,8 @@ public class NullableStuffInspectionBase extends AbstractBaseJavaLocalInspection
public boolean REPORT_NULLS_PASSED_TO_NOT_NULL_PARAMETER = true;
@SuppressWarnings("WeakerAccess") public boolean REPORT_REDUNDANT_NULLABILITY_ANNOTATION_IN_THE_SCOPE_OF_ANNOTATED_CONTAINER = true;
@SuppressWarnings("WeakerAccess") public boolean REPORT_CONFLICT_IN_ASSIGNMENTS = true;
private static final Logger LOG = Logger.getInstance(NullableStuffInspectionBase.class);
@Override
@@ -102,6 +104,7 @@ public class NullableStuffInspectionBase extends AbstractBaseJavaLocalInspection
"REPORT_NULLS_PASSED_TO_NOT_NULL_PARAMETER".equals(name) && "true".equals(value) ||
"REPORT_NOT_NULL_TO_NULLABLE_CONFLICTS_IN_ASSIGNMENTS".equals(name) && "false".equals(value) ||
"REPORT_NOT_ANNOTATED_INSTANTIATION_NOT_NULL_TYPE".equals(name) && "false".equals(value) ||
"REPORT_CONFLICT_IN_ASSIGNMENTS".equals(name) && "true".equals(value) ||
"REPORT_REDUNDANT_NULLABILITY_ANNOTATION_IN_THE_SCOPE_OF_ANNOTATED_CONTAINER".equals(name) && "true".equals(value)) {
node.removeContent(child);
}
@@ -210,6 +213,12 @@ public class NullableStuffInspectionBase extends AbstractBaseJavaLocalInspection
checkConstructorParameters(field, annotated, anno, annoToRemove, holder);
}
PsiExpression initializer = field.getInitializer();
PsiElement identifyingElement = field.getIdentifyingElement();
if (REPORT_CONFLICT_IN_ASSIGNMENTS && initializer != null && identifyingElement != null) {
checkNestedGenericClasses(identifyingElement, field.getType(), initializer.getType(),
ConflictNestedTypeProblem.ASSIGNMENT_NESTED_TYPE_PROBLEM);
}
}
@Override
@@ -483,13 +492,30 @@ public class NullableStuffInspectionBase extends AbstractBaseJavaLocalInspection
@Override
public void visitAssignmentExpression(@NotNull PsiAssignmentExpression expression) {
checkCollectionNullityOnAssignment(expression.getOperationSign(), expression.getLExpression().getType(), expression.getRExpression());
PsiExpression rExpression = expression.getRExpression();
if (rExpression == null) return;
if (REPORT_CONFLICT_IN_ASSIGNMENTS) {
checkNestedGenericClasses(expression.getOperationSign(),
expression.getLExpression().getType(),
rExpression.getType(),
ConflictNestedTypeProblem.ASSIGNMENT_NESTED_TYPE_PROBLEM);
}
else {
checkCollectionNullityOnAssignment(expression.getOperationSign(), expression.getLExpression().getType(), expression.getRExpression());
}
}
@Override
public void visitLocalVariable(@NotNull PsiLocalVariable variable) {
PsiIdentifier identifier = variable.getNameIdentifier();
if (identifier != null) {
if (identifier == null) return;
PsiExpression initializer = variable.getInitializer();
if (initializer == null) return;
if (REPORT_CONFLICT_IN_ASSIGNMENTS) {
checkNestedGenericClasses(identifier, variable.getType(), initializer.getType(),
ConflictNestedTypeProblem.ASSIGNMENT_NESTED_TYPE_PROBLEM);
}
else {
checkCollectionNullityOnAssignment(identifier, variable.getType(), variable.getInitializer());
}
}
@@ -498,8 +524,9 @@ public class NullableStuffInspectionBase extends AbstractBaseJavaLocalInspection
public void visitReturnStatement(@NotNull PsiReturnStatement statement) {
PsiExpression returnValue = statement.getReturnValue();
if (returnValue == null) return;
checkGenericClassOnReturn(PsiTypesUtil.getMethodReturnType(statement), returnValue);
checkNestedGenericClasses(returnValue,
PsiTypesUtil.getMethodReturnType(statement), returnValue.getType(),
ConflictNestedTypeProblem.RETURN_NESTED_TYPE_PROBLEM);
}
@Override
@@ -530,17 +557,19 @@ public class NullableStuffInspectionBase extends AbstractBaseJavaLocalInspection
}
}
private void checkGenericClassOnReturn(@Nullable PsiType expectedType,
@NotNull PsiExpression returnValue) {
PsiType returnType = returnValue.getType();
private void checkNestedGenericClasses(@NotNull PsiElement errorElement,
@Nullable PsiType expectedType,
@Nullable PsiType actualType,
@NotNull ConflictNestedTypeProblem problem) {
if(expectedType == null || actualType == null) return;
JavaTypeNullabilityUtil.NullabilityConflictContext
context = JavaTypeNullabilityUtil.getNullabilityConflictInAssignment(expectedType, returnType,
context = JavaTypeNullabilityUtil.getNullabilityConflictInAssignment(expectedType, actualType,
REPORT_NOT_NULL_TO_NULLABLE_CONFLICTS_IN_ASSIGNMENTS);
if (context.nullabilityConflict() == JavaTypeNullabilityUtil.NullabilityConflict.UNKNOWN) return;
String messageKey = context.nullabilityConflict() == JavaTypeNullabilityUtil.NullabilityConflict.NOT_NULL_TO_NULL ?
"returning.a.class.with.notnull.arguments" : "returning.a.class.with.nullable.arguments";
problem.notNullToNullProblem() : problem.nullToNotNullProblem();
reportProblem(holder, returnValue, LocalQuickFix.EMPTY_ARRAY,
reportProblem(holder, errorElement, LocalQuickFix.EMPTY_ARRAY,
messageKey, new Object[]{""},
messageKey, new Object[]{NullableStuffInspectionUtil.getNullabilityConflictPresentation(context)});
}
@@ -1347,4 +1376,37 @@ public class NullableStuffInspectionBase extends AbstractBaseJavaLocalInspection
return JavaAnalysisBundle.message("inspection.annotate.overridden.method.nullable.quickfix.name");
}
}
private enum ConflictNestedTypeProblem{
RETURN_NESTED_TYPE_PROBLEM("returning.a.class.with.notnull.arguments", "returning.a.class.with.nullable.arguments"),
ASSIGNMENT_NESTED_TYPE_PROBLEM("assigning.a.collection.of.notnull.elements", "assigning.a.collection.of.nullable.elements"),
;
@NotNull
@PropertyKey(resourceBundle = JavaAnalysisBundle.BUNDLE)
private final String notNullToNullProblemMessage;
@NotNull
@PropertyKey(resourceBundle = JavaAnalysisBundle.BUNDLE)
private final String nullToNotNullProblemMessage;
ConflictNestedTypeProblem(@NotNull @PropertyKey(resourceBundle = JavaAnalysisBundle.BUNDLE) String notNullToNullProblemMessage,
@NotNull @PropertyKey(resourceBundle = JavaAnalysisBundle.BUNDLE) String nullToNotNullProblemMessage) {
this.notNullToNullProblemMessage = notNullToNullProblemMessage;
this.nullToNotNullProblemMessage = nullToNotNullProblemMessage;
}
@NotNull
@PropertyKey(resourceBundle = JavaAnalysisBundle.BUNDLE)
String notNullToNullProblem() {
return notNullToNullProblemMessage;
}
@NotNull
@PropertyKey(resourceBundle = JavaAnalysisBundle.BUNDLE)
String nullToNotNullProblem() {
return nullToNotNullProblemMessage;
}
}
}
@@ -48,6 +48,8 @@ public class NullableStuffInspection extends NullableStuffInspectionBase {
checkbox("REPORT_NOT_ANNOTATED_GETTER", JavaBundle.message("inspection.nullable.problems.not.annotated.getters.for.annotated.fields")),
checkbox("REPORT_NULLS_PASSED_TO_NOT_NULL_PARAMETER", JavaBundle.message("inspection.nullable.problems.notnull.parameters.with.null.literal.option")),
checkbox("REPORT_NULLABILITY_ANNOTATION_ON_LOCALS", JavaBundle.message("inspection.nullable.problems.nullability.on.locals")),
checkbox("REPORT_CONFLICT_IN_ASSIGNMENTS", JavaBundle.message(
"inspection.assignment.conflicts")),
checkbox("REPORT_NOT_NULL_TO_NULLABLE_CONFLICTS_IN_ASSIGNMENTS", JavaBundle.message(
"inspection.nullable.problems.notnull.to.nullable.assignment.conflicts")),
checkbox("REPORT_REDUNDANT_NULLABILITY_ANNOTATION_IN_THE_SCOPE_OF_ANNOTATED_CONTAINER", JavaBundle.message(
@@ -0,0 +1,54 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
class TestListWrapperAssignment {
ListArrayWrapper<@Nullable String> listByArrayWithNullField = ListArrayWrapper.createArray(new @Nullable String[] {"a", null, "c"});
ListArrayWrapper<String> <warning descr="Assigning a class with nullable type arguments when a class with not-null type arguments is expected">listWithNullField</warning> = ListArrayWrapper.createVarargs("a", null, "c");
public static void main(String[] args) {
ListArrayWrapper.createVarargs("a", "c").getList().forEach(s -> System.out.println(s.toUpperCase(Locale.ROOT)));
ListArrayWrapper<String> listArrayWrapper = ListArrayWrapper.createVarargs("a", "c");
listArrayWrapper.getList().forEach(s -> System.out.println(s.toUpperCase(Locale.ROOT)));
ListArrayWrapper<@Nullable String> listArrayWrapperNullable = ListArrayWrapper.createVarargs("a", "c");
listArrayWrapperNullable.getList().forEach(s -> System.out.println(s.toUpperCase(Locale.ROOT)));
ListArrayWrapper<String> <warning descr="Assigning a class with nullable type arguments when a class with not-null type arguments is expected">listWithNull</warning> = ListArrayWrapper.createVarargs("a", null, "c");
ListArrayWrapper<@Nullable String> listByArrayWithNull = ListArrayWrapper.createArray(new @Nullable String[] {"a", null, "c"});
ListArrayWrapper<String> varargs = ListArrayWrapper.createVarargs("1");
ListArrayWrapper<@Nullable String> <warning descr="Assigning a class with not-null type arguments when a class with nullable type arguments is expected">listByArrayWithoutNull</warning> = varargs;
ListArrayWrapper<String> <warning descr="Assigning a class with nullable type arguments when a class with not-null type arguments is expected">listByArrayWithNullCorrectInspection1</warning> = ListArrayWrapper.createArray(new @Nullable String[] {"a", null, "c"});
ListArrayWrapper<@Nullable String> listByArrayWithNullCorrectInspection2 = ListArrayWrapper.createArray(new String[] {"a", null, "c"});
}
static class ListArrayWrapper<T extends @Nullable Object> {
private final List<T> list;
@SafeVarargs
static <E extends @Nullable Object> ListArrayWrapper<E> createVarargs(E... elements) {
return new ListArrayWrapper<>(new ArrayList<>(Arrays.asList(elements)));
}
static <E extends @Nullable Object> ListArrayWrapper<E> createArray(E[] array) {
return new ListArrayWrapper<>(new ArrayList<>(Arrays.asList(array)));
}
private ListArrayWrapper(List<T> list) {
this.list = list;
}
List<T> getList() {
return list;
}
}
}
@@ -28,6 +28,6 @@ class Main {
AtomicReferenceArray<String> arr = <warning descr="Constructor is not compatible with a non-null type argument">new AtomicReferenceArray<>(10)</warning>;
AtomicReferenceArray<String> arr2 = new AtomicReferenceArray<>(new String[10]); // technically wrong and should be highlighted, as array contains nulls
@Nullable String[] data = new String[10];
AtomicReferenceArray<String> arr3 = new AtomicReferenceArray<>(data); // technically wrong and should be highlighted, as array contains nulls
AtomicReferenceArray<String> <warning descr="Assigning a class with nullable type arguments when a class with not-null type arguments is expected">arr3</warning> = new AtomicReferenceArray<>(data);
}
}
@@ -28,6 +28,6 @@ class Main {
AtomicReferenceArray<@Nullable String> arr = new AtomicReferenceArray<>(10);
AtomicReferenceArray<String> arr2 = new AtomicReferenceArray<>(new String[10]); // technically wrong and should be highlighted, as array contains nulls
@Nullable String[] data = new String[10];
AtomicReferenceArray<String> arr3 = new AtomicReferenceArray<>(data); // technically wrong and should be highlighted, as array contains nulls
AtomicReferenceArray<String> arr3 = new AtomicReferenceArray<>(data);
}
}
@@ -6,15 +6,15 @@ class JC {
void testList() {
List<@Nullable String> nullableList = new ArrayList<>();
print(<warning descr="Assigning a collection of nullable elements into a collection of non-null elements">nullableList</warning>);
print(<warning descr="Assigning a class with nullable type arguments when a class with not-null type arguments is expected{0}">nullableList</warning>);
List<@NotNull String> <warning descr="Assigning a collection of nullable elements into a collection of non-null elements">list2</warning> = nullableList;
List<@NotNull String> <warning descr="Assigning a class with nullable type arguments when a class with not-null type arguments is expected">list2</warning> = nullableList;
List<@NotNull String> list3;
list2 <warning descr="Assigning a collection of nullable elements into a collection of non-null elements">=</warning> nullableList;
list2 <warning descr="Assigning a class with nullable type arguments when a class with not-null type arguments is expected">=</warning> nullableList;
List<? super @NotNull String> list4 = nullableList;
List<? extends @NotNull String> <warning descr="Assigning a collection of nullable elements into a collection of non-null elements">list5</warning> = nullableList;
List<? extends @NotNull String> <warning descr="Assigning a class with nullable type arguments when a class with not-null type arguments is expected">list5</warning> = nullableList;
}
private static void print(List<@NotNull String> list) {
@@ -28,8 +28,8 @@ class JC {
List<@NotNull String> testReturnValue() {
List<@Nullable String> list = new ArrayList<>();
Supplier<List<@NotNull String>> supplier = () -> <warning descr="Assigning a collection of nullable elements into a collection of non-null elements">list</warning>;
Supplier<List<@NotNull String>> supplierRef = <warning descr="Assigning a collection of nullable elements into a collection of non-null elements">this::getNullableList</warning>;
Supplier<List<@NotNull String>> supplier = () -> <warning descr="Assigning a class with nullable type arguments when a class with not-null type arguments is expected{0}">list</warning>;
Supplier<List<@NotNull String>> supplierRef = <warning descr="Assigning a class with nullable type arguments when a class with not-null type arguments is expected{0}">this::getNullableList</warning>;
Supplier<List<@NotNull String>> supplier3 = () -> { return <warning descr="Returning a class with nullable type arguments when a class with not-null type arguments is expected">list</warning>;};
@@ -41,6 +41,6 @@ class JC {
class Test<T extends @Nullable CharSequence> {
public void test() {
List<T> nullableList = new ArrayList<>();
List<? extends @NotNull CharSequence> <warning descr="Assigning a collection of nullable elements into a collection of non-null elements">list2</warning> = nullableList;
List<? extends @NotNull CharSequence> <warning descr="Assigning a class with nullable type arguments when a class with not-null type arguments is expected">list2</warning> = nullableList;
}
}
@@ -4,11 +4,11 @@ import java.util.*;
class JC {
void testMap() {
Map<@NotNull String, @NotNull String> <warning descr="Assigning a collection of nullable elements into a collection of non-null elements">m1</warning> = new HashMap<@Nullable String, String>();
m1 <warning descr="Assigning a collection of nullable elements into a collection of non-null elements">=</warning> new HashMap<String, @Nullable String>();
Map<@NotNull String, @NotNull String> <warning descr="Assigning a class with nullable type arguments when a class with not-null type arguments is expected">m1</warning> = new HashMap<@Nullable String, String>();
m1 <warning descr="Assigning a class with nullable type arguments when a class with not-null type arguments is expected">=</warning> new HashMap<String, @Nullable String>();
m1 = new HashMap<String, String>();
Map<@NotNull String, ? extends @NotNull String> <warning descr="Assigning a collection of nullable elements into a collection of non-null elements">m2</warning> = new HashMap<@Nullable String, String>();
Map<@NotNull String, ? extends @NotNull String> <warning descr="Assigning a class with nullable type arguments when a class with not-null type arguments is expected">m2</warning> = new HashMap<@Nullable String, String>();
}
}
@@ -113,11 +113,17 @@ public class JSpecifyFilteredAnnotationTest extends LightJavaCodeInsightFixtureT
new Pair<>("MultiBoundTypeVariableUnionNullToSelf.java", 62), // see: IDEA-377697
new Pair<>("WildcardCapturesToBoundOfTypeParameterNotToTypeVariableItself.java", 24), // see: IDEA-377699
new Pair<>("WildcardCapturesToBoundOfTypeParameterNotToTypeVariableItself.java", 24) ,// see: IDEA-377699
new Pair<>("SelfType.java", 34), // see: IDEA-377707 (also see the commented case in warning matchers)
new Pair<>("SelfType.java", 43), // see: IDEA-377707 (also see the commented case in warning matchers)
new Pair<>("OutOfBoundsTypeVariable.java", 21) // see: IDEA-377707 (also see the commented case in warning matchers)
new Pair<>("OutOfBoundsTypeVariable.java", 21), // see: IDEA-377707 (also see the commented case in warning matchers)
new Pair<>("TypeParameterBounds.java", 40), // see: IDEA-377707
new Pair<>("AugmentedInferenceAgreesWithBaseInference.java", 33), // see: IDEA-377683
new Pair<>("NullnessUnspecifiedTypeParameter.java", 33), // see: IDEA-377683
new Pair<>("TypeVariableMinusNullVsTypeVariable.java", 28), // see: IDEA-377683
new Pair<>("TypeVariableMinusNullVsTypeVariable.java", 30) // see: IDEA-377683
)
),
new SkipIndividuallyFilter( //cases to investigate later (with unspecified annotation and complicated to understand). (line number starts from 0)
@@ -179,8 +185,7 @@ public class JSpecifyFilteredAnnotationTest extends LightJavaCodeInsightFixtureT
new Pair<>("SuperNullableForNonNullableTypeParameter.java", 27) // see: IDEA-379303
)
),
new CallWithParameterWithNestedGenericsFilter(), // see: IDEA-377682
new VariableWithNestedGenericsFilter() // see: IDEA-377683
new CallWithParameterWithNestedGenericsFilter() // see: IDEA-377682
);
private static final LightProjectDescriptor PROJECT_DESCRIPTOR = new DefaultLightProjectDescriptor() {
@@ -576,6 +581,7 @@ public class JSpecifyFilteredAnnotationTest extends LightJavaCodeInsightFixtureT
case "inspection.nullable.problems.Nullable.method.overrides.NotNull",
"inspection.nullable.problems.NotNull.parameter.overrides.Nullable",
"assigning.a.collection.of.nullable.elements",
"assigning.a.collection.of.notnull.elements",
"returning.a.class.with.nullable.arguments",
"returning.a.class.with.notnull.arguments"
//, "non.null.type.argument.is.expected" //todo see IDEA-377707
@@ -516,6 +516,13 @@ public class NullableStuffInspectionTest extends LightJavaCodeInsightFixtureTest
doTest();
}
public void testAssignmentIncompatibilitiesWithGeneric() {
myInspection.REPORT_NOT_NULL_TO_NULLABLE_CONFLICTS_IN_ASSIGNMENTS = true;
addJSpecifyNullMarked(myFixture);
setupTypeUseAnnotations("org.jspecify.annotations", myFixture);
doTest();
}
public void testArrayUnderContainerAnnotation() {
myInspection.REPORT_NOT_ANNOTATED_INSTANTIATION_NOT_NULL_TYPE = true;
addJSpecifyNullMarked(myFixture);
@@ -564,6 +564,7 @@ inspection.nullable.problems.not.annotated.getters.for.annotated.fields=Report n
inspection.nullable.problems.notnull.overrides.option=Report @NotNull &parameters overriding non-annotated
inspection.nullable.problems.notnull.parameters.with.null.literal.option=Report @NotNull parameters with null-literal argument usages
inspection.nullable.problems.nullability.on.locals=Report nullability annotations on local variables when they are non-applicable
inspection.assignment.conflicts=Report nullability conflicts for nested types in assignment and variable declarations
inspection.nullable.problems.notnull.to.nullable.assignment.conflicts=Report assignment of a not-null type argument to a nullable type argument
inspection.nullable.problems.redundant.nullability.inside.container=Report redundant nullability annotation in the scope of annotated container
inspection.optional.get.without.is.present.message=<code>{0}.#ref()</code> without ''isPresent()'' check