From e4f3444eed42e638f0dc1742684b1c4e179588b8 Mon Sep 17 00:00:00 2001 From: Bart van Helvert Date: Sat, 12 Apr 2025 18:20:21 +0200 Subject: [PATCH] [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 --- .../module/JdkApiCompatabilityCache.java | 155 +++++++++++++++++ .../openapi/module/LanguageLevelUtil.java | 162 +++++------------- .../CharsetObjectCanBeUsedInspection.java | 6 +- .../FunctionalInterfaceSuggester.java | 6 +- .../completion/JavaCompletionUtil.java | 6 +- .../MissingOverrideAnnotationInspection.java | 5 +- ...ImplicitDefaultCharsetUsageInspection.java | 6 +- .../SizeReplaceableByIsEmptyInspection.java | 6 +- .../codeInspection/JavaApiUsageInspection.kt | 39 +++-- .../testFramework/JavaApiUsageGenerator.kt | 6 +- .../inspections/Java15FormInspection.java | 7 +- 11 files changed, 243 insertions(+), 161 deletions(-) create mode 100644 java/java-analysis-api/src/com/intellij/openapi/module/JdkApiCompatabilityCache.java diff --git a/java/java-analysis-api/src/com/intellij/openapi/module/JdkApiCompatabilityCache.java b/java/java-analysis-api/src/com/intellij/openapi/module/JdkApiCompatabilityCache.java new file mode 100644 index 000000000000..4844595fa37a --- /dev/null +++ b/java/java-analysis-api/src/com/intellij/openapi/module/JdkApiCompatabilityCache.java @@ -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> 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. + *

+ * Examples: + *

+ * + * @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 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 getIntroducedApis(@NotNull LanguageLevel languageLevel) { + String featureString = languageLevel.toJavaVersion().toFeatureString(); + List 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. + *

+ * 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; + } +} diff --git a/java/java-analysis-api/src/com/intellij/openapi/module/LanguageLevelUtil.java b/java/java-analysis-api/src/com/intellij/openapi/module/LanguageLevelUtil.java index 4ce154b49ce8..8c18159fbb77 100644 --- a/java/java-analysis-api/src/com/intellij/openapi/module/LanguageLevelUtil.java +++ b/java/java-analysis-api/src/com/intellij/openapi/module/LanguageLevelUtil.java @@ -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>> 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 levels = LanguageLevel.getEntries(); + for (LanguageLevel level : levels.subList(api.ordinal() + 1, levels.size())) { if (!level.isPreview()) return level; } return null; } - private static final Map 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 getForbiddenApi(@NotNull LanguageLevel languageLevel) { - String message = getShortMessage(languageLevel); - if (message == null) return null; - Reference> ref = ourForbiddenAPI.get(languageLevel); - Set 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 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 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); } - } diff --git a/java/java-impl-inspections/src/com/intellij/codeInspection/CharsetObjectCanBeUsedInspection.java b/java/java-impl-inspections/src/com/intellij/codeInspection/CharsetObjectCanBeUsedInspection.java index 289a79776de1..a6d7b86e9fc6 100644 --- a/java/java-impl-inspections/src/com/intellij/codeInspection/CharsetObjectCanBeUsedInspection.java +++ b/java/java-impl-inspections/src/com/intellij/codeInspection/CharsetObjectCanBeUsedInspection.java @@ -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); diff --git a/java/java-impl/src/com/intellij/codeInsight/FunctionalInterfaceSuggester.java b/java/java-impl/src/com/intellij/codeInsight/FunctionalInterfaceSuggester.java index 91bb6eadfec6..4e3811e3cd39 100644 --- a/java/java-impl/src/com/intellij/codeInsight/FunctionalInterfaceSuggester.java +++ b/java/java-impl/src/com/intellij/codeInsight/FunctionalInterfaceSuggester.java @@ -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; } diff --git a/java/java-impl/src/com/intellij/codeInsight/completion/JavaCompletionUtil.java b/java/java-impl/src/com/intellij/codeInsight/completion/JavaCompletionUtil.java index c9196ba041c4..990732fe841c 100644 --- a/java/java-impl/src/com/intellij/codeInsight/completion/JavaCompletionUtil.java +++ b/java/java-impl/src/com/intellij/codeInsight/completion/JavaCompletionUtil.java @@ -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, LookupElementDecorator> generator) { if (object instanceof PsiMember) { - if (LanguageLevelUtil.getLastIncompatibleLanguageLevel((PsiMember)object, myLanguageLevel) != null) { + if (JdkApiCompatabilityCache.getInstance().firstCompatibleLanguageLevel((PsiMember)object, myLanguageLevel) != null) { LookupElementDecorator element = generator.apply(presentation -> presentation.setItemTextForeground(JBColor.RED)); return PrioritizedLookupElement.withExplicitProximity(element, -1); } diff --git a/java/java-impl/src/com/siyeh/ig/inheritance/MissingOverrideAnnotationInspection.java b/java/java-impl/src/com/siyeh/ig/inheritance/MissingOverrideAnnotationInspection.java index caaf83538411..a37b111899ca 100644 --- a/java/java-impl/src/com/siyeh/ig/inheritance/MissingOverrideAnnotationInspection.java +++ b/java/java-impl/src/com/siyeh/ig/inheritance/MissingOverrideAnnotationInspection.java @@ -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; } } diff --git a/java/java-impl/src/com/siyeh/ig/internationalization/ImplicitDefaultCharsetUsageInspection.java b/java/java-impl/src/com/siyeh/ig/internationalization/ImplicitDefaultCharsetUsageInspection.java index 524debf2e258..f63cf20808ed 100644 --- a/java/java-impl/src/com/siyeh/ig/internationalization/ImplicitDefaultCharsetUsageInspection.java +++ b/java/java-impl/src/com/siyeh/ig/internationalization/ImplicitDefaultCharsetUsageInspection.java @@ -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); } diff --git a/java/java-impl/src/com/siyeh/ig/style/SizeReplaceableByIsEmptyInspection.java b/java/java-impl/src/com/siyeh/ig/style/SizeReplaceableByIsEmptyInspection.java index 7601996cfd4f..63abc05406df 100644 --- a/java/java-impl/src/com/siyeh/ig/style/SizeReplaceableByIsEmptyInspection.java +++ b/java/java-impl/src/com/siyeh/ig/style/SizeReplaceableByIsEmptyInspection.java @@ -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); } 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 94276dd3d3e9..e756e73cd376 100644 --- a/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/JavaApiUsageInspection.kt +++ b/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/JavaApiUsageInspection.kt @@ -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 = LanguageLevel.values().map { option(it.name, it.presentableText) } + var levels: List = 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() != 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) } - } \ No newline at end of file diff --git a/jvm/jvm-analysis-internal-testFramework/src/com/intellij/jvm/analysis/internal/testFramework/JavaApiUsageGenerator.kt b/jvm/jvm-analysis-internal-testFramework/src/com/intellij/jvm/analysis/internal/testFramework/JavaApiUsageGenerator.kt index b3fa67daf55b..64336c434a6f 100644 --- a/jvm/jvm-analysis-internal-testFramework/src/com/intellij/jvm/analysis/internal/testFramework/JavaApiUsageGenerator.kt +++ b/jvm/jvm-analysis-internal-testFramework/src/com/intellij/jvm/analysis/internal/testFramework/JavaApiUsageGenerator.kt @@ -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 diff --git a/plugins/ui-designer/src/com/intellij/uiDesigner/inspections/Java15FormInspection.java b/plugins/ui-designer/src/com/intellij/uiDesigner/inspections/Java15FormInspection.java index 34dc2bfa6d4e..50847162a5f8 100644 --- a/plugins/ui-designer/src/com/intellij/uiDesigner/inspections/Java15FormInspection.java +++ b/plugins/ui-designer/src/com/intellij/uiDesigner/inspections/Java15FormInspection.java @@ -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()); } } }