From ac754128caee22939a9c5b76a800616af0b99c5e Mon Sep 17 00:00:00 2001 From: Tagir Valeev Date: Mon, 20 Jan 2025 15:48:22 +0100 Subject: [PATCH] [java-highlighting] modifier checks -> ModifierChecker Part of IDEA-365344 Create a new Java error highlighter with minimal dependencies (PSI only) GitOrigin-RevId: 43e782e0950b1a0e2a05de63186ea1d97a786865 --- .../JavaCompilationErrorBundle.properties | 6 + .../highlighting/JavaErrorVisitor.java | 6 + .../highlighting/ModifierChecker.java | 182 +++++++++++ .../highlighting/errors/JavaErrorKinds.java | 15 + .../impl/analysis/HighlightClassUtil.java | 2 +- .../daemon/impl/analysis/HighlightUtil.java | 305 ------------------ .../impl/analysis/HighlightVisitorImpl.java | 2 - .../impl/analysis/JavaErrorFixProvider.java | 25 +- .../FinalPrivateMethodInspection.java | 4 +- .../FinalStaticMethodInspection.java | 4 +- .../psi/util/JavaPsiModifierUtil.java | 177 ++++++++++ .../SealedTypesBasics.java | 2 +- .../valueClass/ValueClass.java | 2 +- 13 files changed, 417 insertions(+), 315 deletions(-) create mode 100644 java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/ModifierChecker.java create mode 100644 java/java-psi-impl/src/com/intellij/psi/util/JavaPsiModifierUtil.java diff --git a/java/codeserver/highlighting/resources/messages/JavaCompilationErrorBundle.properties b/java/codeserver/highlighting/resources/messages/JavaCompilationErrorBundle.properties index e7184773e6dd..d70cd9882256 100644 --- a/java/codeserver/highlighting/resources/messages/JavaCompilationErrorBundle.properties +++ b/java/codeserver/highlighting/resources/messages/JavaCompilationErrorBundle.properties @@ -228,3 +228,9 @@ array.type.expected=Array type expected; found: ''{0}'' pattern.type.pattern.expected=Type pattern expected expression.expected=Expression expected + +modifier.not.allowed=Modifier ''{0}'' not allowed here +modifier.not.allowed.local.class=Modifier ''{0}'' not allowed on local classes +modifier.not.allowed.non.sealed=Modifier 'non-sealed' is not allowed on classes that do not have a sealed superclass +modifier.repeated=Repeated modifier ''{0}'' +modifier.incompatible=Illegal combination of modifiers ''{0}'' and ''{1}'' diff --git a/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/JavaErrorVisitor.java b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/JavaErrorVisitor.java index bf5c0f664913..858f9c4ac08d 100644 --- a/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/JavaErrorVisitor.java +++ b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/JavaErrorVisitor.java @@ -45,6 +45,7 @@ final class JavaErrorVisitor extends JavaElementVisitor { private final @NotNull GenericsChecker myGenericsChecker = new GenericsChecker(this); final @NotNull MethodChecker myMethodChecker = new MethodChecker(this); private final @NotNull ReceiverChecker myReceiverChecker = new ReceiverChecker(this); + private final @NotNull ModifierChecker myModifierChecker = new ModifierChecker(this); final @NotNull ExpressionChecker myExpressionChecker = new ExpressionChecker(this); private final @NotNull StatementChecker myStatementChecker = new StatementChecker(this); private final @NotNull LiteralChecker myLiteralChecker = new LiteralChecker(this); @@ -364,6 +365,11 @@ final class JavaErrorVisitor extends JavaElementVisitor { super.visitKeyword(keyword); if (!hasErrorResults()) myClassChecker.checkStaticDeclarationInInnerClass(keyword); if (!hasErrorResults()) myExpressionChecker.checkIllegalVoidType(keyword); + PsiElement parent = keyword.getParent(); + if (parent instanceof PsiModifierList psiModifierList) { + if (!hasErrorResults()) myModifierChecker.checkNotAllowedModifier(keyword, psiModifierList); + if (!hasErrorResults()) myModifierChecker.checkIllegalModifierCombination(keyword, psiModifierList); + } } @Override diff --git a/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/ModifierChecker.java b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/ModifierChecker.java new file mode 100644 index 000000000000..3c5def9d9fbe --- /dev/null +++ b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/ModifierChecker.java @@ -0,0 +1,182 @@ +// 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.highlighting; + +import com.intellij.java.codeserver.highlighting.errors.JavaErrorKinds; +import com.intellij.pom.java.JavaFeature; +import com.intellij.pom.java.LanguageLevel; +import com.intellij.psi.*; +import com.intellij.psi.util.FileTypeUtils; +import com.intellij.psi.util.JavaPsiModifierUtil; +import com.intellij.psi.util.JavaPsiRecordUtil; +import com.intellij.psi.util.PsiUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.Set; + +final class ModifierChecker { + private static final Set ourConstructorNotAllowedModifiers = + Set.of(PsiModifier.ABSTRACT, PsiModifier.STATIC, PsiModifier.NATIVE, PsiModifier.FINAL, PsiModifier.STRICTFP, PsiModifier.SYNCHRONIZED); + + private final @NotNull JavaErrorVisitor myVisitor; + + ModifierChecker(@NotNull JavaErrorVisitor visitor) { myVisitor = visitor; } + + void checkNotAllowedModifier(@NotNull PsiKeyword keyword, @NotNull PsiModifierList modifierList) { + PsiElement modifierOwner = modifierList.getParent(); + if (modifierOwner == null) return; + @SuppressWarnings("MagicConstant") + @PsiModifier.ModifierConstant String modifier = keyword.getText(); + + PsiElement modifierOwnerParent = + modifierOwner instanceof PsiMember psiMember ? psiMember.getContainingClass() : modifierOwner.getParent(); + if (modifierOwnerParent == null) modifierOwnerParent = modifierOwner.getParent(); + boolean isAllowed = true; + String message = null; + if (modifierOwner instanceof PsiClass aClass) { + boolean privateOrProtected = PsiModifier.PRIVATE.equals(modifier) || PsiModifier.PROTECTED.equals(modifier); + if (aClass.isInterface()) { + if (PsiModifier.STATIC.equals(modifier) || privateOrProtected || PsiModifier.PACKAGE_LOCAL.equals(modifier)) { + isAllowed = modifierOwnerParent instanceof PsiClass; + } + if (PsiModifier.PUBLIC.equals(modifier)) { + isAllowed = !(modifierOwnerParent instanceof PsiDeclarationStatement); + } + if (PsiModifier.SEALED.equals(modifier)) { + isAllowed = !aClass.isAnnotationType(); + } + } + else { + if (PsiModifier.PUBLIC.equals(modifier)) { + isAllowed = modifierOwnerParent instanceof PsiImportHolder || + // PsiJavaFile or JavaDummyHolder + modifierOwnerParent instanceof PsiClass psiClass && + (modifierOwnerParent instanceof PsiSyntheticClass || + PsiUtil.isAvailable(JavaFeature.INNER_STATICS, modifierOwnerParent) || + psiClass.getQualifiedName() != null || + !modifierOwnerParent.isPhysical()); + } + else { + if (PsiModifier.STATIC.equals(modifier) || privateOrProtected || PsiModifier.PACKAGE_LOCAL.equals(modifier)) { + isAllowed = modifierOwnerParent instanceof PsiClass psiClass && + (PsiModifier.STATIC.equals(modifier) || + PsiUtil.isAvailable(JavaFeature.INNER_STATICS, modifierOwnerParent) || + psiClass.getQualifiedName() != null) || + FileTypeUtils.isInServerPageFile(modifierOwnerParent) || + // non-physical dummy holder might not have FQN + !modifierOwnerParent.isPhysical(); + } + } + + if (aClass.isEnum()) { + isAllowed &= + !PsiModifier.FINAL.equals(modifier) && !PsiModifier.ABSTRACT.equals(modifier) && !PsiModifier.SEALED.equals(modifier) + && !PsiModifier.VALUE.equals(modifier); + } + else if (aClass.isRecord()) { + isAllowed &= !PsiModifier.ABSTRACT.equals(modifier); + } + + if (aClass.getContainingClass() instanceof PsiAnonymousClass && + privateOrProtected && !PsiUtil.getLanguageLevel(modifierOwnerParent).isAtLeast(LanguageLevel.JDK_16)) { + isAllowed = false; + } + } + if ((PsiModifier.NON_SEALED.equals(modifier) || PsiModifier.SEALED.equals(modifier)) && + modifierOwnerParent instanceof PsiDeclarationStatement) { + // JLS 14.3 + myVisitor.report(JavaErrorKinds.MODIFIER_NOT_ALLOWED_LOCAL_CLASS.create(keyword, modifier)); + return; + } + else if (PsiModifier.NON_SEALED.equals(modifier) && !aClass.hasModifierProperty(PsiModifier.SEALED)) { + isAllowed = Arrays.stream(aClass.getSuperTypes()) + .map(PsiClassType::resolve) + .anyMatch(superClass -> superClass != null && superClass.hasModifierProperty(PsiModifier.SEALED)); + if (!isAllowed) { + myVisitor.report(JavaErrorKinds.MODIFIER_NOT_ALLOWED_NON_SEALED.create(keyword, modifier)); + return; + } + } + } + else if (modifierOwner instanceof PsiMethod method) { + isAllowed = !(method.isConstructor() && ourConstructorNotAllowedModifiers.contains(modifier)); + PsiClass containingClass = method.getContainingClass(); + if ((method.hasModifierProperty(PsiModifier.PUBLIC) || method.hasModifierProperty(PsiModifier.PROTECTED)) && method.isConstructor() && + containingClass != null && containingClass.isEnum()) { + isAllowed = false; + } + + boolean isInterface = modifierOwnerParent instanceof PsiClass psiClass && psiClass.isInterface(); + if (PsiModifier.PRIVATE.equals(modifier) && modifierOwnerParent instanceof PsiClass psiClass) { + isAllowed &= !isInterface || PsiUtil.isAvailable(JavaFeature.PRIVATE_INTERFACE_METHODS, modifierOwner) && !psiClass.isAnnotationType(); + } + else if (PsiModifier.STRICTFP.equals(modifier)) { + isAllowed &= !isInterface || PsiUtil.isAvailable(JavaFeature.EXTENSION_METHODS, modifierOwner); + } + else if (PsiModifier.PROTECTED.equals(modifier) || + PsiModifier.TRANSIENT.equals(modifier) || + PsiModifier.FINAL.equals(modifier)) { + isAllowed &= !isInterface; + } + else if (PsiModifier.SYNCHRONIZED.equals(modifier)) { + isAllowed &= !isInterface && (containingClass == null || !containingClass.isValueClass()); + } + + if (containingClass != null && (containingClass.isInterface() || containingClass.isRecord())) { + isAllowed &= !PsiModifier.NATIVE.equals(modifier); + } + + if (containingClass != null && containingClass.isAnnotationType()) { + isAllowed &= !PsiModifier.STATIC.equals(modifier); + isAllowed &= !PsiModifier.DEFAULT.equals(modifier); + } + + if (JavaPsiRecordUtil.getRecordComponentForAccessor(method) != null) { + isAllowed &= !PsiModifier.STATIC.equals(modifier); + } + } + else if (modifierOwner instanceof PsiField) { + if (PsiModifier.PRIVATE.equals(modifier) || PsiModifier.PROTECTED.equals(modifier) || PsiModifier.TRANSIENT.equals(modifier) || + PsiModifier.STRICTFP.equals(modifier)) { + isAllowed = modifierOwnerParent instanceof PsiClass psiClass && !psiClass.isInterface(); + } + } + else if (modifierOwner instanceof PsiClassInitializer) { + isAllowed = PsiModifier.STATIC.equals(modifier); + } + else if (modifierOwner instanceof PsiLocalVariable || modifierOwner instanceof PsiParameter) { + isAllowed = PsiModifier.FINAL.equals(modifier); + } + else if (modifierOwner instanceof PsiReceiverParameter || modifierOwner instanceof PsiRecordComponent) { + isAllowed = false; + } + + if (isAllowed && !JavaPsiModifierUtil.isAllowed(modifier, modifierList)) { + isAllowed = false; + } + if (!isAllowed) { + myVisitor.report(JavaErrorKinds.MODIFIER_NOT_ALLOWED.create(keyword, modifier)); + } + } + + + void checkIllegalModifierCombination(@NotNull PsiKeyword keyword, @NotNull PsiModifierList modifierList) { + @SuppressWarnings("MagicConstant") @PsiModifier.ModifierConstant String modifier = keyword.getText(); + String incompatible = JavaPsiModifierUtil.getIncompatibleModifier(modifier, modifierList); + if (incompatible != null) { + if (incompatible.equals(modifier)) { + for (PsiElement child = modifierList.getFirstChild(); child != null; child = child.getNextSibling()) { + if (modifier.equals(child.getText())) { + if (child == keyword) return; + else break; + } + } + myVisitor.report(JavaErrorKinds.MODIFIER_REPEATED.create(keyword, modifier)); + } + else { + myVisitor.report(JavaErrorKinds.MODIFIER_INCOMPATIBLE.create(keyword, incompatible)); + } + } + } + +} diff --git a/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/JavaErrorKinds.java b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/JavaErrorKinds.java index 6aabffbc80b4..e239f5ae1b5c 100644 --- a/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/JavaErrorKinds.java +++ b/java/codeserver/highlighting/src/com/intellij/java/codeserver/highlighting/errors/JavaErrorKinds.java @@ -614,6 +614,21 @@ public final class JavaErrorKinds { public static final Simple LITERAL_TEXT_BLOCK_NO_NEW_LINE = error(PsiLiteralValue.class, "literal.text.block.no.new.line").withRange(e -> TextRange.create(0, 3)); + public static final Parameterized MODIFIER_NOT_ALLOWED = + parameterized(PsiKeyword.class, String.class, "modifier.not.allowed") + .withRawDescription((keyword, text) -> message("modifier.not.allowed", text)); + public static final Parameterized MODIFIER_REPEATED = + parameterized(PsiKeyword.class, String.class, "modifier.repeated") + .withRawDescription((keyword, text) -> message("modifier.repeated", text)); + public static final Parameterized MODIFIER_INCOMPATIBLE = + parameterized(PsiKeyword.class, String.class, "modifier.incompatible") + .withRawDescription((keyword, text) -> message("modifier.incompatible", keyword.getText(), text)); + public static final Parameterized MODIFIER_NOT_ALLOWED_LOCAL_CLASS = + parameterized(PsiKeyword.class, String.class, "modifier.not.allowed.local.class") + .withRawDescription((keyword, text) -> message("modifier.not.allowed.local.class", text)); + public static final Parameterized MODIFIER_NOT_ALLOWED_NON_SEALED = + parameterized(PsiKeyword.class, String.class, "modifier.not.allowed.non.sealed"); + public static final Simple CALL_SUPER_ENUM_CONSTRUCTOR = error("call.super.enum.constructor"); public static final Parameterized CALL_SUPER_QUALIFIER_NOT_INNER_CLASS = parameterized(PsiExpression.class, PsiClass.class, "call.super.qualifier.not.inner.class") diff --git a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightClassUtil.java b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightClassUtil.java index 2077cde974f2..c508f5bab053 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightClassUtil.java +++ b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightClassUtil.java @@ -36,7 +36,7 @@ public final class HighlightClassUtil { if (result == null || aClass == null) return; if (aClass.getContainingClass() != null) { PsiModifierList modifierList = aClass.getModifierList(); - if (modifierList != null && HighlightUtil.getIncompatibleModifier(PsiModifier.STATIC, modifierList) == null) { + if (modifierList != null && JavaPsiModifierUtil.getIncompatibleModifier(PsiModifier.STATIC, modifierList) == null) { IntentionAction action = QuickFixFactory.getInstance().createModifierListFix(aClass, PsiModifier.STATIC, true, false); result.registerFix(action, null, null, null, null); } diff --git a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightUtil.java b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightUtil.java index 80fdbff109af..2515deada270 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightUtil.java +++ b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightUtil.java @@ -80,61 +80,6 @@ import static com.intellij.util.ObjectUtils.tryCast; public final class HighlightUtil { private static final Logger LOG = Logger.getInstance(HighlightUtil.class); - private static final Map> ourInterfaceIncompatibleModifiers = Map.of( - PsiModifier.ABSTRACT, Set.of(), - PsiModifier.PACKAGE_LOCAL, Set.of(PsiModifier.PRIVATE, PsiModifier.PUBLIC, PsiModifier.PROTECTED), - PsiModifier.PRIVATE, Set.of(PsiModifier.PACKAGE_LOCAL, PsiModifier.PUBLIC, PsiModifier.PROTECTED), - PsiModifier.PUBLIC, Set.of(PsiModifier.PACKAGE_LOCAL, PsiModifier.PRIVATE, PsiModifier.PROTECTED), - PsiModifier.PROTECTED, Set.of(PsiModifier.PACKAGE_LOCAL, PsiModifier.PUBLIC, PsiModifier.PRIVATE), - PsiModifier.STRICTFP, Set.of(), - PsiModifier.STATIC, Set.of(), - PsiModifier.SEALED, Set.of(PsiModifier.NON_SEALED), - PsiModifier.NON_SEALED, Set.of(PsiModifier.SEALED)); - private static final Map> ourMethodIncompatibleModifiers = Map.ofEntries( - Map.entry(PsiModifier.ABSTRACT, Set.of( - PsiModifier.NATIVE, PsiModifier.STATIC, PsiModifier.FINAL, PsiModifier.PRIVATE, PsiModifier.STRICTFP, PsiModifier.SYNCHRONIZED, - PsiModifier.DEFAULT)), - Map.entry(PsiModifier.NATIVE, Set.of(PsiModifier.ABSTRACT, PsiModifier.STRICTFP)), - Map.entry(PsiModifier.PACKAGE_LOCAL, Set.of(PsiModifier.PRIVATE, PsiModifier.PUBLIC, PsiModifier.PROTECTED)), - Map.entry(PsiModifier.PRIVATE, Set.of(PsiModifier.PACKAGE_LOCAL, PsiModifier.PUBLIC, PsiModifier.PROTECTED)), - Map.entry(PsiModifier.PUBLIC, Set.of(PsiModifier.PACKAGE_LOCAL, PsiModifier.PRIVATE, PsiModifier.PROTECTED)), - Map.entry(PsiModifier.PROTECTED, Set.of(PsiModifier.PACKAGE_LOCAL, PsiModifier.PUBLIC, PsiModifier.PRIVATE)), - Map.entry(PsiModifier.STATIC, Set.of(PsiModifier.ABSTRACT, PsiModifier.DEFAULT)), - Map.entry(PsiModifier.DEFAULT, Set.of(PsiModifier.ABSTRACT, PsiModifier.STATIC, PsiModifier.PRIVATE)), - Map.entry(PsiModifier.SYNCHRONIZED, Set.of(PsiModifier.ABSTRACT)), - Map.entry(PsiModifier.STRICTFP, Set.of(PsiModifier.ABSTRACT)), - Map.entry(PsiModifier.FINAL, Set.of(PsiModifier.ABSTRACT))); - private static final Map> ourFieldIncompatibleModifiers = Map.of( - PsiModifier.FINAL, Set.of(PsiModifier.VOLATILE), - PsiModifier.PACKAGE_LOCAL, Set.of(PsiModifier.PRIVATE, PsiModifier.PUBLIC, PsiModifier.PROTECTED), - PsiModifier.PRIVATE, Set.of(PsiModifier.PACKAGE_LOCAL, PsiModifier.PUBLIC, PsiModifier.PROTECTED), - PsiModifier.PUBLIC, Set.of(PsiModifier.PACKAGE_LOCAL, PsiModifier.PRIVATE, PsiModifier.PROTECTED), - PsiModifier.PROTECTED, Set.of(PsiModifier.PACKAGE_LOCAL, PsiModifier.PUBLIC, PsiModifier.PRIVATE), - PsiModifier.STATIC, Set.of(), - PsiModifier.TRANSIENT, Set.of(), - PsiModifier.VOLATILE, Set.of(PsiModifier.FINAL)); - private static final Map> ourClassIncompatibleModifiers = Map.ofEntries( - Map.entry(PsiModifier.ABSTRACT, Set.of(PsiModifier.FINAL)), - Map.entry(PsiModifier.FINAL, Set.of(PsiModifier.ABSTRACT, PsiModifier.SEALED, PsiModifier.NON_SEALED)), - Map.entry(PsiModifier.PACKAGE_LOCAL, Set.of(PsiModifier.PRIVATE, PsiModifier.PUBLIC, PsiModifier.PROTECTED)), - Map.entry(PsiModifier.PRIVATE, Set.of(PsiModifier.PACKAGE_LOCAL, PsiModifier.PUBLIC, PsiModifier.PROTECTED)), - Map.entry(PsiModifier.PUBLIC, Set.of(PsiModifier.PACKAGE_LOCAL, PsiModifier.PRIVATE, PsiModifier.PROTECTED)), - Map.entry(PsiModifier.PROTECTED, Set.of(PsiModifier.PACKAGE_LOCAL, PsiModifier.PUBLIC, PsiModifier.PRIVATE)), - Map.entry(PsiModifier.STRICTFP, Set.of()), - Map.entry(PsiModifier.STATIC, Set.of()), - Map.entry(PsiModifier.SEALED, Set.of(PsiModifier.FINAL, PsiModifier.NON_SEALED)), - Map.entry(PsiModifier.NON_SEALED, Set.of(PsiModifier.FINAL, PsiModifier.SEALED)), - Map.entry(PsiModifier.VALUE, Set.of()) - ); - private static final Map> ourClassInitializerIncompatibleModifiers = Map.of(PsiModifier.STATIC, Set.of()); - private static final Map> ourModuleIncompatibleModifiers = Map.of(PsiModifier.OPEN, Set.of()); - private static final Map> ourRequiresIncompatibleModifiers = Map.of( - PsiModifier.STATIC, Set.of(), - PsiModifier.TRANSITIVE, Set.of()); - - private static final Set ourConstructorNotAllowedModifiers = - Set.of(PsiModifier.ABSTRACT, PsiModifier.STATIC, PsiModifier.NATIVE, PsiModifier.FINAL, PsiModifier.STRICTFP, PsiModifier.SYNCHRONIZED); - private static final String SERIAL_PERSISTENT_FIELDS_FIELD_NAME = "serialPersistentFields"; private static final @NlsSafe String ANONYMOUS = "anonymous "; @@ -144,50 +89,6 @@ public final class HighlightUtil { return QuickFixFactory.getInstance(); } - private static String getIncompatibleModifier(@NotNull String modifier, - @NotNull PsiModifierList modifierList, - @NotNull Map> incompatibleModifiersHash) { - // modifier is always incompatible with itself - int modifierCount = 0; - for (PsiElement otherModifier = modifierList.getFirstChild(); otherModifier != null; otherModifier = otherModifier.getNextSibling()) { - if (modifier.equals(otherModifier.getText())) modifierCount++; - } - if (modifierCount > 1) return modifier; - - Set incompatibles = incompatibleModifiersHash.get(modifier); - if (incompatibles == null) return null; - PsiElement parent = modifierList.getParent(); - boolean level8OrHigher = PsiUtil.isLanguageLevel8OrHigher(modifierList); - boolean level9OrHigher = PsiUtil.isLanguageLevel9OrHigher(modifierList); - for (@PsiModifier.ModifierConstant String incompatible : incompatibles) { - if (level8OrHigher) { - if (modifier.equals(PsiModifier.STATIC) && incompatible.equals(PsiModifier.ABSTRACT)) { - continue; - } - } - if (parent instanceof PsiMethod psiMethod) { - if (level9OrHigher && modifier.equals(PsiModifier.PRIVATE) && incompatible.equals(PsiModifier.PUBLIC)) { - continue; - } - - if (modifier.equals(PsiModifier.STATIC) && incompatible.equals(PsiModifier.FINAL)) { - PsiClass containingClass = psiMethod.getContainingClass(); - if (containingClass == null || !containingClass.isInterface()) { - continue; - } - } - } - if (modifierList.hasModifierProperty(incompatible)) { - return incompatible; - } - if (PsiModifier.ABSTRACT.equals(incompatible) && modifierList.hasExplicitModifier(incompatible)) { - return incompatible; - } - } - - return null; - } - static void checkInstanceOfApplicable(@NotNull PsiInstanceOfExpression expression, @NotNull Consumer errorSink) { PsiExpression operand = expression.getOperand(); @@ -1064,212 +965,6 @@ public final class HighlightUtil { return null; } - static HighlightInfo.Builder checkIllegalModifierCombination(@NotNull PsiKeyword keyword, @NotNull PsiModifierList modifierList) { - @PsiModifier.ModifierConstant String modifier = keyword.getText(); - String incompatible = getIncompatibleModifier(modifier, modifierList); - if (incompatible != null) { - String message; - if (incompatible.equals(modifier)) { - for (PsiElement child = modifierList.getFirstChild(); child != null; child = child.getNextSibling()) { - if (modifier.equals(child.getText())) { - if (child == keyword) return null; - else break; - } - } - message = JavaErrorBundle.message("repeated.modifier", incompatible); - } - else { - message = JavaErrorBundle.message("incompatible.modifiers", modifier, incompatible); - } - HighlightInfo.Builder highlightInfo = - HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(keyword).descriptionAndTooltip(message); - IntentionAction action = getFixFactory().createModifierListFix(modifierList, modifier, false, false); - highlightInfo.registerFix(action, null, null, null, null); - return highlightInfo; - } - - return null; - } - - /** - * Checks if the supplied modifier list contains incompatible modifiers (e.g. "public private"). - * - * @param modifierList a {@link PsiModifierList} to check - * @return true if the supplied modifier list contains compatible modifiers - */ - public static boolean isLegalModifierCombination(@NotNull PsiModifierList modifierList) { - for (PsiElement child = modifierList.getFirstChild(); child != null; child = child.getNextSibling()) { - if (child instanceof PsiKeyword && getIncompatibleModifier(child.getText(), modifierList) != null) { - return false; - } - } - return true; - } - - private static Map> getIncompatibleModifierMap(@NotNull PsiElement modifierListOwner) { - if (PsiUtilCore.hasErrorElementChild(modifierListOwner)) return null; - if (modifierListOwner instanceof PsiClass psiClass) { - return psiClass.isInterface() ? ourInterfaceIncompatibleModifiers : ourClassIncompatibleModifiers; - } - if (modifierListOwner instanceof PsiMethod) return ourMethodIncompatibleModifiers; - if (modifierListOwner instanceof PsiVariable) return ourFieldIncompatibleModifiers; - if (modifierListOwner instanceof PsiClassInitializer) return ourClassInitializerIncompatibleModifiers; - if (modifierListOwner instanceof PsiJavaModule) return ourModuleIncompatibleModifiers; - if (modifierListOwner instanceof PsiRequiresStatement) return ourRequiresIncompatibleModifiers; - return null; - } - - static @Nullable String getIncompatibleModifier(@NotNull String modifier, @NotNull PsiModifierList modifierList) { - PsiElement parent = modifierList.getParent(); - if (parent == null) return null; - Map> incompatibleModifierMap = getIncompatibleModifierMap(parent); - return incompatibleModifierMap == null ? null : getIncompatibleModifier(modifier, modifierList, incompatibleModifierMap); - } - - static HighlightInfo.Builder checkNotAllowedModifier(@NotNull PsiKeyword keyword, @NotNull PsiModifierList modifierList) { - PsiElement modifierOwner = modifierList.getParent(); - if (modifierOwner == null) return null; - Map> incompatibleModifierMap = getIncompatibleModifierMap(modifierOwner); - if (incompatibleModifierMap == null) return null; - - @PsiModifier.ModifierConstant String modifier = keyword.getText(); - Set incompatibles = incompatibleModifierMap.get(modifier); - PsiElement modifierOwnerParent = - modifierOwner instanceof PsiMember psiMember ? psiMember.getContainingClass() : modifierOwner.getParent(); - if (modifierOwnerParent == null) modifierOwnerParent = modifierOwner.getParent(); - boolean isAllowed = true; - String message = null; - IntentionAction fix = null; - if (modifierOwner instanceof PsiClass aClass) { - boolean privateOrProtected = PsiModifier.PRIVATE.equals(modifier) || PsiModifier.PROTECTED.equals(modifier); - if (aClass.isInterface()) { - if (PsiModifier.STATIC.equals(modifier) || privateOrProtected || PsiModifier.PACKAGE_LOCAL.equals(modifier)) { - isAllowed = modifierOwnerParent instanceof PsiClass; - } - if (PsiModifier.PUBLIC.equals(modifier)) { - isAllowed = !(modifierOwnerParent instanceof PsiDeclarationStatement); - } - if (PsiModifier.SEALED.equals(modifier)) { - isAllowed = !aClass.isAnnotationType(); - } - } - else { - if (PsiModifier.PUBLIC.equals(modifier)) { - isAllowed = modifierOwnerParent instanceof PsiImportHolder || - // PsiJavaFile or JavaDummyHolder - modifierOwnerParent instanceof PsiClass psiClass && - (modifierOwnerParent instanceof PsiSyntheticClass || - PsiUtil.isAvailable(JavaFeature.INNER_STATICS, modifierOwnerParent) || - psiClass.getQualifiedName() != null || - !modifierOwnerParent.isPhysical()); - } - else { - if (PsiModifier.STATIC.equals(modifier) || privateOrProtected || PsiModifier.PACKAGE_LOCAL.equals(modifier)) { - isAllowed = modifierOwnerParent instanceof PsiClass psiClass && - (PsiModifier.STATIC.equals(modifier) || - PsiUtil.isAvailable(JavaFeature.INNER_STATICS, modifierOwnerParent) || - psiClass.getQualifiedName() != null) || - FileTypeUtils.isInServerPageFile(modifierOwnerParent) || - // non-physical dummy holder might not have FQN - !modifierOwnerParent.isPhysical(); - } - if (privateOrProtected && !isAllowed) { - fix = getFixFactory().createChangeModifierFix(); - } - } - - if (aClass.isEnum()) { - isAllowed &= - !PsiModifier.FINAL.equals(modifier) && !PsiModifier.ABSTRACT.equals(modifier) && !PsiModifier.SEALED.equals(modifier) - && !PsiModifier.VALUE.equals(modifier); - } - else if (aClass.isRecord()) { - isAllowed &= !PsiModifier.ABSTRACT.equals(modifier); - } - - if (aClass.getContainingClass() instanceof PsiAnonymousClass && - privateOrProtected && !PsiUtil.getLanguageLevel(modifierOwnerParent).isAtLeast(LanguageLevel.JDK_16)) { - isAllowed = false; - } - } - if ((PsiModifier.NON_SEALED.equals(modifier) || PsiModifier.SEALED.equals(modifier)) && - modifierOwnerParent instanceof PsiDeclarationStatement) { - isAllowed = false; // JLS 14.3 - message = JavaErrorBundle.message("modifier.not.allowed.on.local.classes", modifier); - } - else if (PsiModifier.NON_SEALED.equals(modifier) && !aClass.hasModifierProperty(PsiModifier.SEALED)) { - isAllowed = Arrays.stream(aClass.getSuperTypes()) - .map(PsiClassType::resolve) - .anyMatch(superClass -> superClass != null && superClass.hasModifierProperty(PsiModifier.SEALED)); - message = JavaErrorBundle.message("modifier.not.allowed.on.classes.without.sealed.super"); - } - } - else if (modifierOwner instanceof PsiMethod method) { - isAllowed = !(method.isConstructor() && ourConstructorNotAllowedModifiers.contains(modifier)); - PsiClass containingClass = method.getContainingClass(); - if ((method.hasModifierProperty(PsiModifier.PUBLIC) || method.hasModifierProperty(PsiModifier.PROTECTED)) && method.isConstructor() && - containingClass != null && containingClass.isEnum()) { - isAllowed = false; - } - - boolean isInterface = modifierOwnerParent instanceof PsiClass psiClass && psiClass.isInterface(); - if (PsiModifier.PRIVATE.equals(modifier) && modifierOwnerParent instanceof PsiClass psiClass) { - isAllowed &= !isInterface || PsiUtil.isAvailable(JavaFeature.PRIVATE_INTERFACE_METHODS, modifierOwner) && !psiClass.isAnnotationType(); - } - else if (PsiModifier.STRICTFP.equals(modifier)) { - isAllowed &= !isInterface || PsiUtil.isAvailable(JavaFeature.EXTENSION_METHODS, modifierOwner); - } - else if (PsiModifier.PROTECTED.equals(modifier) || - PsiModifier.TRANSIENT.equals(modifier) || - PsiModifier.FINAL.equals(modifier)) { - isAllowed &= !isInterface; - } - else if (PsiModifier.SYNCHRONIZED.equals(modifier)) { - isAllowed &= !isInterface && (containingClass == null || !containingClass.isValueClass()); - } - - if (containingClass != null && (containingClass.isInterface() || containingClass.isRecord())) { - isAllowed &= !PsiModifier.NATIVE.equals(modifier); - } - - if (containingClass != null && containingClass.isAnnotationType()) { - isAllowed &= !PsiModifier.STATIC.equals(modifier); - isAllowed &= !PsiModifier.DEFAULT.equals(modifier); - } - - if (JavaPsiRecordUtil.getRecordComponentForAccessor(method) != null) { - isAllowed &= !PsiModifier.STATIC.equals(modifier); - } - } - else if (modifierOwner instanceof PsiField) { - if (PsiModifier.PRIVATE.equals(modifier) || PsiModifier.PROTECTED.equals(modifier) || PsiModifier.TRANSIENT.equals(modifier) || - PsiModifier.STRICTFP.equals(modifier)) { - isAllowed = modifierOwnerParent instanceof PsiClass psiClass && !psiClass.isInterface(); - } - } - else if (modifierOwner instanceof PsiClassInitializer) { - isAllowed = PsiModifier.STATIC.equals(modifier); - } - else if (modifierOwner instanceof PsiLocalVariable || modifierOwner instanceof PsiParameter) { - isAllowed = PsiModifier.FINAL.equals(modifier); - } - else if (modifierOwner instanceof PsiReceiverParameter || modifierOwner instanceof PsiRecordComponent) { - isAllowed = false; - } - - isAllowed &= incompatibles != null; - if (!isAllowed) { - if (message == null) message = JavaErrorBundle.message("modifier.not.allowed", modifier); - HighlightInfo.Builder highlightInfo = - HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(keyword).descriptionAndTooltip(message); - IntentionAction action = fix != null ? fix : getFixFactory().createModifierListFix(modifierList, modifier, false, false); - highlightInfo.registerFix(action, null, null, null, null); - return highlightInfo; - } - - return null; - } - static @NotNull UnhandledExceptions collectUnhandledExceptions(@NotNull PsiTryStatement statement) { UnhandledExceptions thrownTypes = UnhandledExceptions.EMPTY; diff --git a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightVisitorImpl.java b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightVisitorImpl.java index 1f3f5c6fb484..f3e705f1e573 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightVisitorImpl.java +++ b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightVisitorImpl.java @@ -760,8 +760,6 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh PsiElement parent = keyword.getParent(); String text = keyword.getText(); if (parent instanceof PsiModifierList psiModifierList) { - if (!hasErrorResults()) add(HighlightUtil.checkNotAllowedModifier(keyword, psiModifierList)); - if (!hasErrorResults()) add(HighlightUtil.checkIllegalModifierCombination(keyword, psiModifierList)); PsiElement pParent = psiModifierList.getParent(); if (PsiModifier.ABSTRACT.equals(text) && pParent instanceof PsiMethod psiMethod) { if (!hasErrorResults()) { diff --git a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/JavaErrorFixProvider.java b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/JavaErrorFixProvider.java index 847c6697351f..ae7e32cda39e 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/JavaErrorFixProvider.java +++ b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/JavaErrorFixProvider.java @@ -25,6 +25,7 @@ import com.intellij.pom.java.JavaFeature; import com.intellij.psi.*; import com.intellij.psi.codeStyle.VariableKind; import com.intellij.psi.impl.light.LightRecordMethod; +import com.intellij.psi.util.JavaPsiModifierUtil; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.util.PsiUtil; import com.intellij.psi.util.TypeConversionUtil; @@ -81,6 +82,7 @@ final class JavaErrorFixProvider { fix(kind, genericRemover); } + createModifierFixes(); createClassFixes(); createConstructorFixes(); createMethodFixes(); @@ -97,6 +99,27 @@ final class JavaErrorFixProvider { return ApplicationManager.getApplication().getService(JavaErrorFixProvider.class); } + private void createModifierFixes() { + fix(MODIFIER_NOT_ALLOWED, error -> { + @SuppressWarnings("MagicConstant") @PsiModifier.ModifierConstant String modifier = error.context(); + PsiModifierList list = (PsiModifierList)error.psi().getParent(); + if (list.getParent() instanceof PsiClass aClass && !aClass.isInterface() + && (PsiModifier.PRIVATE.equals(modifier) || PsiModifier.PROTECTED.equals(modifier))) { + return myFactory.createChangeModifierFix(); + } + return removeModifierFix((PsiModifierListOwner)list.getParent(), modifier); + }); + JavaFixProvider removeModifier = error -> { + @SuppressWarnings("MagicConstant") @PsiModifier.ModifierConstant String modifier = error.psi().getText(); + PsiModifierList list = (PsiModifierList)error.psi().getParent(); + return removeModifierFix((PsiModifierListOwner)list.getParent(), modifier); + }; + fix(MODIFIER_NOT_ALLOWED_LOCAL_CLASS, removeModifier); + fix(MODIFIER_REPEATED, removeModifier); + fix(MODIFIER_INCOMPATIBLE, removeModifier); + fix(MODIFIER_NOT_ALLOWED_NON_SEALED, removeModifier); + } + private void createMethodFixes() { JavaFixProvider addBody = error -> myFactory.createAddMethodBodyFix(error.psi()); fix(METHOD_DEFAULT_SHOULD_HAVE_BODY, addBody); @@ -563,7 +586,7 @@ final class JavaErrorFixProvider { private @Nullable IntentionAction maybeAddModifierFix(@NotNull PsiModifierListOwner owner, @PsiModifier.ModifierConstant String modifier) { PsiModifierList modifierList = owner.getModifierList(); - if (modifierList != null && HighlightUtil.getIncompatibleModifier(modifier, modifierList) != null) return null; + if (modifierList != null && JavaPsiModifierUtil.getIncompatibleModifier(modifier, modifierList) != null) return null; return addModifierFix(owner, modifier); } diff --git a/java/java-analysis-impl/src/com/siyeh/ig/classlayout/FinalPrivateMethodInspection.java b/java/java-analysis-impl/src/com/siyeh/ig/classlayout/FinalPrivateMethodInspection.java index d117bf3c76ac..a25c3c1d803a 100644 --- a/java/java-analysis-impl/src/com/siyeh/ig/classlayout/FinalPrivateMethodInspection.java +++ b/java/java-analysis-impl/src/com/siyeh/ig/classlayout/FinalPrivateMethodInspection.java @@ -16,12 +16,12 @@ package com.siyeh.ig.classlayout; import com.intellij.codeInsight.AnnotationUtil; -import com.intellij.codeInsight.daemon.impl.analysis.HighlightUtil; import com.intellij.codeInspection.LocalQuickFix; import com.intellij.codeInspection.ProblemHighlightType; import com.intellij.psi.CommonClassNames; import com.intellij.psi.PsiMethod; import com.intellij.psi.PsiModifier; +import com.intellij.psi.util.JavaPsiModifierUtil; import com.intellij.psi.util.PsiUtil; import com.siyeh.InspectionGadgetsBundle; import com.siyeh.ig.BaseInspection; @@ -62,7 +62,7 @@ public final class FinalPrivateMethodInspection extends BaseInspection { if (!PsiUtil.isLanguageLevel9OrHigher(method) && AnnotationUtil.isAnnotated(method, CommonClassNames.JAVA_LANG_SAFE_VARARGS, 0)) { return; } - if (!HighlightUtil.isLegalModifierCombination(method.getModifierList())) { + if (!JavaPsiModifierUtil.isLegalModifierCombination(method.getModifierList())) { return; } registerModifierError(PsiModifier.FINAL, method, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, PsiModifier.FINAL); diff --git a/java/java-analysis-impl/src/com/siyeh/ig/classlayout/FinalStaticMethodInspection.java b/java/java-analysis-impl/src/com/siyeh/ig/classlayout/FinalStaticMethodInspection.java index e2b2276384e0..0b08916bb93d 100644 --- a/java/java-analysis-impl/src/com/siyeh/ig/classlayout/FinalStaticMethodInspection.java +++ b/java/java-analysis-impl/src/com/siyeh/ig/classlayout/FinalStaticMethodInspection.java @@ -15,10 +15,10 @@ */ package com.siyeh.ig.classlayout; -import com.intellij.codeInsight.daemon.impl.analysis.HighlightUtil; import com.intellij.codeInspection.LocalQuickFix; import com.intellij.psi.PsiMethod; import com.intellij.psi.PsiModifier; +import com.intellij.psi.util.JavaPsiModifierUtil; import com.siyeh.InspectionGadgetsBundle; import com.siyeh.ig.BaseInspection; import com.siyeh.ig.BaseInspectionVisitor; @@ -58,7 +58,7 @@ public final class FinalStaticMethodInspection extends BaseInspection { || !method.hasModifierProperty(PsiModifier.STATIC)) { return; } - if (!HighlightUtil.isLegalModifierCombination(method.getModifierList())) { + if (!JavaPsiModifierUtil.isLegalModifierCombination(method.getModifierList())) { return; } registerModifierError(PsiModifier.FINAL, method, PsiModifier.FINAL); diff --git a/java/java-psi-impl/src/com/intellij/psi/util/JavaPsiModifierUtil.java b/java/java-psi-impl/src/com/intellij/psi/util/JavaPsiModifierUtil.java new file mode 100644 index 000000000000..4a11ac6d503d --- /dev/null +++ b/java/java-psi-impl/src/com/intellij/psi/util/JavaPsiModifierUtil.java @@ -0,0 +1,177 @@ +// 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.psi.util; + +import com.intellij.psi.*; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; +import java.util.Set; + +/** + * Utilities related to Java PSI modifiers + * + * @see PsiModifier + * @see PsiModifierList + */ +public final class JavaPsiModifierUtil { + + private static final Map> ourInterfaceIncompatibleModifiers = ContainerUtil.>immutableMapBuilder() + .put(PsiModifier.ABSTRACT, ContainerUtil.immutableSet()) + .put(PsiModifier.PACKAGE_LOCAL, ContainerUtil.immutableSet(PsiModifier.PRIVATE, PsiModifier.PUBLIC, PsiModifier.PROTECTED)) + .put(PsiModifier.PRIVATE, ContainerUtil.immutableSet(PsiModifier.PACKAGE_LOCAL, PsiModifier.PUBLIC, PsiModifier.PROTECTED)) + .put(PsiModifier.PUBLIC, ContainerUtil.immutableSet(PsiModifier.PACKAGE_LOCAL, PsiModifier.PRIVATE, PsiModifier.PROTECTED)) + .put(PsiModifier.PROTECTED, ContainerUtil.immutableSet(PsiModifier.PACKAGE_LOCAL, PsiModifier.PUBLIC, PsiModifier.PRIVATE)) + .put(PsiModifier.STRICTFP, ContainerUtil.immutableSet()) + .put(PsiModifier.STATIC, ContainerUtil.immutableSet()) + .put(PsiModifier.SEALED, ContainerUtil.immutableSet(PsiModifier.NON_SEALED)) + .put(PsiModifier.NON_SEALED, ContainerUtil.immutableSet(PsiModifier.SEALED)) + .build(); + private static final Map> ourMethodIncompatibleModifiers = ContainerUtil.>immutableMapBuilder() + .put(PsiModifier.ABSTRACT, ContainerUtil.immutableSet( + PsiModifier.NATIVE, PsiModifier.STATIC, PsiModifier.FINAL, PsiModifier.PRIVATE, PsiModifier.STRICTFP, PsiModifier.SYNCHRONIZED, + PsiModifier.DEFAULT)) + .put(PsiModifier.NATIVE, ContainerUtil.immutableSet(PsiModifier.ABSTRACT, PsiModifier.STRICTFP)) + .put(PsiModifier.PACKAGE_LOCAL, ContainerUtil.immutableSet(PsiModifier.PRIVATE, PsiModifier.PUBLIC, PsiModifier.PROTECTED)) + .put(PsiModifier.PRIVATE, ContainerUtil.immutableSet(PsiModifier.PACKAGE_LOCAL, PsiModifier.PUBLIC, PsiModifier.PROTECTED)) + .put(PsiModifier.PUBLIC, ContainerUtil.immutableSet(PsiModifier.PACKAGE_LOCAL, PsiModifier.PRIVATE, PsiModifier.PROTECTED)) + .put(PsiModifier.PROTECTED, ContainerUtil.immutableSet(PsiModifier.PACKAGE_LOCAL, PsiModifier.PUBLIC, PsiModifier.PRIVATE)) + .put(PsiModifier.STATIC, ContainerUtil.immutableSet(PsiModifier.ABSTRACT, PsiModifier.DEFAULT)) + .put(PsiModifier.DEFAULT, ContainerUtil.immutableSet(PsiModifier.ABSTRACT, PsiModifier.STATIC, PsiModifier.PRIVATE)) + .put(PsiModifier.SYNCHRONIZED, ContainerUtil.immutableSet(PsiModifier.ABSTRACT)) + .put(PsiModifier.STRICTFP, ContainerUtil.immutableSet(PsiModifier.ABSTRACT)) + .put(PsiModifier.FINAL, ContainerUtil.immutableSet(PsiModifier.ABSTRACT)) + .build(); + + private static final Map> ourFieldIncompatibleModifiers = ContainerUtil.>immutableMapBuilder() + .put(PsiModifier.FINAL, ContainerUtil.immutableSet(PsiModifier.VOLATILE)) + .put(PsiModifier.PACKAGE_LOCAL, ContainerUtil.immutableSet(PsiModifier.PRIVATE, PsiModifier.PUBLIC, PsiModifier.PROTECTED)) + .put(PsiModifier.PRIVATE, ContainerUtil.immutableSet(PsiModifier.PACKAGE_LOCAL, PsiModifier.PUBLIC, PsiModifier.PROTECTED)) + .put(PsiModifier.PUBLIC, ContainerUtil.immutableSet(PsiModifier.PACKAGE_LOCAL, PsiModifier.PRIVATE, PsiModifier.PROTECTED)) + .put(PsiModifier.PROTECTED, ContainerUtil.immutableSet(PsiModifier.PACKAGE_LOCAL, PsiModifier.PUBLIC, PsiModifier.PRIVATE)) + .put(PsiModifier.STATIC, ContainerUtil.immutableSet()) + .put(PsiModifier.TRANSIENT, ContainerUtil.immutableSet()) + .put(PsiModifier.VOLATILE, ContainerUtil.immutableSet(PsiModifier.FINAL)) + .build(); + private static final Map> ourClassIncompatibleModifiers = ContainerUtil.>immutableMapBuilder() + .put(PsiModifier.ABSTRACT, ContainerUtil.immutableSet(PsiModifier.FINAL)) + .put(PsiModifier.FINAL, ContainerUtil.immutableSet(PsiModifier.ABSTRACT, PsiModifier.SEALED, PsiModifier.NON_SEALED)) + .put(PsiModifier.PACKAGE_LOCAL, ContainerUtil.immutableSet(PsiModifier.PRIVATE, PsiModifier.PUBLIC, PsiModifier.PROTECTED)) + .put(PsiModifier.PRIVATE, ContainerUtil.immutableSet(PsiModifier.PACKAGE_LOCAL, PsiModifier.PUBLIC, PsiModifier.PROTECTED)) + .put(PsiModifier.PUBLIC, ContainerUtil.immutableSet(PsiModifier.PACKAGE_LOCAL, PsiModifier.PRIVATE, PsiModifier.PROTECTED)) + .put(PsiModifier.PROTECTED, ContainerUtil.immutableSet(PsiModifier.PACKAGE_LOCAL, PsiModifier.PUBLIC, PsiModifier.PRIVATE)) + .put(PsiModifier.STRICTFP, ContainerUtil.immutableSet()) + .put(PsiModifier.STATIC, ContainerUtil.immutableSet()) + .put(PsiModifier.SEALED, ContainerUtil.immutableSet(PsiModifier.FINAL, PsiModifier.NON_SEALED)) + .put(PsiModifier.NON_SEALED, ContainerUtil.immutableSet(PsiModifier.FINAL, PsiModifier.SEALED)) + .put(PsiModifier.VALUE, ContainerUtil.immutableSet()) + .build(); + private static final Map> ourClassInitializerIncompatibleModifiers = + ContainerUtil.>immutableMapBuilder() + .put(PsiModifier.STATIC, ContainerUtil.immutableSet()) + .build(); + private static final Map> ourModuleIncompatibleModifiers = ContainerUtil.>immutableMapBuilder() + .put(PsiModifier.OPEN, ContainerUtil.immutableSet()) + .build(); + private static final Map> ourRequiresIncompatibleModifiers = ContainerUtil.>immutableMapBuilder() + .put(PsiModifier.STATIC, ContainerUtil.immutableSet()) + .put(PsiModifier.TRANSITIVE, ContainerUtil.immutableSet()) + .build(); + + private static String getIncompatibleModifier(@NotNull String modifier, + @NotNull PsiModifierList modifierList, + @NotNull Map> incompatibleModifiersHash) { + // modifier is always incompatible with itself + int modifierCount = 0; + for (PsiElement otherModifier = modifierList.getFirstChild(); otherModifier != null; otherModifier = otherModifier.getNextSibling()) { + if (modifier.equals(otherModifier.getText())) modifierCount++; + } + if (modifierCount > 1) return modifier; + + Set incompatibles = incompatibleModifiersHash.get(modifier); + if (incompatibles == null) return null; + PsiElement parent = modifierList.getParent(); + boolean level8OrHigher = PsiUtil.isLanguageLevel8OrHigher(modifierList); + boolean level9OrHigher = PsiUtil.isLanguageLevel9OrHigher(modifierList); + for (@PsiModifier.ModifierConstant String incompatible : incompatibles) { + if (level8OrHigher) { + if (modifier.equals(PsiModifier.STATIC) && incompatible.equals(PsiModifier.ABSTRACT)) { + continue; + } + } + if (parent instanceof PsiMethod) { + if (level9OrHigher && modifier.equals(PsiModifier.PRIVATE) && incompatible.equals(PsiModifier.PUBLIC)) { + continue; + } + + if (modifier.equals(PsiModifier.STATIC) && incompatible.equals(PsiModifier.FINAL)) { + PsiClass containingClass = ((PsiMethod)parent).getContainingClass(); + if (containingClass == null || !containingClass.isInterface()) { + continue; + } + } + } + if (modifierList.hasModifierProperty(incompatible)) { + return incompatible; + } + if (PsiModifier.ABSTRACT.equals(incompatible) && modifierList.hasExplicitModifier(incompatible)) { + return incompatible; + } + } + + return null; + } + + /** + * Checks if the supplied modifier list contains incompatible modifiers (e.g. "public private"). + * + * @param modifierList a {@link PsiModifierList} to check + * @return true if the supplied modifier list contains compatible modifiers + */ + public static boolean isLegalModifierCombination(@NotNull PsiModifierList modifierList) { + for (PsiElement child = modifierList.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child instanceof PsiKeyword && getIncompatibleModifier(child.getText(), modifierList) != null) { + return false; + } + } + return true; + } + + private static Map> getIncompatibleModifierMap(@NotNull PsiElement modifierListOwner) { + if (PsiUtilCore.hasErrorElementChild(modifierListOwner)) return null; + if (modifierListOwner instanceof PsiClass) { + return ((PsiClass)modifierListOwner).isInterface() ? ourInterfaceIncompatibleModifiers : ourClassIncompatibleModifiers; + } + if (modifierListOwner instanceof PsiMethod) return ourMethodIncompatibleModifiers; + if (modifierListOwner instanceof PsiVariable) return ourFieldIncompatibleModifiers; + if (modifierListOwner instanceof PsiClassInitializer) return ourClassInitializerIncompatibleModifiers; + if (modifierListOwner instanceof PsiJavaModule) return ourModuleIncompatibleModifiers; + if (modifierListOwner instanceof PsiRequiresStatement) return ourRequiresIncompatibleModifiers; + return null; + } + + /** + * @param modifier modifier to check + * @param modifierList modifier list to check + * @return true if the modifier can be used in a given modifier list + */ + public static boolean isAllowed(@NotNull String modifier, @NotNull PsiModifierList modifierList) { + PsiElement parent = modifierList.getParent(); + if (parent == null) return false; + Map> incompatibleModifierMap = getIncompatibleModifierMap(parent); + return incompatibleModifierMap == null || incompatibleModifierMap.containsKey(modifier); + } + + /** + * @param modifier modifier to check (it does not yet belong to the modifier list supplied) + * @param modifierList modifier list to check + * @return a modifier that exists inside modifierList, which is incompatible with the supplied modifier + */ + public static @Nullable String getIncompatibleModifier(@NotNull String modifier, @NotNull PsiModifierList modifierList) { + PsiElement parent = modifierList.getParent(); + if (parent == null) return null; + Map> incompatibleModifierMap = getIncompatibleModifierMap(parent); + return incompatibleModifierMap == null ? null : getIncompatibleModifier(modifier, modifierList, incompatibleModifierMap); + } +} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advHighlightingSealedTypes/SealedTypesBasics.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advHighlightingSealedTypes/SealedTypesBasics.java index 7230b34a4a5b..d1d7962fbce7 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advHighlightingSealedTypes/SealedTypesBasics.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advHighlightingSealedTypes/SealedTypesBasics.java @@ -20,7 +20,7 @@ sealed interface Foos { sealed interface ID0 {} non-sealed interface ID1 extends ID0 {} -non-sealed interface ID extends ID1 {} +non-sealed interface ID extends ID1 {} enum ImplicitlySealedEnum { A {} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/valueClass/ValueClass.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/valueClass/ValueClass.java index 8641f256bd05..27a300896d12 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/valueClass/ValueClass.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/valueClass/ValueClass.java @@ -13,7 +13,7 @@ class Six extends Four {} // it's valid to extend a value class with an identity value enum Eight {} value record Nine(int no) {} sealed value class Ten {} -non-sealed value class Eleven {} +non-sealed value class Eleven {} abstract sealed value class Twelve {} value class Thirteen extends Twelve { void x() {