From 954aed5dda2323ddfa26268f3ba1141a4170bcf3 Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Tue, 26 Aug 2025 13:31:30 +0000 Subject: [PATCH] [java-inspections] IDEA-376220 Fix JavaApiUsageInspection not respecting preview features Previously: Assume we use language level 24, but JDK 25. And that we used some *preview* API from JDK 25. The quick-fix from JavaApiUsageInspection suggested bumping language level to 25. Now: It is fixed, i.e. the quick-fix suggests bumping language level to 25 preview. Merge-request: IJ-MR-172531 Merged-by: Bartek Pacia GitOrigin-RevId: 4966e043c75a3b7ff5c19debe8e600579aad47c3 --- .../core/JavaPreviewFeatureUtil.java | 18 +++-- .../intellij/testFramework/IdeaTestUtil.java | 2 +- .../messages/JvmAnalysisBundle.properties | 1 + .../codeInspection/JavaApiUsageInspection.kt | 24 ++++-- .../codeInspection/apiUsage/classes/.gitkeep | 0 .../codeInspection/apiUsage/compile.sh | 0 .../codeInspection/apiUsage/src/.gitkeep | 0 ...ApiUsageInspectionWithCustomMockJdkTest.kt | 73 +++++++++++++++++-- 8 files changed, 102 insertions(+), 16 deletions(-) create mode 100644 jvm/jvm-analysis-java-tests/testData/codeInspection/apiUsage/classes/.gitkeep mode change 100644 => 100755 jvm/jvm-analysis-java-tests/testData/codeInspection/apiUsage/compile.sh create mode 100644 jvm/jvm-analysis-java-tests/testData/codeInspection/apiUsage/src/.gitkeep diff --git a/java/codeserver/core/src/com/intellij/java/codeserver/core/JavaPreviewFeatureUtil.java b/java/codeserver/core/src/com/intellij/java/codeserver/core/JavaPreviewFeatureUtil.java index bfa4f3d3c8ea..632abb56b030 100644 --- a/java/codeserver/core/src/com/intellij/java/codeserver/core/JavaPreviewFeatureUtil.java +++ b/java/codeserver/core/src/com/intellij/java/codeserver/core/JavaPreviewFeatureUtil.java @@ -86,11 +86,7 @@ public final class JavaPreviewFeatureUtil { if (element instanceof PsiJavaCodeReferenceElement refElement) { PsiElement resolved = refElement.resolve(); if (resolved instanceof PsiModifierListOwner owner) { - PsiAnnotation annotation = getPreviewFeatureAnnotationInternal(owner); - JavaFeature feature = fromPreviewFeatureAnnotation(annotation); - if (feature == null) return null; - if (isParticipating(refElement, owner)) return null; - return new PreviewFeatureUsage(feature, refElement, owner, annotation); + return getPreviewFeatureUsage(refElement, owner); } } else if (element instanceof PsiRequiresStatement requiresStatement) { @@ -122,6 +118,18 @@ public final class JavaPreviewFeatureUtil { return null; } + /** + * @see #getPreviewFeatureUsage(PsiElement) + */ + public static @Nullable PreviewFeatureUsage getPreviewFeatureUsage(@NotNull PsiJavaCodeReferenceElement refElement, + @NotNull PsiModifierListOwner owner) { + PsiAnnotation annotation = getPreviewFeatureAnnotationInternal(owner); + JavaFeature feature = fromPreviewFeatureAnnotation(annotation); + if (feature == null) return null; + if (isParticipating(refElement, owner)) return null; + return new PreviewFeatureUsage(feature, refElement, owner, annotation); + } + /** * Participating source code means that such code can access preview feature api in the same package without warnings. * diff --git a/java/testFramework/shared/src/com/intellij/testFramework/IdeaTestUtil.java b/java/testFramework/shared/src/com/intellij/testFramework/IdeaTestUtil.java index 77a74228a3d4..b8ccd5f4da5e 100644 --- a/java/testFramework/shared/src/com/intellij/testFramework/IdeaTestUtil.java +++ b/java/testFramework/shared/src/com/intellij/testFramework/IdeaTestUtil.java @@ -128,7 +128,7 @@ public final class IdeaTestUtil { private static Sdk createMockJdkFromRepository(String name, int version) { List repos = MavenDependencyUtil.getRemoteRepositoryDescriptions(); - String coordinates = "org.jetbrains.mockjdk:"+ MOCK_JDK_GROUP_ID + ":" + version + ".0"; + String coordinates = "org.jetbrains.mockjdk:" + MOCK_JDK_GROUP_ID + ":" + version + ".0"; RepositoryLibraryProperties libraryProperties = new RepositoryLibraryProperties(coordinates, false); Collection roots = JarRepositoryManager.loadDependenciesModal(ProjectManager.getInstance().getDefaultProject(), libraryProperties, false, false, null, diff --git a/jvm/jvm-analysis-impl/resources/messages/JvmAnalysisBundle.properties b/jvm/jvm-analysis-impl/resources/messages/JvmAnalysisBundle.properties index 2355915da725..330a3365adbc 100644 --- a/jvm/jvm-analysis-impl/resources/messages/JvmAnalysisBundle.properties +++ b/jvm/jvm-analysis-impl/resources/messages/JvmAnalysisBundle.properties @@ -220,6 +220,7 @@ jvm.inspections.string.touppercase.tolowercase.without.locale.description= jvm.inspections.api.display.name=Usages of API which isn't available at the configured language level assertequals.between.inconvertible.types.display.name='assertEquals()' between objects of inconvertible types jvm.inspections.1.5.problem.descriptor=Usage of API documented as @since {0}+ +jvm.inspections.1.5.problem.descriptor.preview=Usage of preview API documented as @since {0}+ jvm.inspections.1.7.problem.descriptor=Usage of generified after 1.6 API which would cause compilation problems with JDK {0} jvm.inspections.1.8.problem.descriptor=Default {0, choice, 0#|1#method is|2# methods are} not overridden. It would cause compilation problems with JDK {1} jvm.inspections.1.8.problem.single.descriptor=Default method ''{0}'' is not overridden. It would cause compilation problems with JDK {1} 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 fb267fc9cf0d..36bfc7bef0df 100644 --- a/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/JavaApiUsageInspection.kt +++ b/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/JavaApiUsageInspection.kt @@ -11,6 +11,7 @@ import com.intellij.codeInspection.options.OptPane import com.intellij.codeInspection.options.OptPane.* import com.intellij.codeInspection.options.OptionController import com.intellij.java.JavaBundle +import com.intellij.java.codeserver.core.JavaPreviewFeatureUtil import com.intellij.lang.Language import com.intellij.openapi.module.JdkApiCompatibilityService import com.intellij.openapi.module.LanguageLevelUtil @@ -191,7 +192,7 @@ class JavaApiUsageInspection : AbstractBaseUastLocalInspectionTool() { languageLevel = sourcePsi.containingFile.getUserData(PsiUtil.FILE_LANGUAGE_LEVEL_KEY) } if (languageLevel == null) return - val firstCompatibleLanguageLevel = JdkApiCompatibilityService.getInstance().firstCompatibleLanguageLevel(target, languageLevel) + var firstCompatibleLanguageLevel = JdkApiCompatibilityService.getInstance().firstCompatibleLanguageLevel(target, languageLevel) if (firstCompatibleLanguageLevel != null) { val psiClass = if (qualifier != null) { PsiUtil.resolveClassInType(qualifier.getExpressionType()) @@ -205,6 +206,15 @@ class JavaApiUsageInspection : AbstractBaseUastLocalInspectionTool() { if (isIgnored(superClass)) return } } + + if (sourcePsi is PsiJavaCodeReferenceElement) { + val previewFeatureUsage = JavaPreviewFeatureUtil.getPreviewFeatureUsage(sourcePsi, target) + val previewLevel = firstCompatibleLanguageLevel.getPreviewLevel() + if (previewFeatureUsage != null && previewLevel != null) { + firstCompatibleLanguageLevel = previewLevel + } + } + registerError(sourcePsi, firstCompatibleLanguageLevel, holder, isOnTheFly) } else if (target is PsiClass && !languageLevel.isAtLeast(LanguageLevel.JDK_1_7)) { @@ -240,9 +250,13 @@ class JavaApiUsageInspection : AbstractBaseUastLocalInspectionTool() { private fun registerError(reference: PsiElement, sinceLanguageLevel: LanguageLevel, holder: ProblemsHolder, isOnTheFly: Boolean) { if (reference.getUastParentOfType() != null) return - val message = JvmAnalysisBundle.message( - "jvm.inspections.1.5.problem.descriptor", sinceLanguageLevel.toJavaVersion().toFeatureString() - ) + val message = if (sinceLanguageLevel.isPreview) { + JvmAnalysisBundle.message("jvm.inspections.1.5.problem.descriptor.preview", sinceLanguageLevel.toJavaVersion().toFeatureString()) + } + else { + JvmAnalysisBundle.message("jvm.inspections.1.5.problem.descriptor", sinceLanguageLevel.toJavaVersion().toFeatureString()) + } + val fix = if (isOnTheFly) { QuickFixFactory.getInstance().createIncreaseLanguageLevelFix(sinceLanguageLevel) as LocalQuickFix } @@ -253,4 +267,4 @@ class JavaApiUsageInspection : AbstractBaseUastLocalInspectionTool() { private fun getEffectiveLanguageLevel(module: Module): LanguageLevel { return effectiveLanguageLevel ?: LanguageLevelUtil.getEffectiveLanguageLevel(module) } -} \ No newline at end of file +} diff --git a/jvm/jvm-analysis-java-tests/testData/codeInspection/apiUsage/classes/.gitkeep b/jvm/jvm-analysis-java-tests/testData/codeInspection/apiUsage/classes/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/jvm/jvm-analysis-java-tests/testData/codeInspection/apiUsage/compile.sh b/jvm/jvm-analysis-java-tests/testData/codeInspection/apiUsage/compile.sh old mode 100644 new mode 100755 diff --git a/jvm/jvm-analysis-java-tests/testData/codeInspection/apiUsage/src/.gitkeep b/jvm/jvm-analysis-java-tests/testData/codeInspection/apiUsage/src/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/tests/java/JavaJavaApiUsageInspectionWithCustomMockJdkTest.kt b/jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/tests/java/JavaJavaApiUsageInspectionWithCustomMockJdkTest.kt index 137dad9b323c..40a1f26b1d71 100644 --- a/jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/tests/java/JavaJavaApiUsageInspectionWithCustomMockJdkTest.kt +++ b/jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/tests/java/JavaJavaApiUsageInspectionWithCustomMockJdkTest.kt @@ -2,14 +2,16 @@ package com.intellij.codeInspection.tests.java import com.intellij.jvm.analysis.JavaJvmAnalysisTestUtil import com.intellij.jvm.analysis.internal.testFramework.JavaApiUsageInspectionTestBase +import com.intellij.jvm.analysis.testFramework.JvmLanguage import com.intellij.openapi.module.Module import com.intellij.openapi.roots.ContentEntry +import com.intellij.openapi.roots.LanguageLevelProjectExtension import com.intellij.openapi.roots.ModifiableRootModel +import com.intellij.pom.java.LanguageLevel +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.search.GlobalSearchScope import com.intellij.testFramework.LightProjectDescriptor import com.intellij.testFramework.PsiTestUtil -import org.junit.Ignore -import org.junit.runner.RunWith -import org.junit.runners.BlockJUnit4ClassRunner /** * This is a base test case for test cases that highlight all the use of APIs @@ -21,8 +23,6 @@ import org.junit.runners.BlockJUnit4ClassRunner * - Set `JAVA_HOME` to JDK 1.8. In this case it's possible to redefine JDK's own classes like `String` or `Class` * - Invoke `./compile.sh`. The new class(es) will appear in `./classes` */ -@Ignore -@RunWith(BlockJUnit4ClassRunner::class) // disabled because there are currently no tests for a JDK higher than the highest mock JDK class JavaJavaApiUsageInspectionWithCustomMockJdkTest : JavaApiUsageInspectionTestBase() { override fun getBasePath(): String = JavaJvmAnalysisTestUtil.TEST_DATA_PROJECT_RELATIVE_BASE_PATH @@ -33,4 +33,67 @@ class JavaJavaApiUsageInspectionWithCustomMockJdkTest : JavaApiUsageInspectionTe PsiTestUtil.newLibrary("JDKMock").classesRoot("$dataDir/classes").addTo(model) } } + + fun `test language level 24 preview with JDK 25`() { + myFixture.addClass(""" + package jdk.internal.javac; + + import java.lang.annotation.*; + + @Target({ElementType.METHOD, + ElementType.CONSTRUCTOR, + ElementType.FIELD, + ElementType.PACKAGE, + ElementType.TYPE}) + // CLASS retention will hopefully be sufficient for the purposes at hand + @Retention(RetentionPolicy.RUNTIME) + // *Not* @Documented + public @interface PreviewFeature { + /** + * Name of the preview feature the annotated API is associated + * with. + */ + public Feature feature(); + + public enum Feature { + PEM_API, + STABLE_VALUES + } + } + """.trimIndent()) + + myFixture.addClass(""" + package java.lang; + + import jdk.internal.javac.PreviewFeature; + + /** + * @since 25 + */ + @PreviewFeature(feature = PreviewFeature.Feature.STABLE_VALUES) + public class StableValue { + private StableValue(T value) {} + public static StableValue of(X value) { + return new StableValue<>(value); + } + } + """.trimIndent()) + + myFixture.setLanguageLevel(LanguageLevel.JDK_24) + println("DBG: language level is: " + LanguageLevelProjectExtension.getInstance(project).getLanguageLevel()) + println("DBGL: location: " + JavaPsiFacade.getInstance(project).findClass("java.lang.String", GlobalSearchScope.allScope(project))?.containingFile?.virtualFile?.path) + myFixture.testHighlighting(JvmLanguage.JAVA, """ + import java.util.concurrent.StructuredTaskScope; + + class Main { + static void main() { + StructuredTaskScope a; // JEP 505 + StableValue b = StableValue.of("foo"); + } + } + """.trimIndent()) + + val intention = myFixture.getAvailableIntention("Set language level to 25 (Preview) - Primitive Types in Patterns, etc.") + myFixture.launchAction(intention!!) + } }