From 490f9370242df9b01fb06f9224ee9326e17a35ec Mon Sep 17 00:00:00 2001 From: TheDrawingCoding-Gamer Date: Fri, 3 Jul 2026 23:22:53 -0400 Subject: [PATCH 1/4] this doesnt work --- .../dotty/tools/dotc/core/Definitions.scala | 8 ++++ .../tools/dotc/core/ImplicitNullInterop.scala | 39 ++++++++++++++++++- .../dotc/core/classfile/ClassfileParser.scala | 32 +++++++++------ tests/explicit-nulls/neg/nullmarked/J.java | 21 ++++++++++ .../neg/nullmarked/NullMarked.java | 9 +++++ .../neg/nullmarked/NullUnmarked.java | 9 +++++ tests/explicit-nulls/neg/nullmarked/S.scala | 14 +++++++ .../neg/nullmarked/package-info.java | 4 ++ 8 files changed, 122 insertions(+), 14 deletions(-) create mode 100644 tests/explicit-nulls/neg/nullmarked/J.java create mode 100644 tests/explicit-nulls/neg/nullmarked/NullMarked.java create mode 100644 tests/explicit-nulls/neg/nullmarked/NullUnmarked.java create mode 100644 tests/explicit-nulls/neg/nullmarked/S.scala create mode 100644 tests/explicit-nulls/neg/nullmarked/package-info.java diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index f606c988dced..9b3097921f24 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1224,6 +1224,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..5d939943122a 100644 --- a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala +++ b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala @@ -67,6 +67,35 @@ 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 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 + + sym.enclosingPackageClass.annotations.foreach: annot => + println(ctx.printer.annotText(annot).mkString()) + // TODO: also check the java module + checkEnclosingClasses(sym.lexicallyEnclosingClass) + // check package + .orElse(checkOne(sym.enclosingPackageClass)) + .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 +114,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 +138,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 +168,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 +207,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/classfile/ClassfileParser.scala b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala index 26d450ffa12a..d9d0565c4282 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,26 @@ 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) + var classInfo: Type = TempClassInfoType(parseParents, instanceScope, classRoot.symbol) + classInfo = parseAttributes(classRoot.owner).complete(classInfo) + println(ctx.printer.toText(classInfo).mkString()) + setClassInfo(classRoot.owner.asClass.classDenot, classInfo, fromScala2 = false) + 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,10 +391,10 @@ final class ClassfileParser( val ifaces = List.fill(ifaceCount.toInt): pool.getSuperClass(in.nextChar).typeRef superType :: ifaces - } + end parseParents val result = unpickleOrParseInnerClasses() - if (!result.isDefined) { + if !result.isDefined then var classInfo: Type = TempClassInfoType(parseParents, instanceScope, classRoot.symbol) // might be reassigned by later parseAttributes val staticInfo = TempClassInfoType(List(), staticScope, moduleRoot.symbol) @@ -417,15 +427,13 @@ final class ClassfileParser( setClassInfo(classRoot, classInfo, fromScala2 = false) NamerOps.addConstructorProxies(moduleRoot.classSymbol) - } - else if (result == Some(NoEmbedded)) - for (sym <- List(moduleRoot.sourceModule, moduleRoot.symbol, classRoot.symbol)) { + else if result == Some(NoEmbedded) then + for sym <- List(moduleRoot.sourceModule, moduleRoot.symbol, classRoot.symbol) do classRoot.owner.asClass.delete(sym) sym.markAbsent() - } result - } + end parseClass /** Add type parameters of enclosing classes */ def addEnclosingTParams()(using Context): Unit = { 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..524b1b9bffd7 --- /dev/null +++ b/tests/explicit-nulls/neg/nullmarked/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package test; + +import org.jspecify.annotations.*; \ No newline at end of file From 10a73ef7f95e0c9bf045729bbbab73b8ded800d7 Mon Sep 17 00:00:00 2001 From: TheDrawingCoding-Gamer Date: Sat, 4 Jul 2026 00:16:17 -0400 Subject: [PATCH 2/4] try the parser more (it now crashes in typer (god help us all)) --- .../tools/dotc/core/ImplicitNullInterop.scala | 4 +-- .../dotc/core/classfile/ClassfileParser.scala | 31 ++++++++++++++++--- .../tools/dotc/parsing/JavaParsers.scala | 9 +++++- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala index 5d939943122a..5e6b027a569f 100644 --- a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala +++ b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala @@ -88,8 +88,8 @@ object ImplicitNullInterop: then checkEnclosingClasses(clazz.owner.lexicallyEnclosingClass) else None - sym.enclosingPackageClass.annotations.foreach: annot => - println(ctx.printer.annotText(annot).mkString()) + + println(ctx.printer.dclText(sym.enclosingPackageClass).mkString()) // TODO: also check the java module checkEnclosingClasses(sym.lexicallyEnclosingClass) // check package diff --git a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala index d9d0565c4282..574918e996d6 100644 --- a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala +++ b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala @@ -365,10 +365,9 @@ final class ClassfileParser( if currentClassName.endsWith("package-info") then // this is only really ever used for annotations, so copy the annotations to our package class assert(currentIsTopLevel) - var classInfo: Type = TempClassInfoType(parseParents, instanceScope, classRoot.symbol) - classInfo = parseAttributes(classRoot.owner).complete(classInfo) - println(ctx.printer.toText(classInfo).mkString()) - setClassInfo(classRoot.owner.asClass.classDenot, classInfo, fromScala2 = false) + + parseAnnotationsOnly(classRoot.owner) + println(ctx.printer.dclText(classRoot.owner).mkString()) return None @@ -956,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..a50cf441f302 100644 --- a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala @@ -1145,7 +1145,14 @@ object JavaParsers { buf ++= typeDeclOrCompact(start, mods) } } - val unit = atSpan(start) { PackageDef(pkg, buf.toList) } + val unit = + if leadingAnnots.nonEmpty && pkg != Ident(nme.EMPTY_PACKAGE) then + atSpan(start): + leadingAnnots.foldLeft[Tree](PackageDef(pkg, buf.toList)): (base, annot) => + Annotated(base, annot) + + else + atSpan(start) { PackageDef(pkg, buf.toList) } accept(EOF) if (compact) EmptyTree else unit match From 5cf0a6154b3455e9b72c3d66a110dbcb5cebf81b Mon Sep 17 00:00:00 2001 From: TheDrawingCoding-Gamer Date: Tue, 7 Jul 2026 22:37:06 -0400 Subject: [PATCH 3/4] works slightly more (but not really) --- .../tools/dotc/parsing/JavaParsers.scala | 25 +++++++++++-------- .../neg/nullmarked/package-info.java | 6 ++--- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala index a50cf441f302..d27f34a81113 100644 --- a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala @@ -1136,23 +1136,26 @@ 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 = - if leadingAnnots.nonEmpty && pkg != Ident(nme.EMPTY_PACKAGE) then - atSpan(start): + atSpan(start): + if leadingAnnots.nonEmpty && pkg != Ident(nme.EMPTY_PACKAGE) then leadingAnnots.foldLeft[Tree](PackageDef(pkg, buf.toList)): (base, annot) => Annotated(base, annot) + else + PackageDef(pkg, buf.toList) - else - atSpan(start) { PackageDef(pkg, buf.toList) } accept(EOF) if (compact) EmptyTree else unit match diff --git a/tests/explicit-nulls/neg/nullmarked/package-info.java b/tests/explicit-nulls/neg/nullmarked/package-info.java index 524b1b9bffd7..cc9d1aa35130 100644 --- a/tests/explicit-nulls/neg/nullmarked/package-info.java +++ b/tests/explicit-nulls/neg/nullmarked/package-info.java @@ -1,4 +1,2 @@ -@NullMarked -package test; - -import org.jspecify.annotations.*; \ No newline at end of file +@org.jspecify.annotations.NullMarked +package test; \ No newline at end of file From df9d9332c902d3f9d824566f3ee05f8be1d404c6 Mon Sep 17 00:00:00 2001 From: TheDrawingCoding-Gamer Date: Wed, 15 Jul 2026 12:07:43 -0400 Subject: [PATCH 4/4] Still doesnt work --- .../dotty/tools/dotc/core/ImplicitNullInterop.scala | 8 +++++++- compiler/src/dotty/tools/dotc/core/StdNames.scala | 1 + .../src/dotty/tools/dotc/parsing/JavaParsers.scala | 12 +++++++----- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala index 5e6b027a569f..32a7dde53786 100644 --- a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala +++ b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala @@ -81,6 +81,12 @@ object ImplicitNullInterop: 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 @@ -93,7 +99,7 @@ object ImplicitNullInterop: // TODO: also check the java module checkEnclosingClasses(sym.lexicallyEnclosingClass) // check package - .orElse(checkOne(sym.enclosingPackageClass)) + .orElse(checkPackage(sym)) .getOrElse(NullMode.Default) /** Transforms the type `tp` of a member `sym` that originates from a source without explicit nulls. 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/parsing/JavaParsers.scala b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala index d27f34a81113..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() @@ -1150,11 +1156,7 @@ object JavaParsers { } val unit = atSpan(start): - if leadingAnnots.nonEmpty && pkg != Ident(nme.EMPTY_PACKAGE) then - leadingAnnots.foldLeft[Tree](PackageDef(pkg, buf.toList)): (base, annot) => - Annotated(base, annot) - else - PackageDef(pkg, buf.toList) + PackageDef(pkg, buf.toList) accept(EOF) if (compact) EmptyTree