[java-highlighting] checkClassMustBeAbstract, checkDuplicateNestedClass, checkCyclicInheritance -> ClassChecker

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

GitOrigin-RevId: ac39c3d4a1198d9ea32c27fc9d3dfe6eee4ed251
This commit is contained in:
Tagir Valeev
2025-01-09 18:51:18 +00:00
committed by intellij-monorepo-bot
parent 6035671a2a
commit 388f3deac0
10 changed files with 136 additions and 94 deletions
@@ -61,6 +61,8 @@ override.on.non-overriding.method=Method does not override method from its super
class.must.implement.method=Class ''{0}'' must implement abstract method ''{1}'' in ''{2}''
class.must.implement.method.or.abstract=Class ''{0}'' must either be declared abstract or implement abstract method ''{1}'' in ''{2}''
class.must.implement.method.enum.constant=Enum constant ''{0}'' must implement abstract method ''{1}'' in ''{2}''
class.duplicate=Duplicate class: ''{0}''
class.cyclic.inheritance=Cyclic inheritance involving ''{0}''
class.reference.list.duplicate=Duplicate reference to ''{0}'' in ''{1}'' list
class.reference.list.name.expected=Class name expected
class.reference.list.inner.private=''{0}'' has private access in ''{1}''
@@ -3,7 +3,6 @@ package com.intellij.java.codeserver.highlighting;
import com.intellij.codeInsight.ClassUtil;
import com.intellij.java.codeserver.highlighting.errors.JavaErrorKinds;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.util.*;
import com.intellij.util.JavaPsiConstructorUtil;
@@ -23,11 +22,11 @@ final class ClassChecker {
if (parent instanceof PsiAnonymousClass aClass
&& parent.getParent() instanceof PsiNewExpression
&& !PsiUtilCore.hasErrorElementChild(parent.getParent())) {
checkClassWithAbstractMethods(aClass, aClass, ref.getTextRange());
checkClassWithAbstractMethods(aClass, aClass);
}
}
private void checkClassWithAbstractMethods(@NotNull PsiClass aClass, @NotNull PsiMember implementsFixElement, @NotNull TextRange range) {
private void checkClassWithAbstractMethods(@NotNull PsiClass aClass, @NotNull PsiMember implementsFixElement) {
PsiMethod abstractMethod = ClassUtil.getAnyAbstractMethod(aClass);
if (abstractMethod == null) {
return;
@@ -108,6 +107,72 @@ final class ClassChecker {
});
}
void checkClassMustBeAbstract(@NotNull PsiClass aClass) {
boolean mustCheck = aClass.isEnum() ? !hasEnumConstantsWithInitializer(aClass) :
!aClass.hasModifierProperty(PsiModifier.ABSTRACT) && aClass.getRBrace() != null;
if (mustCheck) {
checkClassWithAbstractMethods(aClass, aClass);
}
}
void checkDuplicateNestedClass(PsiClass aClass) {
String name = aClass.getName();
if (name == null) return;
PsiElement parent = aClass.getParent();
boolean checkSiblings;
if (parent instanceof PsiClass psiClass && !PsiUtil.isLocalOrAnonymousClass(psiClass) && !PsiUtil.isLocalOrAnonymousClass(aClass)) {
// optimization: instead of iterating PsiClass children manually we can get'em all from caches
PsiClass innerClass = psiClass.findInnerClassByName(name, false);
if (innerClass != null && innerClass != aClass) {
if (innerClass.getTextOffset() > aClass.getTextOffset()) {
// report duplicate lower in text
PsiClass c = innerClass;
innerClass = aClass;
aClass = c;
}
myVisitor.report(JavaErrorKinds.CLASS_DUPLICATE.create(aClass, innerClass));
return;
}
checkSiblings = false; // there still might be duplicates in parents
}
else {
checkSiblings = true;
}
if (!(parent instanceof PsiDeclarationStatement)) {
parent = aClass;
}
while (parent != null) {
if (parent instanceof PsiFile) break;
PsiElement element = checkSiblings ? parent.getPrevSibling() : null;
if (element == null) {
element = parent.getParent();
// JLS 14.3:
// The name of a local class C may not be redeclared
// as a local class of the directly enclosing method, constructor, or initializer block within the scope of C, or a compile-time
// error occurs. However, a local class declaration may be shadowed (6.3.1)
// anywhere inside a class declaration nested within the local class declaration's scope.
if (element instanceof PsiMethod ||
element instanceof PsiClass ||
element instanceof PsiCodeBlock && element.getParent() instanceof PsiClassInitializer) {
checkSiblings = false;
}
}
parent = element;
if (element instanceof PsiDeclarationStatement) element = PsiTreeUtil.getChildOfType(element, PsiClass.class);
if (element instanceof PsiClass psiClass && name.equals(psiClass.getName())) {
myVisitor.report(JavaErrorKinds.CLASS_DUPLICATE.create(aClass, psiClass));
}
}
}
void checkCyclicInheritance(@NotNull PsiClass aClass) {
PsiClass circularClass = InheritanceUtil.getCircularClass(aClass);
if (circularClass != null) {
myVisitor.report(JavaErrorKinds.CLASS_CYCLIC_INHERITANCE.create(aClass, circularClass));
}
}
/**
* 15.9 Class Instance Creation Expressions | 15.9.2 Determining Enclosing Instances
*/
@@ -89,6 +89,37 @@ final class JavaErrorVisitor extends JavaElementVisitor {
myAnnotationChecker.checkNameValuePair(pair);
}
@Override
public void visitEnumConstantInitializer(@NotNull PsiEnumConstantInitializer enumConstantInitializer) {
super.visitEnumConstantInitializer(enumConstantInitializer);
if (!hasErrorResults()) myClassChecker.checkClassMustBeAbstract(enumConstantInitializer);
}
@Override
public void visitModifierList(@NotNull PsiModifierList list) {
super.visitModifierList(list);
PsiElement parent = list.getParent();
if (parent instanceof PsiMethod method) {
}
else if (parent instanceof PsiClass aClass) {
if (!hasErrorResults()) myClassChecker.checkDuplicateNestedClass(aClass);
if (!hasErrorResults() && !(aClass instanceof PsiAnonymousClass)) {
/* anonymous class is highlighted in HighlightClassUtil.checkAbstractInstantiation()*/
myClassChecker.checkClassMustBeAbstract(aClass);
}
if (!hasErrorResults()) {
//myClassChecker.checkClassDoesNotCallSuperConstructorOrHandleExceptions(aClass, getResolveHelper(getProject()));
}
//if (!hasErrorResults()) add(HighlightMethodUtil.checkOverrideEquivalentInheritedMethods(aClass, myFile, myLanguageLevel));
if (!hasErrorResults()) {
//GenericsHighlightUtil.computeOverrideEquivalentMethodErrors(aClass, myOverrideEquivalentMethodsVisitedClasses, myOverrideEquivalentMethodsErrors);
//myErrorSink.accept(myOverrideEquivalentMethodsErrors.get(aClass));
}
if (!hasErrorResults()) myClassChecker.checkCyclicInheritance(aClass);
}
}
@Override
public void visitReferenceExpression(@NotNull PsiReferenceExpression expression) {
JavaResolveResult resultForIncompleteCode = doVisitReferenceElement(expression);
@@ -10,6 +10,11 @@ public enum JavaErrorHighlightType {
*/
ERROR,
/**
* Error highlighting applied for the whole file
*/
FILE_LEVEL_ERROR,
/**
* Error highlighting for unresolved/unknown reference
*/
@@ -305,6 +305,17 @@ public sealed interface JavaErrorKind<Psi extends PsiElement, Context> {
return new Parameterized<>(myKey, myDescription, anchor, myRange, myHighlightType, myValidator);
}
/**
* Creates a new instance of Parameterized with the specified range function.
*
* @param range a BiFunction that determines the {@link TextRange} for a given Psi object.
* The range is relative to anchor returned from {@link #anchor(PsiElement, Object)}
* @return a new Parameterized instance with the updated range function.
*/
public Parameterized<Psi, Context> withRange(@NotNull BiFunction<? super Psi, ? super Context, ? extends TextRange> range) {
return new Parameterized<>(myKey, myDescription, myAnchor, range, myHighlightType, myValidator);
}
/**
* Creates a new instance of Parameterized with a specified validator function.
*
@@ -202,6 +202,16 @@ public final class JavaErrorKinds {
return message(messageKey, referenceName, formatMethod(abstractMethod),
formatClass(requireNonNull(abstractMethod.getContainingClass()), false));
});
public static final Parameterized<PsiClass, PsiClass> CLASS_DUPLICATE =
error(PsiClass.class, "class.duplicate")
.withAnchor(cls -> requireNonNullElse(cls.getNameIdentifier(), cls))
.withHighlightType(cls -> cls instanceof PsiImplicitClass ? JavaErrorHighlightType.FILE_LEVEL_ERROR : JavaErrorHighlightType.ERROR)
.withRawDescription(cls -> message("class.duplicate", cls.getName()))
.withContext();
public static final Parameterized<PsiClass, PsiClass> CLASS_CYCLIC_INHERITANCE =
error(PsiClass.class, "class.cyclic.inheritance")
.withRange(JavaErrorFormatUtil::getClassDeclarationTextRange).<PsiClass>withContext()
.withRawDescription((aClass, circularClass) -> message("class.cyclic.inheritance", formatClass(circularClass)));
public static final Parameterized<PsiJavaCodeReferenceElement, PsiClass> CLASS_REFERENCE_LIST_DUPLICATE =
parameterized(PsiJavaCodeReferenceElement.class, PsiClass.class, "class.reference.list.duplicate")
.withRawDescription(
@@ -109,16 +109,6 @@ public final class HighlightClassUtil {
return errorResult;
}
static HighlightInfo.Builder checkClassMustBeAbstract(@NotNull PsiClass aClass, @NotNull TextRange textRange) {
if (aClass.isEnum()) {
if (hasEnumConstantsWithInitializer(aClass)) return null;
}
else if (aClass.hasModifierProperty(PsiModifier.ABSTRACT) || aClass.getRBrace() == null) {
return null;
}
return checkClassWithAbstractMethods(aClass, aClass, textRange);
}
static HighlightInfo.Builder checkInstantiationOfAbstractClass(@NotNull PsiClass aClass, @NotNull PsiElement highlightElement) {
HighlightInfo.Builder errorResult = null;
if (aClass.hasModifierProperty(PsiModifier.ABSTRACT) &&
@@ -219,68 +209,6 @@ public final class HighlightClassUtil {
return info;
}
static HighlightInfo.Builder checkDuplicateNestedClass(@NotNull PsiClass aClass) {
String name = aClass.getName();
if (name == null) {
return null;
}
PsiElement parent = aClass.getParent();
boolean checkSiblings;
if (parent instanceof PsiClass psiClass && !PsiUtil.isLocalOrAnonymousClass(psiClass) && !PsiUtil.isLocalOrAnonymousClass(aClass)) {
// optimization: instead of iterating PsiClass children manually we can get'em all from caches
PsiClass innerClass = psiClass.findInnerClassByName(name, false);
if (innerClass != null && innerClass != aClass) {
if (innerClass.getTextOffset() > aClass.getTextOffset()) {
// report duplicate lower in text
PsiClass c = innerClass;innerClass=aClass;aClass=c;
}
HighlightInfo.Builder info = createInfoAndRegisterRenameFix(aClass, name, "duplicate.class");
IntentionAction action = QuickFixFactory.getInstance().createNavigateToDuplicateElementFix(innerClass);
if (info != null) {
info.registerFix(action, null, null, null, null);
}
return info;
}
checkSiblings = false; // there still might be duplicates in parents
}
else {
checkSiblings = true;
}
if (!(parent instanceof PsiDeclarationStatement)) {
parent = aClass;
}
while (parent != null) {
if (parent instanceof PsiFile) break;
PsiElement element = checkSiblings ? parent.getPrevSibling() : null;
if (element == null) {
element = parent.getParent();
// JLS 14.3:
// The name of a local class C may not be redeclared
// as a local class of the directly enclosing method, constructor, or initializer block within the scope of C, or a compile-time
// error occurs. However, a local class declaration may be shadowed (6.3.1)
// anywhere inside a class declaration nested within the local class declaration's scope.
if (element instanceof PsiMethod ||
element instanceof PsiClass ||
element instanceof PsiCodeBlock && element.getParent() instanceof PsiClassInitializer) {
checkSiblings = false;
}
}
parent = element;
if (element instanceof PsiDeclarationStatement) element = PsiTreeUtil.getChildOfType(element, PsiClass.class);
if (element instanceof PsiClass psiClass && name.equals(psiClass.getName())) {
HighlightInfo.Builder info = createInfoAndRegisterRenameFix(aClass, name, "duplicate.class");
IntentionAction action = QuickFixFactory.getInstance().createNavigateToDuplicateElementFix(psiClass);
if (info != null) {
info.registerFix(action, null, null, null, null);
}
return info;
}
}
return null;
}
static HighlightInfo.Builder checkPublicClassInRightFile(@NotNull PsiClass aClass) {
PsiFile containingFile = aClass.getContainingFile();
if (aClass.getParent() != containingFile || !aClass.hasModifierProperty(PsiModifier.PUBLIC) || !(containingFile instanceof PsiJavaFile file))
@@ -14,6 +14,7 @@ import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInsight.intention.QuickFixFactory;
import com.intellij.codeInspection.ex.GlobalInspectionContextBase;
import com.intellij.java.codeserver.highlighting.JavaErrorCollector;
import com.intellij.java.codeserver.highlighting.errors.JavaErrorHighlightType;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.lang.jvm.JvmModifier;
@@ -204,13 +205,17 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh
myJavaModule = JavaFeature.MODULES.isSufficient(myLanguageLevel) ? JavaModuleGraphUtil.findDescriptorByElement(file) : null;
myPreviewFeatureVisitor = myLanguageLevel.isPreview() ? null : new PreviewFeatureUtil.PreviewFeatureVisitor(myLanguageLevel, myErrorSink);
myCollector = new JavaErrorCollector(myFile, error -> {
HighlightInfoType type = switch (error.highlightType()) {
case ERROR -> HighlightInfoType.ERROR;
JavaErrorHighlightType javaHighlightType = error.highlightType();
HighlightInfoType type = switch (javaHighlightType) {
case ERROR, FILE_LEVEL_ERROR -> HighlightInfoType.ERROR;
case WRONG_REF -> HighlightInfoType.WRONG_REF;
};
TextRange range = error.range();
HighlightInfo.Builder info = HighlightInfo.newHighlightInfo(type)
.descriptionAndTooltip(error.description().toString());
if (javaHighlightType == JavaErrorHighlightType.FILE_LEVEL_ERROR) {
info.fileLevelAnnotation();
}
if (range != null) {
info.range(error.anchor(), range);
} else {
@@ -509,15 +514,6 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh
if (!hasErrorResults()) add(HighlightUtil.checkUnhandledExceptions(enumConstant));
}
@Override
public void visitEnumConstantInitializer(@NotNull PsiEnumConstantInitializer enumConstantInitializer) {
super.visitEnumConstantInitializer(enumConstantInitializer);
if (!hasErrorResults()) {
TextRange textRange = HighlightNamesUtil.getClassDeclarationTextRange(enumConstantInitializer);
add(HighlightClassUtil.checkClassMustBeAbstract(enumConstantInitializer, textRange));
}
}
@Override
public void visitExpression(@NotNull PsiExpression expression) {
ProgressManager.checkCanceled(); // visitLiteralExpression is invoked very often in array initializers
@@ -937,11 +933,6 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh
}
else if (parent instanceof PsiClass aClass) {
try {
if (!hasErrorResults()) add(HighlightClassUtil.checkDuplicateNestedClass(aClass));
if (!hasErrorResults() && !(aClass instanceof PsiAnonymousClass)/* anonymous class is highlighted in HighlightClassUtil.checkAbstractInstantiation()*/) {
TextRange textRange = HighlightNamesUtil.getClassDeclarationTextRange(aClass);
add(HighlightClassUtil.checkClassMustBeAbstract(aClass, textRange));
}
if (!hasErrorResults()) {
add(HighlightClassUtil.checkClassDoesNotCallSuperConstructorOrHandleExceptions(aClass, getResolveHelper(getProject())));
}
@@ -950,7 +941,6 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh
GenericsHighlightUtil.computeOverrideEquivalentMethodErrors(aClass, myOverrideEquivalentMethodsVisitedClasses, myOverrideEquivalentMethodsErrors);
myErrorSink.accept(myOverrideEquivalentMethodsErrors.get(aClass));
}
if (!hasErrorResults()) add(HighlightClassUtil.checkCyclicInheritance(aClass));
}
catch (IndexNotReadyException ignored) {
}
@@ -96,6 +96,8 @@ final class JavaErrorFixProvider {
ContainerUtil.map(List.of(PsiModifier.PUBLIC, PsiModifier.PROTECTED),
(@PsiModifier.ModifierConstant String modifier) ->
factory.createModifierListFix(error.context(), modifier, true, false)));
single(CLASS_DUPLICATE, error -> factory.createRenameFix(Objects.requireNonNullElse(error.psi().getNameIdentifier(), error.psi())));
single(CLASS_DUPLICATE, error -> factory.createNavigateToDuplicateElementFix(error.context()));
}
private static void createReceiverParameterFixes(@NotNull QuickFixFactory factory) {
@@ -80,8 +80,6 @@ enum.constant.must.implement.method=Enum constant ''{0}'' must implement abstrac
class.must.implement.method=Class ''{0}'' must implement abstract method ''{1}'' in ''{2}''
abstract.cannot.be.instantiated=''{0}'' is abstract; cannot be instantiated
duplicate.class.in.other.file=Duplicate class found in the file ''{0}''
duplicate.class=Duplicate class: ''{0}''
duplicate.reference.in.list=Duplicate reference to ''{0}'' in ''{1}'' list
public.class.should.be.named.after.file=Class ''{0}'' is public, should be declared in a file named ''{0}.java''
inheritance.from.final.class=Cannot inherit from {1, choice, 1#final class|2#enum|3#record|4#non-abstract value class} ''{0}''
value.class.can.only.inherit=Value classes may only extend abstract value classes or 'java.lang.Object'