Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
45 changes: 43 additions & 2 deletions compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://jspecify.dev/docs/spec/#null-marked-scope>
*/
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.
*/
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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`),
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/StdNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<annotated java package>"

final val Any: N = "Any"
final val AnyKind: N = "AnyKind"
Expand Down
51 changes: 41 additions & 10 deletions compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -349,24 +349,33 @@ 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)
val isEnum = (jflags & JAVA_ACC_ENUM) != 0
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
Expand All @@ -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) {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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)
Expand Down
28 changes: 20 additions & 8 deletions compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down
21 changes: 21 additions & 0 deletions tests/explicit-nulls/neg/nullmarked/J.java
Original file line number Diff line number Diff line change
@@ -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";
}
}
9 changes: 9 additions & 0 deletions tests/explicit-nulls/neg/nullmarked/NullMarked.java
Original file line number Diff line number Diff line change
@@ -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 {

}
9 changes: 9 additions & 0 deletions tests/explicit-nulls/neg/nullmarked/NullUnmarked.java
Original file line number Diff line number Diff line change
@@ -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 {

}
14 changes: 14 additions & 0 deletions tests/explicit-nulls/neg/nullmarked/S.scala
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions tests/explicit-nulls/neg/nullmarked/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@org.jspecify.annotations.NullMarked
package test;
Loading