diff --git a/java/java-analysis-api/src/com/intellij/openapi/module/JdkApiCompatibilityService.java b/java/java-analysis-api/src/com/intellij/openapi/module/JdkApiCompatibilityService.java index bbfd828a8d16..d9b748076442 100644 --- a/java/java-analysis-api/src/com/intellij/openapi/module/JdkApiCompatibilityService.java +++ b/java/java-analysis-api/src/com/intellij/openapi/module/JdkApiCompatibilityService.java @@ -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> 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}. * *
  • if {@code member} is not annotated with a {@code @since} tag this method will return null. + * + *
  • 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.
  • + * + *
  • 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}.
  • * * * @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.naturalOrder()).compare(outOfPreviewLevel, level.outOfPreviewLevel) < 0; + } + } + + /** * Serializes a {@code member} for storage in apiX.txt files. *

    diff --git a/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/JavaApiUsageInspection.kt b/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/JavaApiUsageInspection.kt index 286bc4093f65..2d055d4c222d 100644 --- a/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/JavaApiUsageInspection.kt +++ b/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/JavaApiUsageInspection.kt @@ -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) { diff --git a/jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/tests/java/JavaJavaApiUsageInspection22Test.kt b/jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/tests/java/JavaJavaApiUsageInspection22Test.kt new file mode 100644 index 000000000000..b067b49aad53 --- /dev/null +++ b/jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/tests/java/JavaJavaApiUsageInspection22Test.kt @@ -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 java.util.stream.Gatherers; + + class Main { + public static void main(String[] args) { + Gatherers 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()) + } +} \ No newline at end of file diff --git a/jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/tests/java/JavaJavaApiUsageInspectionTest.kt b/jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/tests/java/JavaJavaApiUsageInspectionTest.kt index 2858da50a4da..a8d26cd30ccb 100644 --- a/jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/tests/java/JavaJavaApiUsageInspectionTest.kt +++ b/jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/tests/java/JavaJavaApiUsageInspectionTest.kt @@ -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) { + Gatherers 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()) + } } \ No newline at end of file