From e35b38f16189cc15848d07fcb88745ab9219f28b Mon Sep 17 00:00:00 2001 From: Artemiy Sartakov Date: Wed, 13 Jan 2021 11:55:58 +0700 Subject: [PATCH] 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 --- .../dataFlow/TypeConstraints.java | 4 + .../intellij/psi/util/TypeConversionUtil.java | 78 ++++++++++++++++++ .../SealedClassCast.java | 79 +++++++++++++++++++ .../fixture/CastToSealedInterface.java | 9 +++ .../dataFlow/fixture/SealedClassCast.java | 13 +++ .../LightSealedTypesHighlightingTest.java | 3 +- ...est.java => DataFlowInspection16Test.java} | 6 +- .../DataFlowInspectionTestSuite.java | 2 +- 8 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advHighlightingSealedTypes/SealedClassCast.java create mode 100644 java/java-tests/testData/inspection/dataFlow/fixture/CastToSealedInterface.java create mode 100644 java/java-tests/testData/inspection/dataFlow/fixture/SealedClassCast.java rename java/java-tests/testSrc/com/intellij/java/codeInspection/{DataFlowInspection14Test.java => DataFlowInspection16Test.java} (84%) diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/TypeConstraints.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/TypeConstraints.java index a7d0507c83f0..161a5dc78cdd 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/TypeConstraints.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/TypeConstraints.java @@ -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; diff --git a/java/java-psi-api/src/com/intellij/psi/util/TypeConversionUtil.java b/java/java-psi-api/src/com/intellij/psi/util/TypeConversionUtil.java index fb2502412190..e64213fcd596 100644 --- a/java/java-psi-api/src/com/intellij/psi/util/TypeConversionUtil.java +++ b/java/java-psi-api/src/com/intellij/psi/util/TypeConversionUtil.java @@ -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: + * + * + *

Note that sealed subclasses are checked recursively, e.g. in hierarchy:

+ * + * + *

sealed class Parent {} + *

sealed class A extends Parent {} + *

final class C extends A {} + * + *

all classes would be checked.

+ *
+ *

See JEP-397 for more details.

+ */ + public static boolean canConvertSealedTo(@NotNull PsiClass sealedClass, @NotNull PsiClass psiClass) { + PsiReferenceList permitsList = sealedClass.getPermitsList(); + List sealedSubClasses = new SmartList<>(); + boolean hasClassInheritors; + if (permitsList == null) { + Set 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 findDirectSubClassesInFile(@NotNull PsiClass sealedClass) { + Set 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 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]; diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advHighlightingSealedTypes/SealedClassCast.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advHighlightingSealedTypes/SealedClassCast.java new file mode 100644 index 000000000000..ff14ac9d4726 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/advHighlightingSealedTypes/SealedClassCast.java @@ -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 (a instanceof Foo) + 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 (h instanceof Foo) + 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 (v instanceof W) + System.out.println("It's a W"); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/dataFlow/fixture/CastToSealedInterface.java b/java/java-tests/testData/inspection/dataFlow/fixture/CastToSealedInterface.java new file mode 100644 index 000000000000..778b4745193a --- /dev/null +++ b/java/java-tests/testData/inspection/dataFlow/fixture/CastToSealedInterface.java @@ -0,0 +1,9 @@ +class Foo { + static void test(Foo foo) { + if (foo instanceof I) + System.out.println("This is a Foo"); + } +} + +sealed interface I {} +final class C implements I {} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/dataFlow/fixture/SealedClassCast.java b/java/java-tests/testData/inspection/dataFlow/fixture/SealedClassCast.java new file mode 100644 index 000000000000..69865394be45 --- /dev/null +++ b/java/java-tests/testData/inspection/dataFlow/fixture/SealedClassCast.java @@ -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 (a instanceof Foo) + System.out.println("This is a Foo"); + } + +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/LightSealedTypesHighlightingTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/LightSealedTypesHighlightingTest.java index c6af04befb1b..275d625201bc 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/LightSealedTypesHighlightingTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/LightSealedTypesHighlightingTest.java @@ -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"); diff --git a/java/java-tests/testSrc/com/intellij/java/codeInspection/DataFlowInspection14Test.java b/java/java-tests/testSrc/com/intellij/java/codeInspection/DataFlowInspection16Test.java similarity index 84% rename from java/java-tests/testSrc/com/intellij/java/codeInspection/DataFlowInspection14Test.java rename to java/java-tests/testSrc/com/intellij/java/codeInspection/DataFlowInspection16Test.java index 1d26f46337c4..9c285c4e4dd7 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInspection/DataFlowInspection14Test.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/DataFlowInspection16Test.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(); } } \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInspection/DataFlowInspectionTestSuite.java b/java/java-tests/testSrc/com/intellij/java/codeInspection/DataFlowInspectionTestSuite.java index 27e4299e756d..180161a9678a 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInspection/DataFlowInspectionTestSuite.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/DataFlowInspectionTestSuite.java @@ -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,