[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 <bartek.pacia@jetbrains.com>

GitOrigin-RevId: 4966e043c75a3b7ff5c19debe8e600579aad47c3
This commit is contained in:
Bartek Pacia
2025-08-26 13:31:30 +00:00
committed by intellij-monorepo-bot
parent b5cc3b7714
commit 954aed5dda
8 changed files with 102 additions and 16 deletions
@@ -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.
*
@@ -128,7 +128,7 @@ public final class IdeaTestUtil {
private static Sdk createMockJdkFromRepository(String name, int version) {
List<RemoteRepositoryDescription> 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<OrderRoot> roots =
JarRepositoryManager.loadDependenciesModal(ProjectManager.getInstance().getDefaultProject(), libraryProperties, false, false, null,
@@ -220,6 +220,7 @@ jvm.inspections.string.touppercase.tolowercase.without.locale.description=<code>
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}
@@ -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<UComment>() != 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)
}
}
}
@@ -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<T> {
private StableValue(T value) {}
public static <X> StableValue<X> 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
<error descr="Usage of preview API documented as @since 25+"><error descr="java.lang.StableValue is a preview API and is disabled by default">StableValue<String></error></error> b = <error descr="Usage of preview API documented as @since 25+"><error descr="java.lang.StableValue is a preview API and is disabled by default">StableValue</error></error>.<caret>of("foo");
}
}
""".trimIndent())
val intention = myFixture.getAvailableIntention("Set language level to 25 (Preview) - Primitive Types in Patterns, etc.")
myFixture.launchAction(intention!!)
}
}