Skip to content

Commit 0d531c3

Browse files
Merge pull request #605 from olafurpg/scalafix-v0.3
Announce scalafix v0.3
2 parents 1e96e8d + 39f54e2 commit 0d531c3

File tree

1 file changed

+158
-0
lines changed

1 file changed

+158
-0
lines changed
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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

Comments
 (0)