-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Apply flexible types to files compiled without explicit nulls #23386
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
099bf33
52d7b22
3cdda29
d9f49d1
705ec4d
a2dfe25
90acf8b
641eb52
79a1a52
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,7 +35,7 @@ import dotty.tools.dotc.core.Decorators.i | |
* to handle the full spectrum of Scala types. Additionally, some kinds of symbols like constructors and | ||
* enum instances get special treatment. | ||
*/ | ||
object JavaNullInterop { | ||
object ImplicitNullInterop { | ||
|
||
/** Transforms the type `tp` of Java member `sym` to be explicitly nullable. | ||
* `tp` is needed because the type inside `sym` might not be set when this method is called. | ||
|
@@ -55,11 +55,11 @@ object JavaNullInterop { | |
*/ | ||
def nullifyMember(sym: Symbol, tp: Type, isEnumValueDef: Boolean)(using Context): Type = trace(i"nullifyMember ${sym}, ${tp}"){ | ||
assert(ctx.explicitNulls) | ||
assert(sym.is(JavaDefined), "can only nullify java-defined members") | ||
|
||
// Some special cases when nullifying the type | ||
if isEnumValueDef || sym.name == nme.TYPE_ then | ||
// Don't nullify the `TYPE` field in every class and Java enum instances | ||
if isEnumValueDef || sym.name == nme.TYPE_ // Don't nullify the `TYPE` field in every class and Java enum instances | ||
|| sym.is(Flags.ModuleVal) // Don't nullify Modules | ||
then | ||
tp | ||
else if sym.name == nme.toString_ || sym.isConstructor || hasNotNullAnnot(sym) then | ||
// Don't nullify the return type of the `toString` method. | ||
|
@@ -80,14 +80,14 @@ object JavaNullInterop { | |
* but the result type is not nullable. | ||
*/ | ||
private def nullifyExceptReturnType(tp: Type)(using Context): Type = | ||
new JavaNullMap(outermostLevelAlreadyNullable = true)(tp) | ||
new ImplicitNullMap(outermostLevelAlreadyNullable = true)(tp) | ||
|
||
/** Nullifies a Java type by adding `| Null` in the relevant places. */ | ||
/** Nullifies a type by adding `| Null` in the relevant places. */ | ||
private def nullifyType(tp: Type)(using Context): Type = | ||
new JavaNullMap(outermostLevelAlreadyNullable = false)(tp) | ||
new ImplicitNullMap(outermostLevelAlreadyNullable = false)(tp) | ||
|
||
/** A type map that implements the nullification function on types. Given a Java-sourced type, this adds `| Null` | ||
* in the right places to make the nulls explicit in Scala. | ||
/** A type map that implements the nullification function on types. Given a Java-sourced type or an | ||
* implicitly null type, this adds `| Null` in the right places to make the nulls explicit. | ||
* | ||
* @param outermostLevelAlreadyNullable whether this type is already nullable at the outermost level. | ||
* For example, `Array[String] | Null` is already nullable at the | ||
|
@@ -97,26 +97,32 @@ object JavaNullInterop { | |
* This is useful for e.g. constructors, and also so that `A & B` is nullified | ||
* to `(A & B) | Null`, instead of `(A | Null & B | Null) | Null`. | ||
*/ | ||
private class JavaNullMap(var outermostLevelAlreadyNullable: Boolean)(using Context) extends TypeMap { | ||
private class ImplicitNullMap(var outermostLevelAlreadyNullable: Boolean)(using Context) extends TypeMap { | ||
def nullify(tp: Type): Type = if ctx.flexibleTypes then FlexibleType(tp) else OrNull(tp) | ||
|
||
/** Should we nullify `tp` at the outermost level? */ | ||
def needsNull(tp: Type): Boolean = | ||
if outermostLevelAlreadyNullable then false | ||
else tp match | ||
case tp: TypeRef if | ||
case tp: TypeRef if !tp.hasSimpleKind | ||
// We don't modify value types because they're non-nullable even in Java. | ||
tp.symbol.isValueClass | ||
|| tp.symbol.isValueClass | ||
// We don't modify unit types. | ||
|| tp.isRef(defn.UnitClass) | ||
// We don't modify `Any` because it's already nullable. | ||
|| tp.isRef(defn.AnyClass) | ||
// We don't nullify Java varargs at the top level. | ||
// Example: if `setNames` is a Java method with signature `void setNames(String... names)`, | ||
// then its Scala signature will be `def setNames(names: (String|Null)*): Unit`. | ||
// This is because `setNames(null)` passes as argument a single-element array containing the value `null`, | ||
// and not a `null` array. | ||
|| !ctx.flexibleTypes && tp.isRef(defn.RepeatedParamClass) => false | ||
|| tp.isRef(defn.AnyClass) => false | ||
case _ => true | ||
|
||
// We don't nullify Java varargs at the top level. | ||
// Example: if `setNames` is a Java method with signature `void setNames(String... names)`, | ||
// then its Scala signature will be `def setNames(names: (String|Null)*): Unit`. | ||
// This is because `setNames(null)` passes as argument a single-element array containing the value `null`, | ||
// and not a `null` array. | ||
def tyconNeedsNull(tp: Type): Boolean = | ||
if outermostLevelAlreadyNullable then false | ||
else tp match | ||
case tp: TypeRef | ||
if !ctx.flexibleTypes && tp.isRef(defn.RepeatedParamClass) => false | ||
case _ => true | ||
|
||
override def apply(tp: Type): Type = tp match { | ||
|
@@ -130,7 +136,7 @@ object JavaNullInterop { | |
val targs2 = targs map this | ||
outermostLevelAlreadyNullable = oldOutermostNullable | ||
val appTp2 = derivedAppliedType(appTp, tycon, targs2) | ||
if needsNull(tycon) then nullify(appTp2) else appTp2 | ||
if tyconNeedsNull(tycon) then nullify(appTp2) else appTp2 | ||
case ptp: PolyType => | ||
derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType)) | ||
case mtp: MethodType => | ||
|
@@ -140,6 +146,7 @@ object JavaNullInterop { | |
outermostLevelAlreadyNullable = oldOutermostNullable | ||
derivedLambdaType(mtp)(paramInfos2, this(mtp.resType)) | ||
case tp: TypeAlias => mapOver(tp) | ||
case tp: TypeBounds => mapOver(tp) | ||
case tp: AndType => | ||
// nullify(A & B) = (nullify(A) & nullify(B)) | Null, but take care not to add | ||
// duplicate `Null`s at the outermost level inside `A` and `B`. | ||
|
@@ -149,6 +156,11 @@ object JavaNullInterop { | |
// In all other cases, return the type unchanged. | ||
// In particular, if the type is a ConstantType, then we don't nullify it because it is the | ||
// type of a final non-nullable field. | ||
case tp: ExprType => mapOver(tp) | ||
case tp: AnnotatedType => mapOver(tp) | ||
case tp: OrType => mapOver(tp) | ||
case tp: MatchType => mapOver(tp) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe it is safer to not nullify match types at all. |
||
case tp: RefinedType => nullify(mapOver(tp)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For |
||
case _ => tp | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
byname-nullables.scala # identity() flexified | ||
HarrisL2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
varargs.scala # Array type flexified | ||
flow-conservative.scala # .length flexified | ||
nn-basic.scala # .length flexified but trim rejected | ||
i21380c.scala # .length flexified but replaceAll rejected | ||
unsafe-scope.scala # .length flexified | ||
i17467.scala # Singleton type flexified | ||
from-nullable.scala # Option argument flexified | ||
flow-in-block.scala # .length flexified | ||
array.scala # Type arugment of Array flexified | ||
flow-forward-ref.scala # .length flexified, forward reference error | ||
flow-implicitly.scala # Singleton type flexified | ||
nn.scala # Flexified elided error [!] | ||
flow-basic.scala # .length flexified | ||
|
||
unsafe-cast.scala # Array type flexified | ||
unsafe-extensions.scala # Function arguments flexified |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import unsafeNulls.Foo.* | ||
import unsafeNulls.Unsafe_1 | ||
import unsafeNulls.{A, B, C, F, G, H, I, J, L, M} | ||
import scala.reflect.Selectable.reflectiveSelectable | ||
|
||
class Inherit_1 extends Unsafe_1 { | ||
override def foo(s: String): String = s | ||
override def bar[T >: String](s: T): T = s | ||
override def bar2[T >: String | Null](s: T): T = s | ||
override def bar3[T <: Function1[String,String]](g: T) = g | ||
override def bar4[HK[_]](i: String | Null): HK[String | Null] = ??? | ||
} | ||
|
||
class Inherit_2 extends Unsafe_1 { | ||
override def foo(s: String | Null): String | Null = null | ||
override def bar[T >: String](s: T | Null): T | Null = s | ||
override def bar2[T >: String](s: T): T = s | ||
override def bar3[T <: Function1[(String|Null),(String|Null)]](g: T) = g | ||
override def bar4[HK[_]](i: String): HK[String] = ??? | ||
} | ||
|
||
class Inherit_3 extends Unsafe_1 { | ||
override def foo(s: String): String | Null = null | ||
override def bar[T >: String](s: T): T | Null = s | ||
} | ||
|
||
class Inherit_4 extends Unsafe_1 { | ||
override def foo(s: String | Null): String = "non-null string" | ||
override def bar[T >: String](s: T | Null): T = "non-null string" | ||
} | ||
|
||
case class cc() | ||
|
||
class K(val b: String) extends J(b) { | ||
} | ||
|
||
@main | ||
def Flexible_2() = | ||
val s2: String | Null = "foo" | ||
val unsafe = new Unsafe_1() | ||
val s: String = unsafe.foo(s2) | ||
unsafe.foo("") | ||
unsafe.foo(null) | ||
|
||
|
||
val a = refinement.b | ||
refinement.b = null | ||
val refinement2: Unsafe_1 { var b: String } = refinement | ||
refinement = null | ||
|
||
val singletonbar: bar.type = singleton | ||
|
||
val extension: String = intersection.reverse | ||
|
||
val stringA: String = intersection.stringA | ||
val stringB: String = intersection.stringB | ||
intersection.stringA = null | ||
intersection.stringB = null | ||
|
||
val intersection2: A & B = intersection | ||
intersection = null | ||
|
||
val stringC: String = union.stringC | ||
union.stringC = null | ||
|
||
val union2: A | B = union | ||
union = null | ||
|
||
val constructorTest = new Unsafe_1(null) | ||
val member: String = constructorTest.member | ||
constructorTest.member = null | ||
|
||
bar match { | ||
case str @ null: String => () | ||
case other => () | ||
} | ||
|
||
val f = new F(null, G(12)) | ||
val F(x, y) = f | ||
|
||
val g: (List[F] | String | List[Int]) = F.many | ||
F.many = null :: null :: Nil | ||
F.many = null | ||
|
||
val h: H { val s: String } = new H { override val s: String = "foo" } | ||
|
||
val jBox: I[J] = new I(new J(null)) | ||
val kBox: I[K] = new I(new K("foo")) | ||
|
||
val box: I[J] = kBox | ||
|
||
val jBox2: L[J] = new L[J](j => ()) | ||
val kBox2: L[K] = new L[K](k => ()) | ||
|
||
val box2: L[K] = jBox2 | ||
val box3: I[J | Null] = box | ||
|
||
val m: String = M.test(null) | ||
|
Uh oh!
There was an error while loading. Please reload this page.