Skip to content
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

In Scala 3 TASTy reader, restrict access to experimental definitions #10020

Merged
merged 1 commit into from May 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 9 additions & 19 deletions src/compiler/scala/tools/nsc/tasty/bridge/AnnotationOps.scala
Expand Up @@ -30,34 +30,24 @@ trait AnnotationOps { self: TastyUniverse =>
throw new Exception(s"unexpected annotation kind from TASTy: ${u.showRaw(tree)}")
}

abstract class DeferredAnnotation {
sealed abstract class DeferredAnnotation {

private[bridge] def eager(annotee: Symbol)(implicit ctx: Context): u.AnnotationInfo
private[bridge] def lzy(annotee: Symbol)(implicit ctx: Context): u.LazyAnnotationInfo = {
u.AnnotationInfo.lazily {
eager(annotee)
}
private[bridge] final def lzy(annotee: Symbol)(implicit ctx: Context): u.LazyAnnotationInfo = {
u.AnnotationInfo.lazily(eager(annotee))
}
}

object DeferredAnnotation {

def fromTree(tree: Symbol => Context => Tree) =
new FromTree(tree)

class FromTree(tree: Symbol => Context => Tree) extends DeferredAnnotation {
private[bridge] def eager(annotee: Symbol)(implicit ctx: Context): u.AnnotationInfo = {
val atree = tree(annotee)(ctx)
val annot = mkAnnotation(atree)
val annotSym = annot.tpe.typeSymbol
if ((annotSym eq defn.TargetNameAnnotationClass) || (annotSym eq defn.StaticMethodAnnotationClass)) {
annotee.addAnnotation(
u.definitions.CompileTimeOnlyAttr,
u.Literal(u.Constant(unsupportedMessage(s"annotation on $annotee: @$annot"))))
def fromTree(tree: Symbol => Context => Tree): DeferredAnnotation = {
new DeferredAnnotation {
private[bridge] final def eager(annotee: Symbol)(implicit ctx: Context): u.AnnotationInfo = {
val atree = tree(annotee)(ctx)
mkAnnotation(atree)
}
annot
}
}

}

}
14 changes: 14 additions & 0 deletions src/compiler/scala/tools/nsc/tasty/bridge/ContextOps.scala
Expand Up @@ -120,6 +120,8 @@ trait ContextOps { self: TastyUniverse =>
*/
private def analyseAnnotations(sym: Symbol)(implicit ctx: Context): Unit = {

def inOwner[T](op: Context => T): T = op(ctx.withOwner(sym.owner))

def lookupChild(childTpe: Type): Symbol = {
val child = symOfType(childTpe)
assert(isSymbol(child), s"did not find symbol of sealed child ${showType(childTpe)}")
Expand All @@ -132,6 +134,8 @@ trait ContextOps { self: TastyUniverse =>
}
}

var problematic: List[String] = Nil

for (annot <- sym.annotations) {
annot.completeInfo()
if (annot.tpe.typeSymbolDirect === defn.ChildAnnot) {
Expand All @@ -154,6 +158,16 @@ trait ContextOps { self: TastyUniverse =>
ctx.log(s"adding sealed child ${showSym(child)} to ${showSym(sym)}")
sym.addChild(child)
}
if ((annot.symbol eq defn.TargetNameAnnotationClass) ||
(annot.symbol eq defn.StaticMethodAnnotationClass)) {
problematic ::= inOwner { implicit ctx =>
unsupportedMessage(s"annotation on $sym: @$annot")
}
}
}
if (problematic.nonEmpty) {
sym.removeAnnotation(u.definitions.CompileTimeOnlyAttr)
sym.addAnnotation(u.definitions.CompileTimeOnlyAttr, u.Literal(u.Constant(problematic.head)))
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/compiler/scala/tools/nsc/tasty/bridge/NameOps.scala
Expand Up @@ -54,6 +54,8 @@ trait NameOps { self: TastyUniverse =>
final val Tuple: String = "Tuple"
final val Matchable: String = "Matchable"

val ErasedFunctionN = raw"ErasedFunction(\d+)".r
val ErasedContextFunctionN = raw"ErasedContextFunction(\d+)".r
val ContextFunctionN = raw"ContextFunction(\d+)".r
val FunctionN = raw"Function(\d+)".r

Expand Down
3 changes: 3 additions & 0 deletions src/compiler/scala/tools/nsc/tasty/bridge/SymbolOps.scala
Expand Up @@ -117,6 +117,9 @@ trait SymbolOps { self: TastyUniverse =>
def safeOwner: Symbol = if (sym.owner eq sym) sym else sym.owner
}

/** Is this symbol annotated with `scala.annotation.experimental`? */
def symIsExperimental(sym: Symbol) = sym.hasAnnotation(defn.ExperimentalAnnotationClass)

/** if isConstructor, make sure it has one non-implicit parameter list */
def normalizeIfConstructor(termParamss: List[List[Symbol]], isConstructor: Boolean): List[List[Symbol]] =
if (isConstructor &&
Expand Down
51 changes: 35 additions & 16 deletions src/compiler/scala/tools/nsc/tasty/bridge/TypeOps.scala
Expand Up @@ -36,8 +36,11 @@ trait TypeOps { self: TastyUniverse =>

/** `*:` erases to either TupleXXL or Product */
@inline final def genTupleIsUnsupported[T](name: String)(implicit ctx: Context): T = unsupportedError(s"generic tuple type $name in ${boundsString(ctx.owner)}")
@inline final def bigFnIsUnsupported[T](tpeStr: String)(implicit ctx: Context): T = unsupportedError(s"function type with more than 22 parameters in ${boundsString(ctx.owner)}: $tpeStr")
@inline final def ctxFnIsUnsupported[T](tpeStr: String)(implicit ctx: Context): T = unsupportedError(s"context function type in ${boundsString(ctx.owner)}: $tpeStr")
@inline final def fnIsUnsupported[T](kind: String => String, tpeStr: String)(implicit ctx: Context): T = unsupportedError(s"${kind("function type")} in ${boundsString(ctx.owner)}: $tpeStr")
@inline final def bigFnIsUnsupported[T](tpeStr: String)(implicit ctx: Context): T = fnIsUnsupported(ft => s"$ft with more than 22 parameters", tpeStr)
@inline final def ctxFnIsUnsupported[T](tpeStr: String)(implicit ctx: Context): T = fnIsUnsupported(ft => s"context $ft", tpeStr)
@inline final def erasedFnIsUnsupported[T](tpeStr: String)(implicit ctx: Context): T = fnIsUnsupported(ft => s"erased $ft", tpeStr)
@inline final def erasedCtxFnIsUnsupported[T](tpeStr: String)(implicit ctx: Context): T = fnIsUnsupported(ft => s"erased context $ft", tpeStr)
@inline final def unionIsUnsupported[T](implicit ctx: Context): T = unsupportedError(s"union in ${boundsString(ctx.owner)}")
@inline final def matchTypeIsUnsupported[T](implicit ctx: Context): T = unsupportedError(s"match type in ${boundsString(ctx.owner)}")
@inline final def erasedRefinementIsUnsupported[T](implicit ctx: Context): T = unsupportedError(s"erased modifier in refinement of ${ctx.owner}")
Expand Down Expand Up @@ -142,6 +145,7 @@ trait TypeOps { self: TastyUniverse =>
final val RepeatedAnnot: Symbol = u.definitions.RepeatedAnnotationClass
final val TargetNameAnnotationClass: Symbol = u.definitions.TargetNameAnnotationClass
final val StaticMethodAnnotationClass: Symbol = u.definitions.StaticMethodAnnotationClass
final val ExperimentalAnnotationClass: Symbol = u.definitions.ExperimentalAnnotationClass

object PolyFunctionType {

Expand Down Expand Up @@ -258,21 +262,26 @@ trait TypeOps { self: TastyUniverse =>

def AppliedType(tycon: Type, args: List[Type])(implicit ctx: Context): Type = {

def formatFnType(arrow: String, arity: Int, args: List[Type]): String = {
def formatFnType(arrow: String, isErased: Boolean, arity: Int, args: List[Type]): String = {
val len = args.length
assert(len == arity + 1) // tasty should be type checked already
val res = args.last
val params = args.init
val paramsBody = params.mkString(",")
val paramsBody = {
val body = params.mkString(",")
if (isErased) s"erased $body" else body
}
val argList = if (len == 2) paramsBody else s"($paramsBody)"
s"$argList $arrow $res"
}

def typeRefUncurried(tycon: Type, args: List[Type]): Type = tycon match {
case tycon: u.TypeRef if tycon.typeArgs.nonEmpty =>
unsupportedError(s"curried type application $tycon[${args.mkString(",")}]")
case ContextFunctionType(n) => ctxFnIsUnsupported(formatFnType("?=>", n, args))
case FunctionXXLType(n) => bigFnIsUnsupported(formatFnType("=>", n, args))
case ContextFunctionType(n) => ctxFnIsUnsupported(formatFnType("?=>", isErased = false, n, args))
case ErasedContextFunctionType(n) => erasedCtxFnIsUnsupported(formatFnType("?=>", isErased = true, n, args))
case ErasedFunctionType(n) => erasedFnIsUnsupported(formatFnType("=>", isErased = true, n, args))
case FunctionXXLType(n) => bigFnIsUnsupported(formatFnType("=>", isErased = false, n, args))
case _ =>
u.appliedType(tycon, args)
}
Expand Down Expand Up @@ -326,15 +335,17 @@ trait TypeOps { self: TastyUniverse =>
if (prefix.typeSymbol === u.definitions.ScalaPackage) {
name match {
case TypeName(SimpleName(raw @ SyntheticScala3Type())) => raw match {
case tpnme.And => AndTpe
case tpnme.Or => unionIsUnsupported
case tpnme.ContextFunctionN(n) if (n.toInt > 0) => ContextFunctionType(n.toInt)
case tpnme.FunctionN(n) if (n.toInt > 22) => FunctionXXLType(n.toInt)
case tpnme.TupleCons => genTupleIsUnsupported("scala.*:")
case tpnme.Tuple if !ctx.mode.is(ReadParents) => genTupleIsUnsupported("scala.Tuple")
case tpnme.AnyKind => u.definitions.AnyTpe
case tpnme.Matchable => u.definitions.AnyTpe
case _ => doLookup
case tpnme.And => AndTpe
case tpnme.Or => unionIsUnsupported
case tpnme.ContextFunctionN(n) => ContextFunctionType(n.toInt)
case tpnme.FunctionN(n) if (n.toInt > 22) => FunctionXXLType(n.toInt)
case tpnme.TupleCons => genTupleIsUnsupported("scala.*:")
case tpnme.Tuple if !ctx.mode.is(ReadParents) => genTupleIsUnsupported("scala.Tuple")
case tpnme.AnyKind => u.definitions.AnyTpe
case tpnme.Matchable => u.definitions.AnyTpe
case tpnme.ErasedContextFunctionN(n) if n.toInt > 0 => ErasedContextFunctionType(n.toInt)
case tpnme.ErasedFunctionN(n) => ErasedFunctionType(n.toInt)
case _ => doLookup
}

case _ => doLookup
Expand Down Expand Up @@ -486,6 +497,14 @@ trait TypeOps { self: TastyUniverse =>
*/
case object AndTpe extends Type

case class ErasedFunctionType(arity: Int) extends Type {
assert(arity > 0)
}

case class ErasedContextFunctionType(arity: Int) extends Type {
assert(arity > 0)
}

case class ContextFunctionType(arity: Int) extends Type {
assert(arity > 0)
}
Expand All @@ -495,7 +514,7 @@ trait TypeOps { self: TastyUniverse =>
}

private val SyntheticScala3Type =
raw"^(?:&|\||AnyKind|(?:Context)?Function\d+|\*:|Tuple|Matchable)$$".r
raw"^(?:&|\||AnyKind|(?:Erased)?(?:Context)?Function\d+|\*:|Tuple|Matchable)$$".r

sealed abstract trait TastyRepr extends u.Type {
def tflags: TastyFlagSet
Expand Down
5 changes: 5 additions & 0 deletions src/compiler/scala/tools/nsc/typechecker/RefChecks.scala
Expand Up @@ -1286,6 +1286,11 @@ abstract class RefChecks extends Transform {
if (changed)
refchecksWarning(pos, s"${sym.fullLocationString} has changed semantics in version ${sym.migrationVersion.get}:\n${sym.migrationMessage.get}", WarningCategory.OtherMigration)
}
if (sym.isExperimental && !currentOwner.ownerChain.exists(x => x.isExperimental)) {
val msg =
s"${sym.fullLocationString} is marked @experimental and therefore its enclosing scope must be experimental."
reporter.error(pos, msg)
}
// See an explanation of compileTimeOnly in its scaladoc at scala.annotation.compileTimeOnly.
// async/await is expanded after erasure
if (sym.isCompileTimeOnly && !inAnnotation && !currentOwner.ownerChain.exists(x => x.isCompileTimeOnly)) {
Expand Down
1 change: 1 addition & 0 deletions src/reflect/scala/reflect/internal/Definitions.scala
Expand Up @@ -1316,6 +1316,7 @@ trait Definitions extends api.StandardDefinitions {
lazy val TargetNameAnnotationClass = getClassIfDefined("scala.annotation.targetName")
lazy val StaticMethodAnnotationClass = getClassIfDefined("scala.annotation.static")
lazy val PolyFunctionClass = getClassIfDefined("scala.PolyFunction")
lazy val ExperimentalAnnotationClass = getClassIfDefined("scala.annotation.experimental")

lazy val BeanPropertyAttr = requiredClass[scala.beans.BeanProperty]
lazy val BooleanBeanPropertyAttr = requiredClass[scala.beans.BooleanBeanProperty]
Expand Down
2 changes: 2 additions & 0 deletions src/reflect/scala/reflect/internal/Symbols.scala
Expand Up @@ -934,6 +934,8 @@ trait Symbols extends api.Symbols { self: SymbolTable =>
def isCompileTimeOnly = hasAnnotation(CompileTimeOnlyAttr)
def compileTimeOnlyMessage = getAnnotation(CompileTimeOnlyAttr) flatMap (_ stringArg 0)

def isExperimental = hasAnnotation(ExperimentalAnnotationClass)

/** Is this symbol an accessor method for outer? */
final def isOuterAccessor = hasFlag(STABLE | ARTIFACT) && (unexpandedName == nme.OUTER)

Expand Down
1 change: 1 addition & 0 deletions src/reflect/scala/reflect/runtime/JavaUniverseForce.scala
Expand Up @@ -443,6 +443,7 @@ trait JavaUniverseForce { self: runtime.JavaUniverse =>
definitions.TargetNameAnnotationClass
definitions.StaticMethodAnnotationClass
definitions.PolyFunctionClass
definitions.ExperimentalAnnotationClass
definitions.BeanPropertyAttr
definitions.BooleanBeanPropertyAttr
definitions.CompileTimeOnlyAttr
Expand Down
64 changes: 53 additions & 11 deletions src/tastytest/scala/tools/tastytest/Dotc.scala
Expand Up @@ -8,6 +8,9 @@ import scala.reflect.runtime.ReflectionUtils
import java.lang.reflect.{Modifier, Method}

import ClasspathOps._
import java.io.OutputStream
import java.io.BufferedReader
import java.io.PrintWriter

object Dotc extends Script.Command {

Expand Down Expand Up @@ -36,45 +39,83 @@ object Dotc extends Script.Command {
def invokeStatic(
className: String,
methodName: String,
args: Seq[String]
args: Seq[(Class[_], Any)],
)(implicit cl: Dotc.ClassLoader): Try[Object] = {
val cls = loadClass(className)
val method = cls.getMethod(methodName, classOf[Array[String]])
val (tpes, provided) = args.unzip
val method = cls.getMethod(methodName, tpes:_*)
Try {
invokeStatic(method, Seq(args.toArray))
invokeStatic(method, provided)
}
}

def invoke(method: Method, obj: AnyRef, args: Seq[Any])(implicit cl: Dotc.ClassLoader) = {
try cl.parent.asContext[AnyRef] {
inClassloader[AnyRef] {
method.invoke(obj, args.toArray:_*)
}
}

def inClassloader[T](op: => T)(implicit cl: Dotc.ClassLoader): T = {
try cl.parent.asContext[T] {
op
}
catch {
case NonFatal(ex) => throw ReflectionUtils.unwrapThrowable(ex)
}
}

private def dotcProcess(args: Seq[String])(implicit cl: Dotc.ClassLoader) = processMethod("dotty.tools.dotc.Main")(args)
def processMethod(className: String)(args: Seq[String])(implicit cl: Dotc.ClassLoader): Try[Boolean] =
processMethodImpl(className)(args, None)

private def makeConsoleReporter(stream: OutputStream)(implicit cl: Dotc.ClassLoader): Try[AnyRef] = Try {
val consoleReporterCls = loadClass("dotty.tools.dotc.reporting.ConsoleReporter")
val ctor = consoleReporterCls.getConstructor(classOf[BufferedReader], classOf[PrintWriter])
val pwriter = new PrintWriter(stream, true)
inClassloader[AnyRef] {
ctor.newInstance(Console.in, pwriter)
}
}

def processMethod(className: String)(args: Seq[String])(implicit cl: Dotc.ClassLoader): Try[Boolean] = {
private def processMethodImpl(className: String)(args: Seq[String], writer: Option[OutputStream])(implicit cl: Dotc.ClassLoader): Try[Boolean] = {
val reporterCls = loadClass("dotty.tools.dotc.reporting.Reporter")
val Reporter_hasErrors = reporterCls.getMethod("hasErrors")
for (reporter <- invokeStatic(className, "process", args)) yield {
val processArgs: Try[Seq[(Class[_], Any)]] = {
writer match {
case Some(stream) =>
val callbackCls = loadClass("dotty.tools.dotc.interfaces.CompilerCallback")
for (myReporter <- makeConsoleReporter(stream)) yield
Seq(classOf[Array[String]] -> args.toArray, reporterCls -> myReporter, callbackCls -> null)
case _ =>
Try(Seq(classOf[Array[String]] -> args.toArray))
}
}
for {
args <- processArgs
reporter <- invokeStatic(className, "process", args)
} yield {
val hasErrors = invoke(Reporter_hasErrors, reporter, Seq.empty).asInstanceOf[Boolean]
!hasErrors
}
}

def mainMethod(className: String)(args: Seq[String])(implicit cl: Dotc.ClassLoader): Try[Unit] =
for (_ <- invokeStatic(className, "main", args)) yield ()
def mainMethod(className: String)(args: Seq[String])(implicit cl: Dotc.ClassLoader): Try[Unit] = {
val mainArgs = Seq(classOf[Array[String]] -> args.toArray)
for (_ <- invokeStatic(className, "main", mainArgs)) yield ()
}

def dotcVersion(implicit cl: Dotc.ClassLoader): String = {
val compilerPropertiesClass = loadClass("dotty.tools.dotc.config.Properties")
val Properties_simpleVersionString = compilerPropertiesClass.getMethod("simpleVersionString")
invokeStatic(Properties_simpleVersionString, Seq.empty).asInstanceOf[String]
}

def dotc(out: String, classpath: String, additionalSettings: Seq[String], sources: String*)(implicit cl: Dotc.ClassLoader): Try[Boolean] = {
def dotc(out: String, classpath: String, additionalSettings: Seq[String], sources: String*)(implicit cl: Dotc.ClassLoader): Try[Boolean] =
dotcImpl(None, out, classpath, additionalSettings, sources:_*)

def dotc(writer: OutputStream, out: String, classpath: String, additionalSettings: Seq[String], sources: String*)(implicit cl: Dotc.ClassLoader): Try[Boolean] =
dotcImpl(Some(writer), out, classpath, additionalSettings, sources:_*)

def dotcImpl(writer: Option[OutputStream], out: String, classpath: String, additionalSettings: Seq[String], sources: String*)(implicit cl: Dotc.ClassLoader): Try[Boolean] = {
if (sources.isEmpty) {
Success(true)
}
Expand All @@ -85,11 +126,12 @@ object Dotc extends Script.Command {
"-classpath", libraryDeps.mkString(classpath + Files.classpathSep, Files.classpathSep, ""),
"-deprecation",
"-Xfatal-warnings",
"-color:never",
) ++ additionalSettings ++ sources
if (TastyTest.verbose) {
println(yellow(s"Invoking dotc (version $dotcVersion) with args: $args"))
}
dotcProcess(args)
processMethodImpl("dotty.tools.dotc.Main")(args, writer)
}
}

Expand Down
13 changes: 10 additions & 3 deletions src/tastytest/scala/tools/tastytest/Scalac.scala
Expand Up @@ -3,10 +3,15 @@ package scala.tools.tastytest
import scala.collection.immutable.ArraySeq
import scala.util.{ Try, Success, chaining }, chaining._
import scala.tools.nsc.{Global, Settings, reporters}, reporters.ConsoleReporter
import java.io.OutputStream
import java.io.PrintWriter

object Scalac extends Script.Command {

def scalac(out: String, additionalSettings: Seq[String], sources: String*): Try[Boolean] = {
def scalac(out: String, additionalSettings: Seq[String], sources: String*): Try[Boolean] =
scalac(Console.out, out, additionalSettings, sources:_*)

def scalac(writer: OutputStream, out: String, additionalSettings: Seq[String], sources: String*) = {

def runCompile(global: Global): Boolean = {
global.reporter.reset()
Expand All @@ -19,8 +24,10 @@ object Scalac extends Script.Command {
def newCompiler(args: String*): Global =
fromSettings(new Settings().tap(_.processArguments(args.toList, true)))

def fromSettings(settings: Settings): Global =
Global(settings, new ConsoleReporter(settings).tap(_.shortname = true))
def fromSettings(settings: Settings): Global = {
val pwriter = new PrintWriter(writer, true)
Global(settings, new ConsoleReporter(settings, Console.in, pwriter).tap(_.shortname = true))
}

def compile(args: String*) =
Try(runCompile(newCompiler(args: _*)))
Expand Down
1 change: 1 addition & 0 deletions src/tastytest/scala/tools/tastytest/SourceKind.scala
Expand Up @@ -9,6 +9,7 @@ object SourceKind {
case object NoSource extends SourceKind("")(filter = _ => false)
case object Scala extends SourceKind(".scala")()
case object ScalaFail extends SourceKind("_fail.scala")()
case object ScalaPre extends SourceKind("_pre.scala")()
case object Check extends SourceKind(".check")()
case object SkipCheck extends SourceKind(".skipcheck")()
case object Java extends SourceKind(".java")()
Expand Down