Scala 3 迁移指南

移植 sbt 项目(使用 sbt-scala3-migrate)

语言

sbt-scala3-migrate 是一个 sbt 插件,可帮助你在将 sbt 项目迁移到 Scala 3 期间提供帮助。它包含四个 sbt 命令

  • migrateDependencies 帮助你更新 libraryDependencies 列表
  • migrateScalacOptions 帮助你更新 scalacOptions 列表
  • migrateSyntax 修复了 Scala 2.13 和 Scala 3 之间的一些语法不兼容性
  • migrateTypes 尝试通过推断类型并在需要时解析隐式内容将你的代码编译为 Scala 3。

下面将详细介绍每条命令。

要求

  • Scala 2.13,建议使用 2.13.13
  • sbt 1.5 或更高版本
  • 免责声明:此工具无法迁移包含宏的库。

建议

在迁移之前,将 -Xsource:3 添加到 scalac 选项中,以在 Scala 2 编译器中启用 Scala 3 迁移警告。有关更多详细信息,请参阅页面 Scala 2 with -Xsource:3

在本教程中,我们将迁移 scalacenter/scala3-migration-example 中的项目。要了解迁移并进行培训,可以克隆此存储库并按照教程步骤进行操作。

1. 安装

在 sbt 项目的 project/plugins.sbt 文件中添加 sbt-scala3-migrate

// project/plugins.sbt
addSbtPlugin("ch.epfl.scala" % "sbt-scala3-migrate" % "0.6.1")

最新发布的版本是 scala3-migrate Scala version support

2. 选择一个模块

如果项目包含多个模块,第一步是选择要首先迁移的模块。

由于 Scala 2.13 和 Scala 3 之间的互操作性,你可以从任何模块开始。但是,从依赖项最少的模块开始可能更简单。

sbt-scala3-migrate 一次处理一个模块。确保你选择的模块不是聚合。

3. 迁移依赖项

本教程中的所有命令都必须在 sbt shell 中运行。

用法: migrateDependencies <project>

出于本教程的目的,我们将考虑以下构建配置

//build.sbt
lazy val main = project
  .in(file("."))
  .settings(
    scalaVersion := "2.13.11",
    libraryDependencies ++= Seq(
      "org.typelevel" %% "cats-core" % "2.4.0",
      "io.github.java-diff-utils" % "java-diff-utils" % "4.12",
      "org.scalameta" %% "parsers" % "4.8.9",
      "org.scalameta" %% "munit" % "0.7.23" % Test,
      "com.softwaremill.scalamacrodebug" %% "macros" % "0.4.1" % Test
    ),
    addCompilerPlugin(("org.typelevel" %% "kind-projector" % "0.13.2").cross(CrossVersion.full)),
    addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1")
  )

运行 migrateDependencies main 输出


sbt:main> migrateDependencies main
[info] 
[info] Starting migration of libraries and compiler plugins of project main
[info] 
[info] Valid dependencies:
[info] "io.github.java-diff-utils" % "java-diff-utils" % "4.12"

[warn] 
[warn] Versions to update:
[warn] "org.typelevel" %% "cats-core" % "2.6.1" (Other versions: 2.7.0, ..., 2.10.0)
[warn] "org.scalameta" %% "munit" % "0.7.25" % Test (Other versions: 0.7.26, ..., 1.0.0-M8)
[warn] 
[warn] For Scala 3 use 2.13:
[warn] ("org.scalameta" %% "parsers" % "4.8.9").cross(CrossVersion.for3Use2_13)
[warn] 
[warn] Integrated compiler plugins:
[warn] addCompilerPlugin(("org.typelevel" %% "kind-projector" % "0.13.2").cross(CrossVersion.full))
[warn] replaced by scalacOptions += "-Ykind-projector"
[error] 
[error] Incompatible Libraries:
[error] "com.softwaremill.scalamacrodebug" %% "macros" % "0.4.1" % Test (Macro Library)
[error] addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1") (Compiler Plugin)
[info] 
[success] Total time: 0 s, completed Aug 28, 2023 9:18:04 AM

让我们仔细看看此输出消息的每个部分。

有效依赖项

有效依赖项与 Scala 3 兼容,原因是它们是标准 Java 库,或者已交叉发布到 Scala 3。


[info] Valid dependencies:
[info] "io.github.java-diff-utils" % "java-diff-utils" % "4.12"

你可以保持原样。

需要更新的版本

这些库已在更高版本中交叉发布到 Scala 3。你需要更新它们的版本。


[warn] Versions to update:
[warn] "org.typelevel" %% "cats-core" % "2.6.1" (Other versions: 2.7.0, ..., 2.10.0)
[warn] "org.scalameta" %% "munit" % "0.7.25" % Test (Other versions: 0.7.26, ..., 1.0.0-M8)

在给定的示例中,我们需要将 cats-core 的版本升级到 2.6.1,并将 munit 的版本升级到 0.7.25。

输出消息的 其他版本 部分指示 Scala 3 中有哪些其他版本可用。如果你愿意,可以升级到最新版本之一,但请注意选择一个源兼容版本。根据 语义版本控制方案,修补程序或次要版本升级是安全的,但主要版本升级则不安全。

对于 Scala 3,使用 2.13

这些库尚未交叉发布到 Scala 3,但它们是交叉兼容的。你可以使用它们的 2.13 版本来编译到 Scala 3。

在库中添加 .cross(CrossVersion.for3Use2_13) 以告诉 sbt 使用 _2.13 后缀,而不是 _3


[warn] For Scala 3 use 2.13:
[warn] ("org.scalameta" %% "parsers" % "4.8.9").cross(CrossVersion.for3Use2_13)

关于 CrossVersion.for3Use2_13 的免责声明

  • 它可能会导致传递依赖项的 _2.13_3 后缀发生冲突。在这种情况下,sbt 将无法解析依赖项,并会显示一条明确的错误消息。
  • 发布依赖于 Scala 2.13 库的 Scala 3 库通常是不安全的。否则,库的用户可能会在同一依赖项上遇到冲突的 _2.13_3 后缀。

集成的编译器插件

一些编译器插件已集成到 Scala 3 编译器本身中。在 Scala 3 中,你无需将它们解析为依赖项,但可以使用编译器标志来激活它们。


[warn] Integrated compiler plugins:
[warn] addCompilerPlugin(("org.typelevel" %% "kind-projector" % "0.13.2").cross(CrossVersion.full))
[warn] replaced by scalacOptions += "-Ykind-projector"

例如,你可以通过将 -Ykind-projector 添加到 scalacOptions 列表中来激活 kind-projector。

在迁移过程中,保持与 Scala 2.13 的兼容性非常重要。后来的 migrateSyntaxmigrateTypes 命令将使用 Scala 2.13 编译来自动重写代码的某些部分。

你可以像这样以交叉兼容的方式配置 kind-projector

// add kind-projector as a dependency on Scala 2
libraryDependencies ++= {
  if (scalaVersion.value.startsWith("3.")) Seq.empty
  else Seq(
    compilerPlugin(("org.typelevel" %% "kind-projector" % "0.13.2").cross(CrossVersion.full))
  )
},
// activate kind-projector in Scala 3
scalacOptions ++= {
  if (scalaVersion.value.startsWith("3.")) Seq("-Ykind-projector")
  else Seq.empty
}

不兼容的库

一些宏库或编译器插件与 Scala 3 不兼容。


[error] Incompatible Libraries:
[error] "com.softwaremill.scalamacrodebug" %% "macros" % "0.4.1" % Test (Macro Library)
[error] addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1") (Compiler Plugin)

要解决这些不兼容性,你可以

  • 向维护者咨询他们是否计划将它们移植到 Scala 3,并可能帮助他们这样做。
  • 从你的构建中删除这些依赖项并相应地调整代码。

更新的构建

更新构建后,它应该如下所示

//build.sbt
lazy val main = project
  .in(file("."))
  .settings(
    scalaVersion := "2.13.11",
    libraryDependencies ++= Seq(
      "org.typelevel" %% "cats-core" % "2.6.1",
      "io.github.java-diff-utils" % "java-diff-utils" % "4.12",
      ("org.scalameta" %% "parsers" % "4.8.9").cross(CrossVersion.for3Use2_13),
      "org.scalameta" %% "munit" % "0.7.25" % Test
    ),
    libraryDependencies ++= {
      if (scalaVersion.value.startsWith("3.")) Seq.empty
      else Seq(
        compilerPlugin(("org.typelevel" %% "kind-projector" % "0.13.2").cross(CrossVersion.full))
      )
    },
    scalacOptions ++= {
      if (scalaVersion.value.startsWith("3.")) Seq("-Ykind-projector")
      else Seq.empty
    }
  )

重新加载 sbt,检查项目是否已编译(到 Scala 2.13),检查测试是否成功运行,并提交更改。你现在可以迁移编译器选项了。

4. 迁移编译器选项

用法: migrateScalacOptions <project>

Scala 3 编译器不包含与 Scala 2 编译器完全相同的选项集。你可以查看 编译器选项表 以获得所有编译器选项的完整比较。

migrateScalacOptions 将帮助你更新构建中的 scalacOptions 列表。

出于本教程的目的,我们将考虑以下构建配置

lazy val main = project
  .in(file("."))
  .settings(
    scalaVersion := "2.13.11",
    scalacOptions ++= Seq(
      "-encoding",
      "UTF-8",
      "-target:jvm-1.8",
      "-Xsource:3",
      "-Wunused:imports,privates,locals",
      "-explaintypes"
    )
  )

运行 migrateScalacOptions main 输出


sbt:main> migrateScalacOptions main
[info] 
[info] Starting migration of scalacOptions in main
[info] 
[info] Valid scalacOptions:
[info] -encoding UTF-8
[info] -Wunused:imports,privates,locals
[warn] 
[warn] Renamed scalacOptions:
[warn] -target:jvm-1.8 -> -Xunchecked-java-output-version:8
[warn] -explaintypes   -> -explain
[warn] 
[warn] Removed scalacOptions:
[warn] -Xsource:3
[warn] -Yrangepos
[success] Total time: 0 s, completed Aug 29, 2023 2:00:57 PM

一些 scalac 选项仍然有效,一些必须重命名,一些必须删除。

一些选项可能出现在 migrateScalacOptions 的输出中,但不会出现在你的 build.sbt 中。它们是由 sbt 或某些 sbt 插件添加的。确保使用最新版本的 sbt 和 sbt 插件。它们应该能够自动将添加的编译器选项调整到 Scala 版本。

同样重要的是,保持与 Scala 2.13 的兼容性,因为 migrateSyntaxmigrateTypes 命令将使用 Scala 2.13 编译自动应用一些补丁。

以下是我们可以如何更新 scalacOptions 列表

lazy val main = project
  .in(file("."))
  .settings(
    scalaVersion := "2.13.11",
    scalacOptions ++= {
      if (scalaVersion.value.startsWith("3.")) scala3Options
      else scala2Options
    }
  )

lazy val sharedScalacOptions =
  Seq("-encoding", "UTF-8", "-Wunused:imports,privates,locals")

lazy val scala2Options = sharedScalacOptions ++
  Seq("-target:jvm-1.8", "-Xsource:3", "-explaintypes")

lazy val scala3Options = sharedScalacOptions ++
  Seq("-Xunchecked-java-output-version:8", "-explain")

重新加载 sbt,检查项目是否已编译(到 Scala 2.13),检查测试是否成功运行,并提交更改。你现在可以迁移语法了。

5. 迁移语法

用法: migrateSyntax <project>

此命令运行许多 Scalafix 规则来修补一些废弃的语法。

应用的 Scalafix 规则列表是

有关 Scala 2.13 和 Scala 3 之间语法更改的更多信息,你可以参考 不兼容性表

不兼容性表 中列出的某些不兼容性不会被 migrateSyntax 修复。它们中的大多数并不频繁,并且可以轻松手动修复。如果你想使用 Scalafix 重写规则做出贡献,我们非常乐意在 migrateSyntax 命令中添加它。

运行 migrateSyntax main 输出


sbt:main> migrateSyntax main
[info] Starting migration of syntax in main
[info] Run syntactic rules in 7 Scala sources successfully
[info] Applied 3 patches in src/main/scala/example/SyntaxRewrites.scala
[info] Run syntactic rules in 8 Scala sources successfully
[info] Applied 1 patch in src/test/scala/example/SyntaxRewritesTests.scala
[info] Migration of syntax in main succeeded.
[success] Total time: 2 s, completed Aug 31, 2023 11:23:51 AM

查看应用的更改,检查项目是否仍在编译,检查测试是否成功运行并提交更改。下一步也是最后一步是迁移类型。

6. 迁移类型

用法: migrateTypes <project>

Scala 3 编译器使用略微不同的类型推断算法。它有时无法推断出与 Scala 2 编译器相同的类型,这可能导致编译错误。此最后一步将添加所需的类型注释,以使代码编译为 Scala 3。

运行 migrateTypes main 输出


sbt:main> migrateTypes main
[info] compiling 8 Scala sources to /home/piquerez/github/scalacenter/scala3-migration-example/target/scala-2.13/classes ...
[warn] 1 deprecation; re-run with -deprecation for details
[warn] one warning found
[info] compiling 8 Scala sources to /home/piquerez/github/scalacenter/scala3-migration-example/target/scala-2.13/test-classes ...
[warn] 2 deprecations; re-run with -deprecation for details
[warn] one warning found
[success] Total time: 7 s, completed Aug 31, 2023 11:26:25 AM
[info] Defining scalaVersion
[info] The new value will be used by Compile / bspBuildTarget, Compile / dependencyTreeCrossProjectId and 68 others.
[info]  Run `last` for details.
[info] Reapplying settings...
[info] set current project to main (in build file:/home/piquerez/github/scalacenter/scala3-migration-example/)
[info] 
[info] Migrating types in main / Compile
[info] 
[info] Found 3 patches in 1 Scala source
[info] Starting migration of src/main/scala/example/TypeIncompat.scala
[info] 3 remaining candidates
[info] 1 remaining candidate
[info] Found 1 required patch in src/main/scala/example/TypeIncompat.scala
[info] Compiling to Scala 3 with -source:3.0-migration -rewrite
[info] compiling 1 Scala source to /home/piquerez/github/scalacenter/scala3-migration-example/target/scala-3.3.1/classes ...
[info] 
[info] Migrating types in main / Test
[info] 
[info] Found 4 patches in 1 Scala source
[info] Starting migration of src/test/scala/example/TypeIncompatTests.scala.scala
[info] 4 remaining candidates
[info] 3 remaining candidates
[info] 2 remaining candidates
[info] Found 1 required patch in src/test/scala/example/TypeIncompatTests.scala.scala
[info] Compiling to Scala 3 with -source:3.0-migration -rewrite
[info] 
[info] You can safely upgrade main to Scala 3:
[info] scalaVersion := "3.3.1"
[success] Total time: 18 s, completed Aug 31, 2023 11:26:45 AM
[info] Defining scalaVersion
[info] The new value will be used by Compile / bspBuildTarget, Compile / dependencyTreeCrossProjectId and 68 others.
[info]  Run `last` for details.
[info] Reapplying settings...
[info] set current project to main (in build file:/home/piquerez/github/scalacenter/scala3-migration-example/)
sbt:main>

migrateTypes main 找到 2 个必需的补丁:一个在 src/test/scala/example/TypeIncompatTests.scala.scala 中,另一个在 src/main/scala/example/TypeIncompat.scala 中。它应用了这些补丁,然后使用 -source:3.0-migration -rewrite 编译为 Scala 3 以完成迁移。

恭喜!您的项目现在可以编译为 Scala 3。

接下来做什么?

如果您的项目仅包含一个模块,则可以设置 scalaVersion := 3.3.1

如果您有多个模块,则可以从 3. 迁移依赖项 重新开始,使用另一个模块。

完成所有模块后,您可以从 project/plugins.abt 中删除 sbt-scala3-migrate,以及所有与 Scala 2.13 相关的设置。

欢迎提供反馈和贡献

每条反馈都将帮助我们改进 sbt-scala3-migrate:错别字、更清晰的日志消息、更好的文档、错误报告、功能创意。请随时打开 GitHub 问题

此页面贡献者