Skip to content

Commit 29d34d3

Browse files
kevinoconnor7copybara-github
authored andcommitted
Forbid native JsTypes from being sealed or directly subtyping a sealed type.
This also adds the sealed bit to the J2CL AST and populates it in both the javac and Kotlin frontends. It is _not_ populated in the JDT frontend as the version we currently use lacks it. PiperOrigin-RevId: 886501391
1 parent f6a0164 commit 29d34d3

File tree

6 files changed

+91
-18
lines changed

6 files changed

+91
-18
lines changed

transpiler/java/com/google/j2cl/transpiler/ast/TypeDeclaration.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,8 @@ public boolean isAbstract() {
277277

278278
public abstract boolean isFinal();
279279

280+
public abstract boolean isSealed();
281+
280282
// TODO(b/322906767): Remove when the bug is fixed.
281283
private static final boolean PRESERVE_EQUALS_FOR_JSTYPE_INTERFACE =
282284
"true"
@@ -820,6 +822,7 @@ public static Builder newBuilder() {
820822
.setAnnotation(false)
821823
.setCapturingEnclosingInstance(false)
822824
.setFinal(false)
825+
.setSealed(false)
823826
.setFunctionalInterface(false)
824827
.setAnnotationsFactory(ImmutableList::of)
825828
.setJsFunctionInterface(false)
@@ -889,6 +892,8 @@ public abstract Builder setEnclosingMethodDescriptorFactory(
889892

890893
public abstract Builder setFinal(boolean isFinal);
891894

895+
public abstract Builder setSealed(boolean isSealed);
896+
892897
public abstract Builder setFunctionalInterface(boolean isFunctionalInterface);
893898

894899
public abstract Builder setOrigin(Origin origin);

transpiler/java/com/google/j2cl/transpiler/frontend/javac/JavaEnvironment.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1370,6 +1370,7 @@ TypeDeclaration createTypeDeclaration(final TypeElement typeElement) {
13701370
.setAnnotation(isAnnotation(typeElement))
13711371
.setCapturingEnclosingInstance(capturesEnclosingInstance((ClassSymbol) typeElement))
13721372
.setFinal(isFinal)
1373+
.setSealed(isSealed(typeElement))
13731374
.setFunctionalInterface(isFunctionalInterface(typeElement.asType()))
13741375
.setJsFunctionInterface(JsInteropUtils.isJsFunction(typeElement))
13751376
.setAnnotationsFactory(() -> createAnnotations(typeElement, isNullMarked))
@@ -1599,6 +1600,10 @@ private static boolean isFinal(Element element) {
15991600
return element.getModifiers().contains(Modifier.FINAL);
16001601
}
16011602

1603+
private static boolean isSealed(Element element) {
1604+
return element.getModifiers().contains(Modifier.SEALED);
1605+
}
1606+
16021607
private static boolean isVolatile(Element element) {
16031608
return element.getModifiers().contains(Modifier.VOLATILE);
16041609
}

transpiler/java/com/google/j2cl/transpiler/frontend/kotlin/KotlinEnvironment.kt

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import com.google.j2cl.transpiler.frontend.kotlin.ir.isJsType
6363
import com.google.j2cl.transpiler.frontend.kotlin.ir.isKFunctionOrKSuspendFunction
6464
import com.google.j2cl.transpiler.frontend.kotlin.ir.isNative
6565
import com.google.j2cl.transpiler.frontend.kotlin.ir.isNativeJsField
66+
import com.google.j2cl.transpiler.frontend.kotlin.ir.isSealed
6667
import com.google.j2cl.transpiler.frontend.kotlin.ir.j2clKind
6768
import com.google.j2cl.transpiler.frontend.kotlin.ir.j2clVisibility
6869
import com.google.j2cl.transpiler.frontend.kotlin.ir.jsName
@@ -274,6 +275,7 @@ internal class KotlinEnvironment(
274275
.setFunctionalInterface(irClass.isFunctionalInterface)
275276
.setHasAbstractModifier(irClass.isAbstract)
276277
.setFinal(irClass.isFinal)
278+
.setSealed(irClass.isSealed)
277279
.setLocal(irClass.isLocal && !irClass.isAnonymousObject)
278280
.setAnonymous(irClass.isAnonymousObject)
279281
.setJsType(irClass.isJsType)
@@ -971,16 +973,15 @@ private fun remapDeclarationTypeVarianceOntoArguments(
971973
arguments: List<IrTypeArgument>,
972974
originalTypeParams: List<IrTypeParameter>,
973975
replacementTypeParams: List<IrTypeParameter>,
974-
) =
975-
arguments.mapIndexed { index, argument ->
976-
if (
977-
argument is IrTypeProjection &&
978-
originalTypeParams[index].variance != Variance.INVARIANT &&
979-
replacementTypeParams[index].variance == Variance.INVARIANT &&
980-
argument.variance == Variance.INVARIANT
981-
) {
982-
makeTypeProjection(argument.type, originalTypeParams[index].variance)
983-
} else {
984-
argument
985-
}
976+
) = arguments.mapIndexed { index, argument ->
977+
if (
978+
argument is IrTypeProjection &&
979+
originalTypeParams[index].variance != Variance.INVARIANT &&
980+
replacementTypeParams[index].variance == Variance.INVARIANT &&
981+
argument.variance == Variance.INVARIANT
982+
) {
983+
makeTypeProjection(argument.type, originalTypeParams[index].variance)
984+
} else {
985+
argument
986986
}
987+
}

transpiler/java/com/google/j2cl/transpiler/frontend/kotlin/ir/IrUtils.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -363,12 +363,11 @@ private fun IrType.resolveWithTypeParameterMapping(
363363
if (arguments.isEmpty()) {
364364
return this
365365
}
366-
val resolvedArguments =
367-
arguments.map {
368-
it.typeOrNull?.resolveWithTypeParameterMapping(parameterTypeMapping)?.let { type ->
369-
makeTypeProjection(type, type.variance)
370-
} ?: it
371-
}
366+
val resolvedArguments = arguments.map {
367+
it.typeOrNull?.resolveWithTypeParameterMapping(parameterTypeMapping)?.let { type ->
368+
makeTypeProjection(type, type.variance)
369+
} ?: it
370+
}
372371
return toBuilder().apply { arguments = resolvedArguments }.buildSimpleType()
373372
}
374373
return this
@@ -540,6 +539,9 @@ val IrClass.isAbstract: Boolean
540539
val IrClass.isFinal: Boolean
541540
get() = modality == Modality.FINAL
542541

542+
val IrClass.isSealed: Boolean
543+
get() = modality == Modality.SEALED
544+
543545
val IrClass.isFromSource: Boolean
544546
get() = source.containingFile != SourceFile.NO_SOURCE_FILE
545547

transpiler/java/com/google/j2cl/transpiler/passes/JsInteropRestrictionsChecker.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1555,6 +1555,26 @@ private boolean checkNativeJsType(Type type) {
15551555
return false;
15561556
}
15571557

1558+
if (typeDeclaration.isSealed()) {
1559+
problems.error(
1560+
type.getSourcePosition(),
1561+
"Sealed %s '%s' cannot be a native JsType.",
1562+
typeDeclaration.isInterface() ? "interface" : "class",
1563+
readableDescription);
1564+
}
1565+
1566+
type.getSuperTypesStream()
1567+
.filter(typeDescriptor -> typeDescriptor.getTypeDeclaration().isSealed())
1568+
.forEach(
1569+
superType ->
1570+
problems.error(
1571+
type.getSourcePosition(),
1572+
"Native JsType '%s' cannot %s sealed %s '%s'.",
1573+
readableDescription,
1574+
type.isInterface() || superType.isClass() ? "extend" : "implement",
1575+
superType.isInterface() ? "interface" : "class",
1576+
superType.getReadableDescription()));
1577+
15581578
type.getSuperTypesStream()
15591579
.filter(Predicates.not(TypeDescriptors::isJavaLangObject))
15601580
.filter(Predicates.not(TypeDescriptor::isNative))

transpiler/javatests/com/google/j2cl/transpiler/JsInteropRestrictionsCheckerTest.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3438,6 +3438,46 @@ default void someMethod() {}
34383438
+ "'void Interface.someOtherMethod()'.");
34393439
}
34403440

3441+
public void testSealedClassExtendingNativeJsTypeSucceeds() {
3442+
assertTranspileSucceeds(
3443+
"test.Buggy",
3444+
"""
3445+
import jsinterop.annotations.*;
3446+
@JsType(isNative=true)
3447+
class NativeBase {}
3448+
sealed class SealedClass extends NativeBase { @JsConstructor SealedClass() {} }
3449+
final class Subtype extends SealedClass { @JsConstructor Subtype() {} }
3450+
""")
3451+
.assertNoWarnings();
3452+
}
3453+
3454+
public void testNativeJsTypeSealedTypeFails() {
3455+
assertTranspileFails(
3456+
"test.Foo",
3457+
"""
3458+
import jsinterop.annotations.*;
3459+
@JsType(isNative = true)
3460+
sealed class SealedNativeClass {}
3461+
@JsType(isNative = true)
3462+
sealed interface SealedNativeInterface {}
3463+
@JsType(isNative = true)
3464+
final class NativeSubtype extends SealedNativeClass implements SealedNativeInterface { @JsConstructor NativeSubtype() {} }
3465+
@JsType(isNative = true)
3466+
sealed class SealedNativeSubtype extends SealedNativeClass implements SealedNativeInterface {}
3467+
final class FinalSubtype extends SealedNativeSubtype { @JsConstructor FinalSubtype() {} }
3468+
""")
3469+
.assertErrorsWithoutSourcePosition(
3470+
"Sealed class 'SealedNativeClass' cannot be a native JsType.",
3471+
"Sealed interface 'SealedNativeInterface' cannot be a native JsType.",
3472+
"Native JsType 'NativeSubtype' cannot implement sealed interface"
3473+
+ " 'SealedNativeInterface'.",
3474+
"Native JsType 'NativeSubtype' cannot extend sealed class 'SealedNativeClass'.",
3475+
"Sealed class 'SealedNativeSubtype' cannot be a native JsType.",
3476+
"Native JsType 'SealedNativeSubtype' cannot implement sealed interface"
3477+
+ " 'SealedNativeInterface'.",
3478+
"Native JsType 'SealedNativeSubtype' cannot extend sealed class 'SealedNativeClass'.");
3479+
}
3480+
34413481
public void testJsOptionalSucceeds() {
34423482
newTesterWithDefaults()
34433483
.addCompilationUnit(

0 commit comments

Comments
 (0)