mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[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:
committed by
intellij-monorepo-bot
parent
917311bcab
commit
3a2becf03d
+48
-5
@@ -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) {
|
||||
|
||||
+48
@@ -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())
|
||||
}
|
||||
}
|
||||
+26
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user