[java-psi] PsiType#getNullability; initial implementation and tests

First step for IDEA-372347 Java type inference should respect nullability

GitOrigin-RevId: 47faf55e657ce6f15680c6183478b42a562a763f
This commit is contained in:
Tagir Valeev
2025-05-27 15:00:40 +02:00
committed by intellij-monorepo-bot
parent 5361c50b07
commit 786c11b372
15 changed files with 601 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ package com.intellij.codeInsight;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiModifierListOwner;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -75,6 +76,20 @@ public class NullabilityAnnotationInfo {
return new NullabilityAnnotationInfo(myAnnotation, myNullability, owner, myContainer);
}
/**
* Converts this object to {@link TypeNullability}. Inheritance information is lost, as it's not applicable to type nullability.
*/
@ApiStatus.Experimental
public @NotNull TypeNullability toTypeNullability() {
NullabilitySource source;
if (myContainer) {
source = new NullabilitySource.ContainerAnnotation(myAnnotation);
} else {
source = new NullabilitySource.ExplicitAnnotation(myAnnotation);
}
return new TypeNullability(myNullability, source);
}
@Override
public String toString() {
return "NullabilityAnnotationInfo{" +

View File

@@ -0,0 +1,189 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight;
import com.intellij.psi.*;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
import static java.util.Objects.requireNonNull;
/**
* Source for type nullability.
*/
@ApiStatus.NonExtendable
@ApiStatus.Experimental
public /* sealed */ interface NullabilitySource {
enum Standard implements NullabilitySource {
/**
* Type nullability is not specified
*/
NONE,
/**
* Type nullability is mandated by language specification
* (e.g., primitive type, or disjunction type)
*/
MANDATED,
/**
* Type nullability is depicted explicitly by means of the language.
* Currently, not possible in Java, but may be used in other languages like Kotlin.
*/
LANGUAGE_DEFINED
}
/**
* Type nullability is explicitly specified by an annotation.
* Annotation owner is normally the type.
*/
final class ExplicitAnnotation implements NullabilitySource {
private final @NotNull PsiAnnotation myAnnotation;
public ExplicitAnnotation(@NotNull PsiAnnotation annotation) { myAnnotation = annotation; }
public @NotNull PsiAnnotation annotation() {
return myAnnotation;
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
ExplicitAnnotation that = (ExplicitAnnotation)o;
return myAnnotation.equals(that.myAnnotation);
}
@Override
public int hashCode() {
return myAnnotation.hashCode();
}
@Override
public String toString() {
PsiJavaCodeReferenceElement ref = annotation().getNameReferenceElement();
if (ref == null) return "@<unknown>";
return "@" + ref.getReferenceName();
}
}
/**
* Type nullability is inherited from a container (member/class/package/module)
*/
final class ContainerAnnotation implements NullabilitySource {
private final @NotNull PsiAnnotation myAnnotation;
public ContainerAnnotation(@NotNull PsiAnnotation annotation) {
PsiModifierList owner = (PsiModifierList)requireNonNull(annotation.getOwner(), "Annotation has no owner");
PsiModifierListOwner parent = (PsiModifierListOwner)requireNonNull(owner.getParent(), "Modifier list has no parent");
if (parent.getModifierList() != owner) {
throw new IllegalStateException("Modifier list parent is incorrect");
}
myAnnotation = annotation;
}
public @NotNull PsiModifierListOwner container() {
PsiModifierList owner = (PsiModifierList)requireNonNull(myAnnotation.getOwner());
PsiModifierListOwner parent = (PsiModifierListOwner)requireNonNull(owner.getParent());
if (parent.getModifierList() != owner) {
throw new IllegalStateException("Modifier list parent is incorrect");
}
return parent;
}
public @NotNull PsiAnnotation annotation() {
return myAnnotation;
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
ContainerAnnotation that = (ContainerAnnotation)o;
return myAnnotation.equals(that.myAnnotation);
}
@Override
public int hashCode() {
return myAnnotation.hashCode();
}
@Override
public String toString() {
PsiModifierListOwner container = container();
String containerInfo;
if (container instanceof PsiClass) {
containerInfo = "class " + ((PsiClass)container).getName();
}
else if (container instanceof PsiField) {
containerInfo = "field " + ((PsiField)container).getName();
}
else if (container instanceof PsiMethod) {
containerInfo = "method " + ((PsiMethod)container).getName();
}
else if (container instanceof PsiPackage) {
containerInfo = "package " + ((PsiPackage)container).getName();
}
else if (container instanceof PsiJavaModule) {
containerInfo = "module " + ((PsiJavaModule)container).getName();
}
else {
containerInfo = container.getClass().getSimpleName();
}
PsiJavaCodeReferenceElement ref = annotation().getNameReferenceElement();
String annotationInfo = ref == null ? "@<unknown>" : "@" + ref.getReferenceName();
return annotationInfo + " on " + containerInfo;
}
}
final class MultiSource implements NullabilitySource {
private final @NotNull Set<@NotNull NullabilitySource> mySources;
MultiSource(@NotNull Set<@NotNull NullabilitySource> sources) {
if (sources.size() <= 1) {
throw new IllegalArgumentException("MultiSource must have at least two sources");
}
mySources = sources;
}
public @NotNull Set<@NotNull NullabilitySource> sources() {
return mySources;
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
MultiSource source = (MultiSource)o;
return mySources.equals(source.mySources);
}
@Override
public int hashCode() {
return mySources.hashCode();
}
@Override
public String toString() {
return mySources.toString();
}
}
/**
* @param sources sources to combine
* @return combined source, or {@link Standard#NONE} if no sources are specified
*/
static @NotNull NullabilitySource multiSource(@NotNull Collection<@NotNull NullabilitySource> sources) {
Set<NullabilitySource> set = new LinkedHashSet<>();
for (NullabilitySource source : sources) {
if (source instanceof MultiSource) {
set.addAll(((MultiSource)source).sources());
}
if (source == Standard.NONE) continue;
set.add(source);
}
if (set.isEmpty()) return Standard.NONE;
if (set.size() == 1) return set.iterator().next();
return new MultiSource(set);
}
}

View File

@@ -0,0 +1,96 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* A class that represents nullability of a type, including the nullability itself, and the nullability source.
*/
@ApiStatus.Experimental
public final class TypeNullability {
/**
* Unknown nullability without the source
*/
public static final TypeNullability UNKNOWN = new TypeNullability(Nullability.UNKNOWN, NullabilitySource.Standard.NONE);
/**
* Mandated not-null nullability
*/
public static final TypeNullability NOT_NULL_MANDATED = new TypeNullability(Nullability.NOT_NULL, NullabilitySource.Standard.MANDATED);
/**
* Mandated nullable nullability
*/
public static final TypeNullability NULLABLE_MANDATED = new TypeNullability(Nullability.NULLABLE, NullabilitySource.Standard.MANDATED);
private final @NotNull Nullability myNullability;
private final @NotNull NullabilitySource mySource;
public TypeNullability(@NotNull Nullability nullability, @NotNull NullabilitySource source) {
myNullability = nullability;
mySource = source;
if (nullability != Nullability.UNKNOWN && source == NullabilitySource.Standard.NONE) {
throw new IllegalArgumentException("Source must be specified for non-unknown nullability");
}
}
/**
* @return the nullability of the type
*/
public @NotNull Nullability nullability() {
return myNullability;
}
/**
* @return the source of the nullability information
*/
public @NotNull NullabilitySource source() {
return mySource;
}
/**
* @param collection type nullabilities to intersect
* @return the intersection of the type nullabilities in the collection
*/
public static @NotNull TypeNullability intersect(@NotNull Collection<@NotNull TypeNullability> collection) {
Map<Nullability, Set<NullabilitySource>> map = collection.stream().collect(Collectors.groupingBy(
TypeNullability::nullability, Collectors.mapping(TypeNullability::source, Collectors.toSet())));
Set<NullabilitySource> sources = map.get(Nullability.NOT_NULL);
if (sources != null) {
return new TypeNullability(Nullability.NOT_NULL, NullabilitySource.multiSource(sources));
}
sources = map.get(Nullability.NULLABLE);
if (sources != null) {
return new TypeNullability(Nullability.NULLABLE, NullabilitySource.multiSource(sources));
}
sources = map.get(Nullability.UNKNOWN);
if (sources != null) {
return new TypeNullability(Nullability.UNKNOWN, NullabilitySource.multiSource(sources));
}
return UNKNOWN;
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
TypeNullability that = (TypeNullability)o;
return myNullability == that.myNullability && mySource.equals(that.mySource);
}
@Override
public int hashCode() {
int result = myNullability.hashCode();
result = 31 * result + mySource.hashCode();
return result;
}
@Override
public String toString() {
return myNullability + " (" + mySource + ")";
}
}

View File

@@ -1,8 +1,10 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.psi;
import com.intellij.codeInsight.TypeNullability;
import com.intellij.lang.jvm.types.JvmArrayType;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.util.JavaTypeNullabilityUtil;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
@@ -110,6 +112,11 @@ public class PsiArrayType extends PsiType.Stub implements JvmArrayType {
return myComponentType;
}
@Override
public @NotNull TypeNullability getNullability() {
return JavaTypeNullabilityUtil.getNullabilityFromAnnotations(getAnnotations());
}
@Override
public boolean equals(Object obj) {
return obj instanceof PsiArrayType &&

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.psi;
import com.intellij.codeInsight.TypeNullability;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.CachedValue;
@@ -80,6 +81,11 @@ public class PsiDisjunctionType extends PsiType.Stub {
return true;
}
@Override
public @NotNull TypeNullability getNullability() {
return TypeNullability.NOT_NULL_MANDATED;
}
@Override
public boolean equalsToText(final @NotNull @NonNls String text) {
return Objects.equals(text, getCanonicalText());

View File

@@ -1,12 +1,14 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.psi;
import com.intellij.codeInsight.TypeNullability;
import com.intellij.core.JavaPsiBundle;
import com.intellij.openapi.util.NullUtils;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
@@ -103,6 +105,12 @@ public final class PsiIntersectionType extends PsiType.Stub {
return StringUtil.join(myConjuncts, psiType -> psiType.getInternalCanonicalText(), " & ");
}
@Override
public @NotNull TypeNullability getNullability() {
List<TypeNullability> nullabilities = ContainerUtil.map(myConjuncts, PsiType::getNullability);
return TypeNullability.intersect(nullabilities);
}
@Override
public boolean isValid() {
for (PsiType conjunct : myConjuncts) {

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.psi;
import com.intellij.codeInsight.TypeNullability;
import com.intellij.psi.search.GlobalSearchScope;
import org.jetbrains.annotations.NotNull;
@@ -54,6 +55,11 @@ public class PsiLambdaExpressionType extends PsiType {
return myExpression;
}
@Override
public @NotNull TypeNullability getNullability() {
return TypeNullability.NOT_NULL_MANDATED;
}
@Override
public boolean equals(Object obj) {
return obj == this || obj instanceof PsiLambdaExpressionType && myExpression.equals(((PsiLambdaExpressionType)obj).myExpression);

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.psi;
import com.intellij.codeInsight.TypeNullability;
import com.intellij.psi.search.GlobalSearchScope;
import org.jetbrains.annotations.NotNull;
@@ -54,6 +55,11 @@ public class PsiMethodReferenceType extends PsiType {
return myReference;
}
@Override
public @NotNull TypeNullability getNullability() {
return TypeNullability.NOT_NULL_MANDATED;
}
@Override
public boolean equals(Object obj) {
return obj == this || obj instanceof PsiMethodReferenceType && myReference.equals(((PsiMethodReferenceType)obj).myReference);

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.psi;
import com.intellij.codeInsight.TypeNullability;
import com.intellij.lang.jvm.types.JvmPrimitiveType;
import com.intellij.lang.jvm.types.JvmPrimitiveTypeKind;
import com.intellij.pom.java.LanguageLevel;
@@ -123,6 +124,13 @@ public final class PsiPrimitiveType extends PsiType.Stub implements JvmPrimitive
return true;
}
@Override
public @NotNull TypeNullability getNullability() {
if (myKind == JvmPrimitiveTypeKind.VOID) return TypeNullability.UNKNOWN;
if (myKind == null) return TypeNullability.NULLABLE_MANDATED;
return TypeNullability.NOT_NULL_MANDATED;
}
@Override
public boolean equalsToText(@NotNull String text) {
return myName.equals(text);

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.psi;
import com.intellij.codeInsight.TypeNullability;
import com.intellij.lang.jvm.types.JvmPrimitiveTypeKind;
import com.intellij.lang.jvm.types.JvmType;
import com.intellij.openapi.project.Project;
@@ -169,6 +170,13 @@ public abstract class PsiType implements PsiAnnotationOwner, Cloneable, JvmType
*/
public abstract boolean equalsToText(@NotNull @NonNls String text);
/**
* @return nullability of this type
*/
public @NotNull TypeNullability getNullability() {
return TypeNullability.UNKNOWN;
}
/**
* Returns the class type for qualified class name.
*

View File

@@ -0,0 +1,79 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.util;
import com.intellij.codeInsight.*;
import com.intellij.psi.*;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
* Helper methods to compute nullability of Java types.
*/
@ApiStatus.Internal
public final class JavaTypeNullabilityUtil {
/**
* Computes the class type nullability
*
* @param type type to compute nullability for
* @param visited set of visited types, used to avoid infinite recursion
* @return type nullability
*/
public static @NotNull TypeNullability getTypeNullability(@NotNull PsiClassType type, @Nullable Set<PsiClassType> visited) {
if (visited != null && visited.contains(type)) return TypeNullability.UNKNOWN;
TypeNullability fromAnnotations = getNullabilityFromAnnotations(type.getAnnotations());
if (!fromAnnotations.equals(TypeNullability.UNKNOWN)) return fromAnnotations;
PsiElement context = type.getPsiContext();
if (context != null) {
NullableNotNullManager manager = NullableNotNullManager.getInstance(context.getProject());
NullabilityAnnotationInfo typeUseNullability = manager.findDefaultTypeUseNullability(context);
if (typeUseNullability != null) {
return typeUseNullability.toTypeNullability();
}
}
PsiClass target = type.resolve();
if (target instanceof PsiTypeParameter) {
PsiTypeParameter typeParameter = (PsiTypeParameter)target;
PsiReferenceList extendsList = typeParameter.getExtendsList();
PsiClassType[] extendTypes = extendsList.getReferencedTypes();
if (extendTypes.length == 0) {
NullableNotNullManager manager = NullableNotNullManager.getInstance(typeParameter.getProject());
// If there's no bound, we assume an implicit `extends Object` bound, which is subject to default annotation if any.
NullabilityAnnotationInfo typeUseNullability = manager.findDefaultTypeUseNullability(typeParameter);
if (typeUseNullability != null) {
return typeUseNullability.toTypeNullability();
}
} else {
Set<PsiClassType> nextVisited = visited == null ? new HashSet<>() : visited;
nextVisited.add(type);
List<TypeNullability> nullabilities = ContainerUtil.map(extendTypes, t -> getTypeNullability(t, nextVisited));
return TypeNullability.intersect(nullabilities);
}
}
return TypeNullability.UNKNOWN;
}
/**
* Computes the nullability from explicit annotations
* @param annotations array of annotations to look for nullability annotations in
* @return nullability from explicit annotations
*/
public static @NotNull TypeNullability getNullabilityFromAnnotations(@NotNull PsiAnnotation @NotNull [] annotations) {
for (PsiAnnotation annotation : annotations) {
String qualifiedName = annotation.getQualifiedName();
Optional<Nullability> optionalNullability = NullableNotNullManager.getInstance(annotation.getProject())
.getAnnotationNullability(qualifiedName);
if (optionalNullability.isPresent()) {
Nullability nullability = optionalNullability.get();
return new TypeNullability(nullability, new NullabilitySource.ExplicitAnnotation(annotation));
}
}
return TypeNullability.UNKNOWN;
}
}

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.psi.impl;
import com.intellij.codeInsight.TypeNullability;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
import com.intellij.psi.impl.light.LightClass;
@@ -153,6 +154,11 @@ class TypeCorrector extends PsiTypeMapper {
return myDelegate.getPsiContext();
}
@Override
public @NotNull TypeNullability getNullability() {
return myDelegate.getNullability();
}
@Override
public PsiType @NotNull [] getParameters() {
return ContainerUtil.map2Array(myDelegate.getParameters(), PsiType.class, type -> {

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.psi.impl.source;
import com.intellij.codeInsight.TypeNullability;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
import com.intellij.psi.augment.PsiAugmentProvider;
@@ -11,6 +12,7 @@ import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.JavaTypeNullabilityUtil;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
@@ -105,6 +107,11 @@ public class PsiClassReferenceType extends PsiClassType.Stub {
return annotations;
}
@Override
public @NotNull TypeNullability getNullability() {
return JavaTypeNullabilityUtil.getTypeNullability(this, null);
}
@Override
public @NotNull LanguageLevel getLanguageLevel() {
if (myLanguageLevel != null) return myLanguageLevel;

View File

@@ -15,6 +15,7 @@
*/
package com.intellij.psi.impl.source;
import com.intellij.codeInsight.TypeNullability;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
@@ -30,6 +31,7 @@ public class PsiImmediateClassType extends PsiClassType.Stub {
private final PsiSubstitutor mySubstitutor;
private final PsiManager myManager;
private final @Nullable PsiElement myPsiContext;
private final @NotNull TypeNullability myNullability;
private String myCanonicalText;
private String myCanonicalTextAnnotated;
private String myPresentableText;
@@ -111,11 +113,21 @@ public class PsiImmediateClassType extends PsiClassType.Stub {
@Nullable LanguageLevel level,
@NotNull TypeAnnotationProvider provider,
@Nullable PsiElement context) {
this(aClass, substitutor, level, provider, context, TypeNullability.UNKNOWN);
}
public PsiImmediateClassType(@NotNull PsiClass aClass,
@NotNull PsiSubstitutor substitutor,
@Nullable LanguageLevel level,
@NotNull TypeAnnotationProvider provider,
@Nullable PsiElement context,
@NotNull TypeNullability nullability) {
super(level, provider);
myClass = aClass;
myManager = aClass.getManager();
mySubstitutor = substitutor;
myPsiContext = context;
myNullability = nullability;
substitutor.ensureValid();
}
@@ -134,6 +146,11 @@ public class PsiImmediateClassType extends PsiClassType.Stub {
return myPsiContext;
}
@Override
public @NotNull TypeNullability getNullability() {
return myNullability;
}
@Override
public int getParameterCount() {
PsiTypeParameterList list = myClass.getTypeParameterList();

View File

@@ -0,0 +1,143 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.java.psi;
import com.intellij.codeInsight.Nullability;
import com.intellij.codeInsight.NullabilitySource;
import com.intellij.codeInsight.TypeNullability;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiJavaFile;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiTypes;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import org.intellij.lang.annotations.Language;
public final class PsiTypeNullabilityTest extends LightJavaCodeInsightFixtureTestCase {
public void testPrimitive() {
TypeNullability nullTypeNullability = PsiTypes.nullType().getNullability();
assertEquals(TypeNullability.NULLABLE_MANDATED, nullTypeNullability);
assertEquals(Nullability.NULLABLE, nullTypeNullability.nullability());
assertEquals(NullabilitySource.Standard.MANDATED, nullTypeNullability.source());
TypeNullability intTypeNullability = PsiTypes.intType().getNullability();
assertEquals(TypeNullability.NOT_NULL_MANDATED, intTypeNullability);
assertEquals(Nullability.NOT_NULL, intTypeNullability.nullability());
assertEquals(NullabilitySource.Standard.MANDATED, intTypeNullability.source());
TypeNullability voidTypeNullability = PsiTypes.voidType().getNullability();
assertEquals(TypeNullability.UNKNOWN, voidTypeNullability);
assertEquals(Nullability.UNKNOWN, voidTypeNullability.nullability());
assertEquals(NullabilitySource.Standard.NONE, voidTypeNullability.source());
}
private PsiType configureAndGetFieldType(@Language("JAVA") String text) {
PsiFile file = myFixture.configureByText("Test.java", text);
return ((PsiJavaFile)file).getClasses()[0].getFields()[0].getType();
}
public void testSimpleUnknown() {
PsiType type = configureAndGetFieldType("""
class A {
String foo;
}
""");
assertEquals("UNKNOWN (NONE)", type.getNullability().toString());
}
public void testSimpleUnknownWithAnnotation() {
PsiType type = configureAndGetFieldType("""
@org.jetbrains.annotations.NotNullByDefault
class A {
@org.jetbrains.annotations.UnknownNullability String foo;
}
""");
assertEquals("UNKNOWN (@UnknownNullability)", type.getNullability().toString());
}
public void testSimpleNotNull() {
PsiType type = configureAndGetFieldType("""
class A {
@org.jetbrains.annotations.NotNull String foo = "";
}
""");
assertEquals("NOT_NULL (@NotNull)", type.getNullability().toString());
}
public void testSimpleNullable() {
PsiType type = configureAndGetFieldType("""
import org.jetbrains.annotations.*;
class A {
@Nullable String foo;
}
""");
assertEquals("NULLABLE (@Nullable)", type.getNullability().toString());
}
public void testContainerNotNull() {
PsiType type = configureAndGetFieldType("""
@org.jetbrains.annotations.NotNullByDefault
class A {
String foo = "";
}
""");
assertEquals("NOT_NULL (@NotNullByDefault on class A)", type.getNullability().toString());
}
public void testTypeParameterSupertype() {
PsiType type = configureAndGetFieldType("""
import org.jetbrains.annotations.NotNull;
class A<T extends @NotNull CharSequence> {
T foo = "";
}
""");
assertEquals("NOT_NULL (@NotNull)", type.getNullability().toString());
}
public void testTypeParameterTwoSupertypes() {
PsiType type = configureAndGetFieldType("""
import org.jetbrains.annotations.NotNull;
class A<T extends @NotNull CharSequence & @NotNull Comparable<T>> {
T foo = "";
}
""");
assertEquals("NOT_NULL ([@NotNull, @NotNull])", type.getNullability().toString());
}
public void testTypeParameterTwoSupertypesDifferentNullability() {
PsiType type = configureAndGetFieldType("""
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class A<T extends @NotNull CharSequence & @Nullable Comparable<T>> {
T foo = "";
}
""");
assertEquals("NOT_NULL (@NotNull)", type.getNullability().toString());
}
public void testTypeParameterSupertypeRecursive() {
PsiType type = configureAndGetFieldType("""
import org.jetbrains.annotations.NotNull;
class A<T extends T> {
T foo = "";
}
""");
assertEquals("UNKNOWN (NONE)", type.getNullability().toString());
}
public void testArrayType() {
PsiType type = configureAndGetFieldType("""
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class A {
@NotNull String @Nullable [] foo;
}
""");
assertEquals("NULLABLE (@Nullable)", type.getNullability().toString());
assertEquals("NOT_NULL (@NotNull)", type.getDeepComponentType().getNullability().toString());
}
}