8246778: Compiler implementation for Sealed Classes (Second Preview)

Co-authored-by: Vicente Romero <vromero@openjdk.org>
Co-authored-by: Harold Seigel <hseigel@openjdk.org>
Reviewed-by: lfoltan, mchung, alanb, mcimadamore, chegar
This commit is contained in:
Jan Lahoda 2020-12-07 11:11:31 +00:00
parent 09707dd4f2
commit 637b0c64b0
16 changed files with 1127 additions and 83 deletions
src
hotspot/share/prims
java.base/share
classes/java/lang
native/libjava
jdk.compiler/share/classes/com/sun/tools/javac
test

@ -2128,16 +2128,32 @@ JVM_ENTRY(jobjectArray, JVM_GetPermittedSubclasses(JNIEnv* env, jclass current))
JvmtiVMObjectAllocEventCollector oam;
Array<u2>* subclasses = ik->permitted_subclasses();
int length = subclasses == NULL ? 0 : subclasses->length();
objArrayOop r = oopFactory::new_objArray(SystemDictionary::String_klass(),
objArrayOop r = oopFactory::new_objArray(SystemDictionary::Class_klass(),
length, CHECK_NULL);
objArrayHandle result(THREAD, r);
int count = 0;
for (int i = 0; i < length; i++) {
int cp_index = subclasses->at(i);
// This returns <package-name>/<class-name>.
Symbol* klass_name = ik->constants()->klass_name_at(cp_index);
assert(klass_name != NULL, "Unexpected null klass_name");
Handle perm_subtype_h = java_lang_String::create_from_symbol(klass_name, CHECK_NULL);
result->obj_at_put(i, perm_subtype_h());
Klass* k = ik->constants()->klass_at(cp_index, THREAD);
if (HAS_PENDING_EXCEPTION) {
if (PENDING_EXCEPTION->is_a(SystemDictionary::VirtualMachineError_klass())) {
return NULL; // propagate VMEs
}
CLEAR_PENDING_EXCEPTION;
continue;
}
if (k->is_instance_klass()) {
result->obj_at_put(count++, k->java_mirror());
}
}
if (count < length) {
objArrayOop r2 = oopFactory::new_objArray(SystemDictionary::Class_klass(),
count, CHECK_NULL);
objArrayHandle result2(THREAD, r2);
for (int i = 0; i < count; i++) {
result2->obj_at_put(i, result->obj_at(i));
}
return (jobjectArray)JNIHandles::make_local(THREAD, result2());
}
return (jobjectArray)JNIHandles::make_local(THREAD, result());
}

@ -58,12 +58,14 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import jdk.internal.loader.BootLoader;
@ -200,8 +202,6 @@ public final class Class<T> implements java.io.Serializable,
private static final int ENUM = 0x00004000;
private static final int SYNTHETIC = 0x00001000;
private static final ClassDesc[] EMPTY_CLASS_DESC_ARRAY = new ClassDesc[0];
private static native void registerNatives();
static {
registerNatives();
@ -3020,6 +3020,37 @@ public final class Class<T> implements java.io.Serializable,
}
}
/*
* Checks if a client loaded in ClassLoader ccl is allowed to access the provided
* classes under the current package access policy. If access is denied,
* throw a SecurityException.
*
* NOTE: this method should only be called if a SecurityManager is active
* classes must be non-empty
* all classes provided must be loaded by the same ClassLoader
* NOTE: this method does not support Proxy classes
*/
private static void checkPackageAccessForPermittedSubclasses(SecurityManager sm,
final ClassLoader ccl, Class<?>[] subClasses) {
final ClassLoader cl = subClasses[0].getClassLoader0();
if (ReflectUtil.needsPackageAccessCheck(ccl, cl)) {
Set<String> packages = new HashSet<>();
for (Class<?> c : subClasses) {
if (Proxy.isProxyClass(c))
throw new InternalError("a permitted subclass should not be a proxy class: " + c);
String pkg = c.getPackageName();
if (pkg != null && !pkg.isEmpty()) {
packages.add(pkg);
}
}
for (String pkg : packages) {
sm.checkPackageAccess(pkg);
}
}
}
/**
* Add a package name prefix if the name is not absolute Remove leading "/"
* if name is absolute
@ -4357,47 +4388,87 @@ public final class Class<T> implements java.io.Serializable,
* may be removed in a future release, or upgraded to permanent
* features of the Java language.}
*
* Returns an array containing {@code ClassDesc} objects representing all the
* direct subclasses or direct implementation classes permitted to extend or
* Returns an array containing {@code Class} objects representing the
* direct subinterfaces or subclasses permitted to extend or
* implement this class or interface if it is sealed. The order of such elements
* is unspecified. If this {@code Class} object represents a primitive type,
* {@code void}, an array type, or a class or interface that is not sealed,
* an empty array is returned.
*
* @return an array of class descriptors of all the permitted subclasses of this class or interface
* For each class or interface {@code C} which is recorded as a permitted
* direct subinterface or subclass of this class or interface,
* this method attempts to obtain the {@code Class}
* object for {@code C} (using {@linkplain #getClassLoader() the defining class
* loader} of the current {@code Class} object).
* The {@code Class} objects which can be obtained and which are direct
* subinterfaces or subclasses of this class or interface,
* are indicated by elements of the returned array. If a {@code Class} object
* cannot be obtained, it is silently ignored, and not included in the result
* array.
*
* @return an array of {@code Class} objects of the permitted subclasses of this class or interface
*
* @throws SecurityException
* If a security manager, <i>s</i>, is present and the caller's
* class loader is not the same as or an ancestor of the class
* loader for that returned class and invocation of {@link
* SecurityManager#checkPackageAccess s.checkPackageAccess()}
* denies access to the package of any class in the returned array.
*
* @jls 8.1 Class Declarations
* @jls 9.1 Interface Declarations
* @since 15
*/
@jdk.internal.PreviewFeature(feature=jdk.internal.PreviewFeature.Feature.SEALED_CLASSES, essentialAPI=false)
public ClassDesc[] permittedSubclasses() {
String[] subclassNames;
if (isArray() || isPrimitive() || (subclassNames = getPermittedSubclasses0()).length == 0) {
return EMPTY_CLASS_DESC_ARRAY;
@CallerSensitive
public Class<?>[] getPermittedSubclasses() {
Class<?>[] subClasses;
if (isArray() || isPrimitive() || (subClasses = getPermittedSubclasses0()).length == 0) {
return EMPTY_CLASS_ARRAY;
}
ClassDesc[] constants = new ClassDesc[subclassNames.length];
int i = 0;
for (String subclassName : subclassNames) {
try {
constants[i++] = ClassDesc.of(subclassName.replace('/', '.'));
} catch (IllegalArgumentException iae) {
throw new InternalError("Invalid type in permitted subclasses information: " + subclassName, iae);
if (subClasses.length > 0) {
if (Arrays.stream(subClasses).anyMatch(c -> !isDirectSubType(c))) {
subClasses = Arrays.stream(subClasses)
.filter(this::isDirectSubType)
.toArray(s -> new Class<?>[s]);
}
}
return constants;
if (subClasses.length > 0) {
// If we return some classes we need a security check:
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkPackageAccessForPermittedSubclasses(sm,
ClassLoader.getClassLoader(Reflection.getCallerClass()),
subClasses);
}
}
return subClasses;
}
private boolean isDirectSubType(Class<?> c) {
if (isInterface()) {
for (Class<?> i : c.getInterfaces(/* cloneArray */ false)) {
if (i == this) {
return true;
}
}
} else {
return c.getSuperclass() == this;
}
return false;
}
/**
* * {@preview Associated with sealed classes, a preview feature of the Java language.
* {@preview Associated with sealed classes, a preview feature of the Java language.
*
* This method is associated with <i>sealed classes</i>, a preview
* feature of the Java language. Preview features
* may be removed in a future release, or upgraded to permanent
* features of the Java language.}
*
* Returns {@code true} if and only if this {@code Class} object represents a sealed class or interface.
* If this {@code Class} object represents a primitive type, {@code void}, or an array type, this method returns
* Returns {@code true} if and only if this {@code Class} object represents
* a sealed class or interface. If this {@code Class} object represents a
* primitive type, {@code void}, or an array type, this method returns
* {@code false}.
*
* @return {@code true} if and only if this {@code Class} object represents a sealed class or interface.
@ -4412,8 +4483,8 @@ public final class Class<T> implements java.io.Serializable,
if (isArray() || isPrimitive()) {
return false;
}
return permittedSubclasses().length != 0;
return getPermittedSubclasses().length != 0;
}
private native String[] getPermittedSubclasses0();
private native Class<?>[] getPermittedSubclasses0();
}

@ -81,7 +81,7 @@ static JNINativeMethod methods[] = {
{"getNestMembers0", "()[" CLS, (void *)&JVM_GetNestMembers},
{"getRecordComponents0", "()[" RC, (void *)&JVM_GetRecordComponents},
{"isRecord0", "()Z", (void *)&JVM_IsRecord},
{"getPermittedSubclasses0", "()[" STR, (void *)&JVM_GetPermittedSubclasses},
{"getPermittedSubclasses0", "()[" CLS, (void *)&JVM_GetPermittedSubclasses},
};
#undef OBJ

@ -1629,32 +1629,69 @@ public class Types {
}
/**
* Is t is castable to s?<br>
* Is t castable to s?<br>
* s is assumed to be an erased type.<br>
* (not defined for Method and ForAll types).
*/
public boolean isCastable(Type t, Type s, Warner warn) {
// if same type
if (t == s)
return true;
// if one of the types is primitive
if (t.isPrimitive() != s.isPrimitive()) {
t = skipTypeVars(t, false);
return (isConvertible(t, s, warn)
|| (s.isPrimitive() &&
isSubtype(boxedClass(s).type, t)));
}
boolean result;
if (warn != warnStack.head) {
try {
warnStack = warnStack.prepend(warn);
checkUnsafeVarargsConversion(t, s, warn);
return isCastable.visit(t,s);
result = isCastable.visit(t,s);
} finally {
warnStack = warnStack.tail;
}
} else {
return isCastable.visit(t,s);
result = isCastable.visit(t,s);
}
if (result && (t.tsym.isSealed() || s.tsym.isSealed())) {
return (t.isCompound() || s.isCompound()) ?
false :
!areDisjoint((ClassSymbol)t.tsym, (ClassSymbol)s.tsym);
}
return result;
}
// where
private boolean areDisjoint(ClassSymbol ts, ClassSymbol ss) {
if (isSubtype(ts.type, ss.type)) {
return false;
}
// if both are classes or both are interfaces, shortcut
if (ts.isInterface() == ss.isInterface() && isSubtype(ss.type, ts.type)) {
return false;
}
if (ts.isInterface() && !ss.isInterface()) {
/* so ts is interface but ss is a class
* an interface is disjoint from a class if the class is disjoint form the interface
*/
return areDisjoint(ss, ts);
}
// a final class that is not subtype of ss is disjoint
if (!ts.isInterface() && ts.isFinal()) {
return true;
}
// if at least one is sealed
if (ts.isSealed() || ss.isSealed()) {
// permitted subtypes have to be disjoint with the other symbol
ClassSymbol sealedOne = ts.isSealed() ? ts : ss;
ClassSymbol other = sealedOne == ts ? ss : ts;
return sealedOne.permitted.stream().allMatch(sym -> areDisjoint((ClassSymbol)sym, other));
}
return false;
}
private TypeRelation isCastable = new TypeRelation() {
public Boolean visitType(Type t, Type s) {

@ -1303,7 +1303,10 @@ public class Check {
SEALED | NON_SEALED)
&& checkDisjoint(pos, flags,
SEALED,
FINAL | NON_SEALED)) {
FINAL | NON_SEALED)
&& checkDisjoint(pos, flags,
SEALED,
ANNOTATION)) {
// skip
}
return flags & (mask | ~ExtendedStandardFlags) | implicit;

@ -4264,11 +4264,19 @@ public class JavacParser implements Parser {
private boolean allowedAfterSealedOrNonSealed(Token next, boolean local, boolean currentIsNonSealed) {
return local ?
switch (next.kind) {
case MONKEYS_AT, ABSTRACT, FINAL, STRICTFP, CLASS, INTERFACE, ENUM -> true;
case MONKEYS_AT -> {
Token afterNext = S.token(2);
yield afterNext.kind != INTERFACE || currentIsNonSealed;
}
case ABSTRACT, FINAL, STRICTFP, CLASS, INTERFACE, ENUM -> true;
default -> false;
} :
switch (next.kind) {
case MONKEYS_AT, PUBLIC, PROTECTED, PRIVATE, ABSTRACT, STATIC, FINAL, STRICTFP, CLASS, INTERFACE, ENUM -> true;
case MONKEYS_AT -> {
Token afterNext = S.token(2);
yield afterNext.kind != INTERFACE || currentIsNonSealed;
}
case PUBLIC, PROTECTED, PRIVATE, ABSTRACT, STATIC, FINAL, STRICTFP, CLASS, INTERFACE, ENUM -> true;
case IDENTIFIER -> isNonSealedIdentifier(next, currentIsNonSealed ? 3 : 1) || next.name() == names.sealed;
default -> false;
};

@ -21,11 +21,10 @@
* questions.
*/
// This class has entries in its PermittedSubclasses attribute that do not exist.
// Test that this does not prevent JVM_GetPermittedSubclasses() from returning
// their names.
// This class has an entry in its PermittedSubclasses attribute that does not exist.
// Test that JVM_GetPermittedSubclasses() only returns the existing class.
//
// sealed class NoLoadSubclasses permits iDontExist, I/Dont/Exist/Either { }
// sealed class NoLoadSubclasses permits ExistingClassFile, I/Dont/Exist/Either { }
//
class NoLoadSubclasses {
0xCAFEBABE;
@ -47,7 +46,7 @@ class NoLoadSubclasses {
Utf8 "NoLoadSubclasses.java"; // #12 at 0x73
Utf8 "PermittedSubclasses"; // #13 at 0x89
class #15; // #14 at 0x9D
Utf8 "iDontExist"; // #15 at 0xA0
Utf8 "ExistingClassFile"; // #15 at 0xA0
class #17; // #16 at 0xAA
Utf8 "I/Dont/Exist/Either"; // #17 at 0xAD
} // Constant Pool
@ -99,7 +98,70 @@ class NoLoadSubclasses {
} // Attributes
} // end class NoLoadSubclasses
// class ExistingClassFile extends NoLoadSubclasses { }
//
class ExistingClassFile {
0xCAFEBABE;
0; // minor version
55; // version
[] { // Constant Pool
; // first element is empty
Method #3 #10; // #1
class #11; // #2
class #12; // #3
Utf8 "<init>"; // #4
Utf8 "()V"; // #5
Utf8 "Code"; // #6
Utf8 "LineNumberTable"; // #7
Utf8 "SourceFile"; // #8
Utf8 "ExistingClassFile.java"; // #9
NameAndType #4 #5; // #10
Utf8 "ExistingClassFile"; // #11
Utf8 "NoLoadSubclasses"; // #12
} // Constant Pool
0x0021; // access
#2;// this_cpx
#3;// super_cpx
[] { // Interfaces
} // Interfaces
[] { // Fields
} // Fields
[] { // Methods
{ // method
0x0001; // access
#4; // name_index
#5; // descriptor_index
[] { // Attributes
Attr(#6) { // Code
1; // max_stack
1; // max_locals
Bytes[]{
0x2AB70001B1;
}
[] { // Traps
} // end Traps
[] { // Attributes
Attr(#7) { // LineNumberTable
[] { // line_number_table
0 1;
}
} // end LineNumberTable
} // Attributes
} // end Code
} // Attributes
}
} // Methods
[] { // Attributes
Attr(#8) { // SourceFile
#9;
} // end SourceFile
} // Attributes
} // end class ExistingClassFile
// This class contains an empty PermittedSubclasses attribute. Test that
// this causes an exception to get thrown.

@ -25,11 +25,12 @@
* @test
* @bug 8225056
* @compile GetPermittedSubclasses.jcod
* @compile --enable-preview -source ${jdk.version} noSubclass/BaseC.java noSubclass/BaseI.java noSubclass/Impl1.java
* @compile --enable-preview -source ${jdk.version} noSubclass/Impl2.java
* @compile --enable-preview -source ${jdk.version} GetPermittedSubclassesTest.java
* @run main/othervm --enable-preview GetPermittedSubclassesTest
*/
import java.lang.constant.ClassDesc;
import java.util.ArrayList;
// Test Class GetPermittedSubtpes() and Class.isSealed() APIs.
@ -50,11 +51,11 @@ public class GetPermittedSubclassesTest {
final class Final4 {}
public static void testSealedInfo(Class<?> c, String[] expected) {
Object[] permitted = c.permittedSubclasses();
var permitted = c.getPermittedSubclasses();
if (permitted.length != expected.length) {
throw new RuntimeException(
"Unexpected number of permitted subclasses for: " + c.toString());
"Unexpected number of permitted subclasses for: " + c.toString() + "(" + java.util.Arrays.asList(permitted));
}
if (permitted.length > 0) {
@ -65,7 +66,7 @@ public class GetPermittedSubclassesTest {
// Create ArrayList of permitted subclasses class names.
ArrayList<String> permittedNames = new ArrayList<String>();
for (int i = 0; i < permitted.length; i++) {
permittedNames.add(((ClassDesc)permitted[i]).descriptorString());
permittedNames.add(permitted[i].getName());
}
if (permittedNames.size() != expected.length) {
@ -102,10 +103,11 @@ public class GetPermittedSubclassesTest {
}
public static void main(String... args) throws Throwable {
testSealedInfo(SealedI1.class, new String[] {"LGetPermittedSubclassesTest$NotSealed;",
"LGetPermittedSubclassesTest$Sub1;",
"LGetPermittedSubclassesTest$Extender;"});
testSealedInfo(Sealed1.class, new String[] {"LGetPermittedSubclassesTest$Sub1;"});
testSealedInfo(SealedI1.class, new String[] {"GetPermittedSubclassesTest$NotSealed",
"GetPermittedSubclassesTest$Sub1",
"GetPermittedSubclassesTest$Extender"});
testSealedInfo(Sealed1.class, new String[] {"GetPermittedSubclassesTest$Sub1"});
testSealedInfo(Final4.class, new String[] { });
testSealedInfo(NotSealed.class, new String[] { });
@ -115,8 +117,8 @@ public class GetPermittedSubclassesTest {
// Test class with an empty PermittedSubclasses attribute.
testBadSealedClass("NoSubclasses", "PermittedSubclasses attribute is empty");
// Test returning names of non-existing classes.
testSealedInfo(NoLoadSubclasses.class, new String[]{"LiDontExist;", "LI/Dont/Exist/Either;"});
// Test returning only names of existing classes.
testSealedInfo(NoLoadSubclasses.class, new String[]{"ExistingClassFile" });
// Test that loading a class with a corrupted PermittedSubclasses attribute
// causes a ClassFormatError.
@ -134,5 +136,10 @@ public class GetPermittedSubclassesTest {
// Test that loading a sealed class with an empty class name in its PermittedSubclasses
// attribute causes a ClassFormatError.
testBadSealedClass("EmptyPermittedSubclassEntry", "Illegal class name \"\" in class file");
//test type enumerated in the PermittedSubclasses attribute,
//which are not direct subtypes of the current class are not returned:
testSealedInfo(noSubclass.BaseC.class, new String[] {"noSubclass.ImplCIntermediate"});
testSealedInfo(noSubclass.BaseI.class, new String[] {"noSubclass.ImplIIntermediateI", "noSubclass.ImplIIntermediateC"});
}
}

@ -0,0 +1,26 @@
/*
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package noSubclass;
public sealed class BaseC permits ImplC, ImplCIntermediate, ImplCIndirect {}

@ -0,0 +1,26 @@
/*
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package noSubclass;
public sealed interface BaseI permits ImplII, ImplIIntermediateI, ImplIIndirectI, ImplIC, ImplIIntermediateC, ImplIIndirectC {}

@ -0,0 +1,36 @@
/*
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package noSubclass;
final class ImplC extends BaseC {}
non-sealed class ImplCIntermediate extends BaseC {}
final class ImplCIndirect extends BaseC {}
non-sealed interface ImplII extends BaseI {}
non-sealed interface ImplIIntermediateI extends BaseI {}
non-sealed interface ImplIIndirectI extends ImplIIntermediateI, BaseI {}
final class ImplIC implements BaseI {}
non-sealed class ImplIIntermediateC implements BaseI {}
final class ImplIIndirectC extends ImplIIntermediateC implements BaseI {}

@ -0,0 +1,36 @@
/*
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package noSubclass;
final class ImplC {}
non-sealed class ImplCIntermediate extends BaseC {}
final class ImplCIndirect extends ImplCIntermediate {}
interface ImplII {}
non-sealed interface ImplIIntermediateI extends BaseI {}
interface ImplIIndirectI extends ImplIIntermediateI {}
final class ImplIC {}
non-sealed class ImplIIntermediateC implements BaseI {}
final class ImplIIndirectC extends ImplIIntermediateC {}

@ -86,8 +86,8 @@ public class SealedClassesReflectionTest {
public void testSealedClasses(Class<?> cls) {
assertTrue(cls.isSealed());
assertTrue(!Modifier.isFinal(cls.getModifiers()));
assertTrue(cls.permittedSubclasses() != null);
assertTrue(cls.permittedSubclasses().length > 0);
assertTrue(cls.getPermittedSubclasses() != null);
assertTrue(cls.getPermittedSubclasses().length > 0);
}
@DataProvider(name = "notSealedClasses")
@ -110,8 +110,8 @@ public class SealedClassesReflectionTest {
@Test(dataProvider = "notSealedClasses")
public void testNotSealedClasses(Class<?> cls) {
assertTrue(!cls.isSealed());
assertTrue(cls.permittedSubclasses() != null);
assertTrue(cls.permittedSubclasses().length == 0);
assertTrue(cls.getPermittedSubclasses() != null);
assertTrue(cls.getPermittedSubclasses().length == 0);
}
@DataProvider(name = "non_sealedClasses")
@ -128,8 +128,8 @@ public class SealedClassesReflectionTest {
assertTrue(!cls.isSealed());
assertTrue(!Modifier.isFinal(cls.getModifiers()));
assertTrue((cls.getSuperclass() != null && cls.getSuperclass().isSealed()) || Arrays.stream(cls.getInterfaces()).anyMatch(Class::isSealed));
assertTrue(cls.permittedSubclasses() != null);
assertTrue(cls.permittedSubclasses().length == 0);
assertTrue(cls.getPermittedSubclasses() != null);
assertTrue(cls.getPermittedSubclasses().length == 0);
}
@DataProvider(name = "reflectionData")
@ -215,10 +215,10 @@ public class SealedClassesReflectionTest {
throws ReflectiveOperationException
{
assertTrue(sealedClass.isSealed());
assertTrue(sealedClass.permittedSubclasses().length == numberOfSubclasses);
assertTrue(sealedClass.getPermittedSubclasses().length == numberOfSubclasses);
int i = 0;
for (ClassDesc cd : sealedClass.permittedSubclasses()) {
assertTrue(cd.displayName().equals(subclassDescriptors[i]), "expected: " + subclassDescriptors[i] + " found: " + cd.displayName());
for (Class<?> cd : sealedClass.getPermittedSubclasses()) {
assertTrue(cd.getName().equals(subclassDescriptors[i]), "expected: " + subclassDescriptors[i] + " found: " + cd.getName());
i++;
}
i = 0;

@ -0,0 +1,275 @@
/*
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* @test
* @bug 8246778
* @summary Test that security checks occur for getPermittedSubclasses
* @library /test/lib
* @modules java.compiler
* @build jdk.test.lib.compiler.CompilerUtils jdk.test.lib.compiler.ModuleInfoMaker TestSecurityManagerChecks
* @run main/othervm --enable-preview TestSecurityManagerChecks named
* @run main/othervm --enable-preview TestSecurityManagerChecks unnamed
*/
import java.io.IOException;
import java.lang.module.Configuration;
import java.lang.module.ModuleFinder;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import jdk.test.lib.compiler.*;
public class TestSecurityManagerChecks {
private static final ClassLoader OBJECT_CL = null;
public static void main(String[] args) throws Throwable {
if ("named".equals(args[0])) {
runNamedModuleTest();
} else {
runUnnamedModuleTest();
}
}
private static void runNamedModuleTest() throws Throwable {
Path classes = compileNamedModuleTest();
URL[] testClassPath = getTestClassPath();
//need to use a different ClassLoader to run the test, so that the checks are performed:
ClassLoader testCL = new URLClassLoader(testClassPath, OBJECT_CL);
testCL.loadClass("TestSecurityManagerChecks")
.getDeclaredMethod("doRunNamedModuleTest", Path.class)
.invoke(null, classes);
}
public static void doRunNamedModuleTest(Path classes) throws Throwable {
Configuration testConfig = ModuleLayer.boot()
.configuration()
.resolve(ModuleFinder.of(),
ModuleFinder.of(classes),
List.of("test"));
ModuleLayer testLayer = ModuleLayer.boot()
.defineModulesWithOneLoader(testConfig,
OBJECT_CL);
// First get hold of the target classes before we enable security
Class<?> sealed = Class.forName(testLayer.findModule("test").get(), "test.Base");
//try without a SecurityManager:
checkPermittedSubclasses(sealed, "test.a.ImplA1",
"test.a.ImplA2",
"test.b.ImplB");
String[] denyPackageAccess = new String[1];
int[] checkPackageAccessCallCount = new int[1];
//try with a SecurityManager:
SecurityManager sm = new SecurityManager() {
@Override
public void checkPackageAccess(String pkg) {
if (pkg.startsWith("test.")) {
checkPackageAccessCallCount[0]++;
}
if (Objects.equals(denyPackageAccess[0], pkg)) {
throw new SecurityException();
}
}
};
System.setSecurityManager(sm);
denyPackageAccess[0] = "test";
//passes - does not return a class from package "test":
checkPermittedSubclasses(sealed, "test.a.ImplA1",
"test.a.ImplA2",
"test.b.ImplB");
if (checkPackageAccessCallCount[0] != 2) {
throw new AssertionError("Unexpected call count: " +
checkPackageAccessCallCount[0]);
}
denyPackageAccess[0] = "test.a";
try {
sealed.getPermittedSubclasses();
throw new Error("getPermittedSubclasses incorrectly succeeded for " +
sealed.getName());
} catch (SecurityException e) {
System.out.println("OK - getPermittedSubclasses for " + sealed.getName() +
" got expected exception: " + e);
}
}
private static Path compileNamedModuleTest() throws IOException {
Path base = Paths.get(".", "named");
Path src = base.resolve("src");
Path classes = base.resolve("classes");
ModuleInfoMaker maker = new ModuleInfoMaker(src);
maker.writeJavaFiles("test",
"module test {}",
"package test; public sealed interface Base permits test.a.ImplA1, test.a.ImplA2, test.b.ImplB, test.c.ImplC {}",
"package test.a; public final class ImplA1 implements test.Base {}",
"package test.a; public final class ImplA2 implements test.Base {}",
"package test.b; public final class ImplB implements test.Base {}",
"package test.c; public final class ImplC implements test.Base {}"
);
if (!CompilerUtils.compile(src, classes.resolve("test"), "--enable-preview", "-source", System.getProperty("java.specification.version"))) {
throw new AssertionError("Compilation didn't succeed!");
}
Files.delete(classes.resolve("test").resolve("test").resolve("c").resolve("ImplC.class"));
return classes;
}
private static void runUnnamedModuleTest() throws Throwable {
Path classes = compileUnnamedModuleTest();
URL[] testClassPath = getTestClassPath();
//need to use a different ClassLoader to run the test, so that the checks are performed:
ClassLoader testCL = new URLClassLoader(testClassPath, OBJECT_CL);
testCL.loadClass("TestSecurityManagerChecks")
.getDeclaredMethod("doRunUnnamedModuleTest", Path.class)
.invoke(null, classes);
}
public static void doRunUnnamedModuleTest(Path classes) throws Throwable {
ClassLoader unnamedModuleCL =
new URLClassLoader(new URL[] {classes.toUri().toURL()}, OBJECT_CL);
// First get hold of the target classes before we enable security
Class<?> sealed = unnamedModuleCL.loadClass("test.Base");
//try without a SecurityManager:
checkPermittedSubclasses(sealed, "test.ImplA1",
"test.ImplA2",
"test.ImplB");
String[] denyPackageAccess = new String[1];
int[] checkPackageAccessCallCount = new int[1];
//try with a SecurityManager:
SecurityManager sm = new SecurityManager() {
@Override
public void checkPackageAccess(String pkg) {
if (pkg.equals("test")) {
checkPackageAccessCallCount[0]++;
}
if (Objects.equals(denyPackageAccess[0], pkg)) {
throw new SecurityException();
}
}
};
System.setSecurityManager(sm);
denyPackageAccess[0] = "test.unknown";
//passes - does not return a class from package "test.unknown":
checkPermittedSubclasses(sealed, "test.ImplA1",
"test.ImplA2",
"test.ImplB");
if (checkPackageAccessCallCount[0] != 1) {
throw new AssertionError("Unexpected call count: " +
checkPackageAccessCallCount[0]);
}
denyPackageAccess[0] = "test";
try {
sealed.getPermittedSubclasses();
throw new Error("getPermittedSubclasses incorrectly succeeded for " +
sealed.getName());
} catch (SecurityException e) {
System.out.println("OK - getPermittedSubclasses for " + sealed.getName() +
" got expected exception: " + e);
}
}
private static Path compileUnnamedModuleTest() throws IOException {
Path base = Paths.get(".", "unnamed");
Path src = base.resolve("src");
Path classes = base.resolve("classes");
ModuleInfoMaker maker = new ModuleInfoMaker(src);
maker.writeJavaFiles("test",
"module test {}",
"package test; public sealed interface Base permits ImplA1, ImplA2, ImplB, ImplC {}",
"package test; public final class ImplA1 implements test.Base {}",
"package test; public final class ImplA2 implements test.Base {}",
"package test; public final class ImplB implements test.Base {}",
"package test; public final class ImplC implements test.Base {}"
);
Files.delete(src.resolve("test").resolve("module-info.java"));
if (!CompilerUtils.compile(src.resolve("test"), classes, "--enable-preview", "-source", System.getProperty("java.specification.version"))) {
throw new AssertionError("Compilation didn't succeed!");
}
Files.delete(classes.resolve("test").resolve("ImplC.class"));
return classes;
}
private static void checkPermittedSubclasses(Class<?> c, String... expected) {
Class<?>[] subclasses = c.getPermittedSubclasses();
List<String> subclassesNames = Arrays.stream(subclasses)
.map(Class::getName)
.collect(Collectors.toList());
if (!subclassesNames.equals(Arrays.asList(expected))) {
throw new AssertionError("Incorrect permitted subclasses: " +
subclassesNames);
}
}
private static URL[] getTestClassPath() {
return Arrays.stream(System.getProperty("test.class.path")
.split(System.getProperty("path.separator")))
.map(TestSecurityManagerChecks::path2URL)
.toArray(s -> new URL[s]);
}
private static URL path2URL(String p) {
try {
return Path.of(p).toUri().toURL();
} catch (MalformedURLException ex) {
throw new AssertionError(ex);
}
}
}

@ -77,9 +77,13 @@ public class BinaryCompatibilityTests extends TestRunner {
@Test
public void testCompatibilityAfterMakingSuperclassSealed(Path base) throws Exception {
// sealing a super class which was not sealed, should fail with IncompatibleClassChangeError
/* If a class that was not declared sealed is changed to be declared sealed, then an
* IncompatibleClassChangeError is thrown if a binary of a pre-existing subclass of
* this class is loaded that is not contained in its permits clause
*/
testCompatibilityAfterModifyingSupertype(
base,
true,
"""
package pkg;
public class Super {
@ -103,16 +107,16 @@ public class BinaryCompatibilityTests extends TestRunner {
"""
package pkg;
class Sub extends Super {}
""",
true
"""
);
}
@Test
public void testCompatibilityAfterMakingSuperInterfaceSealed(Path base) throws Exception {
// sealing a super interface which was not sealed, should fail with IncompatibleClassChangeError
// test similar to testCompatibilityAfterMakingSuperclassSealed but with interfaces
testCompatibilityAfterModifyingSupertype(
base,
true,
"""
package pkg;
public interface Super {
@ -136,8 +140,7 @@ public class BinaryCompatibilityTests extends TestRunner {
"""
package pkg;
class Sub implements Super {}
""",
true
"""
);
}
@ -149,18 +152,17 @@ public class BinaryCompatibilityTests extends TestRunner {
*/
private void testCompatibilityAfterModifyingSupertype(
Path base,
boolean shouldFail,
String superClassCode1,
String superClassCode2,
String subClassCode,
boolean shouldFail) throws Exception {
String... subClassesCode) throws Exception {
Path src = base.resolve("src");
Path pkg = src.resolve("pkg");
Path superClass = pkg.resolve("Super");
Path sub = pkg.resolve("Sub");
// super class initially not sealed
tb.writeJavaFiles(superClass, superClassCode1);
tb.writeJavaFiles(sub, subClassCode);
tb.writeJavaFiles(sub, subClassesCode);
Path out = base.resolve("out");
Files.createDirectories(out);
@ -220,9 +222,12 @@ public class BinaryCompatibilityTests extends TestRunner {
@Test
public void testRemoveSealedModifierToClass(Path base) throws Exception {
// should execute without error
/* Changing a class that is declared sealed to no longer be declared
* sealed does not break compatibility with pre-existing binaries.
*/
testCompatibilityAfterModifyingSupertype(
base,
false,
"""
package pkg;
public sealed class Super permits pkg.Sub {
@ -244,16 +249,16 @@ public class BinaryCompatibilityTests extends TestRunner {
"""
package pkg;
final class Sub extends Super {}
""",
false
"""
);
}
@Test
public void testRemoveSealedModifierToInterface(Path base) throws Exception {
// should execute without error
// same as testRemoveSealedModifierToClass but with an interface
testCompatibilityAfterModifyingSupertype(
base,
false,
"""
package pkg;
public sealed interface Super permits pkg.Sub {
@ -275,14 +280,15 @@ public class BinaryCompatibilityTests extends TestRunner {
"""
package pkg;
final class Sub implements Super {}
""",
false
"""
);
}
@Test
public void testAddNonSealedModifierToClass(Path base) throws Exception {
// should execute without error
/* Changing a class that is not declared non-sealed to be declared
* non-sealed does not break compatibility with pre-existing binaries
*/
testCompatibilityOKAfterSubclassChange(
base,
"""
@ -307,7 +313,7 @@ public class BinaryCompatibilityTests extends TestRunner {
@Test
public void testAddNonSealedModifierToInterface(Path base) throws Exception {
// should execute without error
// same as `testAddNonSealedModifierToClass` but with interfaces
testCompatibilityOKAfterSubclassChange(
base,
"""
@ -332,7 +338,9 @@ public class BinaryCompatibilityTests extends TestRunner {
@Test
public void testRemoveNonSealedModifier(Path base) throws Exception {
// should execute without error
/* Changing a class that is declared non-sealed to no longer be declared
* non-sealed does not break compatibility with pre-existing binaries
*/
testCompatibilityOKAfterSubclassChange(
base,
"""
@ -357,7 +365,7 @@ public class BinaryCompatibilityTests extends TestRunner {
@Test
public void testRemoveNonSealedModifierFromInterface(Path base) throws Exception {
// should execute without error
// same as `testRemoveNonSealedModifier` but with interfaces
testCompatibilityOKAfterSubclassChange(
base,
"""
@ -452,9 +460,13 @@ public class BinaryCompatibilityTests extends TestRunner {
@Test
public void testAfterChangingPermitsClause(Path base) throws Exception {
// the VM will throw IncompatibleClassChangeError
/* If a class is removed from the set of permitted direct subclasses of
* a sealed class then an IncompatibleClassChangeError is thrown if the
* pre-existing binary of the removed class is loaded
*/
testCompatibilityAfterModifyingSupertype(
base,
true,
"""
package pkg;
public sealed class Super permits pkg.Sub1, Sub2 {
@ -480,8 +492,139 @@ public class BinaryCompatibilityTests extends TestRunner {
"""
package pkg;
final class Sub1 extends Super {}
""",
true
"""
);
}
@Test
public void testAfterChangingPermitsClause2(Path base) throws Exception {
/* If a class is removed from the set of permitted direct subclasses of
* a sealed class then an IncompatibleClassChangeError is thrown if the
* pre-existing binary of the removed class is loaded
*/
testCompatibilityAfterModifyingSupertype(
base,
true,
"""
package pkg;
public sealed class Super permits pkg.Sub1, pkg.Sub2 {
public static void main(String... args) {
pkg.Sub1 sub1 = new pkg.Sub1();
pkg.Sub2 sub2 = new pkg.Sub2();
System.out.println("done");
}
}
""",
"""
package pkg;
public sealed class Super permits pkg.Sub1 {
public static void main(String... args) {
pkg.Sub1 sub1 = new pkg.Sub1();
pkg.Sub2 sub2 = new pkg.Sub2();
System.out.println("done");
}
}
""",
"""
package pkg;
final class Sub1 extends Super {}
""",
"""
package pkg;
final class Sub2 extends Super {}
"""
);
}
@Test
public void testAfterChangingPermitsClause3(Path base) throws Exception {
/* Changing the set of permitted direct subclasses of a sealed class will
* not break compatibility with pre-existing binaries, provided that the
* total set of permitted direct subclasses of the sealed class loses no
* members
*/
String superClassCode1 =
"""
package pkg;
public sealed class Super permits pkg.Sub1 {
public static void main(String... args) {
pkg.Sub1 sub1 = new pkg.Sub1();
System.out.println("done");
}
}
""";
String subClass1Code =
"""
package pkg;
final class Sub1 extends Super {}
""";
String superClassCode2 =
"""
package pkg;
public sealed class Super permits pkg.Sub1, pkg.Sub2 {
public static void main(String... args) {
pkg.Sub1 sub1 = new pkg.Sub1();
pkg.Sub2 sub2 = new pkg.Sub2();
System.out.println("done");
}
}
""";
String subClass2Code =
"""
package pkg;
final class Sub2 extends Super {}
""";
Path src = base.resolve("src");
Path pkg = src.resolve("pkg");
Path superClass = pkg.resolve("Super");
Path sub1 = pkg.resolve("Sub1");
tb.writeJavaFiles(superClass, superClassCode1);
tb.writeJavaFiles(sub1, subClass1Code);
Path out = base.resolve("out");
Files.createDirectories(out);
new JavacTask(tb)
.options("--enable-preview",
"-source", Integer.toString(Runtime.version().feature()))
.outdir(out)
.files(findJavaFiles(pkg))
.run();
// let's execute to check that it's working
String output = new JavaTask(tb)
.vmOptions("--enable-preview")
.classpath(out.toString())
.classArgs("pkg.Super")
.run()
.writeAll()
.getOutput(Task.OutputKind.STDOUT);
// let's first check that it runs wo issues
if (!output.contains("done")) {
throw new AssertionError("execution of Super didn't finish");
}
// now lets change the super class
tb.writeJavaFiles(superClass, superClassCode2);
Path sub2 = pkg.resolve("Sub2");
tb.writeJavaFiles(sub2, subClass2Code);
new JavacTask(tb)
.options("--enable-preview",
"-source", Integer.toString(Runtime.version().feature()))
.classpath(out)
.outdir(out)
.files(findJavaFiles(superClass)[0], findJavaFiles(sub2)[0])
.run();
new JavaTask(tb)
.vmOptions("--enable-preview")
.classpath(out.toString())
.classArgs("pkg.Super")
.run(Task.Expect.SUCCESS);
}
}

@ -468,6 +468,26 @@ public class SealedCompilationTests extends CompilationTestCase {
final class D extends C { }
}
}
""",
"""
sealed class C {
void m() {
class L {
final class D extends C { }
}
}
}
""",
"""
sealed class C {
void m() {
class L {
void foo() {
final class D extends C { }
}
}
}
}
"""))
assertFail("compiler.err.local.classes.cant.extend.sealed", s);
}
@ -613,7 +633,7 @@ public class SealedCompilationTests extends CompilationTestCase {
float.class, float[].class, double.class, double[].class, char.class, char[].class, boolean.class, boolean[].class, void.class,
String[].class}) {
Assert.check(!c.isSealed());
Assert.check(c.permittedSubclasses().length == 0);
Assert.check(c.getPermittedSubclasses().length == 0);
}
}
@ -956,4 +976,282 @@ public class SealedCompilationTests extends CompilationTestCase {
assertOK(s);
}
}
public void testDoNotAllowSealedAnnotation() {
for (String s : List.of(
"""
sealed @interface A {}
non-sealed interface I extends A {}
"""
)) {
assertFail("compiler.err.expected4", s);
}
}
public void testNarrowConversion() {
for (String s : List.of(
"""
interface I {}
sealed class C permits D {}
final class D extends C {}
class Test {
void test () {
C c = null;
I i = (I) c;
}
}
""",
"""
sealed interface I permits C {}
final class C implements I {}
interface J {}
class Test {
void test () {
I i = null;
J j = (J) i;
}
}
""",
"""
interface I {}
sealed interface J permits C {}
final class C implements J {}
class Test {
void test () {
I i = null;
J j = (J) i;
}
}
""",
"""
sealed interface I permits A {}
sealed interface J permits B {}
final class A implements I {}
final class B implements J {}
class Test {
void test () {
I i = null;
J j = (J) i;
}
}
""",
"""
class C {}
sealed interface I permits A {}
final class A implements I {}
class Test {
void test () {
C c = null;
I i = (I) c;
}
}
""",
"""
final class C {}
interface I {}
class Test {
void test () {
C c = null;
I i = (I) c;
}
}
""",
"""
final class C {}
sealed interface I permits D {}
final class D implements I {}
class Test {
void test () {
C c = null;
I i = (I) c;
}
}
""",
"""
sealed class C permits D {}
final class D extends C {}
non-sealed interface I {}
class Test {
void test () {
C c = null;
I i = (I) c;
}
}
""",
"""
sealed class C permits D {}
final class D {}
sealed interface I permits E {}
final class E {}
class Test {
void test () {
C c = null;
I i = (I) c;
}
}
""",
"""
sealed class C permits D {}
sealed class D permits NS {}
non-sealed class NS extends D {}
sealed interface I permits E {}
final class E {}
class Test {
void test () {
C c = null;
I i = (I) c;
}
}
""",
"""
interface I {}
final class C {}
class Test {
void test () {
I i = null;
C c = (C) i;
}
}
""",
"""
interface I {}
sealed class C permits D {}
final class D {}
class Test {
void test () {
I i = null;
C c = (C) i;
}
}
""",
"""
sealed interface I permits D {}
final class D {}
class C {}
class Test {
void test () {
I i = null;
C c = (C) i;
}
}
""",
"""
sealed interface I permits D {}
final class D implements I {}
final class C {}
class Test {
void test () {
I i = null;
C c = (C) i;
}
}
""",
"""
sealed interface I permits D {}
final class D implements I {}
sealed class C permits E {}
final class E extends C {}
class Test {
void test () {
I i = null;
C c = (C) i;
}
}
"""
)) {
assertFail("compiler.err.prob.found.req", s);
}
for (String s : List.of(
"""
interface I {}
sealed class C permits D, E {}
non-sealed class D extends C {}
final class E extends C {}
class Test {
void test () {
C c = null;
I i = (I) c;
}
}
""",
"""
interface I {}
interface J {}
class Test {
void test () {
I i = null;
J j = (J) i;
}
}
""",
"""
class C {}
interface I {}
class Test {
void test () {
C c = null;
I i = (I) c;
}
}
""",
"""
interface I {}
class C {}
class Test {
void test () {
I i = null;
C c = (C) i;
}
}
""",
"""
sealed class C permits D {}
sealed class D extends C permits NS {}
non-sealed class NS extends D {}
interface I {}
class Test {
void test () {
C c = null;
I i = (I) c;
}
}
""",
"""
sealed interface A permits B { }
non-sealed interface B extends A { }
interface C { }
class D implements C, B { }
class Test {
void m(A a, C c) {
a = (A)c;
}
}
"""
)) {
assertOK(s);
}
}
}