|
| 1 | +--- |
| 2 | +layout: blog |
| 3 | +post-type: blog |
| 4 | +by: Ólafur Páll Geirsson |
| 5 | +title: "Refactor with scalafix v0.3" |
| 6 | +--- |
| 7 | + |
| 8 | +I am happy to announce the release of [scalafix v0.3][scalafix], a library and tool to rewrite Scala source code. |
| 9 | +Scalafix is developed at the [Scala Center][sc] with the long-term mission to help automate the migration of between Scala versions. |
| 10 | +However, as I hope to demonstrate in this post, scalafix can be used for more than just migrating between Scala versions. |
| 11 | +Scalafix can also be used for ad-hoc library and application migrations. |
| 12 | + |
| 13 | +Scalafix v0.3 uses the new scala.meta semantic API to provide a re-designed `Rewrite` and `Patch` API to implement custom refactorings. |
| 14 | +Let me explain what that means word by word. |
| 15 | + |
| 16 | +> Note. This is the second post on scalafix and scala.meta. You might be |
| 17 | +> interested in reading the first post |
| 18 | +> [here](http://scala-lang.org/blog/2016/10/24/scalafix.html). |
| 19 | +
|
| 20 | +## Scala.meta semantic API |
| 21 | + |
| 22 | +Scala.meta recently announced the [first release of its semantic API][meta-1.6]. |
| 23 | +This release is the product of close collaboration for the past several months between |
| 24 | +[@xeno_by][] at Twitter and [myself][@olafurpg] at the Scala Center. |
| 25 | +The objective of the semantic API is to provide operations to query information from the compiler. |
| 26 | + |
| 27 | +The first version of the scala.meta semantic API makes it possible to query for the resolved "symbol" of a name that appears in a Scala source file. |
| 28 | +A name is a reference to some definition, for example `println` or `scala.Predef.println`. |
| 29 | +A symbol is a unique identifier of a single definition. |
| 30 | +For example, `println` from the standard library has the symbol `_root_.scala.Predef.println(Ljava/lang/Object;)V.`. |
| 31 | +The compiler is responsible for resolving names to symbols. |
| 32 | + |
| 33 | +[Scalahost][] is a compiler plugin in the scala.meta project that extracts symbols from the compiler and maps them to scala.meta syntax trees. |
| 34 | +Scalahost emits the extracted symbols into a "semantic database". |
| 35 | +The semantic database can be persisted to files on disk and loaded for later analysis. |
| 36 | +Semantic databases from different compilation units, potentially produced by different |
| 37 | +versions of the Scala compiler, can be merged. |
| 38 | +This opens possibilities for large-scale code analysis. |
| 39 | + |
| 40 | +The introduction of the scala.meta semantic API is a game changer for scalafix. |
| 41 | +The ability to resolve names to symbols opens possibilities for many scalafix rewrites. |
| 42 | +Before we cover a few example rewrites, let's look closer at what exactly "rewrite" means. |
| 43 | + |
| 44 | +## Rewrite: meta.Tree => Seq[Patch] |
| 45 | +In a nutshell, a scalafix `Rewrite` is a `scala.meta.Tree => Seq[Patch]` function. |
| 46 | +The tree is backed by the scala.meta semantic API, so the rewrite is able to query for compiler information such as symbols. |
| 47 | +A scalafix `Patch` is a small operation that can produce a diff on a Scala source file. |
| 48 | +A patch can either be a "token patch" or a "tree patch". |
| 49 | + |
| 50 | +Token patches are low-level but give full control over how every detail in a source file is handled, for example formatting and comments. |
| 51 | +Example token patches are `Remove(token)` and `AddLeft(token, toAdd: String)`, which removes or prepends a string to `token`, respectively. |
| 52 | + |
| 53 | +Tree patches are high-level and allow the rewrite author to declaratively explain what operation to perform. |
| 54 | +An example tree patch is `AddGlobalImport(importer)`, which adds a new import to the top of a file if it does not exist. |
| 55 | +Observe that `AddGlobalImport` does not worry about token-level details such as whether the user groups imports by prefix (`import a.{b, c}`) or not (`import a.b; import a.c`). |
| 56 | + |
| 57 | +Tree and token patches build a small algebra of operations that can be composed to build complex refactorings. |
| 58 | +A challenge with composing patches is to figure out what to do on conflicts. |
| 59 | +For example, what happens when one patch renames a token while the other patch removes the same token? |
| 60 | +The current strategy in scalafix is to try and resolve as many conflicts as possible on the tree patch level. |
| 61 | +It can be harder to resolve conflicts on the token level since the original intent of the patch is lost. |
| 62 | +Unsolvable conflicts abort the refactoring. |
| 63 | +In the future, we hope to support more advanced conflict resolution strategies. |
| 64 | + |
| 65 | +To demonstrate how rewrites are implemented with scalafix v0.3, let's step through an example use-case. |
| 66 | + |
| 67 | +## Example: Xor to Either |
| 68 | + |
| 69 | +The functional programming library [cats][] migrated recently from its `Xor` data type to `Either` from the standard library. |
| 70 | +It requires a few mechanical steps to migrate code to use `Either` instead of `Xor`. |
| 71 | +For example, below is a diff that's taken from [circe][]'s migration to `Either`. |
| 72 | + |
| 73 | +```diff |
| 74 | +-final def either: Xor[HCursor, HCursor] = if (succeeded) Xor.right(any) else Xor.left(any) |
| 75 | ++final def either: Either[HCursor, HCursor] = if (succeeded) Right(any) else Left(any) |
| 76 | +``` |
| 77 | + |
| 78 | +For this particular rewrite, we are able to get away with only using tree patches. |
| 79 | +`Replace` is one tree patch that can be used to replace usage of a reference such as a type, term or a static method. |
| 80 | + |
| 81 | +```scala |
| 82 | +Replace(Symbol("_root_.cats.data.Xor."), q"Either") |
| 83 | +Replace(Symbol("_root_.cats.data.Xor.Left."), q"Left") |
| 84 | +Replace(Symbol("_root_.cats.data.Xor.Right."), q"Right") |
| 85 | +``` |
| 86 | + |
| 87 | +The `Symbol(_root_...data.Xor)` part is the scala.meta symbol referencing the class definition of `cats.data.Xor`. |
| 88 | +As we saw in the `either: Xor[HCursor` diff above, references to `Xor` should become `Either` after the rewrite. |
| 89 | + |
| 90 | +Symbols are normalized by default, so the `cats.data.Xor.Right` replace patch will handle the `Xor.Right` type, `Xor.Right` companion object as well as the `Right.apply` constructor method. |
| 91 | + |
| 92 | +To introduce new imports, it's possible to pass in `additionalImports` |
| 93 | + |
| 94 | +```scala |
| 95 | +Replace(Symbol("_root_.cats.data.XorT."), q"EitherT", |
| 96 | + additionalImports = List(importer"cats.data.EitherT")), |
| 97 | +``` |
| 98 | + |
| 99 | +Imports can be removed with the `RemoveGlobalImport` patch |
| 100 | + |
| 101 | +```scala |
| 102 | +RemoveGlobalImport(importer"cats.data.Xor") |
| 103 | +``` |
| 104 | + |
| 105 | +Nothing happens if the import does not appear in the source file. |
| 106 | +Likewise, it's OK to add the same import twice, scalafix will de-duplicate it. |
| 107 | + |
| 108 | +The full `Xor` to `Either` scalafix rewrite can be found [here][xor2either] and its accompanying test |
| 109 | +suite [here][xor2either-test]. |
| 110 | + |
| 111 | +## Try it out! |
| 112 | +You can use scalafix both as a library and a tool. |
| 113 | + |
| 114 | +The recommended way to use scalafix as a library is with the [`sbt-scalahost`][sbt-scalahost] plugin. |
| 115 | +By using scalafix as a library, you have full control of how, where and when to run rewrites. |
| 116 | + |
| 117 | +The recommended way to use scalafix as tool is the [`sbt-scalafix`][sbt-scalafix] plugin. |
| 118 | +It's possible define custom tree patches in the [`.scalafix.conf` configuration file][config-patches]. |
| 119 | + |
| 120 | +I am excited to see what applications the community can build with scala.meta and scalafix. |
| 121 | +Some promising ideas that have floated around include |
| 122 | + |
| 123 | +- parse and run rewrites from `@deprecated` warning messages. This would enable |
| 124 | + library authors to provide executable migration guides. |
| 125 | +- shim/cross-build libraries with similar APIs. For example, |
| 126 | + a library could be developed with the scalaz API, and use scalafix to |
| 127 | + code-generate a version of the library that uses cats instead. |
| 128 | +- build a code search web-interface with "jump to definition" functionality |
| 129 | + powered by the scala.meta's semantic database (similar to [sxr][]). |
| 130 | +- replace usage of `any2stringadd` with string interpolators or explicit `.toString`. |
| 131 | +- replace usage of `scala.Seq`, which can be mutable, in favor of `scala.collection.immutable.Seq`. |
| 132 | + |
| 133 | +If this sounds exciting to you, join us! |
| 134 | +I am happy to answer any question in the [scalafix gitter][gitter] channel. |
| 135 | + |
| 136 | +PS. I want to thank [@ShaneDelmore][] who has provided invaluable feedback from |
| 137 | +the early days of scalafix development and come up with several brilliant ideas |
| 138 | +for scalafix use-cases. |
| 139 | + |
| 140 | + |
| 141 | +[sc]: http://scala.epfl.ch/ |
| 142 | +[sxr]: https://github.com/harrah/browse |
| 143 | +[Scalahost]: https://github.com/scalameta/sbt-semantic-example |
| 144 | +[@ShaneDelmore]: https://twitter.com/ShaneDelmore |
| 145 | +[@olafurpg]: https://twitter.com/olafurpg |
| 146 | +[@xeno_by]: https://twitter.com/xeno_by |
| 147 | +[xor2either-test]: https://github.com/scalacenter/scalafix/blob/f61136fad79afcdbb03528ce78c7928afc6eafd6/scalafix-nsc/src/test/resources/syntactic/Xor2Either.source |
| 148 | +[xor2either]: https://github.com/scalacenter/scalafix/blob/f61136fad79afcdbb03528ce78c7928afc6eafd6/core/src/main/scala/scalafix/rewrite/Xor2Either.scala |
| 149 | +[circe]: https://github.com/circe/circe/pull/343/files |
| 150 | +[cats]: http://github.com/typelevel/cats |
| 151 | +[ghpages]: http://github.com/scalacenter/scalafix |
| 152 | +[scalafix]: https://scalacenter.github.io/scalafix/#0.3.0 |
| 153 | +[sbt-scalafix]: https://scalacenter.github.io/scalafix/#sbt-scalafix |
| 154 | +[sbt-scalahost]: https://scalacenter.github.io/scalafix/#sbt-scalahost |
| 155 | +[install]: https://scalacenter.github.io/scalafix/#Installation |
| 156 | +[meta-1.6]: https://github.com/scalameta/scalameta/blob/master/changelog/1.6.0.md#semantic-api |
| 157 | +[config-patches]: https://scalacenter.github.io/scalafix/#patches |
| 158 | +[gitter]: https://gitter.im/scalacenter/scalafix |
0 commit comments