How to migrate Scala 2.13 project to Scala 3?

Reading Time: 9 minutes

Are you a Scala developer looking to migrate your existing Scala 2.13 projects to the latest version of the language? If so, you’ll be happy to know that Scala 3 is now available and comes with a range of new features and improvements. With its streamlined syntax, improved performance, and better compatibility with Java 8 and above, Scala 3 offers a host of benefits for developers working with the language.

However, migrating to a new major version of any programming language can be a daunting task, and Scala 3 is no exception. But don’t worry – we’ve got you covered. In this blog post, we’ll provide you with a step-by-step guide to help you migrate your projects from Scala 2.13 to Scala 3 using the Scala 3 Migrate Plugin. Whether you’re interested in the new features of Scala 3 or just looking to stay up-to-date with the latest version of the language, this guide is for you.

So, let’s get started and take your Scala development to the next level with Scala 3.

Scala 3 Migrate Plugin

The Scala 3 Migrate Plugin is a valuable tool that can help you migrate your codebase to Scala 3. It has been designed to make the migration to scala 3 as easy as possible. It provides a set of automated tools and manual suggestions to make the process as smooth and painless as possible.

The migration process consists of four independent steps that are packaged into an sbt plugin:

  1. migrate-libs: This step helps you update the list of library dependencies in your build file to use the corresponding Scala 3 versions of your dependencies. It ensures that your project’s dependencies are compatible with Scala 3 and can be resolved correctly during the build process.
  2. migrate-scalacOptions: This step helps you update the list of compiler options (scalacOptions) in your build file to use the corresponding Scala 3 options. It ensures that the compiler is using the correct set of options for Scala 3, which can help improve the quality and performance of your code.
  3. migrate-syntax: This step fixes a number of syntax incompatibilities in your Scala 2.13 code so that it can be compiled in Scala 3. It handles common syntax changes between the two versions of Scala and can help you quickly fix issues that would otherwise require significant manual changes.
  4. migrate: This step tries to make your code compile with Scala 3 by adding the minimum required inferred types and implicit arguments. It automates the process of making your code compatible with Scala 3 and can help you quickly identify issues that would otherwise require significant manual changes.

Each of these steps is an sbt command that we will understand in detail in the following sections. So make sure to run them in an sbt shell.

Prerequisites

Before using the scala3-migrate plugin, you’ll need to make sure that your development environment meets the following prerequisites:

  1. SBT 1.5 or later: You’ll need to be using SBT as your build tool, and have a version of 1.5 or later installed on your system.
  2. Java 8 or later: The scala3-migrate plugin requires Java 8 or later to run. Make sure it is installed on your system.
  3. Scala 2.13: The scala3-migrate plugin requires Scala 2.13(preferred 2.13.5) to work correctly. If you’re using an earlier version of Scala, you’ll need to upgrade first.

By ensuring that your development environment meets these prerequisites, you’ll be able to use the scala3-migrate plugin with confidence and make a smooth transition to Scala 3.

Installation

You can install the scala3-migrate plugin by adding it to your plugins.sbt file:

addSbtPlugin("ch.epfl.scala" % "sbt-scala3-migrate" % "0.5.1")

Choosing a Module to Migrate

scala3-migrate plugin operates on one module at a time. So for projects with multiple modules, the first step is to choose the right one to migrate first.

Choosing the right module to migrate is an important first step in the process of migrating to Scala 3. Here are a few considerations to help you decide which module to migrate first:

  • Start with a small module: Migrating a large codebase all at once can be overwhelming, so it’s best to start with a small, self-contained module that is easy to test and debug. This will allow you to gain confidence in the migration process before tackling larger and more complex modules.
  • Choose a module with clear dependencies: Look for a module that has clear dependencies and is less likely to have complex interactions with other parts of your codebase. This will make it easier to identify any issues that arise during the migration process and ensure that you’re not introducing new bugs or breaking existing functionality.
  • Select a module that uses fewer language features: Some Scala 2 language features have been removed or changed in Scala 3, so it’s best to start with a module that uses fewer of these features. This will make it easier to identify and fix any issues related to the changes in the language.
  • Select a module that is actively developed: It’s a good idea to select a module that is currently under active development, as this will give you the opportunity to address any issues that arise during the migration process as part of your regular development workflow.

Consider these factors to choose a suitable module for migration and gain confidence before tackling more complex code.

Note:

Make sure the module you choose is not an aggregate project, otherwise only its own sources will be migrated, not the sources of its subprojects.

Migrate library dependencies

command: migrate-libs projectId

Migrating library dependencies is an important step in upgrading a Scala 2.13 project to Scala 3. Library dependencies can include external packages, plugins, and other code that your project relies on. Fortunately, the scala3-migrate plugin provides the migrate-libs projectId command(where projectId is the name of the module chosen to be migrated), which can help you to update your library dependencies to be compatible with Scala 3.

Let’s consider the following sbt build that is supposed to be migrated:

//build.sbt
val akkaHttpVersion = "10.2.4"
val akkaVersion = "2.6.5"
val jdbcAndLiftJsonVersion = "3.4.1"
val flywayCore = "3.2.1"
val keycloakVersion = "4.0.0.Final"

scapegoatVersion in ThisBuild := "1.4.8"

lazy val ticketService = project
  .in(file("."))
  .settings(
    name := "ticket-service",
    scalaVersion := "2.13.6",
    semanticdbEnabled := true,
    scalacOptions ++= Seq("-explaintypes", "-Wunused"),
    libraryDependencies ++= Seq(
      "com.typesafe.akka" %% "akka-http" % akkaHttpVersion,
      "com.typesafe.akka" %% "akka-stream" % akkaVersion,
      "net.liftweb" %% "lift-json" % jdbcAndLiftJsonVersion,
      "org.postgresql" % "postgresql" % "42.2.11",
      "org.scalikejdbc" %% "scalikejdbc" % jdbcAndLiftJsonVersion,
      "ch.qos.logback" % "logback-classic" % "1.2.3",
      "com.typesafe.scala-logging" %% "scala-logging" % "3.9.3",
      "ch.megard" %% "akka-http-cors" % "0.4.3",
      "org.apache.commons" % "commons-io" % "1.3.2",
      "org.fusesource.jansi" % "jansi" % "1.12",
      "com.google.api-client" % "google-api-client" % "1.30.9",
      "com.google.apis" % "google-api-services-sheets" % "v4-rev1-1.21.0",
      "com.google.apis" % "google-api-services-admin-directory" % "directory_v1-rev20191003-1.30.8",
      "com.google.oauth-client" % "google-oauth-client-jetty" % "1.30.5",
      "com.google.auth" % "google-auth-library-oauth2-http" % "1.3.0",
      // test lib
      "com.typesafe.akka" %% "akka-stream-testkit" % akkaVersion % Test,
      "com.typesafe.akka" %% "akka-http-testkit" % akkaHttpVersion % Test,
      "com.typesafe.akka" %% "akka-http-spray-json" % akkaHttpVersion,
      "org.scalatest" %% "scalatest" % "3.1.0" % Test,
      "org.mockito" %% "mockito-scala" % "1.11.4" % Test,
      "com.typesafe.akka" %% "akka-testkit" % akkaVersion % Test,
      "com.h2database" % "h2" % "1.4.196",
      //flyway
      "org.flywaydb" % "flyway-core" % flywayCore,
      //swagger-akka-http
      "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.4.2",
      "com.github.swagger-akka-http" %% "swagger-scala-module" % "2.3.1",
      //javax
      "javax.ws.rs" % "javax.ws.rs-api" % "2.0.1",
      "org.keycloak" % "keycloak-core" % keycloakVersion,
      "org.keycloak" % "keycloak-adapter-core" % keycloakVersion,
      "com.github.jwt-scala" %% "jwt-circe" % "9.0.1",
      "org.jboss.logging" % "jboss-logging" % "3.3.0.Final" % Runtime,
      "org.keycloak" % "keycloak-admin-client" % "12.0.2",
      "com.rabbitmq" % "amqp-client" % "5.12.0",
      "org.apache.commons" % "commons-text" % "1.9",
      "org.typelevel" %% "cats-core" % "2.3.0"
    )
  )

Next, we’ll run the command and see the output:

Output

The output lists project dependencies with their current version and required Scala 3-compatible version.

The Valid status indicates that the current version of the dependency is compatible with Scala 3. In contrast, the X status indicates that the dependency is not compatible with the Scala 3 version. The To be updated status displays the latest Scala 3 compatible version of the dependency.

In the given result, it appears that several dependencies are already valid and doesn’t require any updates. However, some dependencies require a specific Scala 3 compatible version, while others cannot be updated to Scala 3 at all.

For example, com.sksamuel.scapegoat:scalac-scapegoat-plugin:1.4.8:provided is marked with an X status, indicating that it is not compatible with Scala 3 and you need to remove it and find an alternative for the same. Moreover, the output suggests that the dependency ch.megard:akka-http-cors:0.4.3 should be updated to "ch.megard" %% "akka-http-cors" % "1.1.3", as the latter version is compatible with Scala 3.

In addition, some dependencies have a cross label next to them, indicating that they need to be used with a specific cross-versioning scheme, as they are not fully compatible with Scala 3. For example, the net.liftweb:lift-json:3.4.1 dependency needs to be used with the cross-versioning scheme CrossVersion.for3Use2_13, as it is only safe to use the 2.13 version if it’s inside an application.

Overall, this output can help identify which dependencies to update or remove when migrating to Scala 3. By following this migration guide, you can ensure that all the dependencies in your project are compatible with Scala 3.

Once you have applied all the changes mentioned in the above output, run the migrate-libs command again. All project dependencies with Valid status indicate successful migration of library dependencies to Scala 3.

Migrate scalacOptions

command: migrate-scalacOptions projectId

The next step for migration is to update the project’s Scala compiler options(scalacOptions) to work with Scala 3.

The Scala compiler options are flags that control the compiler’s behavior when passed to the Scala compiler. These flags can affect the code generation, optimization, and error reporting of the compiler.

In Scala 3, some of the compiler options have been renamed or removed, while others have been added. Therefore, it is important to review and update the scalacOptions when migrating from Scala 2.13 to Scala 3.

To perform this step, we’ll run the migrate-scalacOptions command which displays the following output:

The output shows a list of scalacOptions that were found in the project and indicate whether each option is still valid, has been renamed, or is no longer available in Scala 3.

For instance, the line -Wunused -> X indicates that the -Wunused option is not available in Scala 3 and needs to be removed. On the other hand, -explaintypes -> -explain-types shows that the -explaintypes option has been renamed to -explain-types and can still be used in Scala 3. So you just need to rename this scalacOption.

Some scalacOptions are not set by you in the build file but by some sbt plugins. For example, scala3-migrate tool enables semanticdb in Scala 2, which adds -Yrangepos option. Here sbt will adapt the semanticdb options in Scala 3. Therefore, all the information specific to the sbt plugins displayed by migrate-scalacOption can be ignored if the previous step has been followed successfully.

Overall, the output is intended to help you identify which scalacOptions need to be updated or removed in order to migrate the project to Scala 3.

After applying the suggested changes, the updated scalacOptions in the build looks like this:

scalacOptions ++=

      (if (scalaVersion.value.startsWith("3"))

        Seq("-explain-types")

      else Seq("-explaintypes", "-Wunused"))

Migrate the syntax

command: migrate-syntax projectId

This step is to fix the syntax incompatibilities that may arise when migrating code from Scala 2.13 to Scala 3. An incompatibility is a piece of code that compiles in Scala 2.13 but does not compile in Scala 3. Migrating a code base involves finding and fixing all the incompatibilities of the source code.

The command migrate-syntax is used to perform this step and fixes a number of syntax incompatibilities by applying the following Scalafix rules:

  • ProcedureSyntax
  • fix.scala213.ConstructorProcedureSyntax
  • fix.scala213.ExplicitNullaryEtaExpansion
  • fix.scala213.ParensAroundLambda
  • fix.scala213.ExplicitNonNullaryApply
  • fix.scala213.Any2StringAdd

This command is very useful in making the syntax migration process more efficient and less error-prone. By automatically identifying and fixing syntax incompatibilities, time and effort are saved from manual code changes.

Note that the migrate-syntax command is not guaranteed to fix all syntax incompatibilities. It is still necessary to manually review and update any remaining issues that the tool may have missed.

Let’s run the command and check the output:

The output displays a list of files that previously had syntax incompatibilities and are now fixed after running this command.

Migrate the code: the final step

command: migrate projectId

The final step in the migration process is to use the migrate command to make your code compile with Scala 3.

The new type inference algorithm in Scala 3 allows its compiler to infer a different type than Scala 2.13’s compiler. This command attempts to compile your code in Scala 3 by adding the minimum required inferred types and implicit arguments.

When you run the migrate command, it will generate a report that lists any errors or warnings encountered during the compilation process. This report identifies areas of your code needing modification for compatibility with Scala 3.

Overall, the migrate command is an essential tool for the final step in the migration process to Scala 3. It automatically identifies migration issues and ensures full compatibility with Scala 3.

Let’s run the command and see the output:

The output indicates that the project has been successfully migrated to Scala 3.1.1.

If your project has multiple modules, repeat the same migration steps for each of them. Once you’ve finished migrating each module, remove the scala3-migrate plugin from your project and update the Scala version to 3.1.1(or add this version to crossScalaVersions).

Conclusion

In conclusion, the process of migrating a Scala 2.13 project to Scala 3 can be made much simpler with the use of the scala3-migrate plugin. The plugin automates many migration changes, such as syntax incompatibilities and updating deprecated code. It also provides helpful diagnostics and suggestions for manual changes that are needed. However, it is still important to manually review and test changes to ensure the project runs correctly after migration. Careful planning and attention to detail ensure a successful migration to Scala 3, providing access to new features and benefits.

That’s it for this blog post. I hope that the information provided has been helpful and informative.

Additionally, if you found this post valuable, please share it with your friends, and colleagues, or on social media. Sharing information is a great way to help others and build a community of like-minded individuals.

To access more fascinating articles on Scala or any other cutting-edge technologies, visit Knoldus Blogs.

Finally, remember to keep learning and growing. With the vast amount of information available today, there’s always something new to discover and explore. So keep an open mind, stay curious, and never stop seeking knowledge.

References

scala3-migrate github

Scala 3 Migration Guide

Written by 

Prateek Gupta is a Software Consultant at Knoldus Inc. He likes to explore new technologies and believes in writing clean code. He has a good understanding of programming languages like Java and Scala. He also has good time management skills. In his leisure time, he like to do singing and watch SciFi movies.

Discover more from Knoldus Blogs

Subscribe now to keep reading and get access to the full archive.

Continue reading