Skip to content

Commit f4847cc

Browse files
authored
Apply Flexible Types to Symbols from files compiled without Explicit Nulls (#22473)
This PR wraps the types of symbols from files compiled without explicit nulls in flexible types. This allows for interop between multiple files in cases where ``` class Unsafe_1 { def foo(s: String): String = { if (s == null) then "nullString" else s } } ``` compiled without explicit nulls can still be used in ``` def Flexible_2() = val s2: String | Null = "foo" val unsafe = new Unsafe_1() val s: String = unsafe.foo(s2) unsafe.foo("") unsafe.foo(null) ``` whereas the argument would have been a **strictly non-null** String, because of the flexible type, the function call is now permitted.
2 parents 2f32850 + 5a87908 commit f4847cc

File tree

8 files changed

+53
-14
lines changed

8 files changed

+53
-14
lines changed

compiler/src/dotty/tools/dotc/core/JavaNullInterop.scala renamed to compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala

+10-10
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import dotty.tools.dotc.core.Decorators.i
3535
* to handle the full spectrum of Scala types. Additionally, some kinds of symbols like constructors and
3636
* enum instances get special treatment.
3737
*/
38-
object JavaNullInterop {
38+
object ImplicitNullInterop {
3939

4040
/** Transforms the type `tp` of Java member `sym` to be explicitly nullable.
4141
* `tp` is needed because the type inside `sym` might not be set when this method is called.
@@ -55,11 +55,11 @@ object JavaNullInterop {
5555
*/
5656
def nullifyMember(sym: Symbol, tp: Type, isEnumValueDef: Boolean)(using Context): Type = trace(i"nullifyMember ${sym}, ${tp}"){
5757
assert(ctx.explicitNulls)
58-
assert(sym.is(JavaDefined), "can only nullify java-defined members")
5958

6059
// Some special cases when nullifying the type
61-
if isEnumValueDef || sym.name == nme.TYPE_ then
62-
// Don't nullify the `TYPE` field in every class and Java enum instances
60+
if isEnumValueDef || sym.name == nme.TYPE_ // Don't nullify the `TYPE` field in every class and Java enum instances
61+
|| sym.is(Flags.ModuleVal) // Don't nullify Modules
62+
then
6363
tp
6464
else if sym.name == nme.toString_ || sym.isConstructor || hasNotNullAnnot(sym) then
6565
// Don't nullify the return type of the `toString` method.
@@ -80,14 +80,14 @@ object JavaNullInterop {
8080
* but the result type is not nullable.
8181
*/
8282
private def nullifyExceptReturnType(tp: Type)(using Context): Type =
83-
new JavaNullMap(outermostLevelAlreadyNullable = true)(tp)
83+
new ImplicitNullMap(outermostLevelAlreadyNullable = true)(tp)
8484

85-
/** Nullifies a Java type by adding `| Null` in the relevant places. */
85+
/** Nullifies a type by adding `| Null` in the relevant places. */
8686
private def nullifyType(tp: Type)(using Context): Type =
87-
new JavaNullMap(outermostLevelAlreadyNullable = false)(tp)
87+
new ImplicitNullMap(outermostLevelAlreadyNullable = false)(tp)
8888

89-
/** A type map that implements the nullification function on types. Given a Java-sourced type, this adds `| Null`
90-
* in the right places to make the nulls explicit in Scala.
89+
/** A type map that implements the nullification function on types. Given a Java-sourced type or an
90+
* implicitly null type, this adds `| Null` in the right places to make the nulls explicit.
9191
*
9292
* @param outermostLevelAlreadyNullable whether this type is already nullable at the outermost level.
9393
* For example, `Array[String] | Null` is already nullable at the
@@ -97,7 +97,7 @@ object JavaNullInterop {
9797
* This is useful for e.g. constructors, and also so that `A & B` is nullified
9898
* to `(A & B) | Null`, instead of `(A | Null & B | Null) | Null`.
9999
*/
100-
private class JavaNullMap(var outermostLevelAlreadyNullable: Boolean)(using Context) extends TypeMap {
100+
private class ImplicitNullMap(var outermostLevelAlreadyNullable: Boolean)(using Context) extends TypeMap {
101101
def nullify(tp: Type): Type = if ctx.flexibleTypes then FlexibleType(tp) else OrNull(tp)
102102

103103
/** Should we nullify `tp` at the outermost level? */

compiler/src/dotty/tools/dotc/core/Types.scala

+1
Original file line numberDiff line numberDiff line change
@@ -3432,6 +3432,7 @@ object Types extends TypeUtils {
34323432
// flexible type is always a subtype of the original type and the Object type.
34333433
// It is not necessary according to the use cases, so we choose to use a simpler
34343434
// rule.
3435+
assert(!tp.isInstanceOf[LazyType])
34353436
FlexibleType(OrNull(tp), tp)
34363437
}
34373438
}

compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,7 @@ class ClassfileParser(
525525
denot.info = translateTempPoly(attrCompleter.complete(denot.info, isVarargs))
526526
if (isConstructor) normalizeConstructorInfo()
527527

528-
if (ctx.explicitNulls) denot.info = JavaNullInterop.nullifyMember(denot.symbol, denot.info, isEnum)
528+
if (ctx.explicitNulls) denot.info = ImplicitNullInterop.nullifyMember(denot.symbol, denot.info, isEnum)
529529

530530
// seal java enums
531531
if (isEnum) {

compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala

+6
Original file line numberDiff line numberDiff line change
@@ -981,6 +981,12 @@ class TreeUnpickler(reader: TastyReader,
981981
sym.info = tpt.tpe
982982
ValDef(tpt)
983983
}
984+
985+
// If explicit nulls is enabled, and the source file did not have explicit
986+
// nulls enabled, nullify the member to allow for compatibility.
987+
if (ctx.explicitNulls && !explicitNulls) then
988+
sym.info = ImplicitNullInterop.nullifyMember(sym, sym.info, sym.is(Enum))
989+
984990
goto(end)
985991
setSpan(start, tree)
986992

compiler/src/dotty/tools/dotc/typer/Namer.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -1902,7 +1902,7 @@ class Namer { typer: Typer =>
19021902

19031903
val mbrTpe = paramFn(checkSimpleKinded(typedAheadType(mdef.tpt, tptProto)).tpe)
19041904
if (ctx.explicitNulls && mdef.mods.is(JavaDefined))
1905-
JavaNullInterop.nullifyMember(sym, mbrTpe, mdef.mods.isAllOf(JavaEnumValue))
1905+
ImplicitNullInterop.nullifyMember(sym, mbrTpe, mdef.mods.isAllOf(JavaEnumValue))
19061906
else mbrTpe
19071907
}
19081908

compiler/test/dotty/tools/dotc/CompilationTests.scala

+12-2
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,18 @@ class CompilationTests {
215215
compileFilesInDir("tests/explicit-nulls/pos", explicitNullsOptions),
216216
compileFilesInDir("tests/explicit-nulls/flexible-types-common", explicitNullsOptions),
217217
compileFilesInDir("tests/explicit-nulls/unsafe-common", explicitNullsOptions and "-language:unsafeNulls" and "-Yno-flexible-types"),
218-
)
219-
}.checkCompile()
218+
).checkCompile()
219+
220+
locally {
221+
val tests = List(
222+
compileFile("tests/explicit-nulls/flexible-unpickle/Unsafe_1.scala", explicitNullsOptions without "-Yexplicit-nulls"),
223+
compileFile("tests/explicit-nulls/flexible-unpickle/Flexible_2.scala", explicitNullsOptions.withClasspath(
224+
defaultOutputDir + testGroup + "/Unsafe_1/flexible-unpickle/Unsafe_1")),
225+
).map(_.keepOutput.checkCompile())
226+
227+
tests.foreach(_.delete())
228+
}
229+
}
220230

221231
@Test def explicitNullsWarn: Unit = {
222232
implicit val testGroup: TestGroup = TestGroup("explicitNullsWarn")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import unsafeNulls.Foo.*
2+
import unsafeNulls.Unsafe_1
3+
4+
@main
5+
def Flexible_2() =
6+
val s2: String | Null = "foo"
7+
val unsafe = new Unsafe_1()
8+
val s: String = unsafe.foo(s2)
9+
unsafe.foo("")
10+
unsafe.foo(null)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package unsafeNulls
2+
3+
class Unsafe_1 {
4+
def foo(s: String): String = {
5+
if (s == null) then "nullString"
6+
else s
7+
}
8+
}
9+
10+
object Foo {
11+
def bar = "bar!"
12+
}

0 commit comments

Comments
 (0)