FillPermitsListAction: intention to add missing inheritors to permits list (IDEA-243901)

GitOrigin-RevId: a0fc70e03b6339890e8fe42ac651fe0ac910d02b
This commit is contained in:
Artemiy Sartakov
2020-07-15 04:37:48 +00:00
committed by intellij-monorepo-bot
parent 14f7907d29
commit 8eaa9d4c52
17 changed files with 293 additions and 21 deletions
@@ -1880,6 +1880,10 @@
<className>com.intellij.codeInsight.intention.impl.SealClassAction</className>
<category>Java/Declaration</category>
</intentionAction>
<intentionAction>
<className>com.intellij.codeInsight.intention.impl.FillPermitsListAction</className>
<category>Java/Declaration</category>
</intentionAction>
<intentionAction>
<className>com.intellij.codeInsight.intention.impl.UnwrapElseBranchAction</className>
<category>Java/Control Flow</category>
@@ -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<PsiClass> sameFileInheritors = sameFileInheritors(psiClass).findAll();
if (permittedRefs.length < sameFileInheritors.size()) return true;
Set<PsiClass> 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<PsiClass> permittedClasses = getPermittedClasses(getPermittedRefs(psiClass));
Collection<String> 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<String> getMissingInheritors(@NotNull Project project,
Editor editor,
@NotNull PsiJavaFile psiJavaFile,
@NotNull PsiClass psiClass,
@NotNull Set<PsiClass> permittedClasses) {
Collection<String> 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<PsiClass> 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<PsiClass> outsideInheritors(@NotNull PsiClass parent) {
GlobalSearchScope scope = GlobalSearchScope.notScope(GlobalSearchScope.fileScope(parent.getContainingFile()));
return ClassInheritorsSearch.search(parent, scope, false);
}
private static @NotNull Query<PsiClass> sameFileInheritors(@NotNull PsiClass parent) {
return ClassInheritorsSearch.search(parent, GlobalSearchScope.fileScope(parent.getContainingFile()), false);
}
private static void fillPermitsList(@NotNull PsiClass parent, @NotNull Collection<String> 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<String> 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());
}
}
@@ -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<PsiClass> inheritors = new ArrayList<>();
Ref<String> 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);
}
@@ -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 {}
@@ -0,0 +1,9 @@
// "Add missing inheritors to permits list" "true"
sealed interface Parent permits Foo {
}
enum Foo implements Parent {
A {}
}
@@ -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 {}
}
@@ -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 {}
@@ -0,0 +1,7 @@
// "Add missing inheritors to permits list" "true"
sealed class A<caret> permits C {}
final class B extends A {}
final class C extends A {}
@@ -0,0 +1,9 @@
// "Add missing inheritors to permits list" "true"
sealed interface Pare<caret>nt {
}
enum Foo implements Parent {
A {}
}
@@ -0,0 +1,11 @@
// "Add missing inheritors to permits list" "true"
sealed class A<caret> /*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 {}
}
@@ -0,0 +1,3 @@
// "Add missing inheritors to permits list" "false"
sealed class A<caret> {}
@@ -0,0 +1,9 @@
// "Add missing inheritors to permits list" "true"
sealed class A<caret> {}
final class D extends A {}
non-sealed class C extends A {}
sealed class F extends A {}
@@ -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";
}
}
@@ -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
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
@@ -0,0 +1,4 @@
public sealed class X permits Y {
}
final class Y extends X {}
@@ -0,0 +1,4 @@
public sealed class <spot>X</spot> {
}
final class Y extends X {}
@@ -0,0 +1,5 @@
<html>
<body>
Adds missing inheritors to permits list of a sealed class.
</body>
</html>