[java-inspections] IDEA-381010 Incorrect highlighting of API which is graduated from preview but used with older JDK

GitOrigin-RevId: 9130a26ccf3592fcf227a4b2176fee36729274d0
This commit is contained in:
Tagir Valeev
2025-10-22 09:25:20 +00:00
committed by intellij-monorepo-bot
parent 917311bcab
commit 3a2becf03d
4 changed files with 142 additions and 13 deletions
@@ -28,6 +28,11 @@ import java.util.concurrent.ConcurrentHashMap;
public final class JdkApiCompatibilityService {
private static final Logger LOG = Logger.getInstance(JdkApiCompatibilityService.class);
/**
* First language level at which preview APIs were introduced.
*/
private static final @NotNull LanguageLevel FIRST_PREVIEW_API_LEVEL = LanguageLevel.JDK_13;
private final Map<LanguageLevel, List<String>> cache = new ConcurrentHashMap<>();
public static JdkApiCompatibilityService getInstance() {
@@ -55,6 +60,12 @@ public final class JdkApiCompatibilityService {
* its super method is annotated as {@code @since 8} this method will return {@link LanguageLevel#JDK_1_8}.
*
* <li>if {@code member} is not annotated with a {@code @since} tag this method will return null.
*
* <li>if {@code member} is annotated as {@code @since 24} but it was in preview since 22,
* and the context language level is {@link LanguageLevel#JDK_22}, then this method will return null.</li>
*
* <li>if {@code member} is annotated as {@code @since 24} but it was in preview since 22,
* and the context language level is {@link LanguageLevel#JDK_21}, then this method will return {@link LanguageLevel#JDK_24}.</li>
* </ul>
*
* @param member The member to find the incompatible language level for
@@ -63,6 +74,14 @@ public final class JdkApiCompatibilityService {
* unknown.
*/
public @Nullable LanguageLevel firstCompatibleLanguageLevel(@NotNull PsiMember member, @NotNull LanguageLevel contextLanguageLevel) {
LevelInfo info = firstCompatibleLanguageLevelInfo(member, contextLanguageLevel);
if (info == null) return null;
LanguageLevel targetLevel = info.outOfPreviewLevel() == null ? info.firstAppearLevel() : info.outOfPreviewLevel();
if (contextLanguageLevel.isLessThan(targetLevel)) return targetLevel;
return null;
}
public @Nullable LevelInfo firstCompatibleLanguageLevelInfo(@NotNull PsiMember member, @NotNull LanguageLevel contextLanguageLevel) {
if (member instanceof PsiAnonymousClass) return null;
PsiClass containingClass = member.getContainingClass();
if (containingClass instanceof PsiAnonymousClass) return null;
@@ -75,11 +94,11 @@ public final class JdkApiCompatibilityService {
}
LanguageLevel incompatibleLevelForContext = contextLanguageLevel.next();
LanguageLevel lowestCompatibleLanguageLevel = null;
LevelInfo lowestCompatibleLanguageLevel = null;
for (PsiMember checkMember : membersToCheck) {
String signature = getSignature(checkMember);
if (signature == null) return null;
LanguageLevel compatibleLanguageLevelForMember = getIntroducedApiLevel(signature, incompatibleLevelForContext);
LevelInfo compatibleLanguageLevelForMember = getIntroducedApiLevel(signature, incompatibleLevelForContext);
if (compatibleLanguageLevelForMember == null) return null;
if (lowestCompatibleLanguageLevel == null || compatibleLanguageLevelForMember.isLessThan(lowestCompatibleLanguageLevel)) {
lowestCompatibleLanguageLevel = compatibleLanguageLevelForMember;
@@ -115,20 +134,44 @@ public final class JdkApiCompatibilityService {
* @param signature The signature, example: "java.util.Iterator#remove()" as specified by {@link #getSignature(PsiMember)}.
* @param languageLevel to start the search.
* @return The newly introduced API if it appears after or including {@code languageLevel}, or null if it was introduced before
* {@code languageLevel}.
* {@code languageLevel}. If the API is in preview for languageLevel, null is returned. If the API was in preview and later standartized,
* but languageLevel is preview preview, then the first standard languageLevel is returned.
*/
@Contract("_, null -> null")
private @Nullable LanguageLevel getIntroducedApiLevel(@NotNull String signature, @Nullable LanguageLevel languageLevel) {
private @Nullable LevelInfo getIntroducedApiLevel(@NotNull String signature, @Nullable LanguageLevel languageLevel) {
if (languageLevel == null) return null;
LanguageLevel curLevel = LanguageLevel.HIGHEST;
while (true) {
if (getIntroducedApis(curLevel).contains(signature)) return curLevel;
if (getIntroducedApis(curLevel).contains(signature)) {
LanguageLevel maybePreview = languageLevel.previous();
while (maybePreview != null && maybePreview.isAtLeast(FIRST_PREVIEW_API_LEVEL)) {
if (getIntroducedApis(maybePreview).contains(signature)) {
return new LevelInfo(maybePreview, curLevel);
}
maybePreview = maybePreview.previous();
}
return new LevelInfo(curLevel, null);
}
if (languageLevel == curLevel) return null;
curLevel = curLevel.previous();
if (curLevel == null) return null;
}
}
/**
* @param firstAppearLevel language level at which the feature appeared the first
* @param outOfPreviewLevel language level at which the feature was standardized if it was in preview first;
* null if the feature was not in preview at all or it's still in preview and was not standardized yet.
*/
public record LevelInfo(@NotNull LanguageLevel firstAppearLevel, @Nullable LanguageLevel outOfPreviewLevel) {
boolean isLessThan(LevelInfo level) {
return firstAppearLevel.isLessThan(level.firstAppearLevel) ||
firstAppearLevel == level.firstAppearLevel &&
Comparator.nullsFirst(Comparator.<LanguageLevel>naturalOrder()).compare(outOfPreviewLevel, level.outOfPreviewLevel) < 0;
}
}
/**
* Serializes a {@code member} for storage in apiX.txt files.
* <p>
@@ -174,20 +174,21 @@ class JavaApiUsageInspection : AbstractBaseUastLocalInspectionTool() {
val sourcePsi = sourceNode.sourcePsi ?: return
if (target !is PsiMember) return
var languageLevel: LanguageLevel? = null
val module = ModuleUtilCore.findModuleForPsiElement(sourcePsi)
val file = sourcePsi.containingFile
val module = ModuleUtilCore.findModuleForPsiElement(file)
if (module != null) {
languageLevel = getEffectiveLanguageLevel(module)
if (languageLevel.isUnsupported) {
languageLevel = languageLevel.getNonPreviewLevel()
}
}
else if (sourcePsi.containingFile.virtualFile is LightVirtualFile) {
else if (file.virtualFile is LightVirtualFile) {
//it is necessary for generated files (for example, check completions)
languageLevel = sourcePsi.containingFile.getUserData(PsiUtil.FILE_LANGUAGE_LEVEL_KEY)
languageLevel = file.getUserData(PsiUtil.FILE_LANGUAGE_LEVEL_KEY)
}
if (languageLevel == null) return
var firstCompatibleLanguageLevel = JdkApiCompatibilityService.getInstance().firstCompatibleLanguageLevel(target, languageLevel)
if (firstCompatibleLanguageLevel != null) {
val info = JdkApiCompatibilityService.getInstance().firstCompatibleLanguageLevelInfo(target, languageLevel)
if (info != null) {
val psiClass = if (qualifier != null) {
PsiUtil.resolveClassInType(qualifier.getExpressionType())
}
@@ -201,15 +202,26 @@ class JavaApiUsageInspection : AbstractBaseUastLocalInspectionTool() {
}
}
var level = info.firstAppearLevel()
val outOfPreviewLevel = info.outOfPreviewLevel()
if (outOfPreviewLevel != null) {
val sdkLevel = JavaVersionService.getInstance().getJavaSdkVersion(file)?.maxLanguageLevel
if (sdkLevel != null && sdkLevel.isAtLeast(level) &&
(sdkLevel == languageLevel || languageLevel.isPreview)) {
// At current SDK level, the API is usable but in preview, so we should not report it
return
}
level = outOfPreviewLevel
}
if (sourcePsi is PsiJavaCodeReferenceElement) {
val previewFeatureUsage = JavaPreviewFeatureUtil.getPreviewFeatureUsage(sourcePsi, target)
val previewLevel = firstCompatibleLanguageLevel.getPreviewLevel()
val previewLevel = level.getPreviewLevel()
if (previewFeatureUsage != null && previewLevel != null) {
firstCompatibleLanguageLevel = previewLevel
level = previewLevel
}
}
registerError(sourcePsi, firstCompatibleLanguageLevel, holder, isOnTheFly)
registerError(sourcePsi, level, holder, isOnTheFly)
}
else if (target is PsiClass && !languageLevel.isAtLeast(LanguageLevel.JDK_1_7)) {
for (generifiedClass in generifiedClasses) {
@@ -0,0 +1,48 @@
package com.intellij.codeInspection.tests.java
import com.intellij.jvm.analysis.internal.testFramework.JavaApiUsageInspectionTestBase
import com.intellij.jvm.analysis.testFramework.JvmLanguage
import com.intellij.pom.java.LanguageLevel
import com.intellij.testFramework.LightProjectDescriptor
class JavaJavaApiUsageInspection22Test : JavaApiUsageInspectionTestBase() {
override fun getProjectDescriptor(): LightProjectDescriptor {
return JAVA_22
}
fun addGatherer() {
myFixture.addClass("""
package java.util.stream;
@jdk.internal.javac.PreviewFeature(feature = jdk.internal.javac.PreviewFeature.Feature.STREAM_GATHERERS)
public final class Gatherers {}
""".trimIndent())
}
fun `test gatherer language level 22 with JDK 22`() {
myFixture.setLanguageLevel(LanguageLevel.JDK_22)
addGatherer()
myFixture.testHighlighting(JvmLanguage.JAVA, """
import <error descr="java.util.stream.Gatherers is a preview API and is disabled by default">java.util.stream.Gatherers</error>;
class Main {
public static void main(String[] args) {
<error descr="java.util.stream.Gatherers is a preview API and is disabled by default">Gatherers</error> gatherers = null;
}
}
""".trimIndent())
}
fun `test gatherer language level 22 preview with JDK 22`() {
myFixture.setLanguageLevel(LanguageLevel.JDK_22_PREVIEW)
addGatherer()
myFixture.testHighlighting(JvmLanguage.JAVA, """
import java.util.stream.Gatherers;
class Main {
public static void main(String[] args) {
Gatherers gatherers = null;
}
}
""".trimIndent())
}
}
@@ -356,4 +356,30 @@ class JavaJavaApiUsageInspectionTest : JavaApiUsageInspectionTestBase() {
myFixture.runQuickFix("Set language level to 25 (Preview) - Primitive Types in Patterns, etc.")
assertEquals(LanguageLevel.JDK_25_PREVIEW, LanguageLevelUtil.getEffectiveLanguageLevel(myFixture.module))
}
fun `test gatherer language level 22 with JDK 25`() {
myFixture.setLanguageLevel(LanguageLevel.JDK_22)
myFixture.testHighlighting(JvmLanguage.JAVA, """
import java.util.stream.Gatherers;
class Main {
public static void main(String[] args) {
<error descr="Usage of API documented as @since 24+">Gatherers</error> gatherers = null;
}
}
""".trimIndent())
}
fun `test gatherer language level 22 preview with JDK 25`() {
myFixture.setLanguageLevel(LanguageLevel.JDK_22_PREVIEW)
myFixture.testHighlighting(JvmLanguage.JAVA, """
import java.util.stream.Gatherers;
class Main {
public static void main(String[] args) {
Gatherers gatherers = null;
}
}
""".trimIndent())
}
}