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