[java-inspections] part of IDEA-380832

- external annotations for type parameters

GitOrigin-RevId: 43e826ca12b06ad607e325e81736fe56c514b69f
This commit is contained in:
Mikhail Pyltsin
2025-11-20 08:44:21 +00:00
committed by intellij-monorepo-bot
parent 0ecc455ce9
commit 89af2dec36
13 changed files with 280 additions and 25 deletions
@@ -22,7 +22,9 @@ import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.pom.java.JavaFeature
import com.intellij.psi.*
import com.intellij.psi.impl.source.PsiClassReferenceType
import com.intellij.psi.tree.TokenSet
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.PsiUtil
import com.intellij.psi.util.createSmartPointer
import org.jetbrains.annotations.PropertyKey
@@ -55,18 +57,52 @@ public class AnnotationInlayProvider : InlayHintsProvider {
if (project.isDefault) return null
return object : SharedBypassCollector {
override fun collectFromElement(element: PsiElement, sink: InlayTreeSink) {
if (element is PsiTypeParameterListOwner) {
sink.whenOptionEnabled(SHOW_EXTERNAL) {
val originalOwner = element.originalElement
if (originalOwner !is PsiCompiledElement || originalOwner !is PsiTypeParameterListOwner) {
return@whenOptionEnabled
}
val typeParameterList = originalOwner.typeParameterList ?: return@whenOptionEnabled
for ((index, originalParameter) in typeParameterList.typeParameters.withIndex()) {
val parameter = element.typeParameters.getOrNull(index) ?: return@whenOptionEnabled
val manager = ExternalAnnotationsManager.getInstance(project)
processTypeParameterRecursively(parameter, originalParameter, sink)
parameter.extendsList.referenceElements.zip(originalParameter.extendsList.referencedTypes)
.forEach { pair: Pair<PsiJavaCodeReferenceElement?, PsiClassType?> ->
val referenceElement = pair.first
val classType = pair.second
if (referenceElement == null || classType == null) {
return@forEach
}
classType.annotations
.filter(manager::isExternalAnnotation)
.forEach {
sink.addAnnotationPresentation(it, project, InlineInlayPosition(referenceElement.textRange.startOffset, false), HINT_FORMAT, TYPE_ANNOTATION_PAYLOADS)
}
}
if (originalParameter.superTypes.size == 1 && parameter.extendsList.referenceElements.isEmpty()) {
if (originalParameter.superTypes[0].equalsToText(CommonClassNames.JAVA_LANG_OBJECT)) {
originalParameter.superTypes[0].annotations
.filter(manager::isExternalAnnotation)
.forEach {
// it is not really correct, because
// annotations should be applied to object, like: `T extends @NotNull Object`
// but it is too long, so let's apply it to type parameters
sink.addAnnotationPresentation(it, project, InlineInlayPosition(parameter.textRange.startOffset, false), HINT_FORMAT, TYPE_ANNOTATION_PAYLOADS)
}
}
}
}
}
}
if (element is PsiTypeElement) {
sink.whenOptionEnabled(SHOW_EXTERNAL) {
val originalElement = element.originalElement
val typeParameter = PsiTreeUtil.getParentOfType(element, PsiTypeParameter::class.java)
if (typeParameter != null) return@whenOptionEnabled
if (originalElement is PsiTypeElement && originalElement is PsiCompiledElement) {
val type = originalElement.type
val offset = element.textRange.startOffset
val manager = ExternalAnnotationsManager.getInstance(project)
type.annotations
.filter(manager::isExternalAnnotation)
.forEach {
sink.addAnnotationPresentation(it, project, InlineInlayPosition(offset, false), HINT_FORMAT, TYPE_ANNOTATION_PAYLOADS)
}
showPsiTypeElement(originalElement, element, sink)
}
}
}
@@ -101,6 +137,78 @@ public class AnnotationInlayProvider : InlayHintsProvider {
}
}
}
private fun showPsiTypeElement(
originalElement: PsiTypeElement,
element: PsiTypeElement,
sink: InlayTreeSink,
) {
val type = originalElement.type
val offset = element.textRange.startOffset
val manager = ExternalAnnotationsManager.getInstance(project)
type.annotations
.filter(manager::isExternalAnnotation)
.forEach {
sink.addAnnotationPresentation(it, project, InlineInlayPosition(offset, false), HINT_FORMAT, TYPE_ANNOTATION_PAYLOADS)
}
}
private fun processTypeParameterRecursively(parameter: PsiTypeParameter, originalParameter: PsiTypeParameter, sink: InlayTreeSink) {
fun recursiveProcessTypeElement(element: PsiElement, originalElement: PsiElement) {
if (element is PsiTypeElement && originalElement is PsiTypeElement) {
showPsiTypeElement(originalElement, element, sink)
}
element.children
.filter {
it is PsiTypeElement ||
it is PsiJavaCodeReferenceElement ||
it is PsiReferenceParameterList
}.zip(
originalElement.children
.filter {
it is PsiTypeElement ||
it is PsiJavaCodeReferenceElement
})
.forEach {
val nestedElement = it.first
val nestedOriginalElement = it.second
if (nestedElement is PsiTypeElement && nestedOriginalElement is PsiTypeElement) {
recursiveProcessTypeElement(nestedElement, nestedOriginalElement)
}
if (nestedElement is PsiReferenceParameterList && nestedOriginalElement is PsiReferenceParameterList) {
nestedElement.typeParameterElements.zip(nestedOriginalElement.typeParameterElements).forEach { nested ->
recursiveProcessTypeElement(nested.first, nested.second)
}
}
if (nestedElement is PsiJavaCodeReferenceElement && nestedOriginalElement is PsiJavaCodeReferenceElement) {
val originalTypeParameterElements = nestedOriginalElement.parameterList?.typeParameterElements
val typeParameterElements = nestedElement.parameterList?.typeParameterElements
if (typeParameterElements != null && originalTypeParameterElements != null) {
typeParameterElements.zip(originalTypeParameterElements).forEach { nested ->
recursiveProcessTypeElement(nested.first, nested.second)
}
}
}
}
}
parameter.extendsList.referenceElements.zip(originalParameter.superTypes)
.forEach {
val referenceElement = it.first
val originalType = it.second
if (referenceElement == null || originalType !is PsiClassReferenceType) return@forEach
val parameterList = referenceElement.parameterList
val originalParameterList = originalType.reference.parameterList
if (parameterList == null || originalParameterList == null) return@forEach
parameterList.typeParameterElements.zip(originalParameterList.typeParameterElements)
.forEach { pair ->
val parameter = pair.first
val originalParameter = pair.second
if (parameter == null || originalParameter == null) return@forEach
recursiveProcessTypeElement(parameter, originalParameter)
}
}
}
}
}
}
@@ -129,7 +129,7 @@ public final class TypeNullability {
if (manager != null) {
NullabilityAnnotationInfo typeUseNullability = manager.findDefaultTypeUseNullability(parameter);
if (typeUseNullability != null) {
return typeUseNullability.toTypeNullability();
return typeUseNullability.toTypeNullability().inherited();
}
}
return UNKNOWN;
@@ -53,6 +53,13 @@ public final class JavaTypeNullabilityUtil {
PsiTypeParameter typeParameter = (PsiTypeParameter)target;
PsiReferenceList extendsList = typeParameter.getExtendsList();
PsiClassType[] extendTypes = extendsList.getReferencedTypes();
Set<PsiClassType> nextVisited = visited == null ? new HashSet<>() : visited;
nextVisited.add(type);
//superTypes returns Object for `empty` parameter list, which can contain external annotations
List<TypeNullability> nullabilities = ContainerUtil.map(typeParameter.getSuperTypes(), t -> getTypeNullability(t, nextVisited, checkContainer));
TypeNullability fromSuper = TypeNullability.intersect(nullabilities).inherited();
if (fromSuper != TypeNullability.UNKNOWN) return fromSuper;
//but this Object cannot hold nullability container, because doesn't have a reference
if (extendTypes.length == 0 && checkContainer) {
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.
@@ -60,11 +67,6 @@ public final class JavaTypeNullabilityUtil {
if (typeUseNullability != null) {
return typeUseNullability.toTypeNullability().inherited();
}
} else {
Set<PsiClassType> nextVisited = visited == null ? new HashSet<>() : visited;
nextVisited.add(type);
List<TypeNullability> nullabilities = ContainerUtil.map(extendTypes, t -> getTypeNullability(t, nextVisited, checkContainer));
return TypeNullability.intersect(nullabilities).inherited();
}
}
return TypeNullability.UNKNOWN;
@@ -16,6 +16,8 @@ import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
import com.intellij.psi.augment.PsiAugmentProvider;
import com.intellij.psi.augment.PsiExtensionMethod;
import com.intellij.psi.impl.cache.ExternalTypeAnnotationContainer;
import com.intellij.psi.impl.cache.TypeAnnotationContainer;
import com.intellij.psi.impl.java.stubs.PsiClassReferenceListStub;
import com.intellij.psi.impl.java.stubs.PsiClassStub;
import com.intellij.psi.impl.java.stubs.impl.PsiClassStubImpl;
@@ -845,6 +847,12 @@ public final class PsiClassImplUtil {
}
PsiManager manager = psiClass.getManager();
PsiClassType objectType = PsiType.getJavaLangObject(manager, psiClass.getResolveScope());
if (psiClass instanceof PsiTypeParameter && psiClass.getParent() instanceof PsiTypeParameterList &&
psiClass.getParent().getParent() instanceof PsiCompiledElement) {
TypeAnnotationContainer annotations = ExternalTypeAnnotationContainer.create((PsiTypeParameter)psiClass);
annotations = annotations.forBound().forConjunction(0);
objectType = objectType.annotate(annotations.getProvider(psiClass));
}
result[0] = objectType;
}
System.arraycopy(implementsTypes, 0, result, extendsListLength, implementsTypes.length);
@@ -2,19 +2,18 @@
package com.intellij.psi.impl.cache;
import com.intellij.codeInsight.ExternalAnnotationsManager;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiModifierListOwner;
import com.intellij.psi.TypeAnnotationProvider;
import com.intellij.psi.*;
import org.jetbrains.annotations.NotNull;
/**
* A container that reports external type annotations. External type annotations are described in annotation.xml files
* with additional {@code typePath} attribute. The attribute contains several components starting with '/' and separated with '/'
* (no ending '/' is allowed). The allowed components are the following:
* with additional {@code typePath} attribute. The attribute contains several components starting with '/' or '-' and separated with '/'
* (no ending '/' is allowed).
* '-' indicates that type parameters are considered.
* The allowed components are the following:
* <ul>
* <li>{@code number} - one-based type argument index (1-255)
* <li>{@code *} (asterisk) - bound of a wildcard type
* <li>{@code *} (asterisk) - go to a bound
* <li>{@code []} (square brackets) - array element (also works for varargs)
* <li>{@code .} (dot) - enclosing type of inner type
* </ul>
@@ -70,4 +69,17 @@ public final class ExternalTypeAnnotationContainer implements TypeAnnotationCont
public static @NotNull TypeAnnotationContainer create(@NotNull PsiModifierListOwner owner) {
return new ExternalTypeAnnotationContainer("", owner);
}
public static @NotNull TypeAnnotationContainer create(@NotNull PsiTypeParameter typeParameter) {
PsiElement parent = typeParameter.getParent();
if (parent instanceof PsiTypeParameterList &&
parent.getParent() instanceof PsiModifierListOwner &&
parent.getParent() instanceof PsiTypeParameterListOwner) {
PsiTypeParameterList parameterList = (PsiTypeParameterList)parent;
int index = parameterList.getTypeParameterIndex(typeParameter);
String path = "-/" + (index + 1);
return new ExternalTypeAnnotationContainer(path, (PsiModifierListOwner)parent.getParent());
}
return EMPTY;
}
}
@@ -75,6 +75,18 @@ public interface TypeAnnotationContainer {
*/
@NotNull TypeAnnotationContainer forTypeArgument(int index);
/**
* Returns a type annotation container for the given conjunction index.
* Used for type parameters.
*
* @param i type argument index, zero-based
* @return type annotation container for a given type argument
* (assuming that this type annotation container is used for a class type with type arguments)
*/
default @NotNull TypeAnnotationContainer forConjunction(int i) {
return forTypeArgument(i);
}
/**
* @param parent parent PSI element for context
* @return TypeAnnotationProvider
@@ -85,4 +97,5 @@ public interface TypeAnnotationContainer {
* Appends to StringBuilder annotation text that applicable to this element immediately (not to sub-elements)
*/
void appendImmediateText(@NotNull StringBuilder sb);
}
@@ -2,6 +2,7 @@
package com.intellij.psi.impl.java.stubs.impl;
import com.intellij.psi.*;
import com.intellij.psi.impl.cache.ExternalTypeAnnotationContainer;
import com.intellij.psi.impl.cache.TypeAnnotationContainer;
import com.intellij.psi.impl.cache.TypeInfo;
import com.intellij.psi.impl.compiled.ClsJavaCodeReferenceElementImpl;
@@ -65,6 +66,13 @@ public class PsiClassReferenceListStubImpl extends StubBase<PsiReferenceList> im
for (int i = 0; i < types.length; i++) {
TypeInfo info = myInfos[i];
TypeAnnotationContainer annotations = info.getTypeAnnotations();
if (annotations == TypeAnnotationContainer.EMPTY) {
PsiElement psi = myParent.getPsi();
if (psi instanceof PsiTypeParameter) {
annotations = ExternalTypeAnnotationContainer.create((PsiTypeParameter)psi);
annotations = annotations.forBound().forConjunction(i);
}
}
ClsJavaCodeReferenceElementImpl reference = new ClsJavaCodeReferenceElementImpl(getPsi(), info.text(), annotations);
types[i] = new PsiClassReferenceType(reference, null).annotate(annotations.getProvider(reference));
}
@@ -0,0 +1,37 @@
package org.example;
import org.jspecify.annotations.*;
import java.util.*;
@NullMarked
class CustomOptionalTest {
@Nullable
public String a;
Optional<String> returnNotNullableDelegate() {
return Delegate.ofNullable(a);
}
Optional<String> returnNotNullable() {
return Optional.ofNullable(a);
}
Comparator<String> getComparator() {
return Comparator.<@Nullable String>naturalOrder();
}
}
class TestList{
static void test(String a) {
if (List.<error descr="Cannot resolve method 'of' in 'List'">of</error>("1", a).getFirst() == null) {
System.out.println("null");
}
}
}
class Delegate {
static <L extends String> Optional<L> ofNullable(@Nullable L t) {
return Optional.ofNullable(t);
}
}
@@ -2,6 +2,7 @@ import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import java.lang.annotation.ElementType;
import java.util.Comparator;
import java.util.function.Supplier;
@NullMarked
@@ -13,6 +14,10 @@ class Main {
}
Comparator<String> getComparator() {
return Comparator.<<warning descr="Non-null type parameter 'T' cannot be instantiated with @Nullable type">@Nullable</warning> String>naturalOrder();
}
static <T extends @Nullable Object> T fNullableBound(Supplier<T> supplier){
return supplier.get();
}
@@ -117,7 +117,22 @@ public class ExternalAnnotationsManagerTest extends LightPlatformTestCase {
assertClassFqn(nameText, psiFile, externalName, null);
String typePath = annotationData.getTypePath();
if (typePath != null) {
if (typePath != null && typePath.startsWith("-")) {
PsiTypeParameterList typeParameterList;
if (listOwner instanceof PsiClass variable) {
typeParameterList = variable.getTypeParameterList();
} else if (listOwner instanceof PsiMethod method) {
typeParameterList = method.getTypeParameterList();
} else {
fail("typePath is not allowed for " + listOwner.getClass().getSimpleName(), psiFile, externalName);
break;
}
String error = validatePath(typePath, typeParameterList);
if (error != null) {
fail("Invalid typePath: " + error, psiFile, externalName);
}
}
else if (typePath != null) {
PsiType type;
if (listOwner instanceof PsiVariable variable) {
type = variable.getType();
@@ -191,6 +206,45 @@ public class ExternalAnnotationsManagerTest extends LightPlatformTestCase {
return Objects.requireNonNull(PsiUtil.getTypeByPsiElement(listOwner), () -> String.valueOf(listOwner));
}
private static String validatePath(@NotNull String pathString, @NotNull PsiTypeParameterList typeParameterList) {
if (!pathString.startsWith("-/")) {
return "Must start with '-/'";
}
String[] components = pathString.split("/", -1);
String component = components[1];
int i;
try {
i = Integer.parseInt(component);
}
catch (NumberFormatException ignored) {
return "Invalid path: " + pathString + "; invalid component: " + component;
}
PsiTypeParameter[] parameters = typeParameterList.getTypeParameters();
if (i <= 0 || parameters.length < i) {
return "Invalid path: " + pathString + "; type parameter index out of bounds: " + i;
}
PsiTypeParameter parameter = parameters[i - 1];
String asterix = components[2];
if (!asterix.equals("*")) {
return "Invalid path: " + pathString + "; only '*' is allowed for type parameter " + parameter.getName();
}
String nextIndex = components[3];
try {
i = Integer.parseInt(nextIndex);
}
catch (NumberFormatException ignored) {
return "Invalid path: " + pathString + "; invalid extends component: " + nextIndex;
}
PsiClassType[] types = parameter.getExtendsList().getReferencedTypes();
if (components.length == 4 && i == 1 && types.length == 0) return null;
if (i <= 0 || types.length < i) {
return "Invalid path: " + pathString + "; extends index out of bounds: " + i;
}
String rest = StringUtil.join(components, 4, components.length, "/");
if (rest.isEmpty()) return null;
return validatePath(rest, types[i - 1]);
}
private static String validatePath(String pathString, PsiType type) {
if (!pathString.startsWith("/")) {
return "Must start with '/'";
@@ -115,7 +115,7 @@ import java.util.stream.Stream;
import jdk.internal.ValueBased;
@ValueBased
public final class Optional<T> {
public final class Optional</*<# @NotNull #>*/T> {
private static final Optional<?> EMPTY = new Optional((Object)null);
private final T value;
@@ -374,4 +374,9 @@ public class DataFlowInspection8Test extends DataFlowInspectionTestCase {
public void testNewMethodReferenceMustBeNonNull() {
doTestWith((insp, __) -> insp.TREAT_UNKNOWN_MEMBERS_AS_NULLABLE = true);
}
public void testExternalTypeParameterAnnotations() {
addJSpecifyNullMarked(myFixture);
setupTypeUseAnnotations("org.jspecify.annotations", myFixture);
doTest();
}
}
@@ -2752,7 +2752,7 @@
</item>
<item name='java.util.Comparator java.util.Comparator&lt;T&gt; naturalOrder()'>
<annotation name='org.jetbrains.annotations.NotNull'/>
<annotation name='org.jetbrains.annotations.NotNull' typePath="/1"/>
<annotation name='org.jetbrains.annotations.NotNull' typePath="-/1/*/1"/>
<annotation name='org.jetbrains.annotations.Contract'>
<val name="pure" val="true"/>
</annotation>
@@ -3517,7 +3517,7 @@
</item>
<item name='java.util.List java.util.List&lt;E&gt; of(E, E)'>
<annotation name="org.jetbrains.annotations.NotNull"/>
<annotation name="org.jetbrains.annotations.NotNull" typePath="/1"/>
<annotation name="org.jetbrains.annotations.NotNull" typePath="-/1/*/1"/>
<annotation name='org.jetbrains.annotations.Contract'>
<val name="value" val="&quot;_,_-&gt;new&quot;"/>
<val name="pure" val="true"/>
@@ -4416,6 +4416,9 @@
<val name="pure" val="true"/>
</annotation>
</item>
<item name='java.util.Optional'>
<annotation name='org.jetbrains.annotations.NotNull' typePath='-/1/*/1'/>
</item>
<item name='java.util.Optional T get()'>
<annotation name='org.jetbrains.annotations.NotNull'/>
<annotation name='org.jetbrains.annotations.Contract'>