From 484a62148ce259d408ebaae5ce46743560286a33 Mon Sep 17 00:00:00 2001 From: Seth Tisue Date: Mon, 6 Oct 2025 15:55:11 -0700 Subject: [PATCH 1/3] slim down CI matrix (just do 8 and 25) --- .github/workflows/ci.yml | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2afc7f9d..7639bf55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,19 +10,11 @@ jobs: strategy: fail-fast: false matrix: - java: [8, 11, 17] + java: [8, 25] scala: [2.11.x, 2.12.x, 2.13.x, 3.x] platform: [JVM, JS, Native] mode: [normal] exclude: - - java: 11 - platform: JS - - java: 11 - platform: Native - - java: 17 - platform: JS - - java: 17 - platform: Native - scala: 2.11.x platform: Native include: @@ -42,14 +34,6 @@ jobs: scala: 2.12.x mode: headerCheck platform: JVM - - java: 11 - scala: 2.12.x - mode: normal - platform: JVM - - java: 17 - scala: 2.12.x - mode: normal - platform: JVM runs-on: ubuntu-latest env: CI_JDK: ${{matrix.java}} From 1afcb7686f8572fef7ad535c8c97dca36b3a896b Mon Sep 17 00:00:00 2001 From: Seth Tisue Date: Mon, 14 Apr 2025 10:40:11 -0700 Subject: [PATCH 2/3] update `Using` from Scala 2.13.17 and add `UsingTest` fixes #692 includes Scala upgrades: Scala 2.13.17, 2.12.20 (was .13, .19), Scala.js (because only latest Scala.js is available for 2.13.17, as per Seb on the contributors forum) copies the sources from scala/scala repo, reformatted using scalafmt both `Using` and `UsingTest` needed slight adjustment to compile on 2.11 and 2.12; and `UsingTest` needed to be adjusted to use scala.util.control.compat.ControlThrowable I tried to get `UsingTest` passing on Scala 3. on the JVM, this was sufficient to get the 2.13.17 stdlib so that the tests pass: dependencyOverrides += "org.scala-lang" % "scala-library" % scala213 but it doesn't do the trick on JS or Native for reasons unknown. so I worked around by putting `UsingTest` under `src-jvm`. once we are on a Scala 3.3 LTS version that has taken the 2.13.17 upgrade, it could be moved to regular `src`. (but anyway, who cares...) --- build.sbt | 14 +- .../main/scala-2.11/scala/util/Using.scala | 504 ++++++++++-------- .../main/scala-2.12/scala/util/Using.scala | 504 ++++++++++-------- .../test}/scala/util/UsingTest.scala | 387 ++++++-------- project/plugins.sbt | 2 +- 5 files changed, 718 insertions(+), 693 deletions(-) rename compat/src/test/{scala => scala-jvm/test}/scala/util/UsingTest.scala (71%) diff --git a/build.sbt b/build.sbt index 8fef275f..2470d1cc 100644 --- a/build.sbt +++ b/build.sbt @@ -50,8 +50,8 @@ lazy val root = project lazy val junit = libraryDependencies += "com.github.sbt" % "junit-interface" % "0.13.3" % Test lazy val scala211 = "2.11.12" -lazy val scala212 = "2.12.19" -lazy val scala213 = "2.13.13" +lazy val scala212 = "2.12.20" +lazy val scala213 = "2.13.17" lazy val scala3 = "3.3.6" lazy val compat = new MultiScalaCrossProject( @@ -63,6 +63,16 @@ lazy val compat = new MultiScalaCrossProject( moduleName := "scala-collection-compat", scalaModuleAutomaticModuleName := Some("scala.collection.compat"), scalacOptions ++= Seq("-feature", "-language:higherKinds", "-language:implicitConversions"), + // we need to force Scala 3 to use the latest Scala 2 stdlib, otherwise + // we might get test failures if there are behavior changes in latest Scala 2 stdlib + dependencyOverrides ++= { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((3, _)) => + Seq("org.scala-lang" % "scala-library" % scala213) + case _ => + Seq() + } + }, Compile / unmanagedSourceDirectories += { val sharedSourceDir = (ThisBuild / baseDirectory).value / "compat/src/main" CrossVersion.partialVersion(scalaVersion.value) match { diff --git a/compat/src/main/scala-2.11/scala/util/Using.scala b/compat/src/main/scala-2.11/scala/util/Using.scala index 26cbd01f..0f60fe1c 100644 --- a/compat/src/main/scala-2.11/scala/util/Using.scala +++ b/compat/src/main/scala-2.11/scala/util/Using.scala @@ -16,132 +16,165 @@ import scala.io.Source import scala.util.control.{ControlThrowable, NonFatal} /** A utility for performing automatic resource management. It can be used to perform an - * operation using resources, after which it releases the resources in reverse order - * of their creation. - * - * ==Usage== - * - * There are multiple ways to automatically manage resources with `Using`. If you only need - * to manage a single resource, the [[Using.apply `apply`]] method is easiest; it wraps the - * resource opening, operation, and resource releasing in a `Try`. - * - * Example: - * {{{ - * import java.io.{BufferedReader, FileReader} - * import scala.util.{Try, Using} - * - * val lines: Try[Seq[String]] = - * Using(new BufferedReader(new FileReader("file.txt"))) { reader => - * Iterator.continually(reader.readLine()).takeWhile(_ != null).toSeq - * } - * }}} - * - * If you need to manage multiple resources, [[Using.Manager$.apply `Using.Manager`]] should - * be used. It allows the managing of arbitrarily many resources, whose creation, use, and - * release are all wrapped in a `Try`. - * - * Example: - * {{{ - * import java.io.{BufferedReader, FileReader} - * import scala.util.{Try, Using} - * - * val lines: Try[Seq[String]] = Using.Manager { use => - * val r1 = use(new BufferedReader(new FileReader("file1.txt"))) - * val r2 = use(new BufferedReader(new FileReader("file2.txt"))) - * val r3 = use(new BufferedReader(new FileReader("file3.txt"))) - * val r4 = use(new BufferedReader(new FileReader("file4.txt"))) - * - * // use your resources here - * def lines(reader: BufferedReader): Iterator[String] = - * Iterator.continually(reader.readLine()).takeWhile(_ != null) - * - * (lines(r1) ++ lines(r2) ++ lines(r3) ++ lines(r4)).toList - * } - * }}} - * - * If you wish to avoid wrapping management and operations in a `Try`, you can use - * [[Using.resource `Using.resource`]], which throws any exceptions that occur. - * - * Example: - * {{{ - * import java.io.{BufferedReader, FileReader} - * import scala.util.Using - * - * val lines: Seq[String] = - * Using.resource(new BufferedReader(new FileReader("file.txt"))) { reader => - * Iterator.continually(reader.readLine()).takeWhile(_ != null).toSeq - * } - * }}} - * - * ==Suppression Behavior== - * - * If two exceptions are thrown (e.g., by an operation and closing a resource), - * one of them is re-thrown, and the other is - * [[java.lang.Throwable#addSuppressed added to it as a suppressed exception]]. - * If the two exceptions are of different 'severities' (see below), the one of a higher - * severity is re-thrown, and the one of a lower severity is added to it as a suppressed - * exception. If the two exceptions are of the same severity, the one thrown first is - * re-thrown, and the one thrown second is added to it as a suppressed exception. - * If an exception is a [[scala.util.control.ControlThrowable `ControlThrowable`]], or - * if it does not support suppression (see - * [[java.lang.Throwable `Throwable`'s constructor with an `enableSuppression` parameter]]), - * an exception that would have been suppressed is instead discarded. - * - * Exceptions are ranked from highest to lowest severity as follows: - * - `java.lang.VirtualMachineError` - * - `java.lang.LinkageError` - * - `java.lang.InterruptedException` and `java.lang.ThreadDeath` - * - [[scala.util.control.NonFatal fatal exceptions]], excluding `scala.util.control.ControlThrowable` - * - `scala.util.control.ControlThrowable` - * - all other exceptions - * - * When more than two exceptions are thrown, the first two are combined and - * re-thrown as described above, and each successive exception thrown is combined - * as it is thrown. - * - * @define suppressionBehavior See the main doc for [[Using `Using`]] for full details of - * suppression behavior. - */ + * operation using resources, after which it releases the resources in reverse order + * of their creation. + * + * ==Usage== + * + * There are multiple ways to automatically manage resources with `Using`. If you only need + * to manage a single resource, the [[Using.apply `apply`]] method is easiest; it wraps the + * resource opening, operation, and resource releasing in a `Try`. + * + * Example: + * {{{ + * import java.io.{BufferedReader, FileReader} + * import scala.util.{Try, Using} + * + * val lines: Try[Seq[String]] = + * Using(new BufferedReader(new FileReader("file.txt"))) { reader => + * Iterator.continually(reader.readLine()).takeWhile(_ != null).toSeq + * } + * }}} + * + * If you need to manage multiple resources, [[Using.Manager$.apply `Using.Manager`]] should + * be used. It allows the managing of arbitrarily many resources, whose creation, use, and + * release are all wrapped in a `Try`. + * + * Example: + * {{{ + * import java.io.{BufferedReader, FileReader} + * import scala.util.{Try, Using} + * + * val files = List("file1.txt", "file2.txt", "file3.txt", "file4.txt") + * val lines: Try[Seq[String]] = Using.Manager { use => + * // acquire resources + * def mkreader(filename: String) = use(new BufferedReader(new FileReader(filename))) + * + * // use your resources here + * def lines(reader: BufferedReader): Iterator[String] = + * Iterator.continually(reader.readLine()).takeWhile(_ != null) + * + * files.map(mkreader).flatMap(lines) + * } + * }}} + * + * Composed or "wrapped" resources may be acquired in order of construction, + * if "underlying" resources are not closed. Although redundant in this case, + * here is the previous example with a wrapped call to `use`: + * {{{ + * def mkreader(filename: String) = use(new BufferedReader(use(new FileReader(filename)))) + * }}} + * + * Custom resources can be registered on construction by requiring an implicit `Manager`. + * This ensures they will be released even if composition fails: + * {{{ + * import scala.util.Using + * + * case class X(x: String)(implicit mgr: Using.Manager) extends AutoCloseable { + * override def close() = println(s"CLOSE $x") + * mgr.acquire(this) + * } + * case class Y(y: String)(x: String)(implicit mgr: Using.Manager) extends AutoCloseable { + * val xres = X(x) + * override def close() = println(s"CLOSE $y") + * // an error during construction releases previously acquired resources + * require(y != null, "y is null") + * mgr.acquire(this) + * } + * + * Using.Manager { implicit mgr => + * val y = Y("Y")("X") + * println(s"USE $y") + * } + * println { + * Using.Manager { implicit mgr => + * Y(null)("X") + * } + * } // Failure(java.lang.IllegalArgumentException: requirement failed: y is null) + * }}} + * + * If you wish to avoid wrapping management and operations in a `Try`, you can use + * [[Using.resource `Using.resource`]], which throws any exceptions that occur. + * + * Example: + * {{{ + * import java.io.{BufferedReader, FileReader} + * import scala.util.Using + * + * val lines: Seq[String] = + * Using.resource(new BufferedReader(new FileReader("file.txt"))) { reader => + * Iterator.continually(reader.readLine()).takeWhile(_ != null).toSeq + * } + * }}} + * + * ==Suppression Behavior== + * + * If two exceptions are thrown (e.g., by an operation and closing a resource), + * one of them is re-thrown, and the other is + * [[java.lang.Throwable#addSuppressed added to it as a suppressed exception]]. + * If the two exceptions are of different 'severities' (see below), the one of a higher + * severity is re-thrown, and the one of a lower severity is added to it as a suppressed + * exception. If the two exceptions are of the same severity, the one thrown first is + * re-thrown, and the one thrown second is added to it as a suppressed exception. + * If an exception is a [[scala.util.control.ControlThrowable `ControlThrowable`]], or + * if it does not support suppression (see + * [[java.lang.Throwable `Throwable`'s constructor with an `enableSuppression` parameter]]), + * an exception that would have been suppressed is instead discarded. + * + * Exceptions are ranked from highest to lowest severity as follows: + * - `java.lang.VirtualMachineError` + * - `java.lang.LinkageError` + * - `java.lang.InterruptedException` and `java.lang.ThreadDeath` + * - [[scala.util.control.NonFatal fatal exceptions]], excluding `scala.util.control.ControlThrowable` + * - all other exceptions, excluding `scala.util.control.ControlThrowable` + * - `scala.util.control.ControlThrowable` + * + * When more than two exceptions are thrown, the first two are combined and + * re-thrown as described above, and each successive exception thrown is combined + * as it is thrown. + * + * @define suppressionBehavior See the main doc for [[Using `Using`]] for full details of + * suppression behavior. + */ object Using { /** Performs an operation using a resource, and then releases the resource, - * even if the operation throws an exception. - * - * $suppressionBehavior - * - * @return a [[Try]] containing an exception if one or more were thrown, - * or the result of the operation if no exceptions were thrown - */ - def apply[R: Releasable, A](resource: => R)(f: R => A): Try[A] = Try { - Using.resource(resource)(f) - } + * even if the operation throws an exception. + * + * $suppressionBehavior + * + * @return a [[Try]] containing an exception if one or more were thrown, + * or the result of the operation if no exceptions were thrown + */ + def apply[R: Releasable, A](resource: => R)(f: R => A): Try[A] = + Try { Using.resource(resource)(f) } /** A resource manager. - * - * Resources can be registered with the manager by calling [[acquire `acquire`]]; - * such resources will be released in reverse order of their acquisition - * when the manager is closed, regardless of any exceptions thrown - * during use. - * - * $suppressionBehavior - * - * @note It is recommended for API designers to require an implicit `Manager` - * for the creation of custom resources, and to call `acquire` during those - * resources' construction. Doing so guarantees that the resource ''must'' be - * automatically managed, and makes it impossible to forget to do so. - * - * - * Example: - * {{{ - * class SafeFileReader(file: File)(implicit manager: Using.Manager) - * extends BufferedReader(new FileReader(file)) { - * - * def this(fileName: String)(implicit manager: Using.Manager) = this(new File(fileName)) - * - * manager.acquire(this) - * } - * }}} - */ + * + * Resources can be registered with the manager by calling [[acquire `acquire`]]; + * such resources will be released in reverse order of their acquisition + * when the manager is closed, regardless of any exceptions thrown + * during use. + * + * $suppressionBehavior + * + * @note It is recommended for API designers to require an implicit `Manager` + * for the creation of custom resources, and to call `acquire` during those + * resources' construction. Doing so guarantees that the resource ''must'' be + * automatically managed, and makes it impossible to forget to do so. + * + * + * Example: + * {{{ + * class SafeFileReader(file: File)(implicit manager: Using.Manager) + * extends BufferedReader(new FileReader(file)) { + * + * def this(fileName: String)(implicit manager: Using.Manager) = this(new File(fileName)) + * + * manager.acquire(this) + * } + * }}} + */ final class Manager private { import Manager._ @@ -149,17 +182,17 @@ object Using { private[this] var resources: List[Resource[_]] = Nil /** Registers the specified resource with this manager, so that - * the resource is released when the manager is closed, and then - * returns the (unmodified) resource. - */ + * the resource is released when the manager is closed, and then + * returns the (unmodified) resource. + */ def apply[R: Releasable](resource: R): R = { acquire(resource) resource } /** Registers the specified resource with this manager, so that - * the resource is released when the manager is closed. - */ + * the resource is released when the manager is closed. + */ def acquire[R: Releasable](resource: R): Unit = { if (resource == null) throw new NullPointerException("null resource") if (closed) throw new IllegalStateException("Manager has already been closed") @@ -196,33 +229,33 @@ object Using { object Manager { /** Performs an operation using a `Manager`, then closes the `Manager`, - * releasing its resources (in reverse order of acquisition). - * - * Example: - * {{{ - * val lines = Using.Manager { use => - * use(new BufferedReader(new FileReader("file.txt"))).lines() - * } - * }}} - * - * If using resources which require an implicit `Manager` as a parameter, - * this method should be invoked with an `implicit` modifier before the function - * parameter: - * - * Example: - * {{{ - * val lines = Using.Manager { implicit use => - * new SafeFileReader("file.txt").lines() - * } - * }}} - * - * See the main doc for [[Using `Using`]] for full details of suppression behavior. - * - * @param op the operation to perform using the manager - * @tparam A the return type of the operation - * @return a [[Try]] containing an exception if one or more were thrown, - * or the result of the operation if no exceptions were thrown - */ + * releasing its resources (in reverse order of acquisition). + * + * Example: + * {{{ + * val lines = Using.Manager { use => + * use(new BufferedReader(new FileReader("file.txt"))).lines() + * } + * }}} + * + * If using resources which require an implicit `Manager` as a parameter, + * this method should be invoked with an `implicit` modifier before the function + * parameter: + * + * Example: + * {{{ + * val lines = Using.Manager { implicit use => + * new SafeFileReader("file.txt").lines() + * } + * }}} + * + * See the main doc for [[Using `Using`]] for full details of suppression behavior. + * + * @param op the operation to perform using the manager + * @tparam A the return type of the operation + * @return a [[Try]] containing an exception if one or more were thrown, + * or the result of the operation if no exceptions were thrown + */ def apply[A](op: Manager => A): Try[A] = Try { (new Manager).manage(op) } private final class Resource[R](resource: R)(implicit releasable: Releasable[R]) { @@ -231,18 +264,17 @@ object Using { } private def preferentiallySuppress(primary: Throwable, secondary: Throwable): Throwable = { + @annotation.nowarn("cat=deprecation") // avoid warning on mention of ThreadDeath def score(t: Throwable): Int = t match { case _: VirtualMachineError => 4 case _: LinkageError => 3 case _: InterruptedException | _: ThreadDeath => 2 - case _: ControlThrowable => 0 + case _: ControlThrowable => -1 // below everything case e if !NonFatal(e) => 1 // in case this method gets out of sync with NonFatal - case _ => -1 + case _ => 0 } - // special-case `ControlThrowable`, which incorrectly suppresses exceptions - // before 2.13 @inline def suppress(t: Throwable, suppressed: Throwable): Throwable = { - if (!t.isInstanceOf[ControlThrowable]) t.addSuppressed(suppressed); t + t.addSuppressed(suppressed); t } if (score(secondary) > score(primary)) suppress(secondary, primary) @@ -250,18 +282,18 @@ object Using { } /** Performs an operation using a resource, and then releases the resource, - * even if the operation throws an exception. This method behaves similarly - * to Java's try-with-resources. - * - * $suppressionBehavior - * - * @param resource the resource - * @param body the operation to perform with the resource - * @tparam R the type of the resource - * @tparam A the return type of the operation - * @return the result of the operation, if neither the operation nor - * releasing the resource throws - */ + * even if the operation throws an exception. This method behaves similarly + * to Java's try-with-resources. + * + * $suppressionBehavior + * + * @param resource the resource + * @param body the operation to perform with the resource + * @tparam R the type of the resource + * @tparam A the return type of the operation + * @return the result of the operation, if neither the operation nor + * releasing the resource throws + */ def resource[R, A](resource: R)(body: R => A)(implicit releasable: Releasable[R]): A = { if (resource == null) throw new NullPointerException("null resource") @@ -276,28 +308,27 @@ object Using { if (toThrow eq null) releasable.release(resource) else { try releasable.release(resource) - catch { - case other: Throwable => toThrow = preferentiallySuppress(toThrow, other) - } finally throw toThrow + catch { case other: Throwable => toThrow = preferentiallySuppress(toThrow, other) } + finally throw toThrow } } } /** Performs an operation using two resources, and then releases the resources - * in reverse order, even if the operation throws an exception. This method - * behaves similarly to Java's try-with-resources. - * - * $suppressionBehavior - * - * @param resource1 the first resource - * @param resource2 the second resource - * @param body the operation to perform using the resources - * @tparam R1 the type of the first resource - * @tparam R2 the type of the second resource - * @tparam A the return type of the operation - * @return the result of the operation, if neither the operation nor - * releasing the resources throws - */ + * in reverse order, even if the operation throws an exception. This method + * behaves similarly to Java's try-with-resources. + * + * $suppressionBehavior + * + * @param resource1 the first resource + * @param resource2 the second resource + * @param body the operation to perform using the resources + * @tparam R1 the type of the first resource + * @tparam R2 the type of the second resource + * @tparam A the return type of the operation + * @return the result of the operation, if neither the operation nor + * releasing the resources throws + */ def resources[R1: Releasable, R2: Releasable, A]( resource1: R1, resource2: => R2 @@ -309,22 +340,22 @@ object Using { } /** Performs an operation using three resources, and then releases the resources - * in reverse order, even if the operation throws an exception. This method - * behaves similarly to Java's try-with-resources. - * - * $suppressionBehavior - * - * @param resource1 the first resource - * @param resource2 the second resource - * @param resource3 the third resource - * @param body the operation to perform using the resources - * @tparam R1 the type of the first resource - * @tparam R2 the type of the second resource - * @tparam R3 the type of the third resource - * @tparam A the return type of the operation - * @return the result of the operation, if neither the operation nor - * releasing the resources throws - */ + * in reverse order, even if the operation throws an exception. This method + * behaves similarly to Java's try-with-resources. + * + * $suppressionBehavior + * + * @param resource1 the first resource + * @param resource2 the second resource + * @param resource3 the third resource + * @param body the operation to perform using the resources + * @tparam R1 the type of the first resource + * @tparam R2 the type of the second resource + * @tparam R3 the type of the third resource + * @tparam A the return type of the operation + * @return the result of the operation, if neither the operation nor + * releasing the resources throws + */ def resources[R1: Releasable, R2: Releasable, R3: Releasable, A]( resource1: R1, resource2: => R2, @@ -339,24 +370,24 @@ object Using { } /** Performs an operation using four resources, and then releases the resources - * in reverse order, even if the operation throws an exception. This method - * behaves similarly to Java's try-with-resources. - * - * $suppressionBehavior - * - * @param resource1 the first resource - * @param resource2 the second resource - * @param resource3 the third resource - * @param resource4 the fourth resource - * @param body the operation to perform using the resources - * @tparam R1 the type of the first resource - * @tparam R2 the type of the second resource - * @tparam R3 the type of the third resource - * @tparam R4 the type of the fourth resource - * @tparam A the return type of the operation - * @return the result of the operation, if neither the operation nor - * releasing the resources throws - */ + * in reverse order, even if the operation throws an exception. This method + * behaves similarly to Java's try-with-resources. + * + * $suppressionBehavior + * + * @param resource1 the first resource + * @param resource2 the second resource + * @param resource3 the third resource + * @param resource4 the fourth resource + * @param body the operation to perform using the resources + * @tparam R1 the type of the first resource + * @tparam R2 the type of the second resource + * @tparam R3 the type of the third resource + * @tparam R4 the type of the fourth resource + * @tparam A the return type of the operation + * @return the result of the operation, if neither the operation nor + * releasing the resources throws + */ def resources[R1: Releasable, R2: Releasable, R3: Releasable, R4: Releasable, A]( resource1: R1, resource2: => R2, @@ -373,20 +404,20 @@ object Using { } } - /** A typeclass describing how to release a particular type of resource. - * - * A resource is anything which needs to be released, closed, or otherwise cleaned up - * in some way after it is finished being used, and for which waiting for the object's - * garbage collection to be cleaned up would be unacceptable. For example, an instance of - * [[java.io.OutputStream]] would be considered a resource, because it is important to close - * the stream after it is finished being used. - * - * An instance of `Releasable` is needed in order to automatically manage a resource - * with [[Using `Using`]]. An implicit instance is provided for all types extending - * [[java.lang.AutoCloseable]]. - * - * @tparam R the type of the resource - */ + /** A type class describing how to release a particular type of resource. + * + * A resource is anything which needs to be released, closed, or otherwise cleaned up + * in some way after it is finished being used, and for which waiting for the object's + * garbage collection to be cleaned up would be unacceptable. For example, an instance of + * [[java.io.OutputStream]] would be considered a resource, because it is important to close + * the stream after it is finished being used. + * + * An instance of `Releasable` is needed in order to automatically manage a resource + * with [[Using `Using`]]. An implicit instance is provided for all types extending + * [[java.lang.AutoCloseable]]. + * + * @tparam R the type of the resource + */ trait Releasable[-R] { /** Releases the specified resource. */ @@ -394,7 +425,8 @@ object Using { } object Releasable { - + // prefer explicit types 2.14 + // implicit val AutoCloseableIsReleasable: Releasable[AutoCloseable] = new Releasable[AutoCloseable] {} /** An implicit `Releasable` for [[java.lang.AutoCloseable `AutoCloseable`s]]. */ implicit object AutoCloseableIsReleasable extends Releasable[AutoCloseable] { def release(resource: AutoCloseable): Unit = resource.close() diff --git a/compat/src/main/scala-2.12/scala/util/Using.scala b/compat/src/main/scala-2.12/scala/util/Using.scala index b59c007d..bcb504a0 100644 --- a/compat/src/main/scala-2.12/scala/util/Using.scala +++ b/compat/src/main/scala-2.12/scala/util/Using.scala @@ -15,132 +15,165 @@ package scala.util import scala.util.control.{ControlThrowable, NonFatal} /** A utility for performing automatic resource management. It can be used to perform an - * operation using resources, after which it releases the resources in reverse order - * of their creation. - * - * ==Usage== - * - * There are multiple ways to automatically manage resources with `Using`. If you only need - * to manage a single resource, the [[Using.apply `apply`]] method is easiest; it wraps the - * resource opening, operation, and resource releasing in a `Try`. - * - * Example: - * {{{ - * import java.io.{BufferedReader, FileReader} - * import scala.util.{Try, Using} - * - * val lines: Try[Seq[String]] = - * Using(new BufferedReader(new FileReader("file.txt"))) { reader => - * Iterator.continually(reader.readLine()).takeWhile(_ != null).toSeq - * } - * }}} - * - * If you need to manage multiple resources, [[Using.Manager$.apply `Using.Manager`]] should - * be used. It allows the managing of arbitrarily many resources, whose creation, use, and - * release are all wrapped in a `Try`. - * - * Example: - * {{{ - * import java.io.{BufferedReader, FileReader} - * import scala.util.{Try, Using} - * - * val lines: Try[Seq[String]] = Using.Manager { use => - * val r1 = use(new BufferedReader(new FileReader("file1.txt"))) - * val r2 = use(new BufferedReader(new FileReader("file2.txt"))) - * val r3 = use(new BufferedReader(new FileReader("file3.txt"))) - * val r4 = use(new BufferedReader(new FileReader("file4.txt"))) - * - * // use your resources here - * def lines(reader: BufferedReader): Iterator[String] = - * Iterator.continually(reader.readLine()).takeWhile(_ != null) - * - * (lines(r1) ++ lines(r2) ++ lines(r3) ++ lines(r4)).toList - * } - * }}} - * - * If you wish to avoid wrapping management and operations in a `Try`, you can use - * [[Using.resource `Using.resource`]], which throws any exceptions that occur. - * - * Example: - * {{{ - * import java.io.{BufferedReader, FileReader} - * import scala.util.Using - * - * val lines: Seq[String] = - * Using.resource(new BufferedReader(new FileReader("file.txt"))) { reader => - * Iterator.continually(reader.readLine()).takeWhile(_ != null).toSeq - * } - * }}} - * - * ==Suppression Behavior== - * - * If two exceptions are thrown (e.g., by an operation and closing a resource), - * one of them is re-thrown, and the other is - * [[java.lang.Throwable#addSuppressed added to it as a suppressed exception]]. - * If the two exceptions are of different 'severities' (see below), the one of a higher - * severity is re-thrown, and the one of a lower severity is added to it as a suppressed - * exception. If the two exceptions are of the same severity, the one thrown first is - * re-thrown, and the one thrown second is added to it as a suppressed exception. - * If an exception is a [[scala.util.control.ControlThrowable `ControlThrowable`]], or - * if it does not support suppression (see - * [[java.lang.Throwable `Throwable`'s constructor with an `enableSuppression` parameter]]), - * an exception that would have been suppressed is instead discarded. - * - * Exceptions are ranked from highest to lowest severity as follows: - * - `java.lang.VirtualMachineError` - * - `java.lang.LinkageError` - * - `java.lang.InterruptedException` and `java.lang.ThreadDeath` - * - [[scala.util.control.NonFatal fatal exceptions]], excluding `scala.util.control.ControlThrowable` - * - `scala.util.control.ControlThrowable` - * - all other exceptions - * - * When more than two exceptions are thrown, the first two are combined and - * re-thrown as described above, and each successive exception thrown is combined - * as it is thrown. - * - * @define suppressionBehavior See the main doc for [[Using `Using`]] for full details of - * suppression behavior. - */ + * operation using resources, after which it releases the resources in reverse order + * of their creation. + * + * ==Usage== + * + * There are multiple ways to automatically manage resources with `Using`. If you only need + * to manage a single resource, the [[Using.apply `apply`]] method is easiest; it wraps the + * resource opening, operation, and resource releasing in a `Try`. + * + * Example: + * {{{ + * import java.io.{BufferedReader, FileReader} + * import scala.util.{Try, Using} + * + * val lines: Try[Seq[String]] = + * Using(new BufferedReader(new FileReader("file.txt"))) { reader => + * Iterator.continually(reader.readLine()).takeWhile(_ != null).toSeq + * } + * }}} + * + * If you need to manage multiple resources, [[Using.Manager$.apply `Using.Manager`]] should + * be used. It allows the managing of arbitrarily many resources, whose creation, use, and + * release are all wrapped in a `Try`. + * + * Example: + * {{{ + * import java.io.{BufferedReader, FileReader} + * import scala.util.{Try, Using} + * + * val files = List("file1.txt", "file2.txt", "file3.txt", "file4.txt") + * val lines: Try[Seq[String]] = Using.Manager { use => + * // acquire resources + * def mkreader(filename: String) = use(new BufferedReader(new FileReader(filename))) + * + * // use your resources here + * def lines(reader: BufferedReader): Iterator[String] = + * Iterator.continually(reader.readLine()).takeWhile(_ != null) + * + * files.map(mkreader).flatMap(lines) + * } + * }}} + * + * Composed or "wrapped" resources may be acquired in order of construction, + * if "underlying" resources are not closed. Although redundant in this case, + * here is the previous example with a wrapped call to `use`: + * {{{ + * def mkreader(filename: String) = use(new BufferedReader(use(new FileReader(filename)))) + * }}} + * + * Custom resources can be registered on construction by requiring an implicit `Manager`. + * This ensures they will be released even if composition fails: + * {{{ + * import scala.util.Using + * + * case class X(x: String)(implicit mgr: Using.Manager) extends AutoCloseable { + * override def close() = println(s"CLOSE $x") + * mgr.acquire(this) + * } + * case class Y(y: String)(x: String)(implicit mgr: Using.Manager) extends AutoCloseable { + * val xres = X(x) + * override def close() = println(s"CLOSE $y") + * // an error during construction releases previously acquired resources + * require(y != null, "y is null") + * mgr.acquire(this) + * } + * + * Using.Manager { implicit mgr => + * val y = Y("Y")("X") + * println(s"USE $y") + * } + * println { + * Using.Manager { implicit mgr => + * Y(null)("X") + * } + * } // Failure(java.lang.IllegalArgumentException: requirement failed: y is null) + * }}} + * + * If you wish to avoid wrapping management and operations in a `Try`, you can use + * [[Using.resource `Using.resource`]], which throws any exceptions that occur. + * + * Example: + * {{{ + * import java.io.{BufferedReader, FileReader} + * import scala.util.Using + * + * val lines: Seq[String] = + * Using.resource(new BufferedReader(new FileReader("file.txt"))) { reader => + * Iterator.continually(reader.readLine()).takeWhile(_ != null).toSeq + * } + * }}} + * + * ==Suppression Behavior== + * + * If two exceptions are thrown (e.g., by an operation and closing a resource), + * one of them is re-thrown, and the other is + * [[java.lang.Throwable#addSuppressed added to it as a suppressed exception]]. + * If the two exceptions are of different 'severities' (see below), the one of a higher + * severity is re-thrown, and the one of a lower severity is added to it as a suppressed + * exception. If the two exceptions are of the same severity, the one thrown first is + * re-thrown, and the one thrown second is added to it as a suppressed exception. + * If an exception is a [[scala.util.control.ControlThrowable `ControlThrowable`]], or + * if it does not support suppression (see + * [[java.lang.Throwable `Throwable`'s constructor with an `enableSuppression` parameter]]), + * an exception that would have been suppressed is instead discarded. + * + * Exceptions are ranked from highest to lowest severity as follows: + * - `java.lang.VirtualMachineError` + * - `java.lang.LinkageError` + * - `java.lang.InterruptedException` and `java.lang.ThreadDeath` + * - [[scala.util.control.NonFatal fatal exceptions]], excluding `scala.util.control.ControlThrowable` + * - all other exceptions, excluding `scala.util.control.ControlThrowable` + * - `scala.util.control.ControlThrowable` + * + * When more than two exceptions are thrown, the first two are combined and + * re-thrown as described above, and each successive exception thrown is combined + * as it is thrown. + * + * @define suppressionBehavior See the main doc for [[Using `Using`]] for full details of + * suppression behavior. + */ object Using { /** Performs an operation using a resource, and then releases the resource, - * even if the operation throws an exception. - * - * $suppressionBehavior - * - * @return a [[Try]] containing an exception if one or more were thrown, - * or the result of the operation if no exceptions were thrown - */ - def apply[R: Releasable, A](resource: => R)(f: R => A): Try[A] = Try { - Using.resource(resource)(f) - } + * even if the operation throws an exception. + * + * $suppressionBehavior + * + * @return a [[Try]] containing an exception if one or more were thrown, + * or the result of the operation if no exceptions were thrown + */ + def apply[R: Releasable, A](resource: => R)(f: R => A): Try[A] = + Try { Using.resource(resource)(f) } /** A resource manager. - * - * Resources can be registered with the manager by calling [[acquire `acquire`]]; - * such resources will be released in reverse order of their acquisition - * when the manager is closed, regardless of any exceptions thrown - * during use. - * - * $suppressionBehavior - * - * @note It is recommended for API designers to require an implicit `Manager` - * for the creation of custom resources, and to call `acquire` during those - * resources' construction. Doing so guarantees that the resource ''must'' be - * automatically managed, and makes it impossible to forget to do so. - * - * - * Example: - * {{{ - * class SafeFileReader(file: File)(implicit manager: Using.Manager) - * extends BufferedReader(new FileReader(file)) { - * - * def this(fileName: String)(implicit manager: Using.Manager) = this(new File(fileName)) - * - * manager.acquire(this) - * } - * }}} - */ + * + * Resources can be registered with the manager by calling [[acquire `acquire`]]; + * such resources will be released in reverse order of their acquisition + * when the manager is closed, regardless of any exceptions thrown + * during use. + * + * $suppressionBehavior + * + * @note It is recommended for API designers to require an implicit `Manager` + * for the creation of custom resources, and to call `acquire` during those + * resources' construction. Doing so guarantees that the resource ''must'' be + * automatically managed, and makes it impossible to forget to do so. + * + * + * Example: + * {{{ + * class SafeFileReader(file: File)(implicit manager: Using.Manager) + * extends BufferedReader(new FileReader(file)) { + * + * def this(fileName: String)(implicit manager: Using.Manager) = this(new File(fileName)) + * + * manager.acquire(this) + * } + * }}} + */ final class Manager private { import Manager._ @@ -148,17 +181,17 @@ object Using { private[this] var resources: List[Resource[_]] = Nil /** Registers the specified resource with this manager, so that - * the resource is released when the manager is closed, and then - * returns the (unmodified) resource. - */ + * the resource is released when the manager is closed, and then + * returns the (unmodified) resource. + */ def apply[R: Releasable](resource: R): R = { acquire(resource) resource } /** Registers the specified resource with this manager, so that - * the resource is released when the manager is closed. - */ + * the resource is released when the manager is closed. + */ def acquire[R: Releasable](resource: R): Unit = { if (resource == null) throw new NullPointerException("null resource") if (closed) throw new IllegalStateException("Manager has already been closed") @@ -195,33 +228,33 @@ object Using { object Manager { /** Performs an operation using a `Manager`, then closes the `Manager`, - * releasing its resources (in reverse order of acquisition). - * - * Example: - * {{{ - * val lines = Using.Manager { use => - * use(new BufferedReader(new FileReader("file.txt"))).lines() - * } - * }}} - * - * If using resources which require an implicit `Manager` as a parameter, - * this method should be invoked with an `implicit` modifier before the function - * parameter: - * - * Example: - * {{{ - * val lines = Using.Manager { implicit use => - * new SafeFileReader("file.txt").lines() - * } - * }}} - * - * See the main doc for [[Using `Using`]] for full details of suppression behavior. - * - * @param op the operation to perform using the manager - * @tparam A the return type of the operation - * @return a [[Try]] containing an exception if one or more were thrown, - * or the result of the operation if no exceptions were thrown - */ + * releasing its resources (in reverse order of acquisition). + * + * Example: + * {{{ + * val lines = Using.Manager { use => + * use(new BufferedReader(new FileReader("file.txt"))).lines() + * } + * }}} + * + * If using resources which require an implicit `Manager` as a parameter, + * this method should be invoked with an `implicit` modifier before the function + * parameter: + * + * Example: + * {{{ + * val lines = Using.Manager { implicit use => + * new SafeFileReader("file.txt").lines() + * } + * }}} + * + * See the main doc for [[Using `Using`]] for full details of suppression behavior. + * + * @param op the operation to perform using the manager + * @tparam A the return type of the operation + * @return a [[Try]] containing an exception if one or more were thrown, + * or the result of the operation if no exceptions were thrown + */ def apply[A](op: Manager => A): Try[A] = Try { (new Manager).manage(op) } private final class Resource[R](resource: R)(implicit releasable: Releasable[R]) { @@ -230,18 +263,17 @@ object Using { } private def preferentiallySuppress(primary: Throwable, secondary: Throwable): Throwable = { + @annotation.nowarn("cat=deprecation") // avoid warning on mention of ThreadDeath def score(t: Throwable): Int = t match { case _: VirtualMachineError => 4 case _: LinkageError => 3 case _: InterruptedException | _: ThreadDeath => 2 - case _: ControlThrowable => 0 + case _: ControlThrowable => -1 // below everything case e if !NonFatal(e) => 1 // in case this method gets out of sync with NonFatal - case _ => -1 + case _ => 0 } - // special-case `ControlThrowable`, which incorrectly suppresses exceptions - // before 2.13 @inline def suppress(t: Throwable, suppressed: Throwable): Throwable = { - if (!t.isInstanceOf[ControlThrowable]) t.addSuppressed(suppressed); t + t.addSuppressed(suppressed); t } if (score(secondary) > score(primary)) suppress(secondary, primary) @@ -249,18 +281,18 @@ object Using { } /** Performs an operation using a resource, and then releases the resource, - * even if the operation throws an exception. This method behaves similarly - * to Java's try-with-resources. - * - * $suppressionBehavior - * - * @param resource the resource - * @param body the operation to perform with the resource - * @tparam R the type of the resource - * @tparam A the return type of the operation - * @return the result of the operation, if neither the operation nor - * releasing the resource throws - */ + * even if the operation throws an exception. This method behaves similarly + * to Java's try-with-resources. + * + * $suppressionBehavior + * + * @param resource the resource + * @param body the operation to perform with the resource + * @tparam R the type of the resource + * @tparam A the return type of the operation + * @return the result of the operation, if neither the operation nor + * releasing the resource throws + */ def resource[R, A](resource: R)(body: R => A)(implicit releasable: Releasable[R]): A = { if (resource == null) throw new NullPointerException("null resource") @@ -275,28 +307,27 @@ object Using { if (toThrow eq null) releasable.release(resource) else { try releasable.release(resource) - catch { - case other: Throwable => toThrow = preferentiallySuppress(toThrow, other) - } finally throw toThrow + catch { case other: Throwable => toThrow = preferentiallySuppress(toThrow, other) } + finally throw toThrow } } } /** Performs an operation using two resources, and then releases the resources - * in reverse order, even if the operation throws an exception. This method - * behaves similarly to Java's try-with-resources. - * - * $suppressionBehavior - * - * @param resource1 the first resource - * @param resource2 the second resource - * @param body the operation to perform using the resources - * @tparam R1 the type of the first resource - * @tparam R2 the type of the second resource - * @tparam A the return type of the operation - * @return the result of the operation, if neither the operation nor - * releasing the resources throws - */ + * in reverse order, even if the operation throws an exception. This method + * behaves similarly to Java's try-with-resources. + * + * $suppressionBehavior + * + * @param resource1 the first resource + * @param resource2 the second resource + * @param body the operation to perform using the resources + * @tparam R1 the type of the first resource + * @tparam R2 the type of the second resource + * @tparam A the return type of the operation + * @return the result of the operation, if neither the operation nor + * releasing the resources throws + */ def resources[R1: Releasable, R2: Releasable, A]( resource1: R1, resource2: => R2 @@ -308,22 +339,22 @@ object Using { } /** Performs an operation using three resources, and then releases the resources - * in reverse order, even if the operation throws an exception. This method - * behaves similarly to Java's try-with-resources. - * - * $suppressionBehavior - * - * @param resource1 the first resource - * @param resource2 the second resource - * @param resource3 the third resource - * @param body the operation to perform using the resources - * @tparam R1 the type of the first resource - * @tparam R2 the type of the second resource - * @tparam R3 the type of the third resource - * @tparam A the return type of the operation - * @return the result of the operation, if neither the operation nor - * releasing the resources throws - */ + * in reverse order, even if the operation throws an exception. This method + * behaves similarly to Java's try-with-resources. + * + * $suppressionBehavior + * + * @param resource1 the first resource + * @param resource2 the second resource + * @param resource3 the third resource + * @param body the operation to perform using the resources + * @tparam R1 the type of the first resource + * @tparam R2 the type of the second resource + * @tparam R3 the type of the third resource + * @tparam A the return type of the operation + * @return the result of the operation, if neither the operation nor + * releasing the resources throws + */ def resources[R1: Releasable, R2: Releasable, R3: Releasable, A]( resource1: R1, resource2: => R2, @@ -338,24 +369,24 @@ object Using { } /** Performs an operation using four resources, and then releases the resources - * in reverse order, even if the operation throws an exception. This method - * behaves similarly to Java's try-with-resources. - * - * $suppressionBehavior - * - * @param resource1 the first resource - * @param resource2 the second resource - * @param resource3 the third resource - * @param resource4 the fourth resource - * @param body the operation to perform using the resources - * @tparam R1 the type of the first resource - * @tparam R2 the type of the second resource - * @tparam R3 the type of the third resource - * @tparam R4 the type of the fourth resource - * @tparam A the return type of the operation - * @return the result of the operation, if neither the operation nor - * releasing the resources throws - */ + * in reverse order, even if the operation throws an exception. This method + * behaves similarly to Java's try-with-resources. + * + * $suppressionBehavior + * + * @param resource1 the first resource + * @param resource2 the second resource + * @param resource3 the third resource + * @param resource4 the fourth resource + * @param body the operation to perform using the resources + * @tparam R1 the type of the first resource + * @tparam R2 the type of the second resource + * @tparam R3 the type of the third resource + * @tparam R4 the type of the fourth resource + * @tparam A the return type of the operation + * @return the result of the operation, if neither the operation nor + * releasing the resources throws + */ def resources[R1: Releasable, R2: Releasable, R3: Releasable, R4: Releasable, A]( resource1: R1, resource2: => R2, @@ -372,20 +403,20 @@ object Using { } } - /** A typeclass describing how to release a particular type of resource. - * - * A resource is anything which needs to be released, closed, or otherwise cleaned up - * in some way after it is finished being used, and for which waiting for the object's - * garbage collection to be cleaned up would be unacceptable. For example, an instance of - * [[java.io.OutputStream]] would be considered a resource, because it is important to close - * the stream after it is finished being used. - * - * An instance of `Releasable` is needed in order to automatically manage a resource - * with [[Using `Using`]]. An implicit instance is provided for all types extending - * [[java.lang.AutoCloseable]]. - * - * @tparam R the type of the resource - */ + /** A type class describing how to release a particular type of resource. + * + * A resource is anything which needs to be released, closed, or otherwise cleaned up + * in some way after it is finished being used, and for which waiting for the object's + * garbage collection to be cleaned up would be unacceptable. For example, an instance of + * [[java.io.OutputStream]] would be considered a resource, because it is important to close + * the stream after it is finished being used. + * + * An instance of `Releasable` is needed in order to automatically manage a resource + * with [[Using `Using`]]. An implicit instance is provided for all types extending + * [[java.lang.AutoCloseable]]. + * + * @tparam R the type of the resource + */ trait Releasable[-R] { /** Releases the specified resource. */ @@ -393,7 +424,8 @@ object Using { } object Releasable { - + // prefer explicit types 2.14 + // implicit val AutoCloseableIsReleasable: Releasable[AutoCloseable] = new Releasable[AutoCloseable] {} /** An implicit `Releasable` for [[java.lang.AutoCloseable `AutoCloseable`s]]. */ implicit object AutoCloseableIsReleasable extends Releasable[AutoCloseable] { def release(resource: AutoCloseable): Unit = resource.close() diff --git a/compat/src/test/scala/scala/util/UsingTest.scala b/compat/src/test/scala-jvm/test/scala/util/UsingTest.scala similarity index 71% rename from compat/src/test/scala/scala/util/UsingTest.scala rename to compat/src/test/scala-jvm/test/scala/util/UsingTest.scala index 32f3a028..475b5d32 100644 --- a/compat/src/test/scala/scala/util/UsingTest.scala +++ b/compat/src/test/scala-jvm/test/scala/util/UsingTest.scala @@ -1,12 +1,12 @@ /* * Scala (https://www.scala-lang.org) * - * Copyright EPFL and Lightbend, Inc. dba Akka +* Copyright EPFL and Lightbend, Inc. dba Akka * - * Licensed under Apache License 2.0 +* Licensed under Apache License 2.0 * (http://www.apache.org/licenses/LICENSE-2.0). * - * See the NOTICE file distributed with this work for +* See the NOTICE file distributed with this work for * additional information regarding copyright ownership. */ @@ -15,10 +15,11 @@ package scala.util import org.junit.Test import org.junit.Assert._ -import scala.io.Source +import scala.annotation.unused import scala.reflect.ClassTag -import scala.runtime.NonLocalReturnControl +import scala.util.control.compat.ControlThrowable +@deprecated("ThreadDeath is deprecated on JDK 20", "") class UsingTest { import UsingTest._ @@ -26,29 +27,29 @@ class UsingTest { private def genericResourceThrowing[CloseT <: Throwable: ClassTag]( resource: => CustomResource[CloseT], - onLinkage: SuppressionBehavior, - onInterruption: SuppressionBehavior, - onControl: SuppressionBehavior, - onException: SuppressionBehavior + onLinkage: ThrowBehavior, + onInterruption: ThrowBehavior, + onControl: ThrowBehavior, + onException: ThrowBehavior ): Unit = { def check[UseT <: Throwable: ClassTag]( t: String => UseT, - behavior: SuppressionBehavior, + behavior: ThrowBehavior, allowsSuppression: Boolean ): Unit = { val ex = use(resource, t) - if (behavior == IsSuppressed) { + if (behavior == UseIsThrown) { assertThrowableClass[UseT](ex) if (allowsSuppression) assertSingleSuppressed[CloseT](ex) else assertNoSuppressed(ex) } else { assertThrowableClass[CloseT](ex) - if (behavior == AcceptsSuppressed) assertSingleSuppressed[UseT](ex) + if (behavior == CloseIsThrown) assertSingleSuppressed[UseT](ex) else assertNoSuppressed(ex) } } - check(new UsingVMError(_), behavior = IsSuppressed, allowsSuppression = true) + check(new UsingVMError(_), behavior = UseIsThrown, allowsSuppression = true) check(new UsingLinkageError(_), onLinkage, allowsSuppression = true) check(_ => new UsingInterruption, onInterruption, allowsSuppression = true) check(new UsingControl(_), onControl, allowsSuppression = false) @@ -60,266 +61,248 @@ class UsingTest { def resourceThrowingVMError(): Unit = { genericResourceThrowing( new VMErrorResource, - onLinkage = AcceptsSuppressed, - onInterruption = AcceptsSuppressed, - onControl = AcceptsSuppressed, - onException = AcceptsSuppressed) + onLinkage = CloseIsThrown, + onInterruption = CloseIsThrown, + onControl = CloseIsThrown, + onException = CloseIsThrown) } @Test def resourceThrowingLinkageError(): Unit = { genericResourceThrowing( new LinkageResource, - onLinkage = IsSuppressed, - onInterruption = AcceptsSuppressed, - onControl = AcceptsSuppressed, - onException = AcceptsSuppressed) + onLinkage = UseIsThrown, + onInterruption = CloseIsThrown, + onControl = CloseIsThrown, + onException = CloseIsThrown) } @Test def resourceThrowingInterruption(): Unit = { genericResourceThrowing( new InterruptionResource, - onLinkage = IsSuppressed, - onInterruption = IsSuppressed, - onControl = AcceptsSuppressed, - onException = AcceptsSuppressed) + onLinkage = UseIsThrown, + onInterruption = UseIsThrown, + onControl = CloseIsThrown, + onException = CloseIsThrown) } @Test def resourceThrowingControl(): Unit = { genericResourceThrowing( new ControlResource, - onLinkage = IsSuppressed, - onInterruption = IsSuppressed, - onControl = IsSuppressed, - onException = IgnoresSuppressed) + onLinkage = UseIsThrown, + onInterruption = UseIsThrown, + onControl = UseIsThrown, + onException = UseIsThrown) } @Test def resourceThrowingError(): Unit = { genericResourceThrowing( new ErrorResource, - onLinkage = IsSuppressed, - onInterruption = IsSuppressed, - onControl = IsSuppressed, - onException = IsSuppressed) + onLinkage = UseIsThrown, + onInterruption = UseIsThrown, + onControl = CloseIsThrown, + onException = UseIsThrown) } @Test def resourceThrowingException(): Unit = { genericResourceThrowing( new ExceptionResource, - onLinkage = IsSuppressed, - onInterruption = IsSuppressed, - onControl = IsSuppressed, - onException = IsSuppressed) + onLinkage = UseIsThrown, + onInterruption = UseIsThrown, + onControl = CloseIsThrown, + onException = UseIsThrown) } /* `Using.apply` exception preference */ private def genericUsingThrowing[CloseT <: Throwable: ClassTag]( resource: => CustomResource[CloseT], - onLinkage: SuppressionBehavior, - onInterruption: SuppressionBehavior, - onControl: SuppressionBehavior, - onException: SuppressionBehavior + onLinkage: ThrowBehavior, + onInterruption: ThrowBehavior, + onControl: ThrowBehavior, + onException: ThrowBehavior ): Unit = { def check[UseT <: Throwable: ClassTag]( t: String => UseT, - behavior: SuppressionBehavior, - allowsSuppression: Boolean, - yieldsTry: Boolean + behavior: ThrowBehavior, + allowsSuppression: Boolean ): Unit = { - val ex = if (yieldsTry) UseWrapped(resource, t) else UseWrapped.catching(resource, t) - if (behavior == IsSuppressed) { + val ex = useWrapped(resource, t) + if (behavior == UseIsThrown) { assertThrowableClass[UseT](ex) if (allowsSuppression) assertSingleSuppressed[CloseT](ex) else assertNoSuppressed(ex) } else { assertThrowableClass[CloseT](ex) - if (behavior == AcceptsSuppressed) assertSingleSuppressed[UseT](ex) + if (behavior == CloseIsThrown) assertSingleSuppressed[UseT](ex) else assertNoSuppressed(ex) } } - check(new UsingVMError(_), behavior = IsSuppressed, allowsSuppression = true, yieldsTry = false) - check(new UsingLinkageError(_), onLinkage, allowsSuppression = true, yieldsTry = false) - check(_ => new UsingInterruption, onInterruption, allowsSuppression = true, yieldsTry = false) - check(new UsingControl(_), onControl, allowsSuppression = false, yieldsTry = false) - check( - new UsingError(_), - onException, - allowsSuppression = true, - yieldsTry = onException == IsSuppressed) - check( - new UsingException(_), - onException, - allowsSuppression = true, - yieldsTry = onException == IsSuppressed) + check(new UsingVMError(_), behavior = UseIsThrown, allowsSuppression = true) + check(new UsingLinkageError(_), onLinkage, allowsSuppression = true) + check(_ => new UsingInterruption, onInterruption, allowsSuppression = true) + check(new UsingControl(_), onControl, allowsSuppression = false) + check(new UsingError(_), onException, allowsSuppression = true) + check(new UsingException(_), onException, allowsSuppression = true) } @Test def usingThrowingVMError(): Unit = { genericUsingThrowing( new VMErrorResource, - onLinkage = AcceptsSuppressed, - onInterruption = AcceptsSuppressed, - onControl = AcceptsSuppressed, - onException = AcceptsSuppressed) + onLinkage = CloseIsThrown, + onInterruption = CloseIsThrown, + onControl = CloseIsThrown, + onException = CloseIsThrown) } @Test def usingThrowingLinkageError(): Unit = { genericUsingThrowing( new LinkageResource, - onLinkage = IsSuppressed, - onInterruption = AcceptsSuppressed, - onControl = AcceptsSuppressed, - onException = AcceptsSuppressed) + onLinkage = UseIsThrown, + onInterruption = CloseIsThrown, + onControl = CloseIsThrown, + onException = CloseIsThrown) } @Test def usingThrowingInterruption(): Unit = { genericUsingThrowing( new InterruptionResource, - onLinkage = IsSuppressed, - onInterruption = IsSuppressed, - onControl = AcceptsSuppressed, - onException = AcceptsSuppressed) + onLinkage = UseIsThrown, + onInterruption = UseIsThrown, + onControl = CloseIsThrown, + onException = CloseIsThrown) } @Test def usingThrowingControl(): Unit = { genericUsingThrowing( new ControlResource, - onLinkage = IsSuppressed, - onInterruption = IsSuppressed, - onControl = IsSuppressed, - onException = IgnoresSuppressed) + onLinkage = UseIsThrown, + onInterruption = UseIsThrown, + onControl = UseIsThrown, + onException = UseIsThrown) } @Test def usingThrowingError(): Unit = { genericUsingThrowing( new ErrorResource, - onLinkage = IsSuppressed, - onInterruption = IsSuppressed, - onControl = IsSuppressed, - onException = IsSuppressed) + onLinkage = UseIsThrown, + onInterruption = UseIsThrown, + onControl = CloseIsThrown, + onException = UseIsThrown) } @Test def usingThrowingException(): Unit = { genericUsingThrowing( new ExceptionResource, - onLinkage = IsSuppressed, - onInterruption = IsSuppressed, - onControl = IsSuppressed, - onException = IsSuppressed) + onLinkage = UseIsThrown, + onInterruption = UseIsThrown, + onControl = CloseIsThrown, + onException = UseIsThrown) } /* `Using.Manager.apply` exception preference */ private def genericManagerThrowing[CloseT <: Throwable: ClassTag]( resource: => CustomResource[CloseT], - onLinkage: SuppressionBehavior, - onInterruption: SuppressionBehavior, - onControl: SuppressionBehavior, - onException: SuppressionBehavior + onLinkage: ThrowBehavior, + onInterruption: ThrowBehavior, + onControl: ThrowBehavior, + onException: ThrowBehavior ): Unit = { def check[UseT <: Throwable: ClassTag]( t: String => UseT, - behavior: SuppressionBehavior, - allowsSuppression: Boolean, - yieldsTry: Boolean + behavior: ThrowBehavior, + allowsSuppression: Boolean ): Unit = { - val ex = if (yieldsTry) UseManager(resource, t) else UseManager.catching(resource, t) - if (behavior == IsSuppressed) { + val ex = useManager(resource, t) + if (behavior == UseIsThrown) { assertThrowableClass[UseT](ex) if (allowsSuppression) assertSingleSuppressed[CloseT](ex) else assertNoSuppressed(ex) } else { assertThrowableClass[CloseT](ex) - if (behavior == AcceptsSuppressed) assertSingleSuppressed[UseT](ex) + if (behavior == CloseIsThrown) assertSingleSuppressed[UseT](ex) else assertNoSuppressed(ex) } } - check(new UsingVMError(_), behavior = IsSuppressed, allowsSuppression = true, yieldsTry = false) - check(new UsingLinkageError(_), onLinkage, allowsSuppression = true, yieldsTry = false) - check(_ => new UsingInterruption, onInterruption, allowsSuppression = true, yieldsTry = false) - check(new UsingControl(_), onControl, allowsSuppression = false, yieldsTry = false) - check( - new UsingError(_), - onException, - allowsSuppression = true, - yieldsTry = onException == IsSuppressed) - check( - new UsingException(_), - onException, - allowsSuppression = true, - yieldsTry = onException == IsSuppressed) + check(new UsingVMError(_), behavior = UseIsThrown, allowsSuppression = true) + check(new UsingLinkageError(_), onLinkage, allowsSuppression = true) + check(_ => new UsingInterruption, onInterruption, allowsSuppression = true) + check(new UsingControl(_), onControl, allowsSuppression = false) + check(new UsingError(_), onException, allowsSuppression = true) + check(new UsingException(_), onException, allowsSuppression = true) } @Test def managerThrowingVMError(): Unit = { genericManagerThrowing( new VMErrorResource, - onLinkage = AcceptsSuppressed, - onInterruption = AcceptsSuppressed, - onControl = AcceptsSuppressed, - onException = AcceptsSuppressed) + onLinkage = CloseIsThrown, + onInterruption = CloseIsThrown, + onControl = CloseIsThrown, + onException = CloseIsThrown) } @Test def managerThrowingLinkageError(): Unit = { genericManagerThrowing( new LinkageResource, - onLinkage = IsSuppressed, - onInterruption = AcceptsSuppressed, - onControl = AcceptsSuppressed, - onException = AcceptsSuppressed) + onLinkage = UseIsThrown, + onInterruption = CloseIsThrown, + onControl = CloseIsThrown, + onException = CloseIsThrown) } @Test def managerThrowingInterruption(): Unit = { genericManagerThrowing( new InterruptionResource, - onLinkage = IsSuppressed, - onInterruption = IsSuppressed, - onControl = AcceptsSuppressed, - onException = AcceptsSuppressed) + onLinkage = UseIsThrown, + onInterruption = UseIsThrown, + onControl = CloseIsThrown, + onException = CloseIsThrown) } @Test def managerThrowingControl(): Unit = { genericManagerThrowing( new ControlResource, - onLinkage = IsSuppressed, - onInterruption = IsSuppressed, - onControl = IsSuppressed, - onException = IgnoresSuppressed) + onLinkage = UseIsThrown, + onInterruption = UseIsThrown, + onControl = UseIsThrown, + onException = UseIsThrown) } @Test def managerThrowingError(): Unit = { genericManagerThrowing( new ErrorResource, - onLinkage = IsSuppressed, - onInterruption = IsSuppressed, - onControl = IsSuppressed, - onException = IsSuppressed) + onLinkage = UseIsThrown, + onInterruption = UseIsThrown, + onControl = CloseIsThrown, + onException = UseIsThrown) } @Test def managerThrowingException(): Unit = { genericManagerThrowing( new ExceptionResource, - onLinkage = IsSuppressed, - onInterruption = IsSuppressed, - onControl = IsSuppressed, - onException = IsSuppressed) + onLinkage = UseIsThrown, + onInterruption = UseIsThrown, + onControl = CloseIsThrown, + onException = UseIsThrown) } /* nested resource usage returns the correct exception */ @@ -467,7 +450,7 @@ class UsingTest { Using(new ErrorResource) { _ => throw new UsingException("nested `Using`") }.get - } + }: @unchecked // uncomment to debug actual suppression nesting // usingException.printStackTrace() @@ -526,10 +509,10 @@ class UsingTest { @Test def managerMultipleResourcesPropagatesCorrectlySimple(): Unit = { val scala.util.Failure(usingException) = Using.Manager { m => - val _r1 = m(new ExceptionResource) - val _r2 = m(new ErrorResource) + @unused val _r1 = m(new ExceptionResource) + @unused val _r2 = m(new ErrorResource) throw new UsingException("`Using.Manager`") - } + }: @unchecked // uncomment to debug actual suppression nesting // usingException.printStackTrace() @@ -541,9 +524,9 @@ class UsingTest { def managerMultipleResourcesPropagatesCorrectlyComplex(): Unit = { val vmError = catchThrowable { Using.Manager { m => - val _r1 = m(new ExceptionResource) - val _r2 = m(new VMErrorResource) - val _r3 = m(new ErrorResource) + @unused val _r1 = m(new ExceptionResource) + @unused val _r2 = m(new VMErrorResource) + @unused val _r3 = m(new ErrorResource) throw new UsingException("`Using.Manager`") } } @@ -558,14 +541,14 @@ class UsingTest { def managerMultiplePropagatesCorrectlyExtremelyComplex(): Unit = { val vmError = catchThrowable { Using.Manager { m => - val _r1 = m(new ExceptionResource) - val _r2 = m(new VMErrorResource) - val _r3 = m(new ControlResource) - val _r4 = m(new ErrorResource) - val _r5 = m(new LinkageResource) - val _r6 = m(new ExceptionResource) - val _r7 = m(new InterruptionResource) - val _r8 = m(new ErrorResource) + @unused val _r1 = m(new ExceptionResource) + @unused val _r2 = m(new VMErrorResource) + @unused val _r3 = m(new ControlResource) + @unused val _r4 = m(new ErrorResource) + @unused val _r5 = m(new LinkageResource) + @unused val _r6 = m(new ExceptionResource) + @unused val _r7 = m(new InterruptionResource) + @unused val _r8 = m(new ErrorResource) throw new UsingException("`Using.Manager`") } } @@ -592,9 +575,7 @@ class UsingTest { @Test def managerWithNoThrow(): Unit = { - val res = Using.Manager { m => - m(new NoOpResource).identity("test") - } + val res = Using.Manager { m => m(new NoOpResource).identity("test") } assertEquals(res, scala.util.Success("test")) } @@ -608,13 +589,13 @@ class UsingTest { @Test def usingOpThrow(): Unit = { - val ex = UseWrapped(new NoOpResource, new UsingException(_)) + val ex = useWrapped(new NoOpResource, new UsingException(_)) assertThrowableClass[UsingException](ex) } @Test def managerOpThrow(): Unit = { - val ex = UseManager(new NoOpResource, new UsingException(_)) + val ex = useManager(new NoOpResource, new UsingException(_)) assertThrowableClass[UsingException](ex) } @@ -634,12 +615,9 @@ class UsingTest { @Test def managerClosingThrow(): Unit = { - val ex = Using - .Manager { m => - m(new ExceptionResource).identity("test") - } - .failed - .get + val ex = Using.Manager { m => + m(new ExceptionResource).identity("test") + }.failed.get assertThrowableClass[ClosingException](ex) } @@ -754,13 +732,10 @@ class UsingTest { @Test def managerDisallowsNull(): Unit = { - val npe = Using - .Manager { m => - m(null: AutoCloseable) - "test" - } - .failed - .get + val npe = Using.Manager { m => + m(null: AutoCloseable) + "test" + }.failed.get assertThrowableClass[NullPointerException](npe) } @@ -772,24 +747,15 @@ class UsingTest { @Test def managerCatchesOpeningException(): Unit = { - val ex = Using - .Manager { m => - m({ throw new RuntimeException }: AutoCloseable) - "test" - } - .failed - .get + val ex = Using.Manager { m => + m({ throw new RuntimeException }: AutoCloseable) + "test" + }.failed.get assertThrowableClass[RuntimeException](ex) } - - @Test - def usingSource(): Unit = { - Using(Source.fromString("Hello, Source!")) { source => - // If this simply compiles, then mission accomplished. - } - } } +@deprecated("ThreadDeath is deprecated on JDK 20", "") object UsingTest { final class ClosingVMError(message: String) extends VirtualMachineError(message) final class UsingVMError(message: String) extends VirtualMachineError(message) @@ -797,10 +763,8 @@ object UsingTest { final class UsingLinkageError(message: String) extends LinkageError(message) type ClosingInterruption = InterruptedException type UsingInterruption = ThreadDeath - // `NonLocalReturnControl` incorrectly suppresses exceptions, so this tests that - // `Using` special-cases it. - final class ClosingControl(message: String) extends NonLocalReturnControl(message, message) - final class UsingControl(message: String) extends NonLocalReturnControl(message, message) + final class ClosingControl(message: String) extends ControlThrowable + final class UsingControl(message: String) extends ControlThrowable final class ClosingError(message: String) extends Error(message) final class UsingError(message: String) extends Error(message) final class ClosingException(message: String) extends Exception(message) @@ -825,16 +789,13 @@ object UsingTest { final class ErrorResource extends CustomResource(new ClosingError(_)) final class ExceptionResource extends CustomResource(new ClosingException(_)) - sealed trait SuppressionBehavior + sealed trait ThrowBehavior - /** is added as a suppressed exception to the other exception, and the other exception is thrown */ - case object IsSuppressed extends SuppressionBehavior + /** resource use exception is thrown */ + case object UseIsThrown extends ThrowBehavior - /** is thrown, and the other exception is added to this as suppressed */ - case object AcceptsSuppressed extends SuppressionBehavior - - /** is thrown, and the other exception is ignored */ - case object IgnoresSuppressed extends SuppressionBehavior + /** resource closing exception is thrown */ + case object CloseIsThrown extends ThrowBehavior def assertThrowableClass[T <: Throwable: ClassTag](t: Throwable): Unit = { assertEquals(s"Caught [${t.getMessage}]", implicitly[ClassTag[T]].runtimeClass, t.getClass) @@ -859,35 +820,25 @@ object UsingTest { } } - object UseWrapped { - def apply(resource: => BaseResource, t: String => Throwable): Throwable = - Using(resource)(opThrowing(t)).failed.get - - def catching(resource: => BaseResource, t: String => Throwable): Throwable = - catchThrowable(Using(resource)(opThrowing(t))) - } - - object UseManager { - def apply(resource: => BaseResource, t: String => Throwable): Throwable = - Using - .Manager { m => - val r = m(resource) - opThrowing(t)(r) - } - .failed - .get - def catching(resource: => BaseResource, t: String => Throwable): Throwable = - catchThrowable { - Using.Manager { m => - val r = m(resource) - opThrowing(t)(r) - } - } - } - def use(resource: BaseResource, t: String => Throwable): Throwable = catchThrowable(Using.resource(resource)(opThrowing(t))) + def useWrapped(resource: => BaseResource, t: String => Throwable): Throwable = + try Using(resource)(opThrowing(t)).failed.get + catch { + case t: Throwable => t + } + + def useManager(resource: => BaseResource, t: String => Throwable): Throwable = + try { + Using.Manager { m => + val r = m(resource) + opThrowing(t)(r) + }.failed.get + } catch { + case t: Throwable => t // this is awful + } + private def opThrowing(t: String => Throwable): BaseResource => Nothing = r => { r.identity("test") diff --git a/project/plugins.sbt b/project/plugins.sbt index 055c4af3..659b4adb 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.12.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.20.1") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.8") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") From e040b46261f7e476b9e01d9b188572f0b919b628 Mon Sep 17 00:00:00 2001 From: Seth Tisue Date: Mon, 6 Oct 2025 17:04:49 -0700 Subject: [PATCH 3/3] keep using old Scala.js version on Scala 2.11 --- .github/workflows/ci.yml | 14 ++++++++++---- build.sbt | 1 + project/plugins.sbt | 5 ++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7639bf55..7b1d230c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,13 +11,18 @@ jobs: fail-fast: false matrix: java: [8, 25] - scala: [2.11.x, 2.12.x, 2.13.x, 3.x] + scala: [2.12.x, 2.13.x, 3.x] + scala-js: [""] platform: [JVM, JS, Native] mode: [normal] - exclude: - - scala: 2.11.x - platform: Native include: + - java: 8 + scala: 2.11.x + platform: JVM + - java: 8 + scala: 2.11.x + platform: JS + scala-js: 1.12.0 - java: 8 scala: 2.12.x mode: testScalafix @@ -38,6 +43,7 @@ jobs: env: CI_JDK: ${{matrix.java}} CI_SCALA_VERSION: ${{matrix.scala}} + CI_SCALAJS_VERSION: ${{matrix.scala-js}} CI_MODE: ${{matrix.mode}} CI_PLATFORM: ${{matrix.platform}} steps: diff --git a/build.sbt b/build.sbt index 2470d1cc..3f8fca4a 100644 --- a/build.sbt +++ b/build.sbt @@ -369,6 +369,7 @@ inThisBuild { else { List( "CI_SCALA_VERSION", + "CI_SCALAJS_VERSION", "CI_PLATFORM", "CI_MODE", "CI_JDK", diff --git a/project/plugins.sbt b/project/plugins.sbt index 659b4adb..b9570bce 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,7 @@ -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.20.1") +val scalaJSVersion = + sys.env.get("CI_SCALAJS_VERSION").filter(_.nonEmpty).getOrElse("1.20.1") + +addSbtPlugin("org.scala-js" % "sbt-scalajs" % scalaJSVersion) addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.8") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2")