diff --git a/library/src/scala/annotation/unchecked/uncheckedOverride.scala b/library/src/scala/annotation/unchecked/uncheckedOverride.scala new file mode 100644 index 000000000000..00457f305234 --- /dev/null +++ b/library/src/scala/annotation/unchecked/uncheckedOverride.scala @@ -0,0 +1,21 @@ +/* + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. dba Akka + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package scala.annotation.unchecked + +import scala.annotation.StaticAnnotation + +/** + * Marking a definition `@uncheckedOverride` is equivalent to the `override` keyword, except that overriding is not + * enforced. A definition marked `@uncheckedOverride` is allowed to override nothing. + */ +final class uncheckedOverride extends StaticAnnotation diff --git a/library/src/scala/collection/IterableOnce.scala b/library/src/scala/collection/IterableOnce.scala index 59fa668fe6e8..8d8ddd532b01 100644 --- a/library/src/scala/collection/IterableOnce.scala +++ b/library/src/scala/collection/IterableOnce.scala @@ -1174,12 +1174,12 @@ transparent trait IterableOnceOps[+A, +CC[_], +C] extends Any { this: IterableOn * * $willNotTerminateInf * - * @param cmp An ordering to be used for comparing elements. + * @param ord An ordering to be used for comparing elements. * @tparam B The result type of the function `f`. * @param f The measuring function. * @throws UnsupportedOperationException if this $coll is empty. * @return the first element of this $coll with the largest value measured by function `f` - * with respect to the ordering `cmp`. + * with respect to the ordering `ord`. */ def maxBy[B](f: A -> B)(implicit ord: Ordering[B]): A = knownSize match { @@ -1214,11 +1214,11 @@ transparent trait IterableOnceOps[+A, +CC[_], +C] extends Any { this: IterableOn * * $willNotTerminateInf * - * @param cmp An ordering to be used for comparing elements. + * @param ord An ordering to be used for comparing elements. * @tparam B The result type of the function `f`. * @param f The measuring function. * @return an option value containing the first element of this $coll with the - * largest value measured by function `f` with respect to the ordering `cmp`. + * largest value measured by function `f` with respect to the ordering `ord`. */ def maxByOption[B](f: A -> B)(implicit ord: Ordering[B]): Option[A] = knownSize match { @@ -1230,12 +1230,12 @@ transparent trait IterableOnceOps[+A, +CC[_], +C] extends Any { this: IterableOn * * $willNotTerminateInf * - * @param cmp An ordering to be used for comparing elements. + * @param ord An ordering to be used for comparing elements. * @tparam B The result type of the function `f`. * @param f The measuring function. * @throws UnsupportedOperationException if this $coll is empty. * @return the first element of this $coll with the smallest value measured by function `f` - * with respect to the ordering `cmp`. + * with respect to the ordering `ord`. */ def minBy[B](f: A -> B)(implicit ord: Ordering[B]): A = knownSize match { @@ -1247,12 +1247,12 @@ transparent trait IterableOnceOps[+A, +CC[_], +C] extends Any { this: IterableOn * * $willNotTerminateInf * - * @param cmp An ordering to be used for comparing elements. + * @param ord An ordering to be used for comparing elements. * @tparam B The result type of the function `f`. * @param f The measuring function. * @return an option value containing the first element of this $coll * with the smallest value measured by function `f` - * with respect to the ordering `cmp`. + * with respect to the ordering `ord`. */ def minByOption[B](f: A -> B)(implicit ord: Ordering[B]): Option[A] = knownSize match { diff --git a/library/src/scala/collection/mutable/Buffer.scala b/library/src/scala/collection/mutable/Buffer.scala index 224d74c11b08..5073c4c49a09 100644 --- a/library/src/scala/collection/mutable/Buffer.scala +++ b/library/src/scala/collection/mutable/Buffer.scala @@ -277,7 +277,7 @@ trait IndexedBuffer[A] extends IndexedSeq[A] /** Replaces the contents of this $coll with the filtered result. * - * @param f the filtering function + * @param p the filtering function * @return this $coll */ def filterInPlace(p: A => Boolean): this.type = { diff --git a/library/src/scala/math/Ordering.scala b/library/src/scala/math/Ordering.scala index ffefbfc01433..3d779fc796f3 100644 --- a/library/src/scala/math/Ordering.scala +++ b/library/src/scala/math/Ordering.scala @@ -18,6 +18,7 @@ import java.util.Comparator import scala.language.implicitConversions import scala.annotation.migration +import scala.annotation.unchecked.uncheckedOverride /** Ordering is a trait whose instances each represent a strategy for sorting * instances of a type. @@ -104,10 +105,10 @@ trait Ordering[T] extends Comparator[T] with PartialOrdering[T] with Serializabl override def equiv(x: T, y: T): Boolean = compare(x, y) == 0 /** Returns `x` if `x` >= `y`, otherwise `y`. */ - def max[U <: T](x: U, y: U): U = if (gteq(x, y)) x else y + @uncheckedOverride def max[U <: T](x: U, y: U): U = if (gteq(x, y)) x else y /** Returns `x` if `x` <= `y`, otherwise `y`. */ - def min[U <: T](x: U, y: U): U = if (lteq(x, y)) x else y + @uncheckedOverride def min[U <: T](x: U, y: U): U = if (lteq(x, y)) x else y /** Returns the opposite ordering of this one. * diff --git a/library/src/scala/runtime/ScalaRunTime.scala b/library/src/scala/runtime/ScalaRunTime.scala index 5ee6a4aee012..534c2fa0ae42 100644 --- a/library/src/scala/runtime/ScalaRunTime.scala +++ b/library/src/scala/runtime/ScalaRunTime.scala @@ -289,20 +289,23 @@ object ScalaRunTime { // Use `null` in places where we want to make sure the reference is cleared. private[scala] inline def nullForGC[T]: T = null.asInstanceOf[T] - // Convert arrays to immutable.ArraySeq for use with Scala varargs. - // By construction, calls to these methods always receive a fresh (and non-null), non-empty array. - // In cases where an empty array would appear, the compiler uses a direct reference to Nil instead. - // Synthetic Java varargs forwarders (@annotation.varargs or varargs bridges when overriding) may pass - // `null` to these methods; but returning `null` or `ArraySeq(null)` makes little difference in practice. - def genericWrapArray[T](xs: Array[T]): ArraySeq[T] = ArraySeq.unsafeWrapArray(xs) - def wrapRefArray[T <: AnyRef | Null](xs: Array[T]): ArraySeq[T] = new ArraySeq.ofRef[T](xs) - def wrapIntArray(xs: Array[Int]): ArraySeq[Int] = new ArraySeq.ofInt(xs) - def wrapDoubleArray(xs: Array[Double]): ArraySeq[Double] = new ArraySeq.ofDouble(xs) - def wrapLongArray(xs: Array[Long]): ArraySeq[Long] = new ArraySeq.ofLong(xs) - def wrapFloatArray(xs: Array[Float]): ArraySeq[Float] = new ArraySeq.ofFloat(xs) - def wrapCharArray(xs: Array[Char]): ArraySeq[Char] = new ArraySeq.ofChar(xs) - def wrapByteArray(xs: Array[Byte]): ArraySeq[Byte] = new ArraySeq.ofByte(xs) - def wrapShortArray(xs: Array[Short]): ArraySeq[Short] = new ArraySeq.ofShort(xs) - def wrapBooleanArray(xs: Array[Boolean]): ArraySeq[Boolean] = new ArraySeq.ofBoolean(xs) - def wrapUnitArray(xs: Array[Unit]): ArraySeq[Unit] = new ArraySeq.ofUnit(xs) + // Convert an array to an immutable.ArraySeq for use with Scala varargs. + // `foo(x, y)` is compiled to `foo(wrapXArray(Array(x, y))). + // For `foo()`, the Scala 2 compiler uses a reference to `Nil` instead; Scala 3 doesn't have this special case. + // + // The `null` checks are there for backwards compatibility. They were removed in 2.13.17 (scala/scala#11021) + // which led to a ticket in Scala 3 (scala/scala3#24204). The argument may be null: + // - When calling a Scala `@varargs` method from Java + // - When using an array as sequence argument in Scala 3: `foo((null: Array[X])*)` + def genericWrapArray[T](xs: Array[T]): ArraySeq[T] = mapNull(xs, ArraySeq.unsafeWrapArray(xs)) + def wrapRefArray[T <: AnyRef | Null](xs: Array[T]): ArraySeq[T] = mapNull(xs, new ArraySeq.ofRef[T](xs)) + def wrapIntArray(xs: Array[Int]): ArraySeq[Int] = mapNull(xs, new ArraySeq.ofInt(xs)) + def wrapDoubleArray(xs: Array[Double]): ArraySeq[Double] = mapNull(xs, new ArraySeq.ofDouble(xs)) + def wrapLongArray(xs: Array[Long]): ArraySeq[Long] = mapNull(xs, new ArraySeq.ofLong(xs)) + def wrapFloatArray(xs: Array[Float]): ArraySeq[Float] = mapNull(xs, new ArraySeq.ofFloat(xs)) + def wrapCharArray(xs: Array[Char]): ArraySeq[Char] = mapNull(xs, new ArraySeq.ofChar(xs)) + def wrapByteArray(xs: Array[Byte]): ArraySeq[Byte] = mapNull(xs, new ArraySeq.ofByte(xs)) + def wrapShortArray(xs: Array[Short]): ArraySeq[Short] = mapNull(xs, new ArraySeq.ofShort(xs)) + def wrapBooleanArray(xs: Array[Boolean]): ArraySeq[Boolean] = mapNull(xs, new ArraySeq.ofBoolean(xs)) + def wrapUnitArray(xs: Array[Unit]): ArraySeq[Unit] = mapNull(xs, new ArraySeq.ofUnit(xs)) } diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 9a633397924a..90529a345819 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -9,6 +9,8 @@ object MiMaFilters { // Additions that require a new minor version of the library Build.mimaPreviousDottyVersion -> Seq( ProblemFilters.exclude[DirectMissingMethodProblem]("scala.caps.package#package.freeze"), + // scala/scala3#24545 / scala/scala3#24788 + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.unchecked.uncheckedOverride"), ), ) diff --git a/project/scripts/syncStdLib.scala b/project/scripts/syncStdLib.scala new file mode 100644 index 000000000000..5dfe9e740bc3 --- /dev/null +++ b/project/scripts/syncStdLib.scala @@ -0,0 +1,154 @@ +//> using scala 3.7 +//> using toolkit default + +import scala.util.matching.Regex + +import scala.util.CommandLineParser.FromString + +given FromString[os.Path] with + def fromString(s: String) = os.Path(s, os.pwd) + +/** Sync standard library changes from scala/scala to scala/scala3. + * + * Usage: scala run syncStdLib.scala -- + * + * Example: scala syncStdLib.scala -- ../scala2 v2.13.17 v2.13.18 + */ +@main def syncStdLib( + scala2RepoPath: os.Path, + fromTag: String, + toTag: String +): Unit = + val scala2Path = os.Path(scala2RepoPath, os.pwd) + val scala3Path = os.pwd + + val workDir = scala3Path / "stdlib-sync-work" + val patchDir = workDir / "patches" + + println(s"Scala 2 repo: $scala2Path") + println(s"Scala 3 repo: $scala3Path") + println(s"Syncing: $fromTag..$toTag") + println() + + // Clean up and create directories + os.remove.all(workDir) + os.makeDir.all(patchDir) + + // ------------------------- + // 1) Fetch latest changes in scala2 repo + // ------------------------- + println(s"Fetching tags $fromTag and $toTag from scala2...") + os.proc("git", "fetch", "--tags", "origin", fromTag, toTag) + .call(cwd = scala2Path, stdout = os.Inherit, stderr = os.Inherit) + + // Show what will be synced + println(s"\nCommits to sync ($fromTag..$toTag):") + val logResult = os + .proc("git", "log", "--oneline", s"$fromTag..$toTag", "--", "src/library") + .call(cwd = scala2Path) + logResult.out.lines().foreach(println) + println() + + // ------------------------- + // 2) Create patch series for src/library only, paths relative to src/library/ + // ------------------------- + println("Creating patches...") + os.proc( + "git", + "format-patch", + "--output-directory", + patchDir.toString, + "--relative=src/library", + "--no-signature", + s"$fromTag..$toTag", + "--", + "src/library" + ).call(cwd = scala2Path, stdout = os.Inherit, stderr = os.Inherit) + + val patchFiles = os.list(patchDir).filter(_.ext == "patch").sorted + println(s"Created ${patchFiles.size} patches") + + // ------------------------- + // 3) Rewrite patches: + // - qualify #NNNN -> scala/scala#NNNN + // - add footer: "Upstream: scala/scala@" + // ------------------------- + println("\nRewriting patches...") + rewritePatches(patchDir) + + // ------------------------- + // 4) Apply onto scala3 + // ------------------------- + val branchName = s"stdlib-sync/${toTag.stripPrefix("v")}" + println(s"\nCreating branch $branchName...") + + os.proc("git", "checkout", "-b", branchName, "origin/main") + .call(cwd = scala3Path, stdout = os.Inherit, stderr = os.Inherit) + + println(s"Applying ${patchFiles.size} patches...") + + val patches = os.list(patchDir).filter(_.ext == "patch").sorted + val amResult = os + .proc("git", "am", "--3way", "--directory=library/src", patches) + .call( + cwd = scala3Path, + check = false, + stdout = os.Inherit, + stderr = os.Inherit + ) + + if amResult.exitCode != 0 then + println() + println("Conflicts occurred.") + println("Resolve, then run: git am --continue") + println("Abort with: git am --abort") + sys.exit(1) + + println("\nPatches applied successfully!") + os.remove.all(workDir) + + println(s"\nTo push the branch, run:") + println(s" git push -u origin $branchName") +end syncStdLib + +/** Rewrite patches to: + * - Qualify GitHub issue refs: #NNNN -> scala/scala#NNNN + * - Add footer: "Upstream: scala/scala@" + */ +def rewritePatches(patchDir: os.Path): Unit = + val patchFiles = os.list(patchDir).filter(_.ext == "patch").sorted + val issueRefPattern = """(? ..." line + val upstream = fromLinePattern.findFirstMatchIn(raw).map(_.group(1)) + + // Qualify github refs in the patch (commit message is inside) + var rewritten = issueRefPattern.replaceAllIn(raw, "scala/scala#$1") + + // Insert "Upstream: ..." footer into the commit message. + // In format-patch output, commit message ends just before "\n---\n" + upstream.foreach { sha => + val footer = s"Upstream: scala/scala@$sha" + if !rewritten.contains(footer) then + rewritten.split("\n---\n", 2) match + case Array(msg, rest) => + var newMsg = msg + if !newMsg.endsWith("\n") then newMsg += "\n" + if !newMsg.endsWith("\n\n") then newMsg += "\n" + newMsg += s"$footer\n" + rewritten = newMsg + "\n---\n" + rest + case _ => // No split point found, leave as is + } + + if rewritten != raw then + os.write.over(patch, rewritten) + rewrittenCount += 1 + + println(s"Rewrote $rewrittenCount patches") +end rewritePatches diff --git a/tests/run/i24204.scala b/tests/run/i24204.scala new file mode 100644 index 000000000000..4ca48f80e903 --- /dev/null +++ b/tests/run/i24204.scala @@ -0,0 +1,23 @@ +class NullArgumentException(msg: String) extends IllegalArgumentException(msg) + +trait Suite +class Suites(suitesToNest: Suite*) { + if suitesToNest == null then + throw NullArgumentException("The passed suites splice is null") + + for + s <- suitesToNest // triggers NullPointerException + if s == null + do + throw new NullArgumentException("One of the passed suite was null") +} + +@main def Test = { + val aNull: Array[Suite] = null + try { + new Suites(aNull*) + throw IllegalStateException("Should not be reached") + } catch { + case ex: NullArgumentException => println(s"ok, expected: $ex") + } +} \ No newline at end of file