[java-highlighting] Refactor unrelated default method analysis; extract JavaPsiMethodUtil

Part of IDEA-365344 Create a new Java error highlighter with minimal dependencies (PSI only)

GitOrigin-RevId: 1e7f4feab6cbaa3d38960a3c0e45aa5dd2def61a
This commit is contained in:
Tagir Valeev
2025-02-20 14:40:20 +01:00
committed by intellij-monorepo-bot
parent c858eedee0
commit 5014925e91
4 changed files with 171 additions and 120 deletions

View File

@@ -0,0 +1,129 @@
// 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.java.codeserver.core;
import com.intellij.openapi.util.Couple;
import com.intellij.psi.*;
import com.intellij.psi.util.MethodSignature;
import com.intellij.psi.util.MethodSignatureUtil;
import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static java.util.Objects.requireNonNull;
/**
* Utilities related to Java methods
*/
public final class JavaPsiMethodUtil {
/**
* @param aClass class to analyze
* @param overrideEquivalentSuperMethods collection of override-equivalent super methods
* @return a couple of unrelated methods from the collection (either both default, or default and abstract),
* so the absence of a method declaration in a current class leads to ambiguous inheritance.
* Returns null if no such a couple is found.
*/
public static @Nullable Couple<@NotNull PsiMethod> getUnrelatedSuperMethods(
@NotNull PsiClass aClass, @NotNull Collection<? extends PsiMethod> overrideEquivalentSuperMethods) {
if (overrideEquivalentSuperMethods.size() <= 1) return null;
List<PsiMethod> defaults = null;
PsiMethod abstractMethod = null;
for (PsiMethod method : overrideEquivalentSuperMethods) {
boolean isDefault = method.hasModifierProperty(PsiModifier.DEFAULT);
boolean isAbstract = method.hasModifierProperty(PsiModifier.ABSTRACT);
boolean isStatic = method.hasModifierProperty(PsiModifier.STATIC);
if (!isDefault && !isAbstract && !isStatic) return null;
if (isDefault) {
if (defaults == null) defaults = new ArrayList<>(2);
defaults.add(method);
}
if (isAbstract && abstractMethod == null) {
abstractMethod = method;
}
}
if (defaults == null) return null;
PsiMethod defaultMethod = defaults.get(0);
PsiClass defaultMethodContainingClass = defaultMethod.getContainingClass();
if (defaultMethodContainingClass == null) return null;
if (abstractMethod == null) {
return findUnrelatedCouple(defaults);
}
PsiClass abstractMethodContainingClass = abstractMethod.getContainingClass();
if (abstractMethodContainingClass == null) return null;
if (aClass.isInterface() || abstractMethodContainingClass.isInterface()) {
Couple<@NotNull PsiMethod> unrelatedCouple = findUnrelatedCouple(defaults);
if (unrelatedCouple != null) {
return unrelatedCouple;
}
if (hasNotOverriddenAbstract(defaults, abstractMethodContainingClass)) {
return Couple.of(defaults.get(0), abstractMethod);
}
}
return null;
}
/**
* @param aClass a class to analyze
* @param overrideEquivalentSuperMethods collection of override-equivalent super methods
* @return an abstract method from the supplied collection that must be implemented, because an override-equivalent
* default method is present, and the ambiguity must be resolved.
*/
public static @Nullable PsiMethod getAbstractMethodToImplementWhenDefaultPresent(
@NotNull PsiClass aClass, @NotNull Collection<? extends PsiMethod> overrideEquivalentSuperMethods) {
if (aClass.hasModifierProperty(PsiModifier.ABSTRACT) || aClass instanceof PsiTypeParameter) return null;
if (overrideEquivalentSuperMethods.size() <= 1) return null;
PsiMethod abstractMethod = null;
PsiMethod defaultMethod = null;
for (PsiMethod method : overrideEquivalentSuperMethods) {
if (method.hasModifierProperty(PsiModifier.DEFAULT)) {
if (defaultMethod == null) defaultMethod = method;
}
else if (method.hasModifierProperty(PsiModifier.ABSTRACT)) {
if (abstractMethod == null) abstractMethod = method;
}
else if (!method.hasModifierProperty(PsiModifier.STATIC)) {
return null;
}
}
if (abstractMethod == null || defaultMethod == null) return null;
PsiClass abstractMethodContainingClass = abstractMethod.getContainingClass();
if (abstractMethodContainingClass == null || !abstractMethodContainingClass.isInterface()) return null;
PsiClass defaultMethodContainingClass = defaultMethod.getContainingClass();
if (defaultMethodContainingClass == null) return null;
if (defaultMethodContainingClass.isInheritor(abstractMethodContainingClass, true)) {
MethodSignature unrelatedMethodSignature = abstractMethod.getSignature(
TypeConversionUtil.getSuperClassSubstitutor(abstractMethodContainingClass, defaultMethodContainingClass, PsiSubstitutor.EMPTY));
if (MethodSignatureUtil.isSubsignature(unrelatedMethodSignature, defaultMethod.getSignature(PsiSubstitutor.EMPTY))) {
return null;
}
}
return abstractMethod;
}
private static @Nullable Couple<@NotNull PsiMethod> findUnrelatedCouple(List<PsiMethod> methods) {
if (methods.size() <= 1) return null;
List<PsiClass> classes = ContainerUtil.map(methods, method -> method.getContainingClass());
ArrayList<PsiMethod> resultMethods = new ArrayList<>(methods);
for (PsiClass aClass1 : classes) {
resultMethods.removeIf(method -> aClass1.isInheritor(requireNonNull(method.getContainingClass()), true));
}
if (resultMethods.size() > 1) {
return Couple.of(resultMethods.get(0), resultMethods.get(1));
}
return null;
}
private static boolean belongToOneHierarchy(@Nullable PsiClass class1, @Nullable PsiClass class2) {
return class1 != null && class2 != null &&
(class1.isInheritor(class2, true) || class2.isInheritor(class1, true));
}
private static boolean hasNotOverriddenAbstract(@NotNull List<PsiMethod> defaults, @NotNull PsiClass abstractMethodContainingClass) {
return !ContainerUtil.exists(defaults, method -> belongToOneHierarchy(method.getContainingClass(), abstractMethodContainingClass));
}
}

View File

@@ -6,113 +6,33 @@ import com.intellij.codeInsight.daemon.impl.HighlightInfo;
import com.intellij.codeInsight.daemon.impl.HighlightInfoType;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInsight.intention.QuickFixFactory;
import com.intellij.ide.IdeBundle;
import com.intellij.java.codeserver.core.JavaPsiMethodUtil;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.psi.*;
import com.intellij.psi.util.MethodSignature;
import com.intellij.psi.util.MethodSignatureUtil;
import com.intellij.psi.util.PsiSuperMethodUtil;
import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import static java.util.Objects.requireNonNull;
public final class GenericsHighlightUtil {
private GenericsHighlightUtil() { }
/**
* @param skipMethodInSelf pass false to check if method in {@code aClass} can be deleted
*
* @return error message if class inherits 2 unrelated default methods or abstract and default methods which do not belong to one hierarchy
*/
public static @Nullable @NlsContexts.DetailedDescription String getUnrelatedDefaultsMessage(@NotNull PsiClass aClass,
@NotNull Collection<? extends PsiMethod> overrideEquivalentSuperMethods,
boolean skipMethodInSelf) {
if (overrideEquivalentSuperMethods.size() <= 1) return null;
boolean isInterface = aClass.isInterface();
List<PsiMethod> defaults = null;
List<PsiMethod> abstracts = null;
boolean hasConcrete = false;
for (PsiMethod method : overrideEquivalentSuperMethods) {
boolean isDefault = method.hasModifierProperty(PsiModifier.DEFAULT);
boolean isAbstract = method.hasModifierProperty(PsiModifier.ABSTRACT);
if (isDefault) {
if (defaults == null) defaults = new ArrayList<>(2);
defaults.add(method);
}
if (isAbstract) {
if (abstracts == null) abstracts = new ArrayList<>(2);
abstracts.add(method);
}
hasConcrete |= !isDefault && !isAbstract && !method.hasModifierProperty(PsiModifier.STATIC);
}
if (!hasConcrete && defaults != null) {
PsiMethod defaultMethod = defaults.get(0);
if (!skipMethodInSelf && MethodSignatureUtil.findMethodBySuperMethod(aClass, defaultMethod, false) != null) return null;
PsiClass defaultMethodContainingClass = defaultMethod.getContainingClass();
if (defaultMethodContainingClass == null) return null;
PsiMethod unrelatedMethod;
if (abstracts != null) {
unrelatedMethod = abstracts.get(0);
}
else if (defaults.size() > 1) {
unrelatedMethod = defaults.get(1);
}
else {
return null;
}
PsiClass unrelatedMethodContainingClass = unrelatedMethod.getContainingClass();
if (unrelatedMethodContainingClass == null) return null;
if (!aClass.hasModifierProperty(PsiModifier.ABSTRACT) && !(aClass instanceof PsiTypeParameter)
&& abstracts != null && unrelatedMethodContainingClass.isInterface()) {
if (defaultMethodContainingClass.isInheritor(unrelatedMethodContainingClass, true) &&
MethodSignatureUtil.isSubsignature(unrelatedMethod.getSignature(TypeConversionUtil.getSuperClassSubstitutor(unrelatedMethodContainingClass, defaultMethodContainingClass, PsiSubstitutor.EMPTY)),
defaultMethod.getSignature(PsiSubstitutor.EMPTY))) {
return null;
}
String key = aClass instanceof PsiEnumConstantInitializer || aClass.isRecord() || aClass.isEnum() ?
"class.must.implement.method" : "class.must.be.abstract";
return JavaErrorBundle.message(key,
HighlightUtil.formatClass(aClass, false),
JavaHighlightUtil.formatMethod(abstracts.get(0)),
HighlightUtil.formatClass(unrelatedMethodContainingClass, false));
}
if (isInterface || abstracts == null || unrelatedMethodContainingClass.isInterface()) {
List<PsiClass> defaultContainingClasses = ContainerUtil.mapNotNull(defaults, PsiMethod::getContainingClass);
String unrelatedDefaults = hasUnrelatedDefaults(defaultContainingClasses);
if (unrelatedDefaults == null &&
(abstracts == null || !hasNotOverriddenAbstract(defaultContainingClasses, unrelatedMethodContainingClass))) {
return null;
}
if (unrelatedDefaults == null) {
return JavaErrorBundle.message("text.class.inherits.abstract.and.default", HighlightUtil.formatClass(aClass),
JavaHighlightUtil.formatMethod(defaultMethod),
HighlightUtil.formatClass(defaultMethodContainingClass),
HighlightUtil.formatClass(unrelatedMethodContainingClass));
}
else {
return JavaErrorBundle.message("text.class.inherits.unrelated.defaults",
HighlightUtil.formatClass(aClass),
JavaHighlightUtil.formatMethod(defaultMethod),
unrelatedDefaults);
}
}
}
return null;
}
static HighlightInfo.Builder checkUnrelatedDefaultMethods(@NotNull PsiClass aClass, @NotNull PsiIdentifier classIdentifier) {
Map<? extends MethodSignature, Set<PsiMethod>> overrideEquivalent = PsiSuperMethodUtil.collectOverrideEquivalents(aClass);
for (Set<PsiMethod> overrideEquivalentMethods : overrideEquivalent.values()) {
String errorMessage = getUnrelatedDefaultsMessage(aClass, overrideEquivalentMethods, false);
if (errorMessage != null) {
String errorMessage = getUnrelatedDefaultsMessage(aClass, overrideEquivalentMethods);
if (errorMessage != null &&
MethodSignatureUtil.findMethodBySuperMethod(aClass, overrideEquivalentMethods.iterator().next(), false) == null) {
HighlightInfo.Builder info = HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR)
.range(classIdentifier)
.descriptionAndTooltip(errorMessage);
@@ -124,29 +44,29 @@ public final class GenericsHighlightUtil {
return null;
}
private static boolean belongToOneHierarchy(@NotNull PsiClass defaultMethodContainingClass, @NotNull PsiClass unrelatedMethodContainingClass) {
return defaultMethodContainingClass.isInheritor(unrelatedMethodContainingClass, true) ||
unrelatedMethodContainingClass.isInheritor(defaultMethodContainingClass, true);
}
private static boolean hasNotOverriddenAbstract(@NotNull List<? extends PsiClass> defaultContainingClasses, @NotNull PsiClass abstractMethodContainingClass) {
return !ContainerUtil.exists(defaultContainingClasses, containingClass -> belongToOneHierarchy(containingClass, abstractMethodContainingClass));
}
private static @Nls String hasUnrelatedDefaults(@NotNull List<? extends PsiClass> defaults) {
if (defaults.size() > 1) {
PsiClass[] defaultClasses = defaults.toArray(PsiClass.EMPTY_ARRAY);
ArrayList<PsiClass> classes = new ArrayList<>(defaults);
for (PsiClass aClass1 : defaultClasses) {
classes.removeIf(aClass2 -> aClass1.isInheritor(aClass2, true));
}
if (classes.size() > 1) {
return IdeBundle.message("x.and.y", HighlightUtil.formatClass(classes.get(0)),
HighlightUtil.formatClass(classes.get(1)));
}
/**
* @return error message if class inherits 2 unrelated default methods or abstract and default methods which do not belong to one hierarchy
*/
public static @Nullable @NlsContexts.DetailedDescription String getUnrelatedDefaultsMessage(
@NotNull PsiClass aClass, @NotNull Collection<? extends PsiMethod> overrideEquivalentSuperMethods) {
PsiMethod abstractMethod = JavaPsiMethodUtil.getAbstractMethodToImplementWhenDefaultPresent(aClass, overrideEquivalentSuperMethods);
if (abstractMethod != null) {
String key = aClass instanceof PsiEnumConstantInitializer || aClass.isRecord() || aClass.isEnum() ?
"class.must.implement.method" : "class.must.be.abstract";
return JavaErrorBundle.message(key,
HighlightUtil.formatClass(aClass, false),
JavaHighlightUtil.formatMethod(abstractMethod),
HighlightUtil.formatClass(requireNonNull(abstractMethod.getContainingClass()), false));
}
return null;
Couple<@NotNull PsiMethod> pair = JavaPsiMethodUtil.getUnrelatedSuperMethods(aClass, overrideEquivalentSuperMethods);
if (pair == null) return null;
String key = pair.getSecond().hasModifierProperty(PsiModifier.ABSTRACT) ?
"text.class.inherits.abstract.and.default" :
"text.class.inherits.unrelated.defaults";
return JavaErrorBundle.message(key, HighlightUtil.formatClass(aClass),
JavaHighlightUtil.formatMethod(pair.getFirst()),
HighlightUtil.formatClass(requireNonNull(pair.getFirst().getContainingClass())),
HighlightUtil.formatClass(requireNonNull(pair.getSecond().getContainingClass())));
}
}

View File

@@ -1,8 +1,8 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInspection.reference;
import com.intellij.codeInsight.daemon.impl.analysis.GenericsHighlightUtil;
import com.intellij.java.analysis.JavaAnalysisBundle;
import com.intellij.java.codeserver.core.JavaPsiMethodUtil;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
@@ -18,10 +18,7 @@ import org.jetbrains.annotations.Nullable;
import org.jetbrains.uast.*;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.*;
public final class RefJavaUtilImpl extends RefJavaUtil {
private static final Logger LOG = Logger.getInstance(RefJavaUtilImpl.class);
@@ -660,8 +657,7 @@ public final class RefJavaUtilImpl extends RefJavaUtil {
}
}
PsiClass aClass = javaMethod.getContainingClass();
if (aClass == null ||
GenericsHighlightUtil.getUnrelatedDefaultsMessage(aClass, Arrays.asList(superMethods), true) != null) {
if (aClass == null || hasUnrelatedDefaults(aClass, Arrays.asList(superMethods))) {
return false;
}
}
@@ -793,4 +789,10 @@ public final class RefJavaUtilImpl extends RefJavaUtil {
}
return element;
}
private static boolean hasUnrelatedDefaults(@NotNull PsiClass aClass,
@NotNull Collection<? extends PsiMethod> overrideEquivalentSuperMethods) {
return JavaPsiMethodUtil.getAbstractMethodToImplementWhenDefaultPresent(aClass, overrideEquivalentSuperMethods) != null ||
JavaPsiMethodUtil.getUnrelatedSuperMethods(aClass, overrideEquivalentSuperMethods) != null;
}
}

View File

@@ -187,7 +187,7 @@ record.compact.constructor=Compact constructor
annotation.on.static.member.qualifying.type.family.name=Move type annotation
create.class.action.this.not.valid.java.qualified.name=This is not a valid Java qualified name
text.class.inherits.abstract.and.default={0} inherits abstract and default for {1} from types {2} and {3}
text.class.inherits.unrelated.defaults={0} inherits unrelated defaults for {1} from types {2}
text.class.inherits.unrelated.defaults={0} inherits unrelated defaults for {1} from types {2} and {3}
text.class.is.not.accessible={0} is not accessible in current context
text.class.cannot.access=Cannot access {0}
remove.unused.imports.quickfix.text=Remove unused imports