[java-intentions] ExtendSealedClassFix: ModCommand

IntentionActionWithChoice UI replaced with multi-step ModCommand, as we don't have IntentionActionWithChoice in ModCommand API, and it's unclear whether it's a good idea, and how to implement it better

GitOrigin-RevId: a702bc672b56f4c51dd7e7b01cf5ee0ded457805
This commit is contained in:
Tagir Valeev
2023-07-21 11:17:34 +00:00
committed by intellij-monorepo-bot
parent 9ab2a2cd01
commit 2c12f7c579
15 changed files with 71 additions and 123 deletions
@@ -426,7 +426,8 @@ 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=Make ''{0}'' {1, choice, 1#extend|2#implement} ''{2}'' and
extend.sealed.title=Make ''{0}'' {1, choice, 1#Extend|2#Implement} ''{2}'' and
extend.sealed.name=Make ''{0}'' {1, choice, 1#extend|2#implement} ''{2}''
implement.or.extend.fix.family=Implement/Extend required base class
implement.or.extend.fix.implement.text=Make ''{0}'' implement ''{1}''
@@ -3,77 +3,72 @@ 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;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.modcommand.*;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.psi.*;
import com.intellij.psi.search.searches.DirectClassInheritorsSearch;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.Query;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class ExtendSealedClassFix implements DefaultIntentionActionWithChoice {
public class ExtendSealedClassFix extends PsiBasedModCommandAction<PsiClass> {
@FileModifier.SafeFieldForPreview private final SmartPsiElementPointer<PsiClass> myParentClassPointer;
@FileModifier.SafeFieldForPreview private final SmartPsiElementPointer<PsiClass> mySubclassPointer;
private final @Nls String myName;
private ExtendSealedClassFix(PsiClass parentClass, PsiClass subclass) {
private ExtendSealedClassFix(@NotNull PsiClass parentClass, @NotNull PsiClass subclass) {
super(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() {
return new ChoiceTitleIntentionAction(QuickFixBundle.message("implement.or.extend.fix.family"), myName);
public @NotNull String getFamilyName() {
return QuickFixBundle.message("implement.or.extend.fix.family");
}
@Override
public @NotNull List<@NotNull ChoiceVariantIntentionAction> getVariants() {
PsiClass subclass = mySubclassPointer.getElement();
protected @Nullable Presentation getPresentation(@NotNull ActionContext context, @NotNull PsiClass subclass) {
PsiClass parentClass = myParentClassPointer.getElement();
if (subclass == null || parentClass == null) return Collections.emptyList();
if (parentClass == null) return null;
int extendsImplements = subclass.isInterface() || !parentClass.isInterface() ? 1 : 2;
String name = QuickFixBundle.message("extend.sealed.name", subclass.getName(), extendsImplements, parentClass.getName());
return Presentation.of(name);
}
@Override
protected @NotNull ModCommand perform(@NotNull ActionContext context, @NotNull PsiClass subclass) {
PsiClass parentClass = myParentClassPointer.getElement();
if (parentClass == null) return ModCommands.nop();
boolean parentIsInterface = parentClass.isInterface();
boolean subclassIsInterface = subclass.isInterface();
if (!parentIsInterface && subclassIsInterface) return Collections.emptyList();
if (!parentIsInterface && subclassIsInterface) return ModCommands.nop();
List<ModCommandAction> fixes;
if (subclassIsInterface) {
return Arrays.asList(new ExtendSealedClassVariantAction(0, PsiModifier.SEALED, myParentClassPointer, mySubclassPointer),
new ExtendSealedClassVariantAction(1, PsiModifier.NON_SEALED, myParentClassPointer, mySubclassPointer));
fixes = List.of(new ExtendSealedClassVariantAction(PsiModifier.SEALED, myParentClassPointer, subclass),
new ExtendSealedClassVariantAction(PsiModifier.NON_SEALED, myParentClassPointer, subclass));
}
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));
else {
fixes = List.of(new ExtendSealedClassVariantAction(PsiModifier.FINAL, myParentClassPointer, subclass),
new ExtendSealedClassVariantAction(PsiModifier.SEALED, myParentClassPointer, subclass),
new ExtendSealedClassVariantAction(PsiModifier.NON_SEALED, myParentClassPointer, subclass));
}
int extendsImplements = subclass.isInterface() || !parentClass.isInterface() ? 1 : 2;
//noinspection DialogTitleCapitalization
String name = QuickFixBundle.message("extend.sealed.title", subclass.getName(), extendsImplements, parentClass.getName());
return new ModChooseAction(name, fixes);
}
/**
* @return fixes or null if given case is not supported (but probably valid)
*/
static LocalQuickFix @Nullable [] createFixes(@NotNull PsiClass parentClass, @NotNull PsiClass subclass) {
static @Nullable ModCommandAction createFix(@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(parentClass, subclass);
List<LocalQuickFix> actions = new ArrayList<>();
actions.add(extendSealedClassFix.getTitle());
actions.addAll(extendSealedClassFix.getVariants());
return actions.toArray(LocalQuickFix.EMPTY_ARRAY);
return new ExtendSealedClassFix(parentClass, subclass);
}
private static boolean hasSealedClassSubclassModifier(PsiClass psiClass) {
@@ -88,30 +83,21 @@ public class ExtendSealedClassFix implements DefaultIntentionActionWithChoice {
modifiers.hasExplicitModifier(PsiModifier.NON_SEALED);
}
private static class ExtendSealedClassVariantAction extends ChoiceVariantIntentionAction {
private final int myIndex;
private static class ExtendSealedClassVariantAction extends PsiUpdateModCommandAction<PsiClass> {
private final @NlsSafe String myModifier;
private final SmartPsiElementPointer<PsiClass> myParentClassPointer;
private final SmartPsiElementPointer<PsiClass> mySubclassPointer;
private ExtendSealedClassVariantAction(int index, @NotNull String modifier,
private ExtendSealedClassVariantAction(@NotNull String modifier,
SmartPsiElementPointer<PsiClass> parentClassPointer,
SmartPsiElementPointer<PsiClass> subclassPointer) {
myIndex = index;
PsiClass subclass) {
super(subclass);
myModifier = modifier;
myParentClassPointer = parentClassPointer;
mySubclassPointer = subclassPointer;
}
@Override
public int getIndex() {
return myIndex;
}
@Override
public @IntentionName @NotNull String getName() {
return myModifier;
protected @Nullable Presentation getPresentation(@NotNull ActionContext context, @NotNull PsiClass element) {
return Presentation.of(myModifier);
}
@Override
@@ -120,21 +106,9 @@ public class ExtendSealedClassFix implements DefaultIntentionActionWithChoice {
}
@Override
public @NlsContexts.Tooltip String getTooltipText() {
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) {
protected void invoke(@NotNull ActionContext context, @NotNull PsiClass subclass, @NotNull ModPsiUpdater updater) {
PsiClass parentClass = myParentClassPointer.getElement();
if (parentClass == null || !parentClass.hasModifierProperty(PsiModifier.SEALED)) return;
PsiClass subclass = mySubclassPointer.getElement();
if (subclass == null) return;
if (ImplementOrExtendFix.implementOrExtend(parentClass, subclass) == null) return;
PsiModifierList modifiers = subclass.getModifierList();
if (modifiers == null) return;
@@ -146,13 +120,5 @@ 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));
}
}
}
@@ -2,18 +2,14 @@
package com.intellij.codeInsight.daemon.impl.quickfix;
import com.intellij.codeInsight.daemon.QuickFixBundle;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.modcommand.ModPsiUpdater;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.modcommand.PsiUpdateModCommandAction;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.modcommand.ModCommandAction;
import com.intellij.modcommand.ModCommandService;
import com.intellij.modcommand.ModPsiUpdater;
import com.intellij.modcommand.PsiUpdateModCommandAction;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.siyeh.ig.psiutils.TypeUtils;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -53,37 +49,25 @@ public final class ImplementOrExtendFix extends PsiUpdateModCommandAction<PsiCla
return QuickFixBundle.message("implement.or.extend.fix.family");
}
public static IntentionAction[] createActions(@NotNull PsiClass subclass,
@NotNull PsiClass parentClass) {
return StreamEx.of(createFixes(subclass, parentClass))
.map(fix -> {
if (fix instanceof IntentionAction action) return action;
ModCommandAction modCommandAction = ModCommandService.getInstance().unwrap(fix);
return modCommandAction == null ? null : modCommandAction.asIntention();
})
.nonNull()
.toArray(IntentionAction.EMPTY_ARRAY);
}
public static LocalQuickFix @NotNull [] createFixes(@NotNull PsiClass subclass,
public static @Nullable ModCommandAction createFix(@NotNull PsiClass subclass,
@NotNull PsiClass parentClass) {
if (!parentClass.isInterface() && (subclass.isInterface() || subclass.isRecord() || subclass.isEnum())) {
return LocalQuickFix.EMPTY_ARRAY;
return null;
}
if (subclass.isAnnotationType()) return LocalQuickFix.EMPTY_ARRAY;
if (subclass.isAnnotationType()) return null;
PsiModifierList modifiers = subclass.getModifierList();
if (modifiers == null) return LocalQuickFix.EMPTY_ARRAY;
if (modifiers == null) return null;
if (parentClass.isInterface()) {
PsiReferenceList targetList = subclass.isInterface() ? subclass.getExtendsList() : subclass.getImplementsList();
if (targetList == null) return LocalQuickFix.EMPTY_ARRAY;
if (targetList == null) return null;
}
else if (subclass.getExtendsList() == null || hasNonObjectParent(subclass)) {
return LocalQuickFix.EMPTY_ARRAY;
return null;
}
LocalQuickFix[] fixes = ExtendSealedClassFix.createFixes(parentClass, subclass);
if (fixes != null) return fixes;
return new LocalQuickFix[]{new ImplementOrExtendFix(subclass, parentClass).asQuickFix()};
ModCommandAction fix = ExtendSealedClassFix.createFix(parentClass, subclass);
if (fix != null) return fix;
return new ImplementOrExtendFix(subclass, parentClass);
}
static boolean hasNonObjectParent(@NotNull PsiClass psiClass) {
@@ -60,7 +60,10 @@ import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
public final class QuickFixFactoryImpl extends QuickFixFactory {
@@ -1050,7 +1053,8 @@ public final class QuickFixFactoryImpl extends QuickFixFactory {
public @NotNull List<IntentionAction> createExtendSealedClassFixes(@NotNull PsiJavaCodeReferenceElement subclassRef,
@NotNull PsiClass parentClass,
@NotNull PsiClass subClass) {
return Arrays.asList(ImplementOrExtendFix.createActions(subClass, parentClass));
ModCommandAction fix = ImplementOrExtendFix.createFix(subClass, parentClass);
return fix == null ? List.of() : List.of(fix.asIntention());
}
@Override
@@ -1,4 +1,4 @@
// "final" "true-preview"
// "Make 'Child' extend 'Parent'|->final" "true-preview"
sealed class Parent permits Child {}
final class Child extends Parent {}
@@ -1,4 +1,4 @@
// "non-sealed" "true-preview"
// "Make 'Child' extend 'Parent'|->non-sealed" "true-preview"
sealed interface Parent permits Child, Foo {}
interface OtherParent {}
@@ -1,4 +1,4 @@
// "final" "true-preview"
// "Make 'Child' extend 'Parent'|->final" "true-preview"
sealed class Parent permits Child {}
final class Child extends Parent {}
@@ -1,4 +1,4 @@
// "final" "false"
// "Make 'Child' extend 'Parent'|->final" "false"
sealed interface Parent permits C<caret>hild {}
interface Child {}
@@ -1,4 +1,4 @@
// "final" "true-preview"
// "Make 'Child' extend 'Parent'|->final" "true-preview"
sealed class Parent permits C<caret>hild {}
class Child {}
@@ -1,4 +1,4 @@
// "non-sealed" "true-preview"
// "Make 'Child' extend 'Parent'|->non-sealed" "true-preview"
sealed interface Parent permits Child<caret>, Foo {}
interface OtherParent {}
@@ -1,4 +1,4 @@
// "final" "false"
// "Make 'Child' extend 'Parent'" "false"
sealed class Parent permits C<caret>hild {}
class OtherParent {}
@@ -1,4 +1,4 @@
// "final" "true-preview"
// "Make 'Child' extend 'Parent'|->final" "true-preview"
sealed class Parent permits C<caret>hild {}
class Child extends Object {}
@@ -2,10 +2,6 @@
package com.intellij.java.codeInsight.intention;
import com.intellij.codeInsight.daemon.LightIntentionActionTestCase;
import com.intellij.ui.ChooserInterceptor;
import com.intellij.ui.UiInterceptors;
import java.util.List;
public class ExpandStaticImportActionTest extends LightIntentionActionTestCase {
@Override
@@ -17,10 +17,6 @@ package com.intellij.java.codeInsight.intention;
import com.intellij.codeInsight.daemon.LightIntentionActionTestCase;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.ui.ChooserInterceptor;
import com.intellij.ui.UiInterceptors;
import java.util.List;
public class ExtractSetFromComparisonChainActionTest extends LightIntentionActionTestCase {
@Override
@@ -6,6 +6,7 @@ import com.intellij.codeInsight.daemon.impl.quickfix.ImplementOrExtendFix;
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo;
import com.intellij.codeInspection.*;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.modcommand.ModCommandAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
@@ -52,13 +53,13 @@ public class RegistrationProblemsInspection extends DevKitUastInspectionBase {
for (PsiClass componentClass : componentClasses) {
if (ActionType.ACTION.myClassName.equals(componentClass.getQualifiedName()) &&
!checkedClass.isInheritor(componentClass, true)) {
LocalQuickFix[] fixes = sourcePsi.getLanguage().is(JavaLanguage.INSTANCE) ?
ImplementOrExtendFix.createFixes(checkedClass, componentClass) :
LocalQuickFix.EMPTY_ARRAY;
ModCommandAction fix = sourcePsi.getLanguage().is(JavaLanguage.INSTANCE) ?
ImplementOrExtendFix.createFix(checkedClass, componentClass) :
null;
ProblemHolderUtilKt.registerUProblem(holder, uClass,
DevKitBundle.message("inspections.registration.problems.incompatible.message",
componentClass.getQualifiedName()),
fixes);
fix == null ? LocalQuickFix.EMPTY_ARRAY : new LocalQuickFix[] {fix.asQuickFix()});
}
}
if (ActionType.ACTION.isOfType(checkedClass) && !hasNoArgConstructor(checkedClass)) {