mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-08 15:09:39 +07:00
[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:
committed by
intellij-monorepo-bot
parent
5361c50b07
commit
786c11b372
@@ -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{" +
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 + ")";
|
||||
}
|
||||
}
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user