diff --git a/java/java-impl/src/META-INF/JavaPlugin.xml b/java/java-impl/src/META-INF/JavaPlugin.xml
index 7ee2acc3486a..c9fb2de7ab42 100644
--- a/java/java-impl/src/META-INF/JavaPlugin.xml
+++ b/java/java-impl/src/META-INF/JavaPlugin.xml
@@ -1880,6 +1880,10 @@
com.intellij.codeInsight.intention.impl.SealClassAction
Java/Declaration
+
+ com.intellij.codeInsight.intention.impl.FillPermitsListAction
+ Java/Declaration
+
com.intellij.codeInsight.intention.impl.UnwrapElseBranchAction
Java/Control Flow
diff --git a/java/java-impl/src/com/intellij/codeInsight/intention/impl/FillPermitsListAction.java b/java/java-impl/src/com/intellij/codeInsight/intention/impl/FillPermitsListAction.java
new file mode 100644
index 000000000000..508aad670637
--- /dev/null
+++ b/java/java-impl/src/com/intellij/codeInsight/intention/impl/FillPermitsListAction.java
@@ -0,0 +1,152 @@
+// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.codeInsight.intention.impl;
+
+import com.intellij.codeInsight.daemon.impl.analysis.HighlightingFeature;
+import com.intellij.codeInsight.daemon.impl.analysis.JavaModuleGraphUtil;
+import com.intellij.codeInsight.intention.BaseElementAtCaretIntentionAction;
+import com.intellij.codeInspection.util.IntentionFamilyName;
+import com.intellij.codeInspection.util.IntentionName;
+import com.intellij.java.JavaBundle;
+import com.intellij.lang.java.JavaLanguage;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.*;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.searches.ClassInheritorsSearch;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.psi.util.PsiUtil;
+import com.intellij.refactoring.util.CommonRefactoringUtil;
+import com.intellij.util.IncorrectOperationException;
+import com.intellij.util.Query;
+import com.intellij.util.SmartList;
+import one.util.streamex.StreamEx;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static com.intellij.util.ObjectUtils.tryCast;
+
+public class FillPermitsListAction extends BaseElementAtCaretIntentionAction {
+ @Override
+ public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) {
+ if (!HighlightingFeature.SEALED_CLASSES.isAvailable(element)) return false;
+ PsiIdentifier identifier = tryCast(element, PsiIdentifier.class);
+ if (identifier == null) return false;
+ PsiClass psiClass = tryCast(identifier.getParent(), PsiClass.class);
+ if (psiClass == null || !(psiClass.getContainingFile() instanceof PsiJavaFile)) return false;
+ PsiModifierList modifiers = psiClass.getModifierList();
+ if (modifiers == null || !modifiers.hasExplicitModifier(PsiModifier.SEALED)) return false;
+ PsiJavaCodeReferenceElement[] permittedRefs = getPermittedRefs(psiClass);
+ Collection sameFileInheritors = sameFileInheritors(psiClass).findAll();
+ if (permittedRefs.length < sameFileInheritors.size()) return true;
+ Set permittedClasses = getPermittedClasses(permittedRefs);
+ for (PsiClass inheritor : sameFileInheritors) {
+ if (PsiUtil.isLocalOrAnonymousClass(inheritor)) return false;
+ if (!permittedClasses.remove(inheritor)) return true;
+ }
+ for (PsiClass inheritor : outsideInheritors(psiClass)) {
+ if (PsiUtil.isLocalOrAnonymousClass(inheritor)) return false;
+ if (!permittedClasses.remove(inheritor)) return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement element) throws IncorrectOperationException {
+ PsiClass psiClass = PsiTreeUtil.getParentOfType(element, PsiClass.class);
+ if (psiClass == null) return;
+ PsiJavaFile psiJavaFile = tryCast(psiClass.getContainingFile(), PsiJavaFile.class);
+ if (psiJavaFile == null) return;
+ Set permittedClasses = getPermittedClasses(getPermittedRefs(psiClass));
+ Collection missingInheritors = getMissingInheritors(project, editor, psiJavaFile, psiClass, permittedClasses);
+ if (missingInheritors == null) return;
+ fillPermitsList(psiClass, missingInheritors);
+ }
+
+ @Override
+ public @IntentionName @NotNull String getText() {
+ return getFamilyName();
+ }
+
+ @Override
+ public @NotNull @IntentionFamilyName String getFamilyName() {
+ return JavaBundle.message("intention.family.name.fill.permits.list");
+ }
+
+ private static PsiJavaCodeReferenceElement @NotNull [] getPermittedRefs(@NotNull PsiClass psiClass) {
+ PsiReferenceList permitsList = psiClass.getPermitsList();
+ return permitsList == null ? PsiJavaCodeReferenceElement.EMPTY_ARRAY : permitsList.getReferenceElements();
+ }
+
+ @Nullable
+ private static Collection getMissingInheritors(@NotNull Project project,
+ Editor editor,
+ @NotNull PsiJavaFile psiJavaFile,
+ @NotNull PsiClass psiClass,
+ @NotNull Set permittedClasses) {
+ Collection missingInheritors = new SmartList<>();
+ PsiJavaModule module = JavaModuleGraphUtil.findDescriptorByElement(psiClass);
+ for (PsiClass inheritor : ClassInheritorsSearch.search(psiClass, false)) {
+ String errorTitle = SealClassAction.checkInheritor(psiJavaFile, module, inheritor);
+ if (errorTitle != null) {
+ reportError(project, editor, JavaBundle.message(errorTitle));
+ return null;
+ }
+ String qualifiedName = Objects.requireNonNull(inheritor.getQualifiedName());
+ if (!permittedClasses.contains(inheritor)) missingInheritors.add(qualifiedName);
+ }
+
+ if (missingInheritors.isEmpty()) {
+ String message = JavaBundle.message("intention.error.fill.permits.list.no.missing.inheritors");
+ reportError(project, editor, message);
+ return null;
+ }
+ return missingInheritors;
+ }
+
+ @NotNull
+ private static Set getPermittedClasses(PsiJavaCodeReferenceElement @NotNull [] permittedRefs) {
+ return Arrays.stream(permittedRefs)
+ .map(ref -> tryCast(ref.resolve(), PsiClass.class))
+ .filter(Objects::nonNull).collect(Collectors.toSet());
+ }
+
+ private static @NotNull Query outsideInheritors(@NotNull PsiClass parent) {
+ GlobalSearchScope scope = GlobalSearchScope.notScope(GlobalSearchScope.fileScope(parent.getContainingFile()));
+ return ClassInheritorsSearch.search(parent, scope, false);
+ }
+
+ private static @NotNull Query sameFileInheritors(@NotNull PsiClass parent) {
+ return ClassInheritorsSearch.search(parent, GlobalSearchScope.fileScope(parent.getContainingFile()), false);
+ }
+
+ private static void fillPermitsList(@NotNull PsiClass parent, @NotNull Collection missingInheritors) {
+ PsiReferenceList permitsList = parent.getPermitsList();
+ PsiFileFactory factory = PsiFileFactory.getInstance(parent.getProject());
+ if (permitsList == null) {
+ PsiReferenceList implementsList = Objects.requireNonNull(parent.getImplementsList());
+ String permitsClause = StreamEx.of(missingInheritors).sorted().joining(",", "permits ", "");
+ parent.addAfter(createPermitsClause(factory, permitsClause), implementsList);
+ }
+ else {
+ Stream curClasses = Arrays.stream(permitsList.getReferenceElements()).map(ref -> ref.getQualifiedName());
+ String permitsClause = StreamEx.of(missingInheritors).append(curClasses).sorted().joining(",", "permits ", "");
+ permitsList.replace(createPermitsClause(factory, permitsClause));
+ }
+ }
+
+ private static void reportError(@NotNull Project project, @NotNull Editor editor, @NotNull String message) {
+ String title = JavaBundle.message("intention.fill.permits.list.hint.title");
+ CommonRefactoringUtil.showErrorHint(project, editor, JavaBundle.message(message), title, null);
+ }
+
+ @NotNull
+ private static PsiReferenceList createPermitsClause(@NotNull PsiFileFactory factory, @NotNull String permitsClause) {
+ PsiJavaFile javaFile = (PsiJavaFile)factory.createFileFromText(JavaLanguage.INSTANCE, "class __Dummy " + permitsClause + "{}");
+ PsiClass newClass = javaFile.getClasses()[0];
+ return Objects.requireNonNull(newClass.getPermitsList());
+ }
+}
diff --git a/java/java-impl/src/com/intellij/codeInsight/intention/impl/SealClassAction.java b/java/java-impl/src/com/intellij/codeInsight/intention/impl/SealClassAction.java
index 37830a3c170b..1f15cd842349 100644
--- a/java/java-impl/src/com/intellij/codeInsight/intention/impl/SealClassAction.java
+++ b/java/java-impl/src/com/intellij/codeInsight/intention/impl/SealClassAction.java
@@ -26,6 +26,7 @@ import com.intellij.util.SequentialTask;
import com.intellij.util.containers.ContainerUtil;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.PropertyKey;
import java.util.*;
@@ -84,29 +85,12 @@ public class SealClassAction extends BaseElementAtCaretIntentionAction {
List inheritors = new ArrayList<>();
Ref message = new Ref<>();
ClassInheritorsSearch.search(aClass, false).forEach(inheritor -> {
- if (PsiUtil.isLocalOrAnonymousClass(inheritor)) {
- message.set("intention.error.make.sealed.class.has.anonymous.or.local.inheritors");
+ String errorTitle = checkInheritor(parentFile, module, inheritor);
+ if (errorTitle != null) {
+ message.set(errorTitle);
return false;
}
- if (module == null) {
- PsiJavaFile file = tryCast(inheritor.getContainingFile(), PsiJavaFile.class);
- if (file == null) {
- message.set("intention.error.make.sealed.class.inheritors.not.in.java.file");
- return false;
- }
- if (!parentFile.getPackageName().equals(file.getPackageName())) {
- message.set("intention.error.make.sealed.class.different.packages");
- return false;
- }
- }
- else {
- if (JavaModuleGraphUtil.findDescriptorByElement(inheritor) != module) {
- message.set("intention.error.make.sealed.class.different.modules");
- return false;
- }
- }
-
inheritors.add(inheritor);
return true;
});
@@ -144,6 +128,26 @@ public class SealClassAction extends BaseElementAtCaretIntentionAction {
});
}
+ static @Nullable String checkInheritor(@NotNull PsiJavaFile parentFile, @Nullable PsiJavaModule module, @NotNull PsiClass inheritor) {
+ if (PsiUtil.isLocalOrAnonymousClass(inheritor)) {
+ return "intention.error.make.sealed.class.has.anonymous.or.local.inheritors";
+ }
+
+ if (module == null) {
+ PsiJavaFile file = tryCast(inheritor.getContainingFile(), PsiJavaFile.class);
+ if (file == null) return "intention.error.make.sealed.class.inheritors.not.in.java.file";
+ if (!parentFile.getPackageName().equals(file.getPackageName())) {
+ return "intention.error.make.sealed.class.different.packages";
+ }
+ }
+ else {
+ if (JavaModuleGraphUtil.findDescriptorByElement(inheritor) != module) {
+ return "intention.error.make.sealed.class.different.modules";
+ }
+ }
+ return null;
+ }
+
private static void showError(@NotNull Project project, Editor editor, @PropertyKey(resourceBundle = JavaBundle.BUNDLE) String message) {
CommonRefactoringUtil.showErrorHint(project, editor, JavaBundle.message(message), getErrorTitle(), null);
}
diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/afterAppendMissingInheritors.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/afterAppendMissingInheritors.java
new file mode 100644
index 000000000000..333a0a538235
--- /dev/null
+++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/afterAppendMissingInheritors.java
@@ -0,0 +1,7 @@
+// "Add missing inheritors to permits list" "true"
+
+sealed class A permits B, C {}
+
+final class B extends A {}
+
+final class C extends A {}
\ No newline at end of file
diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/afterEnumInheritor.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/afterEnumInheritor.java
new file mode 100644
index 000000000000..1b777985e999
--- /dev/null
+++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/afterEnumInheritor.java
@@ -0,0 +1,9 @@
+// "Add missing inheritors to permits list" "true"
+
+sealed interface Parent permits Foo {
+
+}
+
+enum Foo implements Parent {
+ A {}
+}
diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/afterIndirectInheritor.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/afterIndirectInheritor.java
new file mode 100644
index 000000000000..a59e20ba2064
--- /dev/null
+++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/afterIndirectInheritor.java
@@ -0,0 +1,11 @@
+// "Add missing inheritors to permits list" "true"
+
+sealed class A permits B, C, D.E /*1*/ {}
+
+sealed class B extends A permits D {}
+
+final class C extends A {}
+
+final class D extends B {
+ non-sealed static class E extends A {}
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/afterSameFileInheritors.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/afterSameFileInheritors.java
new file mode 100644
index 000000000000..8133f2563051
--- /dev/null
+++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/afterSameFileInheritors.java
@@ -0,0 +1,9 @@
+// "Add missing inheritors to permits list" "true"
+
+sealed class A permits C, D, F {}
+
+final class D extends A {}
+
+non-sealed class C extends A {}
+
+sealed class F extends A {}
\ No newline at end of file
diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/beforeAppendMissingInheritors.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/beforeAppendMissingInheritors.java
new file mode 100644
index 000000000000..a2d7da492812
--- /dev/null
+++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/beforeAppendMissingInheritors.java
@@ -0,0 +1,7 @@
+// "Add missing inheritors to permits list" "true"
+
+sealed class A permits C {}
+
+final class B extends A {}
+
+final class C extends A {}
\ No newline at end of file
diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/beforeEnumInheritor.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/beforeEnumInheritor.java
new file mode 100644
index 000000000000..4395971948ab
--- /dev/null
+++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/beforeEnumInheritor.java
@@ -0,0 +1,9 @@
+// "Add missing inheritors to permits list" "true"
+
+sealed interface Parent {
+
+}
+
+enum Foo implements Parent {
+ A {}
+}
diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/beforeIndirectInheritor.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/beforeIndirectInheritor.java
new file mode 100644
index 000000000000..5b6eef019791
--- /dev/null
+++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/beforeIndirectInheritor.java
@@ -0,0 +1,11 @@
+// "Add missing inheritors to permits list" "true"
+
+sealed class A /*1*/ {}
+
+sealed class B extends A permits D {}
+
+final class C extends A {}
+
+final class D extends B {
+ non-sealed static class E extends A {}
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/beforeNoInheritors.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/beforeNoInheritors.java
new file mode 100644
index 000000000000..2390f83c5912
--- /dev/null
+++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/beforeNoInheritors.java
@@ -0,0 +1,3 @@
+// "Add missing inheritors to permits list" "false"
+
+sealed class A {}
\ No newline at end of file
diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/beforeSameFileInheritors.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/beforeSameFileInheritors.java
new file mode 100644
index 000000000000..72c7673525b7
--- /dev/null
+++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList/beforeSameFileInheritors.java
@@ -0,0 +1,9 @@
+// "Add missing inheritors to permits list" "true"
+
+sealed class A {}
+
+final class D extends A {}
+
+non-sealed class C extends A {}
+
+sealed class F extends A {}
\ No newline at end of file
diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/intention/FillPermitsListActionTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/intention/FillPermitsListActionTest.java
new file mode 100644
index 000000000000..da427b7144ac
--- /dev/null
+++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/intention/FillPermitsListActionTest.java
@@ -0,0 +1,21 @@
+// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.java.codeInsight.intention;
+
+import com.intellij.codeInsight.daemon.LightIntentionActionTestCase;
+import com.intellij.testFramework.LightProjectDescriptor;
+import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
+import org.jetbrains.annotations.NotNull;
+
+public class FillPermitsListActionTest extends LightIntentionActionTestCase {
+
+ @Override
+ protected @NotNull LightProjectDescriptor getProjectDescriptor() {
+ return LightJavaCodeInsightFixtureTestCase.JAVA_15;
+ }
+
+ @Override
+ protected String getBasePath() {
+ return "/codeInsight/daemonCodeAnalyzer/quickFix/fillPermitsList";
+ }
+
+}
\ No newline at end of file
diff --git a/java/openapi/resources/messages/JavaBundle.properties b/java/openapi/resources/messages/JavaBundle.properties
index f9f0a7cb8646..81eeb85654bc 100644
--- a/java/openapi/resources/messages/JavaBundle.properties
+++ b/java/openapi/resources/messages/JavaBundle.properties
@@ -1270,4 +1270,7 @@ intention.error.make.sealed.class.different.packages=Module is unnamed and some
intention.error.make.sealed.class.inheritors.not.in.java.file=Some of the inheritors are not in java files
intention.error.make.sealed.class.different.modules=Some of the inheritors are in different modules
intention.error.make.sealed.class.interface.has.no.inheritors=Interface has no inheritors
-intention.make.sealed.class.task.title.set.inheritors.modifiers=Setting inheritors modifiers
\ No newline at end of file
+intention.make.sealed.class.task.title.set.inheritors.modifiers=Setting inheritors modifiers
+intention.family.name.fill.permits.list=Add missing inheritors to permits list
+intention.error.fill.permits.list.no.missing.inheritors=Sealed class has no missing inheritors
+intention.fill.permits.list.hint.title=Fill Permits List
\ No newline at end of file
diff --git a/plugins/IntentionPowerPak/src/intentionDescriptions/FillPermitsListAction/after.java.template b/plugins/IntentionPowerPak/src/intentionDescriptions/FillPermitsListAction/after.java.template
new file mode 100644
index 000000000000..5465ffb91b31
--- /dev/null
+++ b/plugins/IntentionPowerPak/src/intentionDescriptions/FillPermitsListAction/after.java.template
@@ -0,0 +1,4 @@
+public sealed class X permits Y {
+}
+
+final class Y extends X {}
\ No newline at end of file
diff --git a/plugins/IntentionPowerPak/src/intentionDescriptions/FillPermitsListAction/before.java.template b/plugins/IntentionPowerPak/src/intentionDescriptions/FillPermitsListAction/before.java.template
new file mode 100644
index 000000000000..8d52a0b7548e
--- /dev/null
+++ b/plugins/IntentionPowerPak/src/intentionDescriptions/FillPermitsListAction/before.java.template
@@ -0,0 +1,4 @@
+public sealed class X {
+}
+
+final class Y extends X {}
\ No newline at end of file
diff --git a/plugins/IntentionPowerPak/src/intentionDescriptions/FillPermitsListAction/description.html b/plugins/IntentionPowerPak/src/intentionDescriptions/FillPermitsListAction/description.html
new file mode 100644
index 000000000000..328c43cbf3a7
--- /dev/null
+++ b/plugins/IntentionPowerPak/src/intentionDescriptions/FillPermitsListAction/description.html
@@ -0,0 +1,5 @@
+
+
+Adds missing inheritors to permits list of a sealed class.
+
+
\ No newline at end of file