[intention-preview] ExtendSealedClassFix and improvements to ImplementOrExtendFix

GitOrigin-RevId: 6e68f66952d5a995a260f4c235fb48cd1217cdf3
This commit is contained in:
Bas Leijdekkers
2023-01-31 21:53:01 +00:00
committed by intellij-monorepo-bot
parent 9dd69ca785
commit 5c444185a3
17 changed files with 102 additions and 154 deletions
@@ -427,12 +427,11 @@ remove.unreachable.branches=Remove unreachable branches
simplify.boolean.expression.extracting.side.effects=\ extracting side effects
intention.move.parenthesis.name=Fix closing parenthesis placement
extend.sealed.title=Extend sealed class and mark as
implement.sealed.title=Implement sealed interface and mark as
extend.sealed.title=Make ''{0}'' {1, choice, 1#extend|2#implement} ''{2}'' and
implement.or.extend.fix.family=Implement/Extend required base class
implement.or.extend.fix.implement.text=Implement ''{0}''
implement.or.extend.fix.extend.text=Extend ''{0}''
implement.or.extend.fix.implement.text=Make ''{0}'' implement ''{1}''
implement.or.extend.fix.extend.text=Make ''{0}'' extend ''{1}''
seal.class.from.permits.list.fix=Seal inheritor
@@ -1,7 +1,8 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.daemon.impl.quickfix;
import com.intellij.codeInsight.daemon.QuickFixBundle;
import com.intellij.codeInsight.intention.FileModifier;
import com.intellij.codeInsight.intention.choice.ChoiceTitleIntentionAction;
import com.intellij.codeInsight.intention.choice.ChoiceVariantIntentionAction;
import com.intellij.codeInsight.intention.choice.DefaultIntentionActionWithChoice;
@@ -16,7 +17,6 @@ import com.intellij.psi.*;
import com.intellij.psi.search.searches.DirectClassInheritorsSearch;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.Query;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -25,61 +25,51 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static com.intellij.util.ObjectUtils.tryCast;
public class ExtendSealedClassFix implements DefaultIntentionActionWithChoice {
@FileModifier.SafeFieldForPreview private final SmartPsiElementPointer<PsiClass> myParentClassPointer;
@FileModifier.SafeFieldForPreview private final SmartPsiElementPointer<PsiClass> mySubclassPointer;
private final @Nls String myName;
private static final String[] SUBCLASS_MODIFIERS = {PsiModifier.FINAL, PsiModifier.NON_SEALED, PsiModifier.SEALED};
private final boolean myParentIsInterface;
private final boolean myChildIsInterface;
private final SmartPsiElementPointer<PsiJavaCodeReferenceElement> mySubclassReferencePointer;
private ExtendSealedClassFix(PsiJavaCodeReferenceElement reference, boolean parentIsInterface, boolean childIsInterface) {
mySubclassReferencePointer = SmartPointerManager.createPointer(reference);
myParentIsInterface = parentIsInterface;
myChildIsInterface = childIsInterface;
private ExtendSealedClassFix(PsiClass parentClass, PsiClass subclass) {
myParentClassPointer = SmartPointerManager.createPointer(parentClass);
mySubclassPointer = SmartPointerManager.createPointer(subclass);
int extendsImplements = subclass.isInterface() || !parentClass.isInterface() ? 1 : 2;
myName = QuickFixBundle.message("extend.sealed.title", subclass.getName(), extendsImplements, parentClass.getName());
}
@Override
public @NotNull ChoiceTitleIntentionAction getTitle() {
String fixName = getName();
return new ChoiceTitleIntentionAction(fixName, fixName);
}
@NotNull
@Nls
private String getName() {
return QuickFixBundle.message(myParentIsInterface ? "implement.sealed.title" : "extend.sealed.title");
return new ChoiceTitleIntentionAction(QuickFixBundle.message("implement.or.extend.fix.family"), myName);
}
@Override
public @NotNull List<ChoiceVariantIntentionAction> getVariants() {
if (!myParentIsInterface && myChildIsInterface) return Collections.emptyList();
if (myChildIsInterface) {
return Arrays.asList(new ExtendSealedClassVariantAction(0, PsiModifier.SEALED),
new ExtendSealedClassVariantAction(1, PsiModifier.NON_SEALED));
PsiClass subclass = mySubclassPointer.getElement();
PsiClass parentClass = myParentClassPointer.getElement();
if (subclass == null || parentClass == null) return Collections.emptyList();
boolean parentIsInterface = parentClass.isInterface();
boolean subclassIsInterface = subclass.isInterface();
if (!parentIsInterface && subclassIsInterface) return Collections.emptyList();
if (subclassIsInterface) {
return Arrays.asList(new ExtendSealedClassVariantAction(0, PsiModifier.SEALED, myParentClassPointer, mySubclassPointer),
new ExtendSealedClassVariantAction(1, PsiModifier.NON_SEALED, myParentClassPointer, mySubclassPointer));
}
return IntStream.range(0, SUBCLASS_MODIFIERS.length)
.mapToObj(i -> new ExtendSealedClassVariantAction(i, SUBCLASS_MODIFIERS[i]))
.collect(Collectors.toList());
return Arrays.asList(new ExtendSealedClassVariantAction(0, PsiModifier.FINAL, myParentClassPointer, mySubclassPointer),
new ExtendSealedClassVariantAction(1, PsiModifier.SEALED, myParentClassPointer, mySubclassPointer),
new ExtendSealedClassVariantAction(2, PsiModifier.NON_SEALED, myParentClassPointer, mySubclassPointer));
}
/**
* @return fixes or null if given case is not supported (but probably valid)
*/
static LocalQuickFix @Nullable [] createFixes(@NotNull PsiJavaCodeReferenceElement subclassReference,
@NotNull PsiClass parentClass,
@NotNull PsiClass subclass) {
static LocalQuickFix @Nullable [] createFixes(@NotNull PsiClass parentClass, @NotNull PsiClass subclass) {
if (!parentClass.hasModifierProperty(PsiModifier.SEALED) || !parentClass.getManager().isInProject(parentClass)) return null;
boolean parentIsInterface = parentClass.isInterface();
if (parentIsInterface && (subclass.isRecord() || subclass.isEnum())) return null;
PsiModifierList modifiers = subclass.getModifierList();
if (modifiers == null || hasSealedClassSubclassModifier(modifiers)) return null;
ExtendSealedClassFix extendSealedClassFix = new ExtendSealedClassFix(subclassReference, parentIsInterface, subclass.isInterface());
ExtendSealedClassFix extendSealedClassFix = new ExtendSealedClassFix(parentClass, subclass);
List<LocalQuickFix> actions = new ArrayList<>();
actions.add(extendSealedClassFix.getTitle());
actions.addAll(extendSealedClassFix.getVariants());
@@ -93,17 +83,25 @@ public class ExtendSealedClassFix implements DefaultIntentionActionWithChoice {
}
private static boolean hasSealedClassSubclassModifier(@NotNull PsiModifierList modifiers) {
return ContainerUtil.exists(SUBCLASS_MODIFIERS, m -> modifiers.hasExplicitModifier(m));
return modifiers.hasExplicitModifier(PsiModifier.FINAL) ||
modifiers.hasExplicitModifier(PsiModifier.SEALED) ||
modifiers.hasExplicitModifier(PsiModifier.NON_SEALED);
}
private class ExtendSealedClassVariantAction extends ChoiceVariantIntentionAction {
private static class ExtendSealedClassVariantAction extends ChoiceVariantIntentionAction {
private final int myIndex;
private final @NlsSafe String myModifier;
private final SmartPsiElementPointer<PsiClass> myParentClassPointer;
private final SmartPsiElementPointer<PsiClass> mySubclassPointer;
private ExtendSealedClassVariantAction(int index, @NotNull String modifier) {
private ExtendSealedClassVariantAction(int index, @NotNull String modifier,
SmartPsiElementPointer<PsiClass> parentClassPointer,
SmartPsiElementPointer<PsiClass> subclassPointer) {
myIndex = index;
myModifier = modifier;
myParentClassPointer = parentClassPointer;
mySubclassPointer = subclassPointer;
}
@Override
@@ -118,7 +116,7 @@ public class ExtendSealedClassFix implements DefaultIntentionActionWithChoice {
@Override
public @IntentionFamilyName @NotNull String getFamilyName() {
return ExtendSealedClassFix.this.getName();
return QuickFixBundle.message("implement.or.extend.fix.family");
}
@Override
@@ -126,14 +124,16 @@ public class ExtendSealedClassFix implements DefaultIntentionActionWithChoice {
return getFamilyName() + " " + myModifier;
}
@Override
public @Nullable PsiElement getElementToMakeWritable(@NotNull PsiFile currentFile) {
return mySubclassPointer.getContainingFile();
}
@Override
public void applyFix(@NotNull Project project, PsiFile file, @Nullable Editor editor) {
if (editor == null) return;
PsiJavaCodeReferenceElement referenceElement = mySubclassReferencePointer.getElement();
if (referenceElement == null) return;
PsiClass parentClass = PsiTreeUtil.getParentOfType(referenceElement, PsiClass.class);
PsiClass parentClass = myParentClassPointer.getElement();
if (parentClass == null || !parentClass.hasModifierProperty(PsiModifier.SEALED)) return;
PsiClass subclass = tryCast(referenceElement.resolve(), PsiClass.class);
PsiClass subclass = mySubclassPointer.getElement();
if (subclass == null) return;
if (ImplementOrExtendFix.implementOrExtend(parentClass, subclass) == null) return;
PsiModifierList modifiers = subclass.getModifierList();
@@ -146,5 +146,13 @@ public class ExtendSealedClassFix implements DefaultIntentionActionWithChoice {
subclass.navigate(true);
}
}
@Override
public @Nullable FileModifier getFileModifierForPreview(@NotNull PsiFile target) {
PsiClass subclass = mySubclassPointer.getElement();
PsiClass copy = PsiTreeUtil.findSameElementInCopy(subclass, target);
if (copy == null) return null;
return new ExtendSealedClassVariantAction(myIndex, myModifier, myParentClassPointer, SmartPointerManager.createPointer(copy));
}
}
}
@@ -1,102 +1,38 @@
// 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.
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.daemon.impl.quickfix;
import com.intellij.CommonBundle;
import com.intellij.codeInsight.FileModificationService;
import com.intellij.codeInsight.daemon.QuickFixBundle;
import com.intellij.codeInsight.intention.FileModifier;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.pom.Navigatable;
import com.intellij.psi.*;
import com.intellij.refactoring.RefactoringBundle;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.containers.ContainerUtil;
import com.siyeh.ig.psiutils.TypeUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import static com.intellij.util.ObjectUtils.tryCast;
public final class ImplementOrExtendFix extends LocalQuickFixAndIntentionActionOnPsiElement {
private final SmartPsiElementPointer<PsiClass> mySubclassPointer;
private final SmartPsiElementPointer<PsiClass> myParentClassPointer;
@SafeFieldForPreview private final SmartPsiElementPointer<PsiClass> mySubclassPointer;
@SafeFieldForPreview private final SmartPsiElementPointer<PsiClass> myParentClassPointer;
private final boolean myOnTheFly;
private final @IntentionName String myName;
private ImplementOrExtendFix(@NotNull PsiElement place, @NotNull PsiClass subclass, @NotNull PsiClass parentClass, boolean onTheFly) {
super(place);
mySubclassPointer = SmartPointerManager.createPointer(subclass);
myParentClassPointer = SmartPointerManager.createPointer(parentClass);
myOnTheFly = onTheFly;
}
@Override
public @NotNull IntentionPreviewInfo generatePreview(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) {
PsiClass subclass = mySubclassPointer.getElement();
PsiClass parent = myParentClassPointer.getElement();
if (subclass == null || !subclass.isValid() || parent == null || !parent.isValid()) return IntentionPreviewInfo.EMPTY;
PsiFile subclassFile = subclass.getContainingFile();
if (!subclassFile.isWritable() || subclass.getImplementsList() == null || subclass.getExtendsList() == null) {
return IntentionPreviewInfo.EMPTY;
}
String before = createClassDescription(subclass, null, null);
String after = createClassDescription(subclass, parent.isInterface() ? null : parent, parent.isInterface() ? parent : null);
if (before == null || after == null) {
return IntentionPreviewInfo.EMPTY;
}
return new IntentionPreviewInfo.CustomDiff(JavaFileType.INSTANCE, subclassFile.getName(), before, after);
}
@Nullable
private static String createClassDescription(@Nullable PsiClass psiClass,
@Nullable PsiClass addToExtends,
@Nullable PsiClass addToImplement) {
if (psiClass == null) {
return null;
}
StringBuilder stringBuilder = new StringBuilder();
PsiElement brace = psiClass.getLBrace();
for (PsiElement child : psiClass.getChildren()) {
if (child == brace) {
break;
}
if (child == psiClass.getImplementsList() && addToImplement!=null){
String text = child.getText();
PsiElementFactory elementFactory = JavaPsiFacade.getInstance(child.getProject()).getElementFactory();
PsiClassType type = elementFactory.createType(addToImplement);
text = StringUtil.isNotEmpty(text) ? text + ", " + type.getName() : "implements " + type.getName();
if (!(child.getPrevSibling() instanceof PsiWhiteSpace)) {
text = " " + text;
}
stringBuilder.append(text);
continue;
}
if (child == psiClass.getExtendsList() && addToExtends!=null) {
PsiElementFactory elementFactory = JavaPsiFacade.getInstance(child.getProject()).getElementFactory();
PsiClassType type = elementFactory.createType(addToExtends);
String text = "extends " + type.getName();
if (!(child.getPrevSibling() instanceof PsiWhiteSpace)) {
text = " " + text;
}
stringBuilder.append(text);
continue;
}
stringBuilder.append(child.getText());
}
return stringBuilder.toString();
myName = parentClass.isInterface() && !subclass.isInterface()
? QuickFixBundle.message("implement.or.extend.fix.implement.text", subclass.getName(), parentClass.getName())
: QuickFixBundle.message("implement.or.extend.fix.extend.text", subclass.getName(), parentClass.getName());
}
@Override
@@ -109,34 +45,32 @@ public final class ImplementOrExtendFix extends LocalQuickFixAndIntentionActionO
// to plugin.xml or java class
PsiClass subclass = mySubclassPointer.getElement();
if (subclass == null || !subclass.isValid()) return;
PsiFile subclassFile = subclass.getContainingFile();
boolean external = file != subclassFile;
if (external && !subclassFile.isWritable()) {
String className = subclass.getQualifiedName();
Messages.showErrorDialog(project, RefactoringBundle.message("0.is.read.only", className), CommonBundle.getErrorTitle());
return;
}
PsiClass parentClass = myParentClassPointer.getElement();
if (parentClass == null) return;
if (!FileModificationService.getInstance().prepareFileForWrite(subclassFile)) return;
boolean external = file != subclass.getContainingFile();
WriteAction.run(() -> {
PsiElement e = implementOrExtend(parentClass, subclass);
if (myOnTheFly && external && e instanceof Navigatable) ((Navigatable)e).navigate(true);
});
PsiElement e = implementOrExtend(parentClass, subclass);
if (myOnTheFly && external && e instanceof Navigatable) ((Navigatable)e).navigate(true);
}
@Override
public boolean startInWriteAction() {
return false;
public @Nullable PsiElement getElementToMakeWritable(@NotNull PsiFile currentFile) {
return mySubclassPointer.getElement();
}
@Override
public @Nullable FileModifier getFileModifierForPreview(@NotNull PsiFile target) {
PsiClass subclass = mySubclassPointer.getElement();
PsiClass copy = PsiTreeUtil.findSameElementInCopy(subclass, target);
PsiElement place = myStartElement.getElement();
PsiClass parentClass = myParentClassPointer.getElement();
if (copy == null || place == null || parentClass == null) return null;
return new ImplementOrExtendFix(place, copy, parentClass, myOnTheFly);
}
@Override
public @IntentionName @NotNull String getText() {
PsiClass parentClass = Objects.requireNonNull(myParentClassPointer.getElement());
return parentClass.isInterface()
? QuickFixBundle.message("implement.or.extend.fix.implement.text", parentClass.getQualifiedName())
: QuickFixBundle.message("implement.or.extend.fix.extend.text", parentClass.getQualifiedName());
return myName;
}
@Override
@@ -165,10 +99,9 @@ public final class ImplementOrExtendFix extends LocalQuickFixAndIntentionActionO
return LocalQuickFix.EMPTY_ARRAY;
}
PsiJavaCodeReferenceElement subclassRef = tryCast(place, PsiJavaCodeReferenceElement.class);
LocalQuickFix[] fixes = subclassRef == null ? null : ExtendSealedClassFix.createFixes(subclassRef, parentClass, subclass);
LocalQuickFix[] fixes = ExtendSealedClassFix.createFixes(parentClass, subclass);
if (fixes != null) return fixes;
return new LocalQuickFix[]{new ImplementOrExtendFix(place, subclass, parentClass, onTheFly)};
return new LocalQuickFix[]{ new ImplementOrExtendFix(place, subclass, parentClass, onTheFly) };
}
static boolean hasNonObjectParent(@NotNull PsiClass psiClass) {
@@ -1,4 +1,4 @@
// "Implement 'Parent'" "true"
// "Make 'MyEnum' implement 'Parent'" "true-preview"
sealed interface Parent permits MyEnum {}
enum MyEnum implements Parent {}
@@ -1,4 +1,4 @@
// "Extend 'Parent'" "true"
// "Make 'Child' extend 'Parent'" "true-preview"
sealed class Parent permits Child {}
final class Child extends Parent {}
@@ -1,4 +1,4 @@
// "Implement 'Parent'" "true"
// "Make 'User' implement 'Parent'" "true-preview"
sealed interface Parent permits User {}
record User(int age) implements Parent {}
@@ -1,4 +1,4 @@
// "final" "true"
// "final" "true-preview"
sealed class Parent permits Child {}
final class Child extends Parent {}
@@ -1,4 +1,4 @@
// "non-sealed" "true"
// "non-sealed" "true-preview"
sealed interface Parent permits Child, Foo {}
interface OtherParent {}
@@ -1,4 +1,4 @@
// "final" "true"
// "final" "true-preview"
sealed class Parent permits Child {}
final class Child extends Parent {}
@@ -0,0 +1,4 @@
// "Make 'Child' extend 'Parent'" "true-preview"
sealed interface Parent permits Child {}
non-sealed interface Child extends Parent {}
@@ -1,4 +1,4 @@
// "Implement 'Parent'" "true"
// "Make 'MyEnum' implement 'Parent'" "true-preview"
sealed interface Parent permits MyEnum<caret> {}
enum MyEnum {}
@@ -1,4 +1,4 @@
// "Extend 'Parent'" "true"
// "Make 'Child' extend 'Parent'" "true-preview"
sealed class Parent permits C<caret>hild {}
final class Child {}
@@ -1,4 +1,4 @@
// "Implement 'Parent'" "true"
// "Make 'User' implement 'Parent'" "true-preview"
sealed interface Parent permits User<caret> {}
record User(int age) {}
@@ -1,4 +1,4 @@
// "final" "true"
// "final" "true-preview"
sealed class Parent permits C<caret>hild {}
class Child {}
@@ -1,4 +1,4 @@
// "non-sealed" "true"
// "non-sealed" "true-preview"
sealed interface Parent permits Child<caret>, Foo {}
interface OtherParent {}
@@ -1,4 +1,4 @@
// "final" "true"
// "final" "true-preview"
sealed class Parent permits C<caret>hild {}
class Child extends Object {}
@@ -0,0 +1,4 @@
// "Make 'Child' extend 'Parent'" "true-preview"
sealed interface Parent permits C<caret>hild {}
non-sealed interface Child {}