[java] Refactor LanguageLevelUtil

Moves everything related to API compatability into JdkApiCompatabilityCache and deprecates some utility methods that already have better alternatives. Also adds documentation and cleans up the utility implementations. #IDEA-370797 Fixed

GitOrigin-RevId: a04bfee96d3f779b9fcb9d96709e1c686a93b543
This commit is contained in:
Bart van Helvert
2025-04-14 09:28:36 +00:00
committed by intellij-monorepo-bot
parent a28baf5235
commit e4f3444eed
11 changed files with 243 additions and 161 deletions
@@ -0,0 +1,155 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.module;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.Service;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiUtil;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* Stores a compatibility matrix for JDK APIs. The matrix is generated from {@code @since} tags in the JDK source code.
*/
@Service(Service.Level.APP)
public final class JdkApiCompatabilityCache {
private static final Logger LOG = Logger.getInstance(JdkApiCompatabilityCache.class);
private final Map<LanguageLevel, List<String>> cache = new EnumMap<>(LanguageLevel.class);
public static JdkApiCompatabilityCache getInstance() {
return ApplicationManager.getApplication().getService(JdkApiCompatabilityCache.class);
}
/**
* Finds the first compatible language level for a given {@code member} that is incompatible given the {@code contextLanguageLevel} or
* null if the {@code member} is compatible with the provided {@code contextLanguageLevel}. This method uses a pre-generated JDK
* compatability matrix that is generated from {@code @since} tags in the JDK source code, meaning that the JDK Javadoc doesn't need to
* be available for this method to work correctly.
* <p>
* Examples:
* <ul>
* <li>if {@code member} is annotated as {@code @since 9} and the context language level is {@link LanguageLevel#JDK_1_8} this method
* will return {@link LanguageLevel#JDK_1_9}.</li>
*
* <li>if {@code member} is annotated as {@code @since 10} and the context language level is {@link LanguageLevel#JDK_11} this method
* will return {@code null}.
*
* <li>if {@code member} is method annotated as {@code @since 9} and the context language level is {@link LanguageLevel#JDK_1_8}, but
* its super method is annotated as {@code @since 8} this method will return null.
*
* <li>if {@code member} is method annotated as {@code @since 9} and the context language level is {@link LanguageLevel#JDK_11}, but
* 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.
* </ul>
*
* @param member The member to find the incompatible language level for
* @param contextLanguageLevel The current language level of the context of {@code member}
* @return The first compatible language level for {@code member} or null if it is lower or equal than {@code contextLanguageLevel} or
* unknown.
*/
public @Nullable LanguageLevel firstCompatibleLanguageLevel(@NotNull PsiMember member, @NotNull LanguageLevel contextLanguageLevel) {
if (member instanceof PsiAnonymousClass) return null;
PsiClass containingClass = member.getContainingClass();
if (containingClass instanceof PsiAnonymousClass) return null;
if (member instanceof PsiClass clazz && PsiUtil.isLocalClass(clazz)) return null;
List<PsiMember> membersToCheck = new ArrayList<>();
membersToCheck.add(member);
if (member instanceof PsiMethod method && !method.isConstructor()) {
membersToCheck.addAll(Arrays.asList(method.findSuperMethods()));
}
LanguageLevel incompatibleLevelForContext = LanguageLevelUtil.getNextLanguageLevel(contextLanguageLevel);
LanguageLevel lowestCompatibleLanguageLevel = null;
for (PsiMember checkMember : membersToCheck) {
String signature = getSignature(checkMember);
if (signature == null) return null;
LanguageLevel compatibleLanguageLevelForMember = getIntroducedApiLevel(signature, incompatibleLevelForContext);
if (compatibleLanguageLevelForMember == null) return null;
if (lowestCompatibleLanguageLevel == null || compatibleLanguageLevelForMember.isLessThan(lowestCompatibleLanguageLevel)) {
lowestCompatibleLanguageLevel = compatibleLanguageLevelForMember;
}
}
return lowestCompatibleLanguageLevel;
}
/**
* Gets all the newly introduced APIs for a given {@code languageLevel} and caches the results.
*
* @param languageLevel to get the newly introduced APis for.
* @return the newly introduced APIs in {@code languageLevel} or empty set if the language level is not supported.
*/
private @NotNull List<String> getIntroducedApis(@NotNull LanguageLevel languageLevel) {
String featureString = languageLevel.toJavaVersion().toFeatureString();
List<String> result = cache.get(languageLevel);
if (result != null) return result;
result = Collections.emptyList();
URL resource = JdkApiCompatabilityCache.class.getResource("api" + featureString + ".txt");
if (resource != null) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.openStream(), StandardCharsets.UTF_8))) {
result = FileUtil.loadLines(reader);
}
catch (IOException ex) {
LOG.error("Cannot load: " + resource.getFile(), ex);
}
}
cache.put(languageLevel, result);
return result;
}
/**
* @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}.
*/
private LanguageLevel getIntroducedApiLevel(@NotNull String signature, @Nullable LanguageLevel languageLevel) {
if (languageLevel == null) return null;
if (getIntroducedApis(languageLevel).contains(signature)) return languageLevel;
return getIntroducedApiLevel(signature, LanguageLevelUtil.getNextLanguageLevel(languageLevel));
}
/**
* Serializes a {@code member} for storage in apiX.txt files.
* <p>
* Example: {@code java.net.URLDecoder#decode(java.lang.String;java.nio.charset.Charset;)}
*/
@ApiStatus.Internal
public @Nullable String getSignature(@Nullable PsiMember member) {
if (member instanceof PsiClass psiClass) return psiClass.getQualifiedName();
if (member instanceof PsiField) {
String containingClass = getSignature(member.getContainingClass());
return containingClass == null ? null : containingClass + "#" + member.getName();
}
if (member instanceof PsiMethod method) {
String containingClass = getSignature(member.getContainingClass());
if (containingClass == null) return null;
StringBuilder buf = new StringBuilder();
buf.append(containingClass);
buf.append('#');
buf.append(method.getName());
buf.append('(');
for (PsiType type : method.getSignature(PsiSubstitutor.EMPTY).getParameterTypes()) {
buf.append(type.getCanonicalText());
buf.append(";");
}
buf.append(')');
return buf.toString();
}
return null;
}
}
@@ -1,159 +1,100 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.module;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.roots.LanguageLevelModuleExtension;
import com.intellij.openapi.roots.LanguageLevelProjectExtension;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
import com.intellij.psi.PsiMember;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;
import static com.intellij.reference.SoftReference.dereference;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public final class LanguageLevelUtil {
private LanguageLevelUtil() { }
/**
* Returns explicitly specified custom language level for {@code module}, or {@code null} if the module uses 'Project default' language level.
* May return {@linkplain LanguageLevel#isUnsupported() unsupported} language level.
* @param module to get the language level for.
* @return explicitly specified language level for a {@link Module}, or {@code null} if the module uses 'Project default' language level.
* May return an {@linkplain LanguageLevel#isUnsupported() unsupported} language level.
*/
public static @Nullable LanguageLevel getCustomLanguageLevel(@NotNull Module module) {
LanguageLevelModuleExtension moduleExtension = ModuleRootManager.getInstance(module).getModuleExtension(LanguageLevelModuleExtension.class);
LanguageLevelModuleExtension moduleExtension =
ModuleRootManager.getInstance(module).getModuleExtension(LanguageLevelModuleExtension.class);
return moduleExtension != null ? moduleExtension.getLanguageLevel() : null;
}
/**
* Returns effective language level for the module (either custom module level, or project level if module level is not specified).
* May return {@linkplain LanguageLevel#isUnsupported() unsupported} language level.
* @param module to get the language level for.
* @return the effective language level for a {@link Module}, which is either the overridden language level for the module or the project
* language level.
* May return {@linkplain LanguageLevel#isUnsupported() unsupported} language level.
*/
public static @NotNull LanguageLevel getEffectiveLanguageLevel(final @NotNull Module module) {
ApplicationManager.getApplication().assertReadAccessAllowed();
public static @NotNull LanguageLevel getEffectiveLanguageLevel(@NotNull Module module) {
LanguageLevel level = getCustomLanguageLevel(module);
if (level != null) return level;
return LanguageLevelProjectExtension.getInstance(module.getProject()).getLanguageLevel();
}
private static final Map<LanguageLevel, Reference<Set<String>>> ourForbiddenAPI = new EnumMap<>(LanguageLevel.class);
/**
* @deprecated Please use {@link LanguageLevel#getShortText()} instead.
*/
@Deprecated(forRemoval = true)
public static String getJdkName(LanguageLevel languageLevel) {
final String presentableText = languageLevel.getPresentableText();
return presentableText.substring(0, presentableText.indexOf(' '));
return languageLevel.getShortText();
}
/**
* @param api The language level to get the next from.
* @return Next {@link LanguageLevel} that is not in preview.
* @return Next {@link LanguageLevel} that is not in preview or null if there is no language level.
*/
public static @Nullable LanguageLevel getNextLanguageLevel(@NotNull LanguageLevel api) {
final LanguageLevel[] levels = LanguageLevel.values();
final int currentLevelId = api.ordinal();
for (int i = currentLevelId + 1; i < levels.length; i++) {
final LanguageLevel level = levels[i];
List<LanguageLevel> levels = LanguageLevel.getEntries();
for (LanguageLevel level : levels.subList(api.ordinal() + 1, levels.size())) {
if (!level.isPreview()) return level;
}
return null;
}
private static final Map<LanguageLevel, String> ourPresentableShortMessage = new EnumMap<>(LanguageLevel.class);
static {
for (LanguageLevel level : LanguageLevel.values()) {
int feature = level.feature() + 1;
if (LanguageLevel.forFeature(feature) == null) break;
ourPresentableShortMessage.put(level, feature >= 9 ? String.valueOf(feature) : "1." + feature);
}
}
/**
* Retrieves the short language-level name like "17" for Java 17 or "1.5" for Java 1.5.
*
* @param languageLevel The language level for which to retrieve the short name.
* @return The short name associated with the specified language level, or null if the language level is not released yet.
* @deprecated Please use {@code languageLevel.toJavaVersion().toFeatureString()} instead.
*/
public static @Nullable String getShortMessage(@NotNull LanguageLevel languageLevel) {
return ourPresentableShortMessage.get(languageLevel);
@Deprecated(forRemoval = true)
public static @NotNull String getShortMessage(@NotNull LanguageLevel languageLevel) {
return languageLevel.toJavaVersion().toFeatureString();
}
/**
* For performance reasons, the forbidden API is pre-generated.
* @see com.intellij.jvm.analysis.internal.testFramework.JavaApiUsageGenerator
*/
private static @Nullable Set<String> getForbiddenApi(@NotNull LanguageLevel languageLevel) {
String message = getShortMessage(languageLevel);
if (message == null) return null;
Reference<Set<String>> ref = ourForbiddenAPI.get(languageLevel);
Set<String> result = dereference(ref);
if (result == null) {
String fileName = "api" + message + ".txt";
URL resource = LanguageLevelUtil.class.getResource(fileName);
if (resource != null) {
result = loadSignatureList(resource);
}
else {
Logger.getInstance(LanguageLevelUtil.class).error("File not found: " + fileName);
result = Collections.emptySet();
}
ourForbiddenAPI.put(languageLevel, new SoftReference<>(result));
}
return result;
}
/**
* @param member The {@link PsiMember} to get the language level from
* @param member The {@link PsiMember} to get the language level from
* @param languageLevel The effective language level
* @return The last incompatible language level for a {@link PsiMember} as annotated by the @since javadoc or null if it is unknown.
* @return The last incompatible language level for a {@link PsiMember} as annotated by the @since Javadoc or null if it is unknown.
* For example, if a method is annotated as @since 9 this method will return {@link LanguageLevel#JDK_1_8}.
* @deprecated Please use {@code JdkIncompatibleApiCache.getInstance().getLastIncompatibleLanguageLevel(member, languageLevel)}
*/
@Deprecated(forRemoval = true)
public static @Nullable LanguageLevel getLastIncompatibleLanguageLevel(@NotNull PsiMember member, @NotNull LanguageLevel languageLevel) {
if (member instanceof PsiAnonymousClass) return null;
PsiClass containingClass = member.getContainingClass();
if (containingClass instanceof PsiAnonymousClass) return null;
if (member instanceof PsiClass && !(member.getParent() instanceof PsiClass || member.getParent() instanceof PsiFile)) return null;
String signature = getSignature(member);
if (signature == null) return null;
LanguageLevel lastLanguageLevel = getLastIncompatibleLanguageLevelForSignature(signature, languageLevel);
if (lastLanguageLevel != null) {
if (member instanceof PsiMethod && !((PsiMethod)member).isConstructor()) {
LanguageLevel lowestSuperLanguageLevel = lastLanguageLevel;
for (PsiMethod method : ((PsiMethod)member).findSuperMethods()) {
String superSignature = getSignature(method);
if (superSignature == null) return null;
LanguageLevel lastSuperLanguageLevel = getLastIncompatibleLanguageLevelForSignature(superSignature, languageLevel);
if (lastSuperLanguageLevel == null) return null;
if (lastSuperLanguageLevel.isLessThan(lowestSuperLanguageLevel)) {
lowestSuperLanguageLevel = lastSuperLanguageLevel;
}
}
return lowestSuperLanguageLevel;
}
}
return lastLanguageLevel;
}
private static LanguageLevel getLastIncompatibleLanguageLevelForSignature(@NotNull String signature, @NotNull LanguageLevel languageLevel) {
Set<String> forbiddenApi = getForbiddenApi(languageLevel);
if (forbiddenApi == null) return null;
if (forbiddenApi.contains(signature)) return languageLevel;
if (languageLevel.compareTo(LanguageLevel.HIGHEST) == 0) return null;
LanguageLevel[] values = LanguageLevel.values();
if (languageLevel.ordinal() == values.length - 1) return null;
LanguageLevel nextLanguageLevel = values[languageLevel.ordinal() + 1];
return getLastIncompatibleLanguageLevelForSignature(signature, nextLanguageLevel);
return JdkApiCompatabilityCache.getInstance().firstCompatibleLanguageLevel(member, languageLevel);
}
/**
* @deprecated Please use {@link JdkApiCompatabilityCache} to check for incompatible APIs.
*/
@Deprecated(forRemoval = true)
public static Set<String> loadSignatureList(@NotNull URL resource) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.openStream(), StandardCharsets.UTF_8))) {
return new HashSet<>(FileUtil.loadLines(reader));
@@ -166,32 +107,11 @@ public final class LanguageLevelUtil {
/**
* For serialization of forbidden api.
*
* @deprecated Please don't use this, this API was moved to {@link JdkApiCompatabilityCache} and is for internal use only.
*/
@Deprecated(forRemoval = true)
public static @Nullable String getSignature(@Nullable PsiMember member) {
if (member instanceof PsiClass) {
return ((PsiClass)member).getQualifiedName();
}
if (member instanceof PsiField) {
String containingClass = getSignature(member.getContainingClass());
return containingClass == null ? null : containingClass + "#" + member.getName();
}
if (member instanceof PsiMethod method) {
String containingClass = getSignature(member.getContainingClass());
if (containingClass == null) return null;
StringBuilder buf = new StringBuilder();
buf.append(containingClass);
buf.append('#');
buf.append(method.getName());
buf.append('(');
for (PsiType type : method.getSignature(PsiSubstitutor.EMPTY).getParameterTypes()) {
buf.append(type.getCanonicalText());
buf.append(";");
}
buf.append(')');
return buf.toString();
}
return null;
return JdkApiCompatabilityCache.getInstance().getSignature(member);
}
}
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInspection;
import com.intellij.codeInsight.ExceptionUtil;
@@ -7,7 +7,7 @@ import com.intellij.codeInsight.daemon.impl.quickfix.DeleteMultiCatchFix;
import com.intellij.java.JavaBundle;
import com.intellij.modcommand.ModPsiUpdater;
import com.intellij.modcommand.PsiUpdateModCommandQuickFix;
import com.intellij.openapi.module.LanguageLevelUtil;
import com.intellij.openapi.module.JdkApiCompatabilityCache;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.pom.java.LanguageLevel;
@@ -205,7 +205,7 @@ public final class CharsetObjectCanBeUsedInspection extends AbstractBaseJavaLoca
PsiMethod[] candidates = method.isConstructor() ? aClass.getConstructors() : aClass.findMethodsByName(method.getName(), false);
PsiMethod charsetMethod = Arrays.stream(candidates)
.filter(psiMethod -> checkMethod(psiMethod, "java.nio.charset.Charset"))
.filter(psiMethod -> LanguageLevelUtil.getLastIncompatibleLanguageLevel(psiMethod, languageLevel) == null)
.filter(psiMethod -> JdkApiCompatabilityCache.getInstance().firstCompatibleLanguageLevel(psiMethod, languageLevel) == null)
.findFirst().orElse(null);
if (charsetMethod == null) return null;
return new CharsetMatch(argument, method, charsetMethod);
@@ -1,11 +1,11 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight;
import com.intellij.java.JavaBundle;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.application.ReadActionProcessor;
import com.intellij.openapi.module.LanguageLevelUtil;
import com.intellij.openapi.module.JdkApiCompatabilityCache;
import com.intellij.openapi.progress.EmptyProgressIndicator;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
@@ -151,7 +151,7 @@ public final class FunctionalInterfaceSuggester {
@Override
public boolean processInReadAction(PsiMember member) {
if (member instanceof PsiClass &&
LanguageLevelUtil.getLastIncompatibleLanguageLevel(member, PsiUtil.getLanguageLevel(element)) == null) {
JdkApiCompatabilityCache.getInstance().firstCompatibleLanguageLevel(member, PsiUtil.getLanguageLevel(element)) == null) {
if (!JavaPsiFacade.getInstance(project).getResolveHelper().isAccessible(member, element, null)) {
return true;
}
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.completion;
import com.intellij.codeInsight.*;
@@ -19,7 +19,7 @@ import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.editor.ex.EditorSettingsExternalizable;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.module.LanguageLevelUtil;
import com.intellij.openapi.module.JdkApiCompatabilityCache;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.*;
@@ -503,7 +503,7 @@ public final class JavaCompletionUtil {
private @Nullable LookupElement generateLookupElementDecorator(@Nullable PsiType qualifierType, @NotNull Object object,
@NotNull Function<Consumer<LookupElementPresentation>, LookupElementDecorator<LookupElement>> generator) {
if (object instanceof PsiMember) {
if (LanguageLevelUtil.getLastIncompatibleLanguageLevel((PsiMember)object, myLanguageLevel) != null) {
if (JdkApiCompatabilityCache.getInstance().firstCompatibleLanguageLevel((PsiMember)object, myLanguageLevel) != null) {
LookupElementDecorator<LookupElement> element = generator.apply(presentation -> presentation.setItemTextForeground(JBColor.RED));
return PrioritizedLookupElement.withExplicitProximity(element, -1);
}
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.siyeh.ig.inheritance;
import com.intellij.codeInsight.ExternalAnnotationsManager;
@@ -6,6 +6,7 @@ import com.intellij.codeInspection.AnnotateMethodFix;
import com.intellij.codeInspection.CleanupLocalInspectionTool;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.options.OptPane;
import com.intellij.openapi.module.JdkApiCompatabilityCache;
import com.intellij.openapi.module.LanguageLevelUtil;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
@@ -215,7 +216,7 @@ public class MissingOverrideAnnotationInspection extends BaseInspection implemen
@Contract("_, _, _,null -> true")
private static boolean ignoreSuperMethod(PsiMethod method, PsiClass methodClass, PsiMethod superMethod, PsiClass superClass) {
return !InheritanceUtil.isInheritorOrSelf(methodClass, superClass, true) ||
LanguageLevelUtil.getLastIncompatibleLanguageLevel(superMethod, PsiUtil.getLanguageLevel(method)) != null;
JdkApiCompatabilityCache.getInstance().firstCompatibleLanguageLevel(superMethod, PsiUtil.getLanguageLevel(method)) != null;
}
}
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.siyeh.ig.internationalization;
import com.intellij.codeInsight.intention.FileModifier.SafeTypeForPreview;
@@ -6,7 +6,7 @@ import com.intellij.codeInspection.CleanupLocalInspectionTool;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.modcommand.ModPsiUpdater;
import com.intellij.modcommand.PsiUpdateModCommandQuickFix;
import com.intellij.openapi.module.LanguageLevelUtil;
import com.intellij.openapi.module.JdkApiCompatabilityCache;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.pom.java.LanguageLevel;
@@ -62,7 +62,7 @@ public final class ImplicitDefaultCharsetUsageInspection extends BaseInspection
}
LocalQuickFix createFix(LanguageLevel level) {
return myMethod == null || LanguageLevelUtil.getLastIncompatibleLanguageLevel(myMethod, level) != null
return myMethod == null || JdkApiCompatabilityCache.getInstance().firstCompatibleLanguageLevel(myMethod, level) != null
? null
: new AddUtf8CharsetFix(this);
}
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.siyeh.ig.style;
import com.intellij.codeInsight.options.JavaClassValidator;
@@ -8,7 +8,7 @@ import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.options.OptPane;
import com.intellij.modcommand.ModPsiUpdater;
import com.intellij.modcommand.PsiUpdateModCommandQuickFix;
import com.intellij.openapi.module.LanguageLevelUtil;
import com.intellij.openapi.module.JdkApiCompatabilityCache;
import com.intellij.openapi.project.Project;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
@@ -115,7 +115,7 @@ public final class SizeReplaceableByIsEmptyInspection extends BaseInspection {
private class SizeReplaceableByIsEmptyVisitor extends BaseInspectionVisitor {
private static boolean isLanguageLevelCompatible(PsiElement element, PsiMethod method) {
LanguageLevel languageLevel = PsiUtil.getLanguageLevel(element);
LanguageLevel lastIncompatibleLanguageLevel = LanguageLevelUtil.getLastIncompatibleLanguageLevel(method, languageLevel);
LanguageLevel lastIncompatibleLanguageLevel = JdkApiCompatabilityCache.getInstance().firstCompatibleLanguageLevel(method, languageLevel);
return lastIncompatibleLanguageLevel == null || lastIncompatibleLanguageLevel.isLessThan(languageLevel);
}
@@ -14,6 +14,7 @@ import com.intellij.java.JavaBundle
import com.intellij.lang.Language
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.module.JdkApiCompatabilityCache
import com.intellij.openapi.module.LanguageLevelUtil
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleUtilCore
@@ -25,6 +26,7 @@ import com.intellij.psi.util.InheritanceUtil
import com.intellij.psi.util.PsiUtil
import com.intellij.testFramework.LightVirtualFile
import com.intellij.uast.UastVisitorAdapter
import com.siyeh.ig.callMatcher.CallMatcher
import org.jdom.Element
import org.jetbrains.uast.*
@@ -51,7 +53,7 @@ class JavaApiUsageInspection : AbstractBaseUastLocalInspectionTool() {
private var effectiveLanguageLevel: LanguageLevel? = null
override fun getOptionsPane(): OptPane {
var levels: List<OptDropdown.Option> = LanguageLevel.values().map { option(it.name, it.presentableText) }
var levels: List<OptDropdown.Option> = LanguageLevel.entries.map { option(it.name, it.presentableText) }
levels = listOf(option("null", JavaBundle.message("label.forbid.api.usages.project"))) + levels
return pane(
dropdown("effectiveLanguageLevel", JavaBundle.message("label.forbid.api.usages"), *levels.toTypedArray())
@@ -91,8 +93,10 @@ class JavaApiUsageInspection : AbstractBaseUastLocalInspectionTool() {
private val holder: ProblemsHolder,
private val isOnTheFly: Boolean
) : ApiUsageUastVisitor(apiUsageProcessor) {
private inline val defaultMethods get() = CallMatcher
.exactInstanceCall(CommonClassNames.JAVA_UTIL_ITERATOR, "remove")
.parameterCount(0)
private inline val defaultMethods get() = setOf("java.util.Iterator#remove()")
private inline val overrideModifierLanguages get() = listOf("kotlin", "scala")
override fun visitClass(node: UClass): Boolean {
@@ -103,13 +107,14 @@ class JavaApiUsageInspection : AbstractBaseUastLocalInspectionTool() {
if (!effectiveLanguageLevel.isAtLeast(LanguageLevel.JDK_1_8)) {
val version = JavaVersionService.getInstance().getJavaSdkVersion(javaPsi)
if (version != null && version.isAtLeast(JavaSdkVersion.JDK_1_8)) {
val mSignatures = javaPsi.visibleSignatures.filter { defaultMethods.contains(LanguageLevelUtil.getSignature(it.method)) }
if (mSignatures.isNotEmpty()) {
val jdkName = LanguageLevelUtil.getJdkName(effectiveLanguageLevel)
val message = if (mSignatures.size == 1) {
JvmAnalysisBundle.message("jvm.inspections.1.8.problem.single.descriptor", mSignatures.first().name, jdkName)
} else {
JvmAnalysisBundle.message("jvm.inspections.1.8.problem.descriptor", mSignatures.size, jdkName)
val signatures = javaPsi.visibleSignatures.filter { signature -> defaultMethods.methodMatches(signature.method) }
if (signatures.isNotEmpty()) {
val jdkName = effectiveLanguageLevel.shortText
val message = if (signatures.size == 1) {
JvmAnalysisBundle.message("jvm.inspections.1.8.problem.single.descriptor", signatures.first().name, jdkName)
}
else {
JvmAnalysisBundle.message("jvm.inspections.1.8.problem.descriptor", signatures.size, jdkName)
}
holder.registerUProblem(node, message, QuickFixFactory.getInstance().createImplementMethodsFix(javaPsi))
}
@@ -137,7 +142,7 @@ class JavaApiUsageInspection : AbstractBaseUastLocalInspectionTool() {
val module = ModuleUtilCore.findModuleForPsiElement(sourcePsi) ?: return
val languageLevel = getEffectiveLanguageLevel(module)
val lastIncompatibleLevel = overriddenMethods.mapNotNull { overriddenMethod ->
LanguageLevelUtil.getLastIncompatibleLanguageLevel(overriddenMethod, languageLevel)
JdkApiCompatabilityCache.getInstance().firstCompatibleLanguageLevel(overriddenMethod, languageLevel)
}.minOrNull() ?: return
val toHighlight = overrideAnnotation?.uastAnchor?.sourcePsi ?: method.uastAnchor?.sourcePsi ?: return
if (shouldReportSinceLevelForElement(lastIncompatibleLevel, sourcePsi) == true) return
@@ -151,13 +156,14 @@ class JavaApiUsageInspection : AbstractBaseUastLocalInspectionTool() {
private inline val generifiedClasses get() = setOf("javax.swing.JComboBox", "javax.swing.ListModel", "javax.swing.JList")
override fun processConstructorInvocation(
sourceNode: UElement, instantiatedClass: PsiClass, constructor: PsiMethod?, subclassDeclaration: UClass?
sourceNode: UElement, instantiatedClass: PsiClass, constructor: PsiMethod?, subclassDeclaration: UClass?,
) {
constructor ?: return
val sourcePsi = sourceNode.sourcePsi ?: return
val module = ModuleUtilCore.findModuleForPsiElement(sourcePsi) ?: return
val languageLevel = getEffectiveLanguageLevel(module)
val lastIncompatibleLevel = LanguageLevelUtil.getLastIncompatibleLanguageLevel(constructor, languageLevel) ?: return
val lastIncompatibleLevel = JdkApiCompatabilityCache.getInstance().firstCompatibleLanguageLevel(constructor, languageLevel)
?: return
if (shouldReportSinceLevelForElement(lastIncompatibleLevel, sourcePsi) == true) return
registerError(sourcePsi, lastIncompatibleLevel, holder, isOnTheFly)
}
@@ -178,7 +184,7 @@ class JavaApiUsageInspection : AbstractBaseUastLocalInspectionTool() {
languageLevel = sourcePsi.containingFile.getUserData(PsiUtil.FILE_LANGUAGE_LEVEL_KEY)
}
if (languageLevel == null) return
val lastIncompatibleLevel = LanguageLevelUtil.getLastIncompatibleLanguageLevel(target, languageLevel)
val lastIncompatibleLevel = JdkApiCompatabilityCache.getInstance().firstCompatibleLanguageLevel(target, languageLevel)
if (lastIncompatibleLevel != null) {
if (shouldReportSinceLevelForElement(lastIncompatibleLevel, sourcePsi) == true) return
val psiClass = if (qualifier != null) {
@@ -200,7 +206,7 @@ class JavaApiUsageInspection : AbstractBaseUastLocalInspectionTool() {
if (InheritanceUtil.isInheritor(target, generifiedClass) && !isRawInheritance(generifiedClass, target, mutableSetOf())) {
val message = JvmAnalysisBundle.message(
"jvm.inspections.1.7.problem.descriptor",
LanguageLevelUtil.getJdkName(languageLevel)
languageLevel.toJavaVersion().feature
)
holder.registerProblem(sourcePsi, message)
break
@@ -239,7 +245,7 @@ class JavaApiUsageInspection : AbstractBaseUastLocalInspectionTool() {
}
if (reference.getUastParentOfType<UComment>() != null) return
val message = JvmAnalysisBundle.message(
"jvm.inspections.1.5.problem.descriptor", LanguageLevelUtil.getShortMessage(sinceLanguageLevel)
"jvm.inspections.1.5.problem.descriptor", sinceLanguageLevel.toJavaVersion().toFeatureString()
)
val fix = if (isOnTheFly) {
QuickFixFactory.getInstance().createIncreaseLanguageLevelFix(targetLanguageLevel) as LocalQuickFix
@@ -248,8 +254,7 @@ class JavaApiUsageInspection : AbstractBaseUastLocalInspectionTool() {
holder.registerProblem(reference, message, *LocalQuickFix.notNullElements(fix))
}
fun getEffectiveLanguageLevel(module: Module): LanguageLevel {
private fun getEffectiveLanguageLevel(module: Module): LanguageLevel {
return effectiveLanguageLevel ?: LanguageLevelUtil.getEffectiveLanguageLevel(module)
}
}
@@ -5,7 +5,7 @@ import com.intellij.jvm.analysis.internal.testFramework.JavaApiUsageGenerator.Co
import com.intellij.jvm.analysis.internal.testFramework.JavaApiUsageGenerator.Companion.LANGUAGE_LEVEL
import com.intellij.jvm.analysis.internal.testFramework.JavaApiUsageGenerator.Companion.PREVIEW_JDK_HOME
import com.intellij.jvm.analysis.internal.testFramework.JavaApiUsageGenerator.Companion.SINCE_VERSION
import com.intellij.openapi.module.LanguageLevelUtil
import com.intellij.openapi.module.JdkApiCompatabilityCache
import com.intellij.openapi.projectRoots.JavaSdk
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ContentIterator
@@ -173,7 +173,7 @@ class JavaApiUsageGenerator : LightJavaCodeInsightFixtureTestCase() {
member.hasAnnotation(JavaPreviewFeatureUtil.JDK_INTERNAL_JAVAC_PREVIEW_FEATURE)
}
.filter { member -> getLanguageLevel(member) == LANGUAGE_LEVEL }
.mapNotNull { LanguageLevelUtil.getSignature(it) }
.mapNotNull { JdkApiCompatabilityCache.getInstance().getSignature(it) }
)
return true
}
@@ -195,7 +195,7 @@ class JavaApiUsageGenerator : LightJavaCodeInsightFixtureTestCase() {
override fun visitElement(element: PsiElement) {
super.visitElement(element)
if (element is PsiMember && element.isPublicApi()) {
val signature = LanguageLevelUtil.getSignature(element) ?: return
val signature = JdkApiCompatabilityCache.getInstance().getSignature(element) ?: return
val className = signature.substringBefore("#")
if (JavaPsiFacade.getInstance(project).findClass(className, GlobalSearchScope.allScope(project)) == null) {
return // If the class is not in all scope, don't generate
@@ -1,6 +1,7 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.uiDesigner.inspections;
import com.intellij.openapi.module.JdkApiCompatabilityCache;
import com.intellij.openapi.module.LanguageLevelUtil;
import com.intellij.openapi.module.Module;
import com.intellij.pom.java.LanguageLevel;
@@ -43,8 +44,8 @@ public final class Java15FormInspection extends BaseFormInspection {
final PsiMethod getter = PropertyUtilBase.findPropertyGetter(aClass, prop.getName(), false, true);
if (getter == null) continue;
final LanguageLevel languageLevel = LanguageLevelUtil.getEffectiveLanguageLevel(module);
if (LanguageLevelUtil.getLastIncompatibleLanguageLevel(getter, languageLevel) != null) {
registerError(component, collector, prop, "@since " + LanguageLevelUtil.getJdkName(languageLevel));
if (JdkApiCompatabilityCache.getInstance().firstCompatibleLanguageLevel(getter, languageLevel) != null) {
registerError(component, collector, prop, "@since " + languageLevel.toJavaVersion().toFeatureString());
}
}
}