From 45f1f38622679d2b0632c59bdd2de4773e024e5f Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Tue, 26 Apr 2022 18:37:52 +0200 Subject: [PATCH] restrict experimental definitions experimental definitions are treated like compileTimeOnly. Test that a scala 3 application can not depend on a scala 2 library that uses experimental definitions. --- .../nsc/tasty/bridge/AnnotationOps.scala | 28 +--- .../tools/nsc/tasty/bridge/ContextOps.scala | 14 ++ .../tools/nsc/tasty/bridge/NameOps.scala | 2 + .../tools/nsc/tasty/bridge/SymbolOps.scala | 3 + .../tools/nsc/tasty/bridge/TypeOps.scala | 51 ++++-- .../tools/nsc/typechecker/RefChecks.scala | 5 + .../scala/reflect/internal/Definitions.scala | 1 + .../scala/reflect/internal/Symbols.scala | 2 + .../reflect/runtime/JavaUniverseForce.scala | 1 + .../scala/tools/tastytest/Dotc.scala | 64 +++++-- .../scala/tools/tastytest/Scalac.scala | 13 +- .../scala/tools/tastytest/SourceKind.scala | 1 + .../scala/tools/tastytest/TastyTest.scala | 157 +++++++++++++++--- .../ExperimentalDefsPre.scala | 10 ++ .../src-3-app/TestExperimentalDefsPre.check | 17 ++ .../TestExperimentalDefsPre_fail.scala | 6 + .../src-3-upstream/ExperimentalClass.scala | 6 + test/tasty/neg/src-2/ExperimentalDefs.check | 25 +++ .../neg/src-2/ExperimentalDefs_fail.scala | 28 ++++ .../neg/src-2/ExperimentalDefs_pre.scala | 9 + .../tasty/neg/src-2/TestSaferExceptions.check | 4 + .../neg/src-2/TestSaferExceptions_fail.scala | 7 + test/tasty/neg/src-3/ExperimentalClass.scala | 6 + test/tasty/neg/src-3/ExperimentalObj.scala | 8 + test/tasty/neg/src-3/SaferExceptions.scala | 21 +++ .../tools/tastytest/TastyTestJUnit.scala | 10 ++ 26 files changed, 424 insertions(+), 75 deletions(-) create mode 100644 test/tasty/neg-full-circle/src-2-downstream/ExperimentalDefsPre.scala create mode 100644 test/tasty/neg-full-circle/src-3-app/TestExperimentalDefsPre.check create mode 100644 test/tasty/neg-full-circle/src-3-app/TestExperimentalDefsPre_fail.scala create mode 100644 test/tasty/neg-full-circle/src-3-upstream/ExperimentalClass.scala create mode 100644 test/tasty/neg/src-2/ExperimentalDefs.check create mode 100644 test/tasty/neg/src-2/ExperimentalDefs_fail.scala create mode 100644 test/tasty/neg/src-2/ExperimentalDefs_pre.scala create mode 100644 test/tasty/neg/src-2/TestSaferExceptions.check create mode 100644 test/tasty/neg/src-2/TestSaferExceptions_fail.scala create mode 100644 test/tasty/neg/src-3/ExperimentalClass.scala create mode 100644 test/tasty/neg/src-3/ExperimentalObj.scala create mode 100644 test/tasty/neg/src-3/SaferExceptions.scala diff --git a/src/compiler/scala/tools/nsc/tasty/bridge/AnnotationOps.scala b/src/compiler/scala/tools/nsc/tasty/bridge/AnnotationOps.scala index da033324bd42..5641c1a3eb24 100644 --- a/src/compiler/scala/tools/nsc/tasty/bridge/AnnotationOps.scala +++ b/src/compiler/scala/tools/nsc/tasty/bridge/AnnotationOps.scala @@ -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 } } - } } diff --git a/src/compiler/scala/tools/nsc/tasty/bridge/ContextOps.scala b/src/compiler/scala/tools/nsc/tasty/bridge/ContextOps.scala index c4f5aeec6fc9..8dabc199e1e9 100644 --- a/src/compiler/scala/tools/nsc/tasty/bridge/ContextOps.scala +++ b/src/compiler/scala/tools/nsc/tasty/bridge/ContextOps.scala @@ -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)}") @@ -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) { @@ -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))) } } diff --git a/src/compiler/scala/tools/nsc/tasty/bridge/NameOps.scala b/src/compiler/scala/tools/nsc/tasty/bridge/NameOps.scala index 7eb7b0e7f03e..ec50156ab0c8 100644 --- a/src/compiler/scala/tools/nsc/tasty/bridge/NameOps.scala +++ b/src/compiler/scala/tools/nsc/tasty/bridge/NameOps.scala @@ -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 diff --git a/src/compiler/scala/tools/nsc/tasty/bridge/SymbolOps.scala b/src/compiler/scala/tools/nsc/tasty/bridge/SymbolOps.scala index 543bbb72d469..8fe679433a9e 100644 --- a/src/compiler/scala/tools/nsc/tasty/bridge/SymbolOps.scala +++ b/src/compiler/scala/tools/nsc/tasty/bridge/SymbolOps.scala @@ -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 && diff --git a/src/compiler/scala/tools/nsc/tasty/bridge/TypeOps.scala b/src/compiler/scala/tools/nsc/tasty/bridge/TypeOps.scala index 513a2bf01cee..9231bcef98b2 100644 --- a/src/compiler/scala/tools/nsc/tasty/bridge/TypeOps.scala +++ b/src/compiler/scala/tools/nsc/tasty/bridge/TypeOps.scala @@ -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}") @@ -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 { @@ -258,12 +262,15 @@ 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" } @@ -271,8 +278,10 @@ trait TypeOps { self: TastyUniverse => 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) } @@ -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 @@ -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) } @@ -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 diff --git a/src/compiler/scala/tools/nsc/typechecker/RefChecks.scala b/src/compiler/scala/tools/nsc/typechecker/RefChecks.scala index 9d163e0edec9..1ba0bc494ef0 100644 --- a/src/compiler/scala/tools/nsc/typechecker/RefChecks.scala +++ b/src/compiler/scala/tools/nsc/typechecker/RefChecks.scala @@ -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)) { diff --git a/src/reflect/scala/reflect/internal/Definitions.scala b/src/reflect/scala/reflect/internal/Definitions.scala index a9e3edcb44db..1659f5d15351 100644 --- a/src/reflect/scala/reflect/internal/Definitions.scala +++ b/src/reflect/scala/reflect/internal/Definitions.scala @@ -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] diff --git a/src/reflect/scala/reflect/internal/Symbols.scala b/src/reflect/scala/reflect/internal/Symbols.scala index a35e33388af7..5f48bccfb8b6 100644 --- a/src/reflect/scala/reflect/internal/Symbols.scala +++ b/src/reflect/scala/reflect/internal/Symbols.scala @@ -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) diff --git a/src/reflect/scala/reflect/runtime/JavaUniverseForce.scala b/src/reflect/scala/reflect/runtime/JavaUniverseForce.scala index 3bd28812ccfb..61108d372dde 100644 --- a/src/reflect/scala/reflect/runtime/JavaUniverseForce.scala +++ b/src/reflect/scala/reflect/runtime/JavaUniverseForce.scala @@ -443,6 +443,7 @@ trait JavaUniverseForce { self: runtime.JavaUniverse => definitions.TargetNameAnnotationClass definitions.StaticMethodAnnotationClass definitions.PolyFunctionClass + definitions.ExperimentalAnnotationClass definitions.BeanPropertyAttr definitions.BooleanBeanPropertyAttr definitions.CompileTimeOnlyAttr diff --git a/src/tastytest/scala/tools/tastytest/Dotc.scala b/src/tastytest/scala/tools/tastytest/Dotc.scala index 8be7725c0810..d117af04747b 100644 --- a/src/tastytest/scala/tools/tastytest/Dotc.scala +++ b/src/tastytest/scala/tools/tastytest/Dotc.scala @@ -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 { @@ -36,37 +39,69 @@ 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") @@ -74,7 +109,13 @@ object Dotc extends Script.Command { 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) } @@ -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) } } diff --git a/src/tastytest/scala/tools/tastytest/Scalac.scala b/src/tastytest/scala/tools/tastytest/Scalac.scala index 1ea3a646a60f..76ed0627818c 100644 --- a/src/tastytest/scala/tools/tastytest/Scalac.scala +++ b/src/tastytest/scala/tools/tastytest/Scalac.scala @@ -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() @@ -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: _*))) diff --git a/src/tastytest/scala/tools/tastytest/SourceKind.scala b/src/tastytest/scala/tools/tastytest/SourceKind.scala index 56dfaef39d76..b22a578c25fb 100644 --- a/src/tastytest/scala/tools/tastytest/SourceKind.scala +++ b/src/tastytest/scala/tools/tastytest/SourceKind.scala @@ -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")() diff --git a/src/tastytest/scala/tools/tastytest/TastyTest.scala b/src/tastytest/scala/tools/tastytest/TastyTest.scala index d3e9122adbdf..51fa8896cf46 100644 --- a/src/tastytest/scala/tools/tastytest/TastyTest.scala +++ b/src/tastytest/scala/tools/tastytest/TastyTest.scala @@ -11,6 +11,7 @@ import java.{ util => ju } import SourceKind._ import Files._ +import java.io.OutputStream object TastyTest { @@ -19,6 +20,17 @@ object TastyTest { private def log(s: => String): Unit = if (verbose) println(s) + /**Simulates a Scala 2 application that depends on a Scala 3 library, where both may depend on a common prelude + * compiled by Scala 2. + * + * Steps: + * 1) compile all Scala files in `pre` with scala 2 to `out` + * 2) compile all Scala files in `src-3` with scala 3 to `out`, with `out` as the classpath + * 3) compile all Scala files in `src-2` with scala 2 to `out`, with `out` as the classpath + * 4) run the main method of all classes in `out/pkgName` that match a file in `src-2`. + * e.g. `out/tastytest/TestFoo.class` should be compiled from a corresponding file + * `src-2/tastytest/TestFoo.scala`. + */ def runSuite(src: String, srcRoot: String, pkgName: String, outDir: Option[String], additionalSettings: Seq[String], additionalDottySettings: Seq[String])(implicit cl: Dotc.ClassLoader): Try[Unit] = for { (pre, src2, src3) <- getRunSources(srcRoot/src) out <- outDir.fold(tempDir(pkgName))(dir) @@ -29,6 +41,14 @@ object TastyTest { _ <- runMainOn(out, testNames:_*) } yield () + /**Simulates a Scala 2 application that depends on a Scala 3 library, where both may depend on a common prelude + * compiled by Scala 2 and Java. In this case the applications are not executed. + * Steps: + * 1) compile all Java files in `pre` with Java to `out` + * 2) compile all Scala files in `pre` with Scala 2 to `out`, with `out` as the classpath + * 3) compile all Scala files in `src-3` with scala 3 to `out`, with `out` as the classpath + * 4) compile all Scala files in `src-2` with scala 2 to `out`, with `out` as the classpath + */ def posSuite(src: String, srcRoot: String, pkgName: String, outDir: Option[String], additionalSettings: Seq[String], additionalDottySettings: Seq[String])(implicit cl: Dotc.ClassLoader): Try[Unit] = for { (pre, src2, src3) <- getRunSources(srcRoot/src, preFilters = Set(Scala, Java)) _ = log(s"Sources to compile under test: ${src2.map(cyan).mkString(", ")}") @@ -39,6 +59,16 @@ object TastyTest { _ <- scalacPos(out, sourceRoot=srcRoot/src/"src-2", additionalSettings, src2:_*) } yield () + /**Simulates a Scala 2 application that depends on a Scala 3 library, and is expected to fail compilation. + * Steps: + * 1) compile all Scala files in `src-3` with scala 3 to `out` + * 2) attempt to compile all Scala files in `src-2` with scala 2 to `out`, with `out` as the classpath. + * - If a file matches `FOO_fail.scala`, then it is expected to fail compilation. + * - For each `FOO_fail.scala`, if the file fails compilation, there is expected to be a corresponding `FOO.check` file, containing + * the captured errors, or else a `FOO.skipcheck` file indicating to skip comparing errors. + * - If `FOO_fail.scala` has a corresponding `FOO_pre.scala` file, then that is compiled first to `out`, + * so that `FOO_fail.scala` may depend on its compilation results. + */ def negSuite(src: String, srcRoot: String, pkgName: String, outDir: Option[String], additionalSettings: Seq[String], additionalDottySettings: Seq[String])(implicit cl: Dotc.ClassLoader): Try[Unit] = for { (src2, src3) <- get2And3Sources(srcRoot/src, src2Filters = Set(Scala, Check, SkipCheck)) out <- outDir.fold(tempDir(pkgName))(dir) @@ -46,15 +76,47 @@ object TastyTest { _ <- scalacNeg(out, additionalSettings, src2:_*) } yield () + /**Simulates a Scala 3 application that depends on a Scala 2 library, where the Scala 2 + * library directly depends on an upstream Scala 3 library. The Scala 3 application is expected to fail compilation. + * Steps: + * 1) compile all Scala files in `src-3-upstream` with scala 3 to `out` + * 2) compile all Scala files in `src-2-downstream` with scala 2 to `out`, with `out` as the classpath. + * 3) attempt to compile all Scala files in `src-3-app` with scala 3 to `out`, with `out` as the classpath, + * following the same steps as `negSuite` to check for errors in compilation. + */ + def negFullCircleSuite(src: String, srcRoot: String, pkgName: String, outDir: Option[String], additionalSettings: Seq[String], additionalDottySettings: Seq[String])(implicit cl: Dotc.ClassLoader): Try[Unit] = for { + (src3u, src2d, src3a) <- getFullCircleSources(srcRoot/src, src3appFilters = Set(Scala, Check, SkipCheck)) + out <- outDir.fold(tempDir(pkgName))(dir) + _ <- dotcPos(out, sourceRoot=srcRoot/src/"src-3-upstream", additionalDottySettings, src3u:_*) + _ <- scalacPos(out, sourceRoot=srcRoot/src/"src-2-downstream", additionalSettings, src2d:_*) + _ <- dotcNeg(out, additionalDottySettings, src3a:_*) + } yield () + + /**Same as `negSuite`, but introduces a dependency on a prelude by both the Scala 3 and Scala 2 libraries. In + * this case, they depend on binary incompatible versions of the same prelude (e.g. some definitions have moved + * between versions). Steps: + * 1) compile all Scala files in `pre-A` with scala 2 to `out1`. + * 2) compile all Scala files in `pre-B` with scala 2 to `out2`. + * 3) compile all Scala files in `src-3` with scala 3 to `out2`, with `out1` as the classpath. + * 4) attempt to compile all Scala files in `src-2` with scala 2 to `out2`, with `out2` as the classpath, + * following the same steps as `negSuite` to check for errors in compilation. + */ def negChangePreSuite(src: String, srcRoot: String, pkgName: String, outDirs: Option[(String, String)], additionalSettings: Seq[String], additionalDottySettings: Seq[String])(implicit cl: Dotc.ClassLoader): Try[Unit] = for { (preA, preB, src2, src3) <- getMovePreChangeSources(srcRoot/src, src2Filters = Set(Scala, Check, SkipCheck)) (out1, out2) <- outDirs.fold(tempDir(pkgName) *> tempDir(pkgName))(p => dir(p._1) *> dir(p._2)) _ <- scalacPos(out1, sourceRoot=srcRoot/src/"pre-A", additionalSettings, preA:_*) - _ <- dotcPos(out2, out1, sourceRoot=srcRoot/src/"src-3", additionalDottySettings, src3:_*) _ <- scalacPos(out2, sourceRoot=srcRoot/src/"pre-B", additionalSettings, preB:_*) + _ <- dotcPos(out2, out1, sourceRoot=srcRoot/src/"src-3", additionalDottySettings, src3:_*) _ <- scalacNeg(out2, additionalSettings, src2:_*) } yield () + /**Same as `negSuite`, but in addition, the Scala 3 library depends on another upstream Scala 3 library, + * which is missing from the classpath when compiling the Scala 2 library. Steps: + * 1) compile all Scala files in `src-3-A` with scala 3 to `out1`. + * 3) compile all Scala files in `src-3-B` with scala 3 to `out2`, with `out1:out2` as the classpath. + * 3) attempt to compile all Scala files in `src-2` with scala 2 to `out2`, with `out2` as the classpath, + * following the same steps as `negSuite` to check for errors in compilation. + */ def negSuiteIsolated(src: String, srcRoot: String, pkgName: String, outDirs: Option[(String, String)], additionalSettings: Seq[String], additionalDottySettings: Seq[String])(implicit cl: Dotc.ClassLoader): Try[Unit] = for { (src2, src3A, src3B) <- getNegIsolatedSources(srcRoot/src, src2Filters = Set(Scala, Check, SkipCheck)) (out1, out2) <- outDirs.fold(tempDir(pkgName) *> tempDir(pkgName))(p => dir(p._1) *> dir(p._2)) @@ -74,39 +136,50 @@ object TastyTest { } private def scalacNeg(out: String, additionalSettings: Seq[String], files: String*): Try[Unit] = { + def compile(source: String, writer: OutputStream) = + Scalac.scalac(writer, out, "-Ytasty-reader" +: additionalSettings, source) + negTestImpl(withCapture(_, compile, identity))(files:_*) + } + + private def withCapture(source: String, compile: (String, OutputStream) => Try[Boolean], post: String => String): (String, Try[Boolean]) = { + val byteArrayStream = new ByteArrayOutputStream(50) + try { + val compiled = compile(source, byteArrayStream) + (post(byteArrayStream.toString), compiled) + } finally byteArrayStream.close() + } + + private def negTestImpl(compile: String => (String, Try[Boolean]))(files: String*): Try[Unit] = { val errors = mutable.ArrayBuffer.empty[String] val unexpectedFail = mutable.ArrayBuffer.empty[String] - val failMap = { + val failMap: Map[String, (Option[String], Option[String])] = { val (sources, rest) = files.partition(ScalaFail.filter) sources.map({ s => - val name = s.stripSuffix(ScalaFail.name) + val name = s.stripSuffix(ScalaFail.name) val check = Check.fileOf(name) - val skip = SkipCheck.fileOf(name) - val found = rest.find(n => n == check || n == skip) - s -> found + val skip = SkipCheck.fileOf(name) + val pre = ScalaPre.fileOf(name) + val foundCheck = rest.find(n => n == check || n == skip) + val foundPre = rest.find(_ == pre) + s -> (foundCheck, foundPre) }).toMap } if (failMap.isEmpty) { printwarnln(s"Warning: there are no source files marked as fail tests. (**/*${ScalaFail.name})") } - for (source <- files.filter(Scala.filter)) { - val buf = new StringBuilder(50) - val compiled = { - val byteArrayStream = new ByteArrayOutputStream(50) - try { - if (ScalaFail.filter(source)) { - log(s"neg test ${cyan(source.stripSuffix(ScalaFail.name))} started") - } - val compiled = Console.withErr(byteArrayStream) { - Console.withOut(byteArrayStream) { - Scalac.scalac(out, "-Ytasty-reader" +: additionalSettings, source) - } + def negCompile(source: String): Unit = { + val (output, compiled) = { + if (ScalaFail.filter(source)) { + val testName = source.stripSuffix(ScalaFail.name) + log(s"neg test ${cyan(testName)} started") + failMap(source) match { + case (_, Some(pre)) => + log(s" - compiling pre file...") + negCompile(pre) + case _ => } - byteArrayStream.flush() - buf.append(byteArrayStream.toString) - compiled } - finally byteArrayStream.close() + compile(source) } if (compiled.getOrElse(false)) { if (failMap.contains(source)) { @@ -115,13 +188,12 @@ object TastyTest { } } else { - val output = buf.toString failMap.get(source) match { case None => unexpectedFail += source System.err.println(output) printerrln(s"ERROR: $source did not compile when expected to. Perhaps it should match (**/*${ScalaFail.name})") - case Some(Some(checkFile)) if Check.filter(checkFile) => + case Some((Some(checkFile), _)) if Check.filter(checkFile) => processLines(checkFile) { stream => val checkLines = stream.iterator().asScala.toSeq val outputLines = Diff.splitIntoLines(output) @@ -131,9 +203,9 @@ object TastyTest { printerrln(s"ERROR: $source failed, unexpected output.\n$diff") } } - case Some(Some(skipCheckFile)) => + case Some((Some(skipCheckFile), _)) => printwarnln(s"warning: skipping check on ${skipCheckFile.stripSuffix(SkipCheck.name)}") - case Some(None) => + case Some((None, _)) => if (output.nonEmpty) { errors += source val diff = Diff.compareContents(output, "") @@ -142,6 +214,9 @@ object TastyTest { } } } + + val sources = files.filter(Scala.filter).filterNot(ScalaPre.filter) + sources.foreach(negCompile) successWhen(errors.isEmpty && unexpectedFail.isEmpty) { if (unexpectedFail.nonEmpty) { val str = if (unexpectedFail.size == 1) "file" else "files" @@ -162,6 +237,21 @@ object TastyTest { successWhen(process)("dotc failed to compile sources.") } + private def dotcNeg(out: String, additionalSettings: Seq[String], files: String*)(implicit cl: Dotc.ClassLoader): Try[Unit] = { + def compile(source: String, writer: OutputStream) = { + Dotc.dotc(writer, out, out, additionalSettings, source) + } + def scrub(source: String, output: String): String = { + output.linesIterator.collect { + case header if header.contains(source) => + val filePart = source.split(Files.pathSep).last + header.trim.replace(source, filePart) + case ok => ok + }.mkString(System.lineSeparator()) + } + negTestImpl(src => withCapture(src, compile, scrub(src, _)))(files:_*) + } + private def getSourceAsName(path: String): String = path.substring(path.lastIndexOf(pathSep) + pathSep.length).stripSuffix(".scala") @@ -195,6 +285,21 @@ object TastyTest { } yield (filterByKind(src2Filters, src2:_*), filterByKind(src3Filters, src3:_*)) } + private def getFullCircleSources(root: String, src3upFilters: Set[SourceKind] = Set(Scala), + src2downFilters: Set[SourceKind] = Set(Scala), + src3appFilters: Set[SourceKind] + ): Try[(Seq[String], Seq[String], Seq[String])] = { + for { + src3up <- getFiles(root/"src-3-upstream") + src2down <- getFiles(root/"src-2-downstream") + src3app <- getFiles(root/"src-3-app") + } yield ( + filterByKind(src3upFilters, src3up:_*), + filterByKind(src2downFilters, src2down:_*), + filterByKind(src3appFilters, src3app:_*) + ) + } + private def getPreChangeSources(root: String, preAFilters: Set[SourceKind] /*= Set(Scala)*/, preBFilters: Set[SourceKind] /*= Set(Scala)*/ ): Try[(Seq[String], Seq[String])] = { diff --git a/test/tasty/neg-full-circle/src-2-downstream/ExperimentalDefsPre.scala b/test/tasty/neg-full-circle/src-2-downstream/ExperimentalDefsPre.scala new file mode 100644 index 000000000000..da405984c1ce --- /dev/null +++ b/test/tasty/neg-full-circle/src-2-downstream/ExperimentalDefsPre.scala @@ -0,0 +1,10 @@ +package downstream + +import upstream.ExperimentalClass + +@scala.annotation.experimental +object ExperimentalDefsPre { + + class SubExperimentalNotExperimental extends ExperimentalClass + +} diff --git a/test/tasty/neg-full-circle/src-3-app/TestExperimentalDefsPre.check b/test/tasty/neg-full-circle/src-3-app/TestExperimentalDefsPre.check new file mode 100644 index 000000000000..a645c25a1366 --- /dev/null +++ b/test/tasty/neg-full-circle/src-3-app/TestExperimentalDefsPre.check @@ -0,0 +1,17 @@ +-- Error: TestExperimentalDefsPre_fail.scala:4:10 +4 | def test = new SubExperimentalNotExperimental + | ^ + |object ExperimentalDefsPre is marked @experimental and therefore may only be used in an experimental scope. +-- Error: TestExperimentalDefsPre_fail.scala:4:17 +4 | def test = new SubExperimentalNotExperimental + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + |object ExperimentalDefsPre is marked @experimental and therefore may only be used in an experimental scope. +-- Error: TestExperimentalDefsPre_fail.scala:6:35 +6 | class SubSubExperimental extends SubExperimentalNotExperimental + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + |object ExperimentalDefsPre is marked @experimental and therefore may only be used in an experimental scope. +-- Error: TestExperimentalDefsPre_fail.scala:6:8 +6 | class SubSubExperimental extends SubExperimentalNotExperimental + | ^ + |extension of experimental class SubExperimentalNotExperimental must have @experimental annotation +4 errors found diff --git a/test/tasty/neg-full-circle/src-3-app/TestExperimentalDefsPre_fail.scala b/test/tasty/neg-full-circle/src-3-app/TestExperimentalDefsPre_fail.scala new file mode 100644 index 000000000000..edb0145fa94f --- /dev/null +++ b/test/tasty/neg-full-circle/src-3-app/TestExperimentalDefsPre_fail.scala @@ -0,0 +1,6 @@ +import downstream.ExperimentalDefsPre.* + +object TestExperimentalDefsPre: + def test = new SubExperimentalNotExperimental + + class SubSubExperimental extends SubExperimentalNotExperimental diff --git a/test/tasty/neg-full-circle/src-3-upstream/ExperimentalClass.scala b/test/tasty/neg-full-circle/src-3-upstream/ExperimentalClass.scala new file mode 100644 index 000000000000..7b793d6b9005 --- /dev/null +++ b/test/tasty/neg-full-circle/src-3-upstream/ExperimentalClass.scala @@ -0,0 +1,6 @@ +package upstream + +import scala.annotation.experimental + +@experimental +class ExperimentalClass extends scala.annotation.Annotation diff --git a/test/tasty/neg/src-2/ExperimentalDefs.check b/test/tasty/neg/src-2/ExperimentalDefs.check new file mode 100644 index 000000000000..2323bdbc4725 --- /dev/null +++ b/test/tasty/neg/src-2/ExperimentalDefs.check @@ -0,0 +1,25 @@ +ExperimentalDefs_fail.scala:10: error: object ExperimentalObj in package tastytest is marked @experimental and therefore its enclosing scope must be experimental. + def termRef = ExperimentalObj.foo // error + ^ +ExperimentalDefs_fail.scala:11: error: class ExperimentalClass in package tastytest is marked @experimental and therefore its enclosing scope must be experimental. + def typeRef = new Box[ExperimentalClass]() // error + ^ +ExperimentalDefs_fail.scala:13: error: class ExperimentalClass in package tastytest is marked @experimental and therefore its enclosing scope must be experimental. + class SubExperimental extends ExperimentalClass // error + ^ +ExperimentalDefs_fail.scala:14: error: class ExperimentalClass in package tastytest is marked @experimental and therefore its enclosing scope must be experimental. + class SuperExperimental extends Box[ExperimentalClass] // error + ^ +ExperimentalDefs_fail.scala:15: error: class ExperimentalClass in package tastytest is marked @experimental and therefore its enclosing scope must be experimental. + type RefExperimental = Box[ExperimentalClass] // error + ^ +ExperimentalDefs_fail.scala:16: error: class ExperimentalClass in package tastytest is marked @experimental and therefore its enclosing scope must be experimental. + type MemberExperimental = { type Ref = ExperimentalClass } // error + ^ +ExperimentalDefs_fail.scala:17: error: class ExperimentalClass in package tastytest is marked @experimental and therefore its enclosing scope must be experimental. + type AnnotatedRef = Box[Int] @ExperimentalClass // error + ^ +ExperimentalDefs_fail.scala:19: error: object ExperimentalDefsPre is marked @experimental and therefore its enclosing scope must be experimental. + def refSubclassOfExperimental = new SubExperimentalNotExperimental() // error + ^ +8 errors diff --git a/test/tasty/neg/src-2/ExperimentalDefs_fail.scala b/test/tasty/neg/src-2/ExperimentalDefs_fail.scala new file mode 100644 index 000000000000..e93c07964e33 --- /dev/null +++ b/test/tasty/neg/src-2/ExperimentalDefs_fail.scala @@ -0,0 +1,28 @@ +import tastytest.ExperimentalObj +import tastytest.ExperimentalClass +import scala.annotation.compileTimeOnly +import ExperimentalDefsPre._ + +object ExperimentalDefs { + + class Box[T] + + def termRef = ExperimentalObj.foo // error + def typeRef = new Box[ExperimentalClass]() // error + + class SubExperimental extends ExperimentalClass // error + class SuperExperimental extends Box[ExperimentalClass] // error + type RefExperimental = Box[ExperimentalClass] // error + type MemberExperimental = { type Ref = ExperimentalClass } // error + type AnnotatedRef = Box[Int] @ExperimentalClass // error + + def refSubclassOfExperimental = new SubExperimentalNotExperimental() // error + + @compileTimeOnly("") + class OnlyAtCompileTime + + def fixMe1 = List[ExperimentalClass]() // TODO: why is checkUndesiredProperties not called? + def fixMe2 = List.apply[ExperimentalClass]() // TODO: why is checkUndesiredProperties not called? + def fixMe3 = List[OnlyAtCompileTime]() // TODO: why is checkUndesiredProperties not called? + def fixMe4 = List.apply[OnlyAtCompileTime]() // TODO: why is checkUndesiredProperties not called? +} diff --git a/test/tasty/neg/src-2/ExperimentalDefs_pre.scala b/test/tasty/neg/src-2/ExperimentalDefs_pre.scala new file mode 100644 index 000000000000..ab9664a66cc7 --- /dev/null +++ b/test/tasty/neg/src-2/ExperimentalDefs_pre.scala @@ -0,0 +1,9 @@ +import tastytest.ExperimentalObj +import tastytest.ExperimentalClass + +@scala.annotation.experimental +object ExperimentalDefsPre { + + class SubExperimentalNotExperimental extends ExperimentalClass + +} diff --git a/test/tasty/neg/src-2/TestSaferExceptions.check b/test/tasty/neg/src-2/TestSaferExceptions.check new file mode 100644 index 000000000000..ad1ae4edd7d8 --- /dev/null +++ b/test/tasty/neg/src-2/TestSaferExceptions.check @@ -0,0 +1,4 @@ +TestSaferExceptions_fail.scala:5: error: Unsupported Scala 3 erased context function type in bounds of type mayThrow: erased tastytest.SaferExceptions.CanThrowCapability[E] ?=> A; found in object tastytest.SaferExceptions. + def test = SaferExceptions.safeDiv(1, 0) // error + ^ +1 error diff --git a/test/tasty/neg/src-2/TestSaferExceptions_fail.scala b/test/tasty/neg/src-2/TestSaferExceptions_fail.scala new file mode 100644 index 000000000000..cd5ede134446 --- /dev/null +++ b/test/tasty/neg/src-2/TestSaferExceptions_fail.scala @@ -0,0 +1,7 @@ +package tastytest + +object TestSaferExceptions { + + def test = SaferExceptions.safeDiv(1, 0) // error + +} diff --git a/test/tasty/neg/src-3/ExperimentalClass.scala b/test/tasty/neg/src-3/ExperimentalClass.scala new file mode 100644 index 000000000000..fb28f00d7709 --- /dev/null +++ b/test/tasty/neg/src-3/ExperimentalClass.scala @@ -0,0 +1,6 @@ +package tastytest + +import scala.annotation.experimental + +@experimental +class ExperimentalClass extends scala.annotation.Annotation diff --git a/test/tasty/neg/src-3/ExperimentalObj.scala b/test/tasty/neg/src-3/ExperimentalObj.scala new file mode 100644 index 000000000000..59c291fc219f --- /dev/null +++ b/test/tasty/neg/src-3/ExperimentalObj.scala @@ -0,0 +1,8 @@ +package tastytest + +import scala.annotation.experimental + +@experimental +object ExperimentalObj { + def foo = 23 +} diff --git a/test/tasty/neg/src-3/SaferExceptions.scala b/test/tasty/neg/src-3/SaferExceptions.scala new file mode 100644 index 000000000000..65bb3cf240c6 --- /dev/null +++ b/test/tasty/neg/src-3/SaferExceptions.scala @@ -0,0 +1,21 @@ +package tastytest + +import scala.language.experimental.erasedDefinitions + +import scala.annotation.experimental + +@experimental +object SaferExceptions { + + class DivByZero extends Exception + + erased class CanThrowCapability[-E <: Exception] + + infix type mayThrow[+A, +E <: Exception] = (erased CanThrowCapability[E]) ?=> A + + def safeDiv(x: Int, y: Int): Int mayThrow DivByZero = { + if (y == 0) throw new DivByZero() + else x / y + } + +} diff --git a/test/tasty/test/scala/tools/tastytest/TastyTestJUnit.scala b/test/tasty/test/scala/tools/tastytest/TastyTestJUnit.scala index 71b901161da1..0ce6bfbffe48 100644 --- a/test/tasty/test/scala/tools/tastytest/TastyTestJUnit.scala +++ b/test/tasty/test/scala/tools/tastytest/TastyTestJUnit.scala @@ -27,6 +27,7 @@ class TastyTestJUnit { additionalDottySettings = Nil ).eval + /** false positives that should fail, but work when annotations are not read */ @test def posFalseNoAnnotations(): Unit = TastyTest.posSuite( src = "pos-false-noannotations", srcRoot = assertPropIsSet(propSrc), @@ -63,6 +64,15 @@ class TastyTestJUnit { additionalDottySettings = Nil ).eval + @test def negFullCircle(): Unit = TastyTest.negFullCircleSuite( + src = "neg-full-circle", + srcRoot = assertPropIsSet(propSrc), + pkgName = assertPropIsSet(propPkgName), + outDir = None, + additionalSettings = Nil, + additionalDottySettings = Nil + ).eval + val propSrc = "tastytest.src" val propPkgName = "tastytest.packageName"