|
| 1 | +package dotty.tools.dotc.core |
| 2 | + |
| 3 | +import dotty.tools.dotc.core.Contexts.Context |
| 4 | +import dotty.tools.dotc.core.Flags.JavaDefined |
| 5 | +import dotty.tools.dotc.core.StdNames.{jnme, nme} |
| 6 | +import dotty.tools.dotc.core.Symbols._ |
| 7 | +import dotty.tools.dotc.core.Types._ |
| 8 | +import NullOpsDecorator._ |
| 9 | + |
| 10 | +/** This module defines methods to interpret types of Java symbols, which are implicitly nullable in Java, |
| 11 | + * as Scala types, which are explicitly nullable. |
| 12 | + * |
| 13 | + * The transformation is (conceptually) a function `n` that adheres to the following rules: |
| 14 | + * (1) n(T) = T|JavaNull if T is a reference type |
| 15 | + * (2) n(T) = T if T is a value type |
| 16 | + * (3) n(C[T]) = C[T]|JavaNull if C is Java-defined |
| 17 | + * (4) n(C[T]) = C[n(T)]|JavaNull if C is Scala-defined |
| 18 | + * (5) n(A|B) = n(A)|n(B)|JavaNull |
| 19 | + * (6) n(A&B) = n(A) & n(B) |
| 20 | + * (7) n((A1, ..., Am)R) = (n(A1), ..., n(Am))n(R) for a method with arguments (A1, ..., Am) and return type R |
| 21 | + * (8) n(T) = T otherwise |
| 22 | + * |
| 23 | + * Treatment of generics (rules 3 and 4): |
| 24 | + * - if `C` is Java-defined, then `n(C[T]) = C[T]|JavaNull`. That is, we don't recurse |
| 25 | + * on the type argument, and only add JavaNull on the outside. This is because |
| 26 | + * `C` itself will be nullified, and in particular so will be usages of `C`'s type argument within C's body. |
| 27 | + * e.g. calling `get` on a `java.util.List[String]` already returns `String|Null` and not `String`, so |
| 28 | + * we don't need to write `java.util.List[String|Null]`. |
| 29 | + * - if `C` is Scala-defined, however, then we want `n(C[T]) = C[n(T)]|JavaNull`. This is because |
| 30 | + * `C` won't be nullified, so we need to indicate that its type argument is nullable. |
| 31 | + * |
| 32 | + * Notice that since the transformation is only applied to types attached to Java symbols, it doesn't need |
| 33 | + * to handle the full spectrum of Scala types. Additionally, some kinds of symbols like constructors and |
| 34 | + * enum instances get special treatment. |
| 35 | + */ |
| 36 | +object JavaNullInterop { |
| 37 | + |
| 38 | + /** Transforms the type `tp` of Java member `sym` to be explicitly nullable. |
| 39 | + * `tp` is needed because the type inside `sym` might not be set when this method is called. |
| 40 | + * |
| 41 | + * e.g. given a Java method |
| 42 | + * String foo(String arg) { return arg; } |
| 43 | + * |
| 44 | + * After calling `nullifyMember`, Scala will see the method as |
| 45 | + * |
| 46 | + * def foo(arg: String|JavaNull): String|JavaNull |
| 47 | + * |
| 48 | + * This nullability function uses `JavaNull` instead of vanilla `Null`, for usability. |
| 49 | + * This means that we can select on the return of `foo`: |
| 50 | + * |
| 51 | + * val len = foo("hello").length |
| 52 | + * |
| 53 | + * But the selection can throw an NPE if the returned value is `null`. |
| 54 | + */ |
| 55 | + def nullifyMember(sym: Symbol, tp: Type)(implicit ctx: Context): Type = { |
| 56 | + assert(ctx.explicitNulls) |
| 57 | + assert(sym.is(JavaDefined), "can only nullify java-defined members") |
| 58 | + |
| 59 | + // Some special cases when nullifying the type |
| 60 | + if (sym.name == nme.TYPE_ || sym.isAllOf(Flags.JavaEnumValue)) |
| 61 | + // Don't nullify the `TYPE` field in every class and Java enum instances |
| 62 | + tp |
| 63 | + else if (sym.name == nme.toString_ || sym.isConstructor || hasNotNullAnnot(sym)) |
| 64 | + // Don't nullify the return type of the `toString` method. |
| 65 | + // Don't nullify the return type of constructors. |
| 66 | + // Don't nullify the return type of methods with a not-null annotation. |
| 67 | + nullifyExceptReturnType(tp) |
| 68 | + else |
| 69 | + // Otherwise, nullify everything |
| 70 | + nullifyType(tp) |
| 71 | + } |
| 72 | + |
| 73 | + private def hasNotNullAnnot(sym: Symbol)(implicit ctx: Context): Boolean = |
| 74 | + ctx.definitions.NotNullAnnots.exists(nna => sym.unforcedAnnotation(nna).isDefined) |
| 75 | + |
| 76 | + /** If tp is a MethodType, the parameters and the inside of return type are nullified, |
| 77 | + * but the result return type is not nullable. |
| 78 | + * If tp is a type of a field, the inside of the type is nullified, |
| 79 | + * but the result type is not nullable. |
| 80 | + */ |
| 81 | + private def nullifyExceptReturnType(tp: Type)(implicit ctx: Context): Type = |
| 82 | + new JavaNullMap(true)(ctx)(tp) |
| 83 | + |
| 84 | + /** Nullifies a Java type by adding `| JavaNull` in the relevant places. */ |
| 85 | + private def nullifyType(tp: Type)(implicit ctx: Context): Type = |
| 86 | + new JavaNullMap(false)(ctx)(tp) |
| 87 | + |
| 88 | + /** A type map that implements the nullification function on types. Given a Java-sourced type, this adds `| JavaNull` |
| 89 | + * in the right places to make the nulls explicit in Scala. |
| 90 | + * |
| 91 | + * @param outermostLevelAlreadyNullable whether this type is already nullable at the outermost level. |
| 92 | + * For example, `Array[String]|JavaNull` is already nullable at the |
| 93 | + * outermost level, but `Array[String|JavaNull]` isn't. |
| 94 | + * If this parameter is set to true, then the types of fields, and the return |
| 95 | + * types of methods will not be nullified. |
| 96 | + * This is useful for e.g. constructors, and also so that `A & B` is nullified |
| 97 | + * to `(A & B) | JavaNull`, instead of `(A|JavaNull & B|JavaNull) | JavaNull`. |
| 98 | + */ |
| 99 | + private class JavaNullMap(var outermostLevelAlreadyNullable: Boolean)(implicit ctx: Context) extends TypeMap { |
| 100 | + /** Should we nullify `tp` at the outermost level? */ |
| 101 | + def needsNull(tp: Type): Boolean = |
| 102 | + !outermostLevelAlreadyNullable && (tp match { |
| 103 | + case tp: TypeRef => |
| 104 | + // We don't modify value types because they're non-nullable even in Java. |
| 105 | + !tp.symbol.isValueClass && |
| 106 | + // We don't modify `Any` because it's already nullable. |
| 107 | + !tp.isRef(defn.AnyClass) && |
| 108 | + // We don't nullify Java varargs at the top level. |
| 109 | + // Example: if `setNames` is a Java method with signature `void setNames(String... names)`, |
| 110 | + // then its Scala signature will be `def setNames(names: (String|JavaNull)*): Unit`. |
| 111 | + // This is because `setNames(null)` passes as argument a single-element array containing the value `null`, |
| 112 | + // and not a `null` array. |
| 113 | + !tp.isRef(defn.RepeatedParamClass) |
| 114 | + case _ => true |
| 115 | + }) |
| 116 | + |
| 117 | + override def apply(tp: Type): Type = { |
| 118 | + // Fast version of Type::toJavaNullableUnion that doesn't check whether the type |
| 119 | + // is already a union. |
| 120 | + def toJavaNullableUnion(tpe: Type): Type = OrType(tpe, defn.JavaNullAliasType) |
| 121 | + |
| 122 | + tp match { |
| 123 | + case tp: TypeRef if needsNull(tp) => toJavaNullableUnion(tp) |
| 124 | + case appTp @ AppliedType(tycon, targs) => |
| 125 | + val oldOutermostNullable = outermostLevelAlreadyNullable |
| 126 | + // We don't make the outmost levels of type arguements nullable if tycon is Java-defined. |
| 127 | + // This is because Java classes are _all_ nullified, so both `java.util.List[String]` and |
| 128 | + // `java.util.List[String|Null]` contain nullable elements. |
| 129 | + outermostLevelAlreadyNullable = tp.classSymbol.is(JavaDefined) |
| 130 | + val targs2 = targs map this |
| 131 | + outermostLevelAlreadyNullable = oldOutermostNullable |
| 132 | + val appTp2 = derivedAppliedType(appTp, tycon, targs2) |
| 133 | + if (needsNull(tycon)) toJavaNullableUnion(appTp2) else appTp2 |
| 134 | + case ptp: PolyType => |
| 135 | + derivedLambdaType(ptp)(ptp.paramInfos, this(ptp.resType)) |
| 136 | + case mtp: MethodType => |
| 137 | + val oldOutermostNullable = outermostLevelAlreadyNullable |
| 138 | + outermostLevelAlreadyNullable = false |
| 139 | + val paramInfos2 = mtp.paramInfos map this |
| 140 | + outermostLevelAlreadyNullable = oldOutermostNullable |
| 141 | + derivedLambdaType(mtp)(paramInfos2, this(mtp.resType)) |
| 142 | + case tp: TypeAlias => mapOver(tp) |
| 143 | + case tp: AndType => |
| 144 | + // nullify(A & B) = (nullify(A) & nullify(B)) | JavaNull, but take care not to add |
| 145 | + // duplicate `JavaNull`s at the outermost level inside `A` and `B`. |
| 146 | + outermostLevelAlreadyNullable = true |
| 147 | + toJavaNullableUnion(derivedAndType(tp, this(tp.tp1), this(tp.tp2))) |
| 148 | + case tp: TypeParamRef if needsNull(tp) => toJavaNullableUnion(tp) |
| 149 | + // In all other cases, return the type unchanged. |
| 150 | + // In particular, if the type is a ConstantType, then we don't nullify it because it is the |
| 151 | + // type of a final non-nullable field. |
| 152 | + case _ => tp |
| 153 | + } |
| 154 | + } |
| 155 | + } |
| 156 | +} |
0 commit comments