Sealed classes: report narrow down conversion for sealed classes that have only sealed / final subclasses and don't have interface inheritors in hierarchy (IDEA-257414)

see JEP-397 - Sealed classes and conversions for details

GitOrigin-RevId: 297386ccd61f7762508b630a60894360215d9e11
This commit is contained in:
Artemiy Sartakov
2021-01-13 04:57:37 +00:00
committed by intellij-monorepo-bot
parent 5beaaf1f5c
commit e35b38f161
8 changed files with 190 additions and 4 deletions
@@ -13,6 +13,8 @@ import org.jetbrains.annotations.Nullable;
import java.util.*;
import static com.intellij.psi.util.TypeConversionUtil.canConvertSealedTo;
public final class TypeConstraints {
/**
* Top constraint (no restriction; any non-primitive value satisfies this)
@@ -374,6 +376,8 @@ public final class TypeConstraints {
}
if (other instanceof ExactClass) {
PsiClass otherClass = ((ExactClass)other).myClass;
if (otherClass.hasModifierProperty(PsiModifier.SEALED)) return canConvertSealedTo(otherClass, myClass);
if (myClass.hasModifierProperty(PsiModifier.SEALED)) return canConvertSealedTo(myClass, otherClass);
if (myClass.isInterface() && otherClass.isInterface()) return true;
if (myClass.isInterface() && !otherClass.hasModifierProperty(PsiModifier.FINAL)) return true;
if (otherClass.isInterface() && !myClass.hasModifierProperty(PsiModifier.FINAL)) return true;
@@ -11,6 +11,7 @@ import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.tree.IElementType;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
@@ -252,6 +253,15 @@ public final class TypeConversionUtil {
PsiManager manager = fromClass.getManager();
final LanguageLevel languageLevel = toClassType.getLanguageLevel();
// jep-397
if (languageLevel.isAtLeast(LanguageLevel.JDK_16_PREVIEW)) {
if (fromClass.hasModifierProperty(PsiModifier.SEALED)) {
if (!canConvertSealedTo(fromClass, toClass)) return false;
}
else if (toClass.hasModifierProperty(PsiModifier.SEALED)) {
if (!canConvertSealedTo(toClass, fromClass)) return false;
}
}
if (!fromClass.isInterface()) {
if (toClass.isInterface()) {
return (!fromClass.hasModifierProperty(PsiModifier.FINAL) || fromClass.isInheritor(toClass, true)) &&
@@ -320,6 +330,74 @@ public final class TypeConversionUtil {
}
}
/**
* Check if sealed class can be narrowed down to a given interface.
* Sealed class can be narrowed down to an interface in one of the following cases:
* <ul>
* <li>sealed class implements interface
* <li>sealed class have at least one non-sealed subclass
* <li>at least one of final/sealed subclasses of sealed parent implement interface
* </ul>
*
* <p>Note that sealed subclasses are checked recursively, e.g. in hierarchy:</p>
*
* <code>
* <p>sealed class Parent {}
* <p>sealed class A extends Parent {}
* <p>final class C extends A {}
* </code>
* <p>all classes would be checked.</p>
* <br>
* <p>See JEP-397 for more details.</p>
*/
public static boolean canConvertSealedTo(@NotNull PsiClass sealedClass, @NotNull PsiClass psiClass) {
PsiReferenceList permitsList = sealedClass.getPermitsList();
List<PsiClass> sealedSubClasses = new SmartList<>();
boolean hasClassInheritors;
if (permitsList == null) {
Set<PsiClass> subClasses = findDirectSubClassesInFile(sealedClass);
hasClassInheritors = subClasses.stream().anyMatch(subClass -> subClassExtendsClass(subClass, psiClass, sealedSubClasses));
}
else {
hasClassInheritors = Arrays.stream(permitsList.getReferencedTypes())
.map(t -> t.resolve())
.anyMatch(subClass -> subClassExtendsClass(subClass, psiClass, sealedSubClasses));
}
return hasClassInheritors || sealedSubClasses.stream().anyMatch(subClass -> canConvertSealedTo(subClass, psiClass));
}
private static @NotNull Set<PsiClass> findDirectSubClassesInFile(@NotNull PsiClass sealedClass) {
Set<PsiClass> subClasses = new HashSet<>();
sealedClass.getContainingFile().accept(new JavaElementVisitor() {
@Override
public void visitJavaFile(PsiJavaFile file) {
for (PsiClass psiClass : file.getClasses()) {
visitClass(psiClass);
}
}
@Override
public void visitClass(PsiClass psiClass) {
for (PsiClass inner : psiClass.getInnerClasses()) {
visitClass(inner);
}
if (psiClass.isInheritor(sealedClass, false)) {
subClasses.add(psiClass);
}
}
});
return subClasses;
}
private static boolean subClassExtendsClass(@Nullable PsiClass subClass,
@NotNull PsiClass psiClass,
@NotNull List<PsiClass> sealedClasses) {
if (subClass == null) return false;
if (subClass.hasModifierProperty(PsiModifier.NON_SEALED) || subClass.isInheritor(psiClass, true)) return true;
if (subClass.hasModifierProperty(PsiModifier.SEALED)) sealedClasses.add(subClass);
return false;
}
@NotNull
private static PsiClassType obtainSafeSuperType(@NotNull PsiTypeParameter typeParameter) {
final PsiClassType superType = typeParameter.getSuperTypes()[0];
@@ -0,0 +1,79 @@
interface Foo {
sealed class A {}
final class B extends A {}
sealed class C {}
non-sealed class D extends C {}
sealed class E {}
sealed class F extends E {}
final class G extends F implements Foo {}
sealed class H permits I {}
final class I extends H {}
sealed class J permits K {}
non-sealed class K extends J {}
final class L implements M {}
sealed interface M permits L {}
class N {}
sealed interface O permits P {}
final class P extends N implements O {}
class R {}
sealed interface S permits T {}
final class T extends U implements S {}
class U extends R {}
class V {}
sealed interface W permits X {}
final class X implements W {}
static void testA(A a) {
if (<error descr="Inconvertible types; cannot cast 'Foo.A' to 'Foo'">a instanceof Foo</error>)
System.out.println("It's a Foo");
}
static void testC(C c) {
if (c instanceof Foo)
System.out.println("It's a Foo");
}
static void testE(E e) {
if (e instanceof Foo)
System.out.println("It's a Foo");
}
static void testH(H h) {
if (<error descr="Inconvertible types; cannot cast 'Foo.H' to 'Foo'">h instanceof Foo</error>)
System.out.println("It's a Foo");
}
static void testJ(J j) {
if (j instanceof Foo)
System.out.println("It's a Foo");
}
static void testL(L l) {
if (l instanceof M)
System.out.println("It's a M");
}
static void testN(N n) {
if (n instanceof O)
System.out.println("It's an O");
}
static void testR(R r) {
if (r instanceof S)
System.out.println("It's a S");
}
static void testV(V v) {
if (<error descr="Inconvertible types; cannot cast 'Foo.V' to 'Foo.W'">v instanceof W</error>)
System.out.println("It's a W");
}
}
@@ -0,0 +1,9 @@
class Foo {
static void test(Foo foo) {
if (<error descr="Inconvertible types; cannot cast 'Foo' to 'I'"><warning descr="Condition 'foo instanceof I' is always 'false'">foo instanceof I</warning></error>)
System.out.println("This is a Foo");
}
}
sealed interface I {}
final class C implements I {}
@@ -0,0 +1,13 @@
interface Foo {
sealed class A permits B, C {}
final class B extends A {}
sealed class C extends A permits D {}
final class D extends C {}
static void test(A a) {
if (<error descr="Inconvertible types; cannot cast 'Foo.A' to 'Foo'"><warning descr="Condition 'a instanceof Foo' is always 'false'">a instanceof Foo</warning></error>)
System.out.println("This is a Foo");
}
}
@@ -15,7 +15,7 @@ public class LightSealedTypesHighlightingTest extends LightJavaCodeInsightFixtur
@NotNull
@Override
protected LightProjectDescriptor getProjectDescriptor() {
return JAVA_15;
return JAVA_16;
}
public void testSealedTypesBasics() { doTest(); }
@@ -26,6 +26,7 @@ public class LightSealedTypesHighlightingTest extends LightJavaCodeInsightFixtur
myFixture.addClass("package p; public class P extends A {}");
doTest();
}
public void testSealedClassCast() { doTest(); }
private void doTest() {
myFixture.configureByFile(getTestName(false) + ".java");
@@ -5,11 +5,11 @@ import com.intellij.JavaTestUtil;
import com.intellij.testFramework.LightProjectDescriptor;
import org.jetbrains.annotations.NotNull;
public class DataFlowInspection14Test extends DataFlowInspectionTestCase {
public class DataFlowInspection16Test extends DataFlowInspectionTestCase {
@NotNull
@Override
protected LightProjectDescriptor getProjectDescriptor() {
return JAVA_15;
return JAVA_16;
}
@Override
@@ -27,4 +27,6 @@ public class DataFlowInspection14Test extends DataFlowInspectionTestCase {
}
public void testSwitchExpressionAndLambdaInlining() { doTest(); }
public void testRecordAccessorStability() { doTest(); }
public void testSealedClassCast() { doTest(); }
public void testCastToSealedInterface() { doTest(); }
}
@@ -30,7 +30,7 @@ import org.junit.runners.Suite;
DataFlowInspection8Test.class,
DataFlowInspection9Test.class,
DataFlowInspection10Test.class,
DataFlowInspection14Test.class,
DataFlowInspection16Test.class,
DataFlowInspectionHeavyTest.class,
DataFlowInspectionAncientTest.class,
DataFlowInspectionCancellingTest.class,