Sealed classes: update hierarchy fixes (IJ-CR-2554):

1. add missing same file inheritors to permits list during safe delete or create subclass refactoring
2. inline isParentSealed for inline to anonymous class refactoring
3. do not make sealed class final when removing last inheritor (just remove sealed modifier)

GitOrigin-RevId: acf3652f12144446116c5d71ea1722b06b9f6b92
This commit is contained in:
Artemiy Sartakov
2020-08-10 09:19:20 +00:00
committed by intellij-monorepo-bot
parent 984fe0931e
commit ec30bf3a97
23 changed files with 91 additions and 28 deletions
@@ -56,13 +56,12 @@ import com.intellij.psi.util.PsiUtilCore;
import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.SmartHashSet;
import com.siyeh.ig.psiutils.ClassUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.*;
public class CreateSubclassAction extends BaseIntentionAction {
private static final Logger LOG = Logger.getInstance(CreateSubclassAction.class);
@@ -265,8 +264,13 @@ public class CreateSubclassAction extends BaseIntentionAction {
ref = (PsiJavaCodeReferenceElement)targetClass.getExtendsList().add(ref);
}
if (psiClass.hasModifierProperty(PsiModifier.SEALED)) {
String createdClassName = targetClass.getQualifiedName();
FillPermitsListFix.fillPermitsList(psiClass, Collections.singleton(createdClassName));
String createdClassName = Objects.requireNonNull(targetClass.getQualifiedName());
SmartHashSet<String> missingInheritors = new SmartHashSet<>();
missingInheritors.add(createdClassName);
if (psiClass.getPermitsList() == null) {
missingInheritors.addAll(ClassUtils.findSameFileInheritors(psiClass));
}
FillPermitsListFix.fillPermitsList(psiClass, missingInheritors);
}
if (psiClass.hasTypeParameters() || includeClassName) {
final Editor editor = CodeInsightUtil.positionCursorAtLBrace(project, targetClass.getContainingFile(), targetClass);
@@ -76,7 +76,8 @@ public class InlineToAnonymousClassHandler extends JavaInlineActionHandler {
if (element instanceof PsiAnonymousClass) return false;
PsiClass psiClass = (PsiClass)element;
if (!findClassInheritors(psiClass)) return false;
return !isParentSealed(psiClass);
boolean hasMethods = PsiTreeUtil.findChildOfType(psiClass, PsiMember.class) != null;
return !hasMethods && !ClassUtils.hasSealedParent(psiClass);
}
private static boolean findClassInheritors(final PsiClass element) {
@@ -95,11 +96,6 @@ public class InlineToAnonymousClassHandler extends JavaInlineActionHandler {
return inheritors.isEmpty();
}
private static boolean isParentSealed(@NotNull PsiClass psiClass) {
if (PsiTreeUtil.findChildOfType(psiClass, PsiMember.class) == null) return false;
return ClassUtils.hasSealedParent(psiClass);
}
@Override
public boolean canInlineElementInEditor(PsiElement element, Editor editor) {
if (canInlineElement(element)) {
@@ -23,9 +23,12 @@ import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.IncorrectOperationException;
import com.siyeh.ig.psiutils.ClassUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
/**
* @author ven
@@ -55,8 +58,8 @@ public class SafeDeleteExtendsClassUsageInfo extends SafeDeleteReferenceUsageInf
final PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(refClass.getProject());
boolean targetTypeParameter = myExtendingClass instanceof PsiTypeParameter;
copyExtendsList(refClass.getExtendsList(), refClass.isInterface() == myExtendingClass.isInterface() || targetTypeParameter, elementFactory);
copyExtendsList(refClass.getImplementsList(), targetTypeParameter, elementFactory);
copyExtendsList(refClass, refClass.getExtendsList(), refClass.isInterface() == myExtendingClass.isInterface() || targetTypeParameter, elementFactory);
copyExtendsList(refClass, refClass.getImplementsList(), targetTypeParameter, elementFactory);
getElement().delete();
@@ -69,7 +72,8 @@ public class SafeDeleteExtendsClassUsageInfo extends SafeDeleteReferenceUsageInf
}
}
private void copyExtendsList(@Nullable PsiReferenceList sourceExtendsList,
private void copyExtendsList(@NotNull PsiClass classToRemove,
@Nullable PsiReferenceList sourceExtendsList,
boolean targetExtends,
PsiElementFactory elementFactory) {
if (sourceExtendsList != null) {
@@ -82,7 +86,17 @@ public class SafeDeleteExtendsClassUsageInfo extends SafeDeleteReferenceUsageInf
PsiElement extendsRef = targetExtendsList.add(elementFactory.createReferenceElementByType(classType));
PsiClass classToExtend = classType.resolve();
if (classToExtend != null && classToExtend.hasModifierProperty(PsiModifier.SEALED)) {
FillPermitsListFix.fillPermitsList(classToExtend, Collections.singleton(myExtendingClass.getQualifiedName()));
String extendingClassName = Objects.requireNonNull(myExtendingClass.getQualifiedName());
if (classToExtend.getPermitsList() == null) {
if (classToExtend.getContainingFile() != myExtendingClass.getContainingFile()) {
Collection<String> missingInheritors = ClassUtils.findSameFileInheritors(classToExtend, classToRemove);
missingInheritors.add(extendingClassName);
FillPermitsListFix.fillPermitsList(classToExtend, missingInheritors);
}
}
else {
FillPermitsListFix.fillPermitsList(classToExtend, Collections.singleton(extendingClassName));
}
}
CodeStyleManager.getInstance(myExtendingClass.getProject()).reformat(extendsRef);
}
@@ -0,0 +1,2 @@
public class Subclass extends Superclass {
}
@@ -0,0 +1,5 @@
public sealed class Superclass permits B, Subclass {
public void foo() {}
}
final class B extends Superclass {}
@@ -0,0 +1,5 @@
public sealed class Superclass {
public void foo() {}
}
final class B extends Superclass {}
@@ -1,6 +1,6 @@
// "Unimplement Class" "true"
public sealed interface A permits B {}
final class C {}
class C {}
non-sealed class B implements A {}
@@ -1,5 +1,5 @@
// "Unimplement Class" "true"
final class A {
class A {
}
@@ -1,4 +1,4 @@
// "Unimplement Class" "true"
final class A {}
class A {}
final class B {}
@@ -1,4 +1,4 @@
// "Unimplement Class" "true"
final class A {}
class A {}
class B {}
@@ -1,5 +1,5 @@
// "Unimplement Class" "true"
final class A {}
class A {}
sealed class B permits C {}
@@ -1,4 +1,4 @@
final class A {
class A {
void doSmth() {}
}
@@ -1,5 +1,5 @@
sealed interface SuperSuper permits Super, Test {}
final class Super implements SuperSuper {}
class Super implements SuperSuper {}
non-sealed class Test implements SuperSuper {}
@@ -1,5 +1,5 @@
sealed interface SuperSuper permits Super, Test {}
final class Super implements SuperSuper {}
class Super implements SuperSuper {}
non-sealed class Test implements SuperSuper {}
@@ -1,3 +1,3 @@
final class Super {}
class Super {}
class Test {}
@@ -1,4 +1,4 @@
final class Parent {
class Parent {
void test() {
Object obj = new Parent();
@@ -0,0 +1,2 @@
public final class C implements GrandParent {
}
@@ -0,0 +1,6 @@
sealed interface GrandParent permits A, C, Child {
}
final class Child implements GrandParent {}
final class A implements GrandParent {}
@@ -0,0 +1,2 @@
public final class C extends Parent {
}
@@ -0,0 +1,8 @@
sealed interface GrandParent {
}
public sealed class Parent<caret> implements GrandParent permits C, Child {}
final class Child extends Parent implements GrandParent {}
final class A implements GrandParent {}
@@ -54,6 +54,10 @@ public class CreateSubclassTest extends LightMultiFileTestCase {
doTest();
}
public void testSealedWithSameFileInheritors() {
doTest();
}
private void doTestInner() {
doTest(() -> {
PsiClass superClass = myFixture.findClass("Test");
@@ -393,6 +393,11 @@ public class SafeDeleteTest extends MultiFileTestCase {
doSingleFileTest();
}
public void testSealedGrandParent() {
LanguageLevelProjectExtension.getInstance(getProject()).setLanguageLevel(LanguageLevel.JDK_15_PREVIEW);
doTest("Parent");
}
private void doTest(@NonNls final String qClassName) {
doTest((rootDir, rootAfter) -> this.performAction(qClassName));
}
@@ -18,10 +18,13 @@ package com.siyeh.ig.psiutils;
import com.intellij.codeInspection.concurrencyAnnotations.JCiPUtil;
import com.intellij.psi.*;
import com.intellij.psi.impl.light.LightElement;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.searches.DirectClassInheritorsSearch;
import com.intellij.psi.search.searches.MethodReferencesSearch;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Processor;
import com.intellij.util.Query;
import com.intellij.util.containers.ContainerUtil;
@@ -341,7 +344,7 @@ public final class ClassUtils {
/**
* Removes exChild class reference from permits list of a parent.
* If this was the last element in permits list then parent class is converted to final if possible (sealed modifier is removed).
* If this was the last element in permits list then sealed modifier of parent class is removed.
*/
public static void removeFromPermitsList(@NotNull PsiClass parent, @NotNull PsiClass exChild) {
PsiReferenceList permitsList = parent.getPermitsList();
@@ -354,7 +357,14 @@ public final class ClassUtils {
PsiModifierList modifiers = parent.getModifierList();
if (modifiers == null) return;
modifiers.setModifierProperty(PsiModifier.SEALED, false);
if (!parent.hasModifierProperty(PsiModifier.ABSTRACT)) modifiers.setModifierProperty(PsiModifier.FINAL, true);
}
public static Collection<String> findSameFileInheritors(@NotNull PsiClass psiClass, PsiClass @NotNull ... classesToExclude) {
GlobalSearchScope fileScope = GlobalSearchScope.fileScope(psiClass.getContainingFile());
return DirectClassInheritorsSearch.search(psiClass, fileScope)
.filtering(inheritor -> !ArrayUtil.contains(inheritor, classesToExclude))
.mapping(inheritor -> inheritor.getQualifiedName())
.findAll();
}
public static boolean hasSealedParent(@NotNull PsiClass psiClass) {