From 859a8fa5ba5155ccf85073c924c01cf139cb54a8 Mon Sep 17 00:00:00 2001 From: Artemiy Sartakov Date: Thu, 10 Sep 2020 16:54:05 +0700 Subject: [PATCH] ExtendSealedClassFix: fix added (IDEA-249603) GitOrigin-RevId: d47a517c15f0a011fae581db7aacf4b1a76bad1b --- .../intention/QuickFixFactory.java | 12 +- .../impl/analysis/HighlightClassUtil.java | 15 +- .../src/messages/QuickFixBundle.properties | 7 + .../impl/quickfix/ExtendSealedClassFix.java | 152 ++++++++++++++++++ .../impl/quickfix/ImplementOrExtendFix.java | 138 ++++++++++++++++ .../impl/config/QuickFixFactoryImpl.java | 7 + .../extendSealedClass/afterEnumSubclass.java | 4 + .../extendSealedClass/afterFinalSubclass.java | 4 + .../afterRecordSubclass.java | 4 + .../extendSealedClass/afterSimple.java | 4 + .../afterSubInterfaceWithParents.java | 8 + .../afterSubclassWithParentObject.java | 4 + .../extendSealedClass/beforeEnumSubclass.java | 4 + .../beforeFinalSubclass.java | 4 + .../beforeInterfaceParentFinalModifier.java | 4 + .../beforeRecordSubclass.java | 4 + .../extendSealedClass/beforeSimple.java | 4 + .../beforeSubInterfaceWithParents.java | 8 + .../beforeSubclassWithParent.java | 6 + .../beforeSubclassWithParentObject.java | 4 + .../quickFix/ExtendSealedClassTest.java | 21 +++ .../DefaultIntentionActionWithChoice.kt | 1 + .../messages/DevKitBundle.properties | 4 - .../RegistrationProblemsInspection.java | 6 +- .../quickfix/ImplementOrExtendFix.java | 93 ----------- 25 files changed, 415 insertions(+), 107 deletions(-) create mode 100644 java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/ExtendSealedClassFix.java create mode 100644 java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/ImplementOrExtendFix.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterEnumSubclass.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterFinalSubclass.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterRecordSubclass.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterSimple.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterSubInterfaceWithParents.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterSubclassWithParentObject.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeEnumSubclass.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeFinalSubclass.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeInterfaceParentFinalModifier.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeRecordSubclass.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeSimple.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeSubInterfaceWithParents.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeSubclassWithParent.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeSubclassWithParentObject.java create mode 100644 java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/quickFix/ExtendSealedClassTest.java delete mode 100644 plugins/devkit/devkit-core/src/inspections/quickfix/ImplementOrExtendFix.java diff --git a/java/java-analysis-api/src/com/intellij/codeInsight/intention/QuickFixFactory.java b/java/java-analysis-api/src/com/intellij/codeInsight/intention/QuickFixFactory.java index 9464c9f2b8bb..6ab57c0349d3 100644 --- a/java/java-analysis-api/src/com/intellij/codeInsight/intention/QuickFixFactory.java +++ b/java/java-analysis-api/src/com/intellij/codeInsight/intention/QuickFixFactory.java @@ -463,7 +463,7 @@ public abstract class QuickFixFactory { @NotNull public abstract IntentionAction createWrapSwitchRuleStatementsIntoBlockFix(@NotNull PsiSwitchLabeledRuleStatement rule); - + @NotNull public abstract IntentionAction createAddParameterListFix(@NotNull PsiMethod method); @@ -487,4 +487,14 @@ public abstract class QuickFixFactory { public abstract IntentionAction createAddToPermitsListFix(@NotNull PsiClass subClass, @NotNull PsiClass superClass); public abstract IntentionAction createMoveClassToPackageFix(@NotNull PsiClass classToMove, @NotNull String packageName); + + /** + * Provides fixes to make class extend sealed class and + * possibly mark extending class with one of sealed subclass modifiers (final, sealed, non-sealed) + * + * @param subclassRef reference in permits list of a parent class + * @return + */ + public abstract @NotNull List createExtendSealedClassFixes(@NotNull PsiJavaCodeReferenceElement subclassRef, + @NotNull PsiClass parentClass, @NotNull PsiClass subClass); } \ No newline at end of file diff --git a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightClassUtil.java b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightClassUtil.java index 3c957621e19a..7dde7e57a7e3 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightClassUtil.java +++ b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightClassUtil.java @@ -1108,12 +1108,15 @@ public final class HighlightClassUtil { if (resolve instanceof PsiClass) { PsiClass inheritorClass = (PsiClass)resolve; if (Arrays.stream(inheritorClass.getSuperTypes()).noneMatch(type -> aClass.equals(type.resolve()))) { - holder.add(HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(permitted) - .descriptionAndTooltip(JavaErrorBundle.message("invalid.permits.clause.direct.implementation", - inheritorClass.getName(), - inheritorClass.isInterface() == aClass.isInterface() ? 1 : 2, - aClass.getName())) - .create()); + HighlightInfo info = HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(permitted) + .descriptionAndTooltip(JavaErrorBundle.message("invalid.permits.clause.direct.implementation", + inheritorClass.getName(), + inheritorClass.isInterface() == aClass.isInterface() ? 1 : 2, + aClass.getName())) + .create(); + QuickFixAction.registerQuickFixActions(info, null, + QUICK_FIX_FACTORY.createExtendSealedClassFixes(permitted, aClass, inheritorClass)); + holder.add(info); } else { if (currentModule == null && !psiFacade.arePackagesTheSame(aClass, inheritorClass)) { diff --git a/java/java-analysis-impl/src/messages/QuickFixBundle.properties b/java/java-analysis-impl/src/messages/QuickFixBundle.properties index b7a18df2ef17..1119ec16b25a 100644 --- a/java/java-analysis-impl/src/messages/QuickFixBundle.properties +++ b/java/java-analysis-impl/src/messages/QuickFixBundle.properties @@ -388,3 +388,10 @@ remove.unreachable.branches=Remove unreachable branches set.inspection.option.fix=Set inspection option 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 + +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}'' \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/ExtendSealedClassFix.java b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/ExtendSealedClassFix.java new file mode 100644 index 000000000000..53b1a1fd1c68 --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/ExtendSealedClassFix.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.daemon.impl.quickfix; + +import com.intellij.codeInsight.daemon.QuickFixBundle; +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.openapi.util.NlsSafe; +import com.intellij.pom.Navigatable; +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; + +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 { + + private static final String[] SUBCLASS_MODIFIERS = {PsiModifier.FINAL, PsiModifier.NON_SEALED, PsiModifier.SEALED}; + + private final boolean myParentIsInterface; + private final boolean myChildIsInterface; + private final SmartPsiElementPointer mySubclassReferencePointer; + + private ExtendSealedClassFix(PsiJavaCodeReferenceElement reference, boolean parentIsInterface, boolean childIsInterface) { + mySubclassReferencePointer = SmartPointerManager.createPointer(reference); + myParentIsInterface = parentIsInterface; + myChildIsInterface = childIsInterface; + } + + @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"); + } + + @Override + public @NotNull List getVariants() { + if (!myParentIsInterface && myChildIsInterface) return Collections.emptyList(); + if (myChildIsInterface) { + return Arrays.asList(new ExtendSealedClassVariantAction(0, PsiModifier.SEALED), + new ExtendSealedClassVariantAction(1, PsiModifier.NON_SEALED)); + } + return IntStream.range(0, SUBCLASS_MODIFIERS.length) + .mapToObj(i -> new ExtendSealedClassVariantAction(i, SUBCLASS_MODIFIERS[i])) + .collect(Collectors.toList()); + } + + /** + * @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) { + if (!parentClass.hasModifierProperty(PsiModifier.SEALED)) 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()); + List actions = new ArrayList<>(); + actions.add(extendSealedClassFix.getTitle()); + actions.addAll(extendSealedClassFix.getVariants()); + return actions.toArray(LocalQuickFix.EMPTY_ARRAY); + } + + private static boolean hasSealedClassSubclassModifier(PsiClass psiClass) { + PsiModifierList modifiers = psiClass.getModifierList(); + if (modifiers == null) return false; + return hasSealedClassSubclassModifier(modifiers); + } + + private static boolean hasSealedClassSubclassModifier(@NotNull PsiModifierList modifiers) { + return ContainerUtil.exists(SUBCLASS_MODIFIERS, m -> modifiers.hasExplicitModifier(m)); + } + + private class ExtendSealedClassVariantAction extends ChoiceVariantIntentionAction { + + private final int myIndex; + private final @NlsSafe String myModifier; + + private ExtendSealedClassVariantAction(int index, @NotNull String modifier) { + myIndex = index; + myModifier = modifier; + } + + @Override + public int getIndex() { + return myIndex; + } + + @Override + public @IntentionName @NotNull String getName() { + return myModifier; + } + + @Override + public @IntentionFamilyName @NotNull String getFamilyName() { + return ExtendSealedClassFix.this.getName(); + } + + @Override + public @NlsContexts.Tooltip String getTooltipText() { + return getFamilyName() + " " + myModifier; + } + + @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); + if (parentClass == null || !parentClass.hasModifierProperty(PsiModifier.SEALED)) return; + PsiClass subclass = tryCast(referenceElement.resolve(), PsiClass.class); + if (subclass == null) return; + if (ImplementOrExtendFix.implementOrExtend(parentClass, subclass) == null) return; + PsiModifierList modifiers = subclass.getModifierList(); + if (modifiers == null) return; + if (hasSealedClassSubclassModifier(modifiers)) return; + modifiers.setModifierProperty(myModifier, true); + Query subclassInheritors = DirectClassInheritorsSearch.search(subclass); + if (PsiModifier.FINAL.equals(myModifier) && subclassInheritors.findFirst() != null || + PsiModifier.SEALED.equals(myModifier) && subclassInheritors.anyMatch(child -> !hasSealedClassSubclassModifier(child))) { + PsiIdentifier subclassIdentifier = subclass.getNameIdentifier(); + if (subclassIdentifier instanceof Navigatable) ((Navigatable)subclassIdentifier).navigate(true); + } + } + } +} diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/ImplementOrExtendFix.java b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/ImplementOrExtendFix.java new file mode 100644 index 000000000000..c4c92c4d1826 --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/ImplementOrExtendFix.java @@ -0,0 +1,138 @@ +// 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.daemon.impl.quickfix; + +import com.intellij.CommonBundle; +import com.intellij.codeInsight.daemon.QuickFixBundle; +import com.intellij.codeInsight.intention.IntentionAction; +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.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.vfs.ReadonlyStatusHandler; +import com.intellij.pom.Navigatable; +import com.intellij.psi.*; +import com.intellij.refactoring.RefactoringBundle; +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.Collections; +import java.util.Objects; + +import static com.intellij.util.ObjectUtils.tryCast; + +public final class ImplementOrExtendFix extends LocalQuickFixAndIntentionActionOnPsiElement { + + private final SmartPsiElementPointer mySubclassPointer; + private final SmartPsiElementPointer myParentClassPointer; + private final boolean myOnTheFly; + + 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 void invoke(@NotNull Project project, + @NotNull PsiFile file, + @Nullable Editor editor, + @NotNull PsiElement startElement, + @NotNull PsiElement endElement) { + // can happen during batch-inspection if resolution has already been applied + // to plugin.xml or java class + PsiClass subclass = mySubclassPointer.getElement(); + if (subclass == null || !subclass.isValid()) return; + boolean external = file != subclass.getContainingFile(); + if (external) { + ReadonlyStatusHandler readonlyStatusHandler = ReadonlyStatusHandler.getInstance(project); + ReadonlyStatusHandler.OperationStatus status = readonlyStatusHandler.ensureFilesWritable( + Collections.singletonList(subclass.getContainingFile().getVirtualFile())); + + if (status.hasReadonlyFiles()) { + 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; + + PsiElement e = implementOrExtend(parentClass, subclass); + if (myOnTheFly && external && e instanceof Navigatable) ((Navigatable)e).navigate(true); + } + + @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()); + } + + @Override + public @IntentionFamilyName @NotNull String getFamilyName() { + return QuickFixBundle.message("implement.or.extend.fix.family"); + } + + public static IntentionAction[] createActions(@NotNull PsiElement place, @NotNull PsiClass subclass, + @NotNull PsiClass parentClass, boolean onTheFly) { + return ContainerUtil.map2Array(createFixes(place, subclass, parentClass, onTheFly), IntentionAction.class, f -> (IntentionAction)f); + } + + public static LocalQuickFix @NotNull [] createFixes(@NotNull PsiElement place, @NotNull PsiClass subclass, + @NotNull PsiClass parentClass, boolean onTheFly) { + if (!parentClass.isInterface() && (subclass.isInterface() || subclass.isRecord() || subclass.isEnum())) { + return LocalQuickFix.EMPTY_ARRAY; + } + if (subclass.isAnnotationType()) return LocalQuickFix.EMPTY_ARRAY; + PsiModifierList modifiers = subclass.getModifierList(); + if (modifiers == null) return LocalQuickFix.EMPTY_ARRAY; + if (parentClass.isInterface()) { + PsiReferenceList targetList = subclass.isInterface() ? subclass.getExtendsList() : subclass.getImplementsList(); + if (targetList == null) return LocalQuickFix.EMPTY_ARRAY; + } + else if (subclass.getExtendsList() == null || hasNonObjectParent(subclass)) { + return LocalQuickFix.EMPTY_ARRAY; + } + + PsiJavaCodeReferenceElement subclassRef = tryCast(place, PsiJavaCodeReferenceElement.class); + LocalQuickFix[] fixes = subclassRef == null ? null : ExtendSealedClassFix.createFixes(subclassRef, parentClass, subclass); + if (fixes != null) return fixes; + return new LocalQuickFix[]{new ImplementOrExtendFix(place, subclass, parentClass, onTheFly)}; + } + + static boolean hasNonObjectParent(@NotNull PsiClass psiClass) { + PsiClassType[] extendsListTypes = psiClass.getExtendsListTypes(); + if (extendsListTypes.length == 0) return false; + if (extendsListTypes.length == 1 && TypeUtils.isJavaLangObject(extendsListTypes[0])) return false; + return true; + } + + static @Nullable PsiElement implementOrExtend(@NotNull PsiClass parentClass, @NotNull PsiClass subclass) { + boolean subclassIsInterface = subclass.isInterface(); + boolean parentIsInterface = parentClass.isInterface(); + if (!parentIsInterface && subclassIsInterface) return null; + final PsiReferenceList targetList; + if (parentIsInterface && !subclassIsInterface) { + PsiReferenceList implementsList = subclass.getImplementsList(); + if (implementsList == null) return null; + targetList = implementsList; + } + else { + PsiReferenceList extendsList = subclass.getExtendsList(); + if (extendsList == null || !subclassIsInterface && hasNonObjectParent(subclass)) return null; + PsiJavaCodeReferenceElement[] parents = extendsList.getReferenceElements(); + if (parents.length > 0 && !subclassIsInterface) parents[0].delete(); + targetList = extendsList; + } + PsiElementFactory elementFactory = JavaPsiFacade.getInstance(parentClass.getProject()).getElementFactory(); + PsiJavaCodeReferenceElement parentReference = elementFactory.createReferenceElementByType(elementFactory.createType(parentClass)); + return targetList.add(parentReference); + } +} diff --git a/java/java-impl/src/com/intellij/codeInsight/intention/impl/config/QuickFixFactoryImpl.java b/java/java-impl/src/com/intellij/codeInsight/intention/impl/config/QuickFixFactoryImpl.java index 0850bf50879e..f334a36754c9 100644 --- a/java/java-impl/src/com/intellij/codeInsight/intention/impl/config/QuickFixFactoryImpl.java +++ b/java/java-impl/src/com/intellij/codeInsight/intention/impl/config/QuickFixFactoryImpl.java @@ -947,4 +947,11 @@ public final class QuickFixFactoryImpl extends QuickFixFactory { public IntentionAction createMoveClassToPackageFix(@NotNull PsiClass classToMove, @NotNull String packageName) { return new MoveToPackageFix(classToMove.getContainingFile(), packageName); } + + @Override + public @NotNull List createExtendSealedClassFixes(@NotNull PsiJavaCodeReferenceElement subclassRef, + @NotNull PsiClass parentClass, + @NotNull PsiClass subClass) { + return Arrays.asList(ImplementOrExtendFix.createActions(subclassRef, subClass, parentClass, false)); + } } diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterEnumSubclass.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterEnumSubclass.java new file mode 100644 index 000000000000..13c3a7f36a8f --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterEnumSubclass.java @@ -0,0 +1,4 @@ +// "Implement 'Parent'" "true" +sealed interface Parent permits MyEnum {} + +enum MyEnum implements Parent {} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterFinalSubclass.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterFinalSubclass.java new file mode 100644 index 000000000000..fe5064fc9991 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterFinalSubclass.java @@ -0,0 +1,4 @@ +// "Extend 'Parent'" "true" +sealed class Parent permits Child {} + +final class Child extends Parent {} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterRecordSubclass.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterRecordSubclass.java new file mode 100644 index 000000000000..59f086b9e5a6 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterRecordSubclass.java @@ -0,0 +1,4 @@ +// "Implement 'Parent'" "true" +sealed interface Parent permits User {} + +record User(int age) implements Parent {} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterSimple.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterSimple.java new file mode 100644 index 000000000000..47ac7f0fe5e4 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterSimple.java @@ -0,0 +1,4 @@ +// "final" "true" +sealed class Parent permits Child {} + +final class Child extends Parent {} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterSubInterfaceWithParents.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterSubInterfaceWithParents.java new file mode 100644 index 000000000000..ba7d5156356a --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterSubInterfaceWithParents.java @@ -0,0 +1,8 @@ +// "non-sealed" "true" +sealed interface Parent permits Child, Foo {} + +interface OtherParent {} + +non-sealed interface Foo extends Parent {} + +non-sealed interface Child extends OtherParent, Parent {} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterSubclassWithParentObject.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterSubclassWithParentObject.java new file mode 100644 index 000000000000..47ac7f0fe5e4 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/afterSubclassWithParentObject.java @@ -0,0 +1,4 @@ +// "final" "true" +sealed class Parent permits Child {} + +final class Child extends Parent {} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeEnumSubclass.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeEnumSubclass.java new file mode 100644 index 000000000000..505bf3a924bd --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeEnumSubclass.java @@ -0,0 +1,4 @@ +// "Implement 'Parent'" "true" +sealed interface Parent permits MyEnum {} + +enum MyEnum {} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeFinalSubclass.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeFinalSubclass.java new file mode 100644 index 000000000000..f3cf14939396 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeFinalSubclass.java @@ -0,0 +1,4 @@ +// "Extend 'Parent'" "true" +sealed class Parent permits Child {} + +final class Child {} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeInterfaceParentFinalModifier.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeInterfaceParentFinalModifier.java new file mode 100644 index 000000000000..f73a14fa96cf --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeInterfaceParentFinalModifier.java @@ -0,0 +1,4 @@ +// "final" "false" +sealed interface Parent permits Child {} + +interface Child {} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeRecordSubclass.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeRecordSubclass.java new file mode 100644 index 000000000000..bee6f5e1a31f --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeRecordSubclass.java @@ -0,0 +1,4 @@ +// "Implement 'Parent'" "true" +sealed interface Parent permits User {} + +record User(int age) {} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeSimple.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeSimple.java new file mode 100644 index 000000000000..5122991f4860 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeSimple.java @@ -0,0 +1,4 @@ +// "final" "true" +sealed class Parent permits Child {} + +class Child {} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeSubInterfaceWithParents.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeSubInterfaceWithParents.java new file mode 100644 index 000000000000..160559df9596 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeSubInterfaceWithParents.java @@ -0,0 +1,8 @@ +// "non-sealed" "true" +sealed interface Parent permits Child, Foo {} + +interface OtherParent {} + +non-sealed interface Foo extends Parent {} + +interface Child extends OtherParent {} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeSubclassWithParent.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeSubclassWithParent.java new file mode 100644 index 000000000000..78f87dfaba57 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeSubclassWithParent.java @@ -0,0 +1,6 @@ +// "final" "false" +sealed class Parent permits Child {} + +class OtherParent {} + +class Child extends OtherParent {} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeSubclassWithParentObject.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeSubclassWithParentObject.java new file mode 100644 index 000000000000..ad72d12f8329 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass/beforeSubclassWithParentObject.java @@ -0,0 +1,4 @@ +// "final" "true" +sealed class Parent permits Child {} + +class Child extends Object {} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/quickFix/ExtendSealedClassTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/quickFix/ExtendSealedClassTest.java new file mode 100644 index 000000000000..b94e8869f582 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/quickFix/ExtendSealedClassTest.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.daemon.quickFix; + +import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase; +import com.intellij.testFramework.LightProjectDescriptor; +import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase; +import org.jetbrains.annotations.NotNull; + +public class ExtendSealedClassTest extends LightQuickFixParameterizedTestCase { + + @Override + protected @NotNull LightProjectDescriptor getProjectDescriptor() { + return LightJavaCodeInsightFixtureTestCase.JAVA_15; + } + + @Override + protected String getBasePath() { + return "/codeInsight/daemonCodeAnalyzer/quickFix/extendSealedClass"; + } + +} diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/intention/choice/DefaultIntentionActionWithChoice.kt b/platform/analysis-impl/src/com/intellij/codeInsight/intention/choice/DefaultIntentionActionWithChoice.kt index 515690d2cb7d..f35e7dd7da32 100644 --- a/platform/analysis-impl/src/com/intellij/codeInsight/intention/choice/DefaultIntentionActionWithChoice.kt +++ b/platform/analysis-impl/src/com/intellij/codeInsight/intention/choice/DefaultIntentionActionWithChoice.kt @@ -15,6 +15,7 @@ interface DefaultIntentionActionWithChoice : IntentionActionWithChoice { val result = ArrayList() result.add(title) diff --git a/plugins/devkit/devkit-core/resources/messages/DevKitBundle.properties b/plugins/devkit/devkit-core/resources/messages/DevKitBundle.properties index cc1c0292740a..22337e008de3 100644 --- a/plugins/devkit/devkit-core/resources/messages/DevKitBundle.properties +++ b/plugins/devkit/devkit-core/resources/messages/DevKitBundle.properties @@ -271,10 +271,6 @@ inspections.registration.problems.option.check.java.actions=Check Java Actions inspections.registration.problems.option.check.java.code=Check Java Code inspections.registration.problems.quickfix.read-only=Class ''{0}'' is read-only inspections.registration.problems.quickfix.create.constructor=Create no-argument constructor -inspections.registration.problems.fix.implement.extend.family.name=Implement/Extend required base class -inspections.registration.problems.fix.implement.name=Implement ''{0}'' -inspections.registration.problems.fix.extend.name=Extend ''{0}'' -inspections.registration.problems.fix.implement.invalid=Invalid inspections.registration.problems.incompatible.message=According to its registration in plugin.xml, the class should {0} ''{1}'' inspections.registration.problems.abstract=Plugin component class must not be abstract diff --git a/plugins/devkit/devkit-core/src/inspections/RegistrationProblemsInspection.java b/plugins/devkit/devkit-core/src/inspections/RegistrationProblemsInspection.java index fdcbc95ca48c..69385dff081b 100644 --- a/plugins/devkit/devkit-core/src/inspections/RegistrationProblemsInspection.java +++ b/plugins/devkit/devkit-core/src/inspections/RegistrationProblemsInspection.java @@ -1,6 +1,7 @@ // 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 org.jetbrains.idea.devkit.inspections; +import com.intellij.codeInsight.daemon.impl.quickfix.ImplementOrExtendFix; import com.intellij.codeInsight.intention.QuickFixFactory; import com.intellij.codeInspection.InspectionManager; import com.intellij.codeInspection.LocalQuickFix; @@ -21,7 +22,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.idea.devkit.DevKitBundle; import org.jetbrains.idea.devkit.inspections.quickfix.CreateConstructorFix; -import org.jetbrains.idea.devkit.inspections.quickfix.ImplementOrExtendFix; import org.jetbrains.idea.devkit.util.ActionType; import org.jetbrains.idea.devkit.util.ComponentType; import org.jetbrains.idea.devkit.util.DescriptorUtil; @@ -128,7 +128,7 @@ public class RegistrationProblemsInspection extends DevKitInspectionBase { DevKitBundle.message("keyword.implement") : DevKitBundle.message("keyword.extend"), compClass.getQualifiedName()), isOnTheFly, - ImplementOrExtendFix.createFix(compClass, checkedClass, isOnTheFly), + ImplementOrExtendFix.createFixes(nameIdentifier, compClass, checkedClass, isOnTheFly), ProblemHighlightType.GENERIC_ERROR_OR_WARNING)); } } @@ -327,7 +327,7 @@ public class RegistrationProblemsInspection extends DevKitInspectionBase { addProblem(token, DevKitBundle.message("inspections.registration.problems.action.incompatible.class", type.myClassName), ProblemHighlightType.GENERIC_ERROR_OR_WARNING, myOnTheFly, - ImplementOrExtendFix.createFix(psiClass, actionClass, myOnTheFly)); + ImplementOrExtendFix.createFixes(token, psiClass, actionClass, myOnTheFly)); } } final ConstructorType noArgCtor = ConstructorType.getNoArgCtor(actionClass); diff --git a/plugins/devkit/devkit-core/src/inspections/quickfix/ImplementOrExtendFix.java b/plugins/devkit/devkit-core/src/inspections/quickfix/ImplementOrExtendFix.java deleted file mode 100644 index 2448725af373..000000000000 --- a/plugins/devkit/devkit-core/src/inspections/quickfix/ImplementOrExtendFix.java +++ /dev/null @@ -1,93 +0,0 @@ -// 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 org.jetbrains.idea.devkit.inspections.quickfix; - -import com.intellij.codeInspection.LocalQuickFix; -import com.intellij.codeInspection.ProblemDescriptor; -import com.intellij.openapi.project.Project; -import com.intellij.pom.Navigatable; -import com.intellij.psi.*; -import com.intellij.util.IncorrectOperationException; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.idea.devkit.DevKitBundle; - -public final class ImplementOrExtendFix extends BaseFix { - private final SmartPsiElementPointer myCompClassPointer; - - private ImplementOrExtendFix(@NotNull PsiClass compClass, - @NotNull PsiClass checkedClass, - boolean onTheFly) { - super(checkedClass, onTheFly); - myCompClassPointer = SmartPointerManager.createPointer(compClass); - } - - public static LocalQuickFix @NotNull [] createFix(PsiClass compClass, PsiClass checkedClass, boolean onTheFly) { - ImplementOrExtendFix fix = null; - - if (compClass.isInterface() && compClass.getImplementsList() != null) { - fix = new ImplementOrExtendFix(compClass, checkedClass, onTheFly); - } - else if (!compClass.isInterface()) { - PsiReferenceList extendsList = checkedClass.getExtendsList(); - if (extendsList != null) { - if (extendsList.getReferenceElements().length == 0) { - fix = new ImplementOrExtendFix(compClass, checkedClass, onTheFly); - } - else if (extendsList.getReferenceElements().length == 1) { - // check for explicit "extends Object" case - PsiClassType javaLangObject = PsiType.getJavaLangObject(checkedClass.getManager(), - checkedClass.getResolveScope()); - if (extendsList.getReferencedTypes()[0].equals(javaLangObject)) { - fix = new ImplementOrExtendFix(compClass, checkedClass, onTheFly); - } - } - } - } - return fix != null ? new LocalQuickFix[]{fix} : LocalQuickFix.EMPTY_ARRAY; - } - - @Override - @NotNull - public String getName() { - PsiClass clazz = myCompClassPointer.getElement(); - if (clazz == null) return DevKitBundle.message("inspections.registration.problems.fix.implement.invalid"); - - if (clazz.isInterface()) { - return DevKitBundle.message("inspections.registration.problems.fix.implement.name", clazz.getQualifiedName()); - } - return DevKitBundle.message("inspections.registration.problems.fix.extend.name", clazz.getQualifiedName()); - } - - @Override - @NotNull - public String getFamilyName() { - return DevKitBundle.message("inspections.registration.problems.fix.implement.extend.family.name"); - } - - @Override - protected void doFix(Project project, ProblemDescriptor descriptor, boolean external) throws IncorrectOperationException { - PsiElement element = myPointer.getElement(); - PsiClass compClass = myCompClassPointer.getElement(); - if (!(element instanceof PsiClass)) return; - if (compClass == null) return; - PsiClass clazz = (PsiClass)element; - PsiElementFactory elementFactory = JavaPsiFacade.getInstance(clazz.getProject()).getElementFactory(); - PsiClassType compType = elementFactory.createType(compClass); - - PsiReferenceList list; - if (compClass.isInterface()) { - list = clazz.getImplementsList(); - assert list != null; - } - else { - PsiReferenceList extendsList = clazz.getExtendsList(); - assert extendsList != null; - if (extendsList.getReferencedTypes().length > 0) { - extendsList.getReferenceElements()[0].delete(); - } - list = extendsList; - } - - PsiElement e = list.add(elementFactory.createReferenceElementByType(compType)); - if (myOnTheFly && external && e instanceof Navigatable) ((Navigatable)e).navigate(true); - } -}