diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 97f7d944bcf5..727543743211 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1225,6 +1225,14 @@ class Definitions { "io.reactivex.rxjava3.annotations.Nullable" :: "org.jspecify.annotations.Nullable" :: Nil) + @tu lazy val NullMarkedAnnots: List[ClassSymbol] = getClassesIfDefined( + "org.jspecify.annotations.NullMarked" :: Nil + ) + + @tu lazy val NullUnmarkedAnnots: List[ClassSymbol] = getClassesIfDefined( + "org.jspecify.annotations.NullUnmarked" :: Nil + ) + // convenient one-parameter method types def methOfAny(tp: Type): MethodType = MethodType(List(AnyType), tp) def methOfAnyVal(tp: Type): MethodType = MethodType(List(AnyValType), tp) diff --git a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala index 7350af348ea0..32a7dde53786 100644 --- a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala +++ b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala @@ -67,6 +67,41 @@ object ImplicitNullInterop: object NullMode: def Default(using Context): NullMode = if ctx.flexibleTypes then Flexible else Explicit + /** + * Does this type's scope (its class, module, or package) have a NullMarked annotation + * that causes it to default to not null? + * + * based off this specification from JSpecify + */ + def defaultModeInScope(sym: Symbol)(using Context): NullMode = + def checkOne(sym: Symbol): Option[NullMode] = + if hasNullMarkedAnnot(sym) && !hasNullUnmarkedAnnot(sym) + then Some(NullMode.Skip) + else if hasNullUnmarkedAnnot(sym) && !hasNullMarkedAnnot(sym) + then Some(NullMode.Default) + else None + + def checkPackage(clazz: Symbol): Option[NullMode] = + val packageClass = clazz.enclosingPackageClass + packageClass.children.find(it => it.isType && it.asType.name == StdNames.nme.CANONICAL_PACKAGE).flatMap: canonicalPackage => + println("Hi") + checkOne(canonicalPackage) + + def checkEnclosingClasses(clazz: Symbol): Option[NullMode] = + checkOne(clazz).orElse: + // use "lexically enclosing class" to _not_ skip static members + if !clazz.is(Flags.Package) && clazz.owner.lexicallyEnclosingClass != clazz + then checkEnclosingClasses(clazz.owner.lexicallyEnclosingClass) + else None + + + println(ctx.printer.dclText(sym.enclosingPackageClass).mkString()) + // TODO: also check the java module + checkEnclosingClasses(sym.lexicallyEnclosingClass) + // check package + .orElse(checkPackage(sym)) + .getOrElse(NullMode.Default) + /** Transforms the type `tp` of a member `sym` that originates from a source without explicit nulls. * `tp` is passed explicitly because the type stored in `sym` might not yet be set when this is called. */ @@ -85,7 +120,7 @@ object ImplicitNullInterop: // Don't nullify Given/implicit parameters if sym.isOneOf(GivenOrImplicitVal) || hasNotNullAnnot(sym) then NullMode.Skip else if hasNullableAnnot(sym) then NullMode.Explicit - else NullMode.Default + else defaultModeInScope(sym) val resultTypeMode = // Don't nullify result type of constructors @@ -109,6 +144,12 @@ object ImplicitNullInterop: private def isNullableAnnot(annot: Annotation)(using Context): Boolean = defn.NullableAnnots.exists(annot.hasSymbol) + private def hasNullMarkedAnnot(sym: Symbol)(using Context): Boolean = + defn.NullMarkedAnnots.exists(sym.unforcedAnnotation(_).isDefined) + + private def hasNullUnmarkedAnnot(sym: Symbol)(using Context): Boolean = + defn.NullUnmarkedAnnots.exists(sym.unforcedAnnotation(_).isDefined) + case class NullMapState( resultTypeMode: NullMode, currentTypeMode: NullMode @@ -133,7 +174,6 @@ object ImplicitNullInterop: val javaDefined: Boolean, var state: NullMapState )(using Context) extends TypeMap: - /** Should we nullify `tp` at the outermost level? * The symbols are still under construction, so we don't have precise information. * We purposely do not rely on precise subtyping checks here (e.g., asking whether `tp <:< AnyRef`), @@ -173,6 +213,7 @@ object ImplicitNullInterop: else if state.currentTypeMode == NullMode.Flexible then FlexibleType.make(tp) else OrNull(tp) + override def apply(tp: Type): Type = tp match case tp: TypeRef => nullify(tp) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 9e705c61dcd9..53909259d040 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -192,6 +192,7 @@ object StdNames { final val WILDCARD_STAR: N = "_*" final val REIFY_TREECREATOR_PREFIX: N = "$treecreator" final val REIFY_TYPECREATOR_PREFIX: N = "$typecreator" + final val CANONICAL_PACKAGE: N = "" final val Any: N = "Any" final val AnyKind: N = "AnyKind" diff --git a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala index 835db2b0262a..6031c31faf81 100644 --- a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala +++ b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala @@ -349,7 +349,7 @@ final class ClassfileParser( var sawPrivateConstructor: Boolean = false - def parseClass()(using ctx: Context, in: DataReader): Option[Embedded] = { + def parseClass()(using ctx: Context, in: DataReader): Option[Embedded] = val jflags = in.nextChar val isAnnotation = hasAnnotation(jflags) val sflags = classTranslation.flags(jflags) @@ -357,16 +357,25 @@ final class ClassfileParser( val nameIdx = in.nextChar currentClassName = pool.getClassName(nameIdx).name - if (currentIsTopLevel && + if currentIsTopLevel && currentClassName != classRoot.fullName.toSimpleName && - currentClassName != classRoot.fullName.encode.toSimpleName) - mismatchError(currentClassName) + currentClassName != classRoot.fullName.encode.toSimpleName + then mismatchError(currentClassName) + + if currentClassName.endsWith("package-info") then + // this is only really ever used for annotations, so copy the annotations to our package class + assert(currentIsTopLevel) + + parseAnnotationsOnly(classRoot.owner) + println(ctx.printer.dclText(classRoot.owner).mkString()) + return None + addEnclosingTParams() /** Parse parents for Java classes. For Scala, return AnyRef, since the real type will be unpickled. * Updates the read pointer of 'in'. */ - def parseParents: List[Type] = { + def parseParents: List[Type] = val superType = val superClass = in.nextChar // Treat these interfaces as universal traits @@ -381,7 +390,7 @@ final class ClassfileParser( val ifaces = List.fill(ifaceCount.toInt): pool.getSuperClass(in.nextChar).typeRef superType :: ifaces - } + end parseParents val result = unpickleOrParseInnerClasses() if (result.isEmpty) { @@ -417,15 +426,13 @@ final class ClassfileParser( setClassInfo(classRoot, classInfo, fromScala2 = false) NamerOps.addConstructorProxies(moduleRoot.classSymbol) - } - else if (result.contains(NoEmbedded)) + } else if (result.contains(NoEmbedded)) for (sym <- List(moduleRoot.sourceModule, moduleRoot.symbol, classRoot.symbol)) { classRoot.owner.asClass.delete(sym) sym.markAbsent() } - result - } + end parseClass /** Add type parameters of enclosing classes */ def addEnclosingTParams()(using Context): Unit = { @@ -948,6 +955,30 @@ final class ClassfileParser( cook.apply(fillInParamNames(newType)) } } + def parseAnnotationsOnly(sym: Symbol)(using ctx: Context, in: DataReader): Unit = + def parseAttribute(): Unit = + val attrName = pool.getName(in.nextChar).name.toTypeName + val attrLen = in.nextInt + val end = in.bp + attrLen + attrName match + case tpnme.RuntimeVisibleAnnotationATTR + | tpnme.RuntimeInvisibleAnnotationATTR => + parseAnnotations(attrLen) + case _ => () + + in.bp = end + + /** Parse a sequence of annotations and attaches them to the + * current symbol sym, except for the ScalaSignature annotation that it returns, if it is available. */ + def parseAnnotations(len: Int): Unit = + val nAttr = in.nextChar + for (n <- 0 until nAttr) + parseAnnotation(in.nextChar) match + case Some(annot) => + sym.addAnnotation(annot) + case None => () + + def parseAttributes(sym: Symbol)(using ctx: Context, in: DataReader): AttributeCompleter = { val res = new AttributeCompleter(sym) diff --git a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala index 67b9f25de98a..56dca238a8cc 100644 --- a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala @@ -1113,6 +1113,12 @@ object JavaParsers { val pkg: RefTree = if in.token == PACKAGE then if leadingAnnots.nonEmpty then + // Invent a fake "canonical package" type? + val canonicalPackage = TypeDef(tpnme.CANONICAL_PACKAGE, Ident(jtpnme.Object)) + + buf += + leadingAnnots.foldLeft[Tree](canonicalPackage): (base, annot) => + Annotated(base, annot) start = in.offset accept(PACKAGE) val pkg = qualId() @@ -1136,16 +1142,22 @@ object JavaParsers { if buf.isEmpty then while (in.token == IMPORT) buf ++= importDecl() - while (in.token != EOF && in.token != RBRACE) { - while (in.token == SEMI) in.nextToken() - if (in.token != EOF) { - val start = in.offset - val mods = modifiers(inInterface = false) - adaptRecordIdentifier() // needed for typeDecl - buf ++= typeDeclOrCompact(start, mods) + // expect nothing when the file is named "package-info.java" + if (!source.file.path.endsWith("package-info.java")) { + while (in.token != EOF && in.token != RBRACE) { + while (in.token == SEMI) in.nextToken() + if (in.token != EOF) { + val start = in.offset + val mods = modifiers(inInterface = false) + adaptRecordIdentifier() // needed for typeDecl + buf ++= typeDeclOrCompact(start, mods) + } } } - val unit = atSpan(start) { PackageDef(pkg, buf.toList) } + val unit = + atSpan(start): + PackageDef(pkg, buf.toList) + accept(EOF) if (compact) EmptyTree else unit match diff --git a/tests/explicit-nulls/neg/nullmarked/J.java b/tests/explicit-nulls/neg/nullmarked/J.java new file mode 100644 index 000000000000..2982b4b873f2 --- /dev/null +++ b/tests/explicit-nulls/neg/nullmarked/J.java @@ -0,0 +1,21 @@ +package test; + +import org.jspecify.annotations.*; + +public class J { + + private static String getK() { + return "k"; + } + + public static final String k = getK(); + + public static String l = "l"; + + @NullUnmarked + public static class J2 { + public static final String k2 = getK(); + + public static String l2 = "l"; + } +} diff --git a/tests/explicit-nulls/neg/nullmarked/NullMarked.java b/tests/explicit-nulls/neg/nullmarked/NullMarked.java new file mode 100644 index 000000000000..57581d963c25 --- /dev/null +++ b/tests/explicit-nulls/neg/nullmarked/NullMarked.java @@ -0,0 +1,9 @@ +package org.jspecify.annotations; + +import java.lang.annotation.*; + +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.MODULE, ElementType.PACKAGE, ElementType.CONSTRUCTOR}) +@Retention(RetentionPolicy.RUNTIME) +public @interface NullMarked { + +} diff --git a/tests/explicit-nulls/neg/nullmarked/NullUnmarked.java b/tests/explicit-nulls/neg/nullmarked/NullUnmarked.java new file mode 100644 index 000000000000..a9d2a8f909e1 --- /dev/null +++ b/tests/explicit-nulls/neg/nullmarked/NullUnmarked.java @@ -0,0 +1,9 @@ +package org.jspecify.annotations; + +import java.lang.annotation.*; + +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.MODULE, ElementType.PACKAGE, ElementType.CONSTRUCTOR}) +@Retention(RetentionPolicy.RUNTIME) +public @interface NullUnmarked { + +} diff --git a/tests/explicit-nulls/neg/nullmarked/S.scala b/tests/explicit-nulls/neg/nullmarked/S.scala new file mode 100644 index 000000000000..9677d91aff01 --- /dev/null +++ b/tests/explicit-nulls/neg/nullmarked/S.scala @@ -0,0 +1,14 @@ +//> using options -Yno-flexible-types + +// Test that null marked scopes are working +import test.* + +class S { + def kk: String = J.k // ok: in null marked scope + + def ll: String = J.l // ok: in null marked scope + + def kk2: String = J.J2.k2 // error: in unmarked scope + + def ll2: String = J.J2.l2 // error: in unmarked scope +} \ No newline at end of file diff --git a/tests/explicit-nulls/neg/nullmarked/package-info.java b/tests/explicit-nulls/neg/nullmarked/package-info.java new file mode 100644 index 000000000000..cc9d1aa35130 --- /dev/null +++ b/tests/explicit-nulls/neg/nullmarked/package-info.java @@ -0,0 +1,2 @@ +@org.jspecify.annotations.NullMarked +package test; \ No newline at end of file