Skip to content

Commit ded45af

Browse files
authored
Merge pull request #203 from olafurpg/docs
Update docs on how to implement rewrites
2 parents 3d6c1cc + 394bd38 commit ded45af

File tree

7 files changed

+261
-57
lines changed

7 files changed

+261
-57
lines changed

Diff for: readme/Faq.scalatex

+19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
@import Main._
22
@import scalafix.Readme._
3+
@import scalafix.{Versions => V}
34

45
@sect{FAQ / Troubleshooting}
56

@@ -16,3 +17,21 @@
1617
You might be using an old version of sbt.
1718
sbt-scalafix requires sbt 0.13.13 or higher.
1819

20+
@sect{Scalafix doesn't do anything}
21+
@ul
22+
@li
23+
Make sure that you are running at least one rewrite.
24+
@li
25+
Make sure that you are using a supported Scala version: @V.supportedScalaVersions.mkString(", ")
26+
@li
27+
It could be that sbt-scalafix failed to enable the @sect.ref{Scalahost} compiler plugin.
28+
You can check if Scalahost is enabled in sbt with @code{show myproject/scalacOptions}.
29+
If it does not contains an option @code{-Xplugin:/path/to/scalahost.jar},
30+
then Scalahost is not enabled.
31+
See @lnk("here", "http://scalameta.org/tutorial/#Installation")
32+
for how to manually install scalahost and verify that
33+
@code{.semanticdb} files are created.
34+
35+
@sect{RemoveUnusedImports does not remove unused imports}
36+
Make sure that you followed the instructions in @sect.ref{RemoveUnusedImports}
37+
regarding scalac options.

Diff for: readme/ImplementingRewrites.scalatex

+190
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
@import Main._
2+
@import scalafix.Readme._
3+
@import scalafix.rewrite._
4+
@import scalafix.{Versions => V}
5+
@import scalafix.cli.Cli
6+
7+
@sect{Creating your own rewrite}
8+
@p
9+
It is possible to implement custom rewrites with Scalafix.
10+
Depending on what your rewrite does, it may be a lot of work or very
11+
little work. Don't hestitate to get an estimate on @gitter for
12+
how complicated it would be to implement your rewrite.
13+
14+
@sect{Before you begin}
15+
Before you dive right into the code of your rewrite, it might be
16+
good to answer the following questions first.
17+
18+
@sect{What diff do you want to make?}
19+
Scalafix is a tool to automatically produce diffs.
20+
Before implementing a rewrite, it's good to manually migrate/refactor a
21+
few examples first. Manually refactoring code is helpful
22+
to estimate how complicated the rewrite is.
23+
24+
@sect{Is the expected output unambiguous?}
25+
Does the rewrite require manual intervention or do you always know what
26+
output the rewrite should produce? Scalafix currently does not yet support
27+
interactive refactoring. However, Scalafix has support for configuration,
28+
which makes it possible to leave some choice to the user on how the rewrite
29+
should behave.
30+
31+
@sect{Who will use your rewrite?}
32+
The target audience/users of your rewrite can impact the implementation the
33+
rewrite. If you are the only end-user of the rewrite, then you can maybe
34+
take shortcuts and worry less about rare corner cases that may be easier to fix
35+
manually. If your rewrite is intended to be used by the entire Scala
36+
community, then you might want to be more careful with corner cases.
37+
38+
@sect{What code will your rewrite fix?}
39+
Is your rewrite specific to a particular codebase? Or is the rewrite intended
40+
to be used on codebases that you don't have access to? If your rewrite is
41+
specific to one codebase, then it's easier to validate if your rewrite
42+
is ready. You may not even need tests, since your codebase is your only test.
43+
If your rewrite is intended to be used in any random codebase, you may
44+
want to have tests and put more effort into handling corner cases.
45+
In general, the smaller the target domain of your rewrite, the easier it
46+
is to implement a rewrite.
47+
48+
@sect{How often will your rewrite run?}
49+
Are you writing a one-off migration script or will your rewrite run on
50+
every pull request? A rewrite that runs on every pull request should ideally
51+
have some unit tests and be documented so that other people can help maintain
52+
the rewrite.
53+
54+
@sect{scalacenter/scalafix.g8}
55+
@p
56+
Run the following commands to generate a skeleton project
57+
58+
@hl.scala
59+
// by convention, --rewrite= should match the GitHub repo name.
60+
// this makes it possible for users to run `scalafix github:org/reponame/v1.0`
61+
sbt new scalacenter/scalafix.g8 --rewrite "reponame" --version=v1.0
62+
cd reponame/scalafix
63+
sbt tests/test
64+
@p
65+
Note that the @code{scalafix} directory is a self-contained sbt build
66+
and can be put into the root directory of your repo.
67+
The tests are written using @sect.ref{scalafix-testkit}.
68+
69+
@sect{Example rewrites}
70+
The Scalafix repository contains several example rewrites and tests,
71+
see @lnk("here", "https://github.com/scalacenter/scalafix/tree/master/scalafix-core/src/main/scala/scalafix/rewrite").
72+
These examples may serve as inspiration for your rewrite.
73+
74+
@sect{Vocabulary}
75+
The following sections explain useful vocabulary when working with Scalafix.
76+
77+
@sect{Rewrite}
78+
A rewrite is a small program/function that can produce diffs.
79+
To implement a rewrite, you extend the
80+
@lnk("Rewrite", "https://github.com/scalacenter/scalafix/blob/master/scalafix-core/src/main/scala/scalafix/rewrite/Rewrite.scala")
81+
class.
82+
To run a rewrite, users execute @code{scalafix --rewrites MyRewrite}.
83+
Multiple rewrites can be composed into a single rewrite.
84+
For example, the migration for Dotty may involve @sect.ref{ProcedureSyntax},
85+
@sect.ref{ExplicitUnit}, @sect.ref{DottyVarArgPattern}, @sect.ref{ExplicitReturnTypes}
86+
and a few other rewrites. It is possible to combine all of those rewrites
87+
into a single @code{Dotty} rewrite so users can run
88+
@code{scalafix --rewrites Dotty}.
89+
90+
@sect{RewriteCtx}
91+
A rewrite context contains data structures and utilities to rewrite a single
92+
source file. For example, the rewrite context contains the parsed @sect.ref{Tree},
93+
@sect.ref{Tokens}, lookup tables for matching parentheses and more.
94+
95+
@sect{Patch}
96+
A "Patch" is a data structure that describes how to produce a diff.
97+
Two patches can combined into a single patch with the @code{+} operator.
98+
A patch can also be empty. Patches can either be low-level "token patches",
99+
that operate on the token level or high-level "tree patches" that operate
100+
on parsed abstract syntax tree nodes. The public API for patch
101+
operations is available in PatchOps.scala
102+
103+
@hl.ref(wd/"scalafix-core"/"src"/"main"/"scala"/"scalafix"/"patch"/"PatchOps.scala", start = "class SyntacticPatch")
104+
105+
Some things are typically easier to do on the token level and other
106+
things are easier to do on the tree level.
107+
The Patch API is constantly evolving and we regularly add more
108+
utility methods to accomplish common tasks.
109+
If you experience that it's difficult to implement something that
110+
seems simple then don't hesitate to ask on @gitter.
111+
112+
@sect{Scalameta}
113+
Scalafix uses @lnk("Scalameta", "http://scalameta.org/") to implement
114+
rewrites.
115+
Scalameta is a clean-room implementation of a metaprogramming toolkit for Scala.
116+
This means it's not necessary to have experience with Scala compiler internals
117+
to implement Scalafix rewrites.
118+
In fact, Scalafix doesn't even depend on the Scala compiler.
119+
Since Scalafix is not tied so a single compiler, this means that Scalafix
120+
rewrites in theory can work with any Scala compiler, including @dotty and
121+
IntelliJ Scala Plugin.
122+
123+
@sect{Scalahost}
124+
Scalahost is a compiler plugin for Scala 2.x in the @sect.ref{Scalameta} project
125+
that collects information to build a @sect.ref{Mirror}.
126+
For more information about Scalahost, see
127+
the @lnk("Scalameta documentation", "http://scalameta.org/tutorial/#Scalahost").
128+
129+
@sect{Token}
130+
A token is for example an identifier @code{println}, a delimiter @code{[} @code{)},
131+
or a whitespace character like space or newline.
132+
In the context of Scalafix, a @code{Token} means the data structure
133+
@code{scala.meta.Token}.
134+
See @lnk("Scalameta tutorial", "http://scalameta.org/tutorial/#Tokens")
135+
for more details.
136+
See @lnk("Wikipedia", "https://en.wikipedia.org/wiki/Lexical_analysis#Token")
137+
for a more general definition.
138+
139+
@sect{Tokens}
140+
@code{Tokens} is a list of @sect.ref{Token}.
141+
See @lnk("Scalameta tutorial", "http://scalameta.org/tutorial/#Tokens")
142+
143+
@sect{Tree}
144+
A @code{Tree} is a parsed abstract syntax tree.
145+
In the context of Scalafix, a @code{Tree} means the data structure
146+
@code{scala.meta.Tree}.
147+
See @lnk("Scalameta tutorial", "http://scalameta.org/tutorial/#Trees")
148+
for more details.
149+
See @lnk("Wikipedia", "https://en.wikipedia.org/wiki/Abstract_syntax_tree")
150+
for a more general definition.
151+
152+
@sect{Syntactic}
153+
A @sect.ref{Rewrite} is "syntactic" when it does not require information
154+
from type-checking such as resolved names (@code{println} => @code{scala.Predef.println}),
155+
types or terms, or inferred implicit arguments.
156+
A syntactic rewrite can use @sect.ref{Tokens} and @sect.ref{Tree}, but
157+
not @sect.ref{Mirror}.
158+
159+
@sect{Semantic}
160+
A @sect.ref{Rewrite} is "semantic" if it requires information from the compiler
161+
such as types, symbols and reported compiler messages.
162+
A semantic rewrite can use a @sect.ref{Mirror}.
163+
164+
@sect{Mirror}
165+
A mirror is a Scalameta concept that encapsulates a compilation context, providing
166+
capabilities to perform semantic operations for @sect.ref{Semantic} rewrites.
167+
To learn more about mirrors and its associated concepts (Symbol,
168+
Denotation, ...), see the
169+
@lnk("Scalameta tutorial", "http://scalameta.org/tutorial/#Mirror").
170+
171+
@scalatex.Testkit()
172+
173+
@sect{Sharing your rewrite}
174+
@p
175+
You have implemented a rewrite, you have tests, it works,
176+
and now you want to share it with the world. Congrats!
177+
There are several ways to share a rewrite if the rewrite is contained in
178+
a single file and uses no external dependencies,
179+
@ul
180+
@li
181+
If you used @sect.ref{scalacenter/scalafix.g8} to build your project,
182+
push your rewrite to github and tell users to run
183+
@code{scalafix github:org/$reponame/$version}.
184+
@li
185+
otherwise, tell users to use the @sect.ref{http:} protocol,
186+
@code{scalafix --rewrites https://gist....} where the url
187+
points to the plaintext contents of your rewrite.
188+
189+
If your rewrite uses a custom library, then it's a bit tricky
190+
to share it. See @issue(201) for more updates.

Diff for: readme/Readme.scalatex

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
@scalatex.Installation()
77
@scalatex.Configuration()
88
@scalatex.Rewrites()
9-
@scalatex.Testkit()
9+
@scalatex.ImplementingRewrites()
1010
@scalatex.Faq()
1111
@scalatex.Changelog()
1212
@scalatex.Footer()

Diff for: readme/Testkit.scalatex

+6-15
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,9 @@
1010
semantic scalafix rewrites.
1111

1212
@p
13-
@b{Note.} Scalafix-testkit is a new module under active development
14-
will be included in the next 0.4 release. The following instructions
15-
may be incomplete. For any questions, don't hesitate to ask on @gitter
16-
17-
18-
@p
19-
To use scalafix-testkit
20-
21-
@hl.scala
22-
@bintrayRepo
23-
libraryDependencies += "ch.epfl.scala" % "scalafix-testkit" % "@V.stable" % Test cross CrossVersion.full
13+
Note. You may prefer to use the @sect.ref{scalacenter/scalafix.g8} template
14+
to generate the following boilerplate.
15+
In case of any problems, don't hestitate to ask on @gitter.
2416

2517
@p
2618
The following instructions assume you are using sbt.
@@ -41,8 +33,7 @@
4133
.in(file("scalafix/tests"))
4234
.settings(
4335
libraryDependencies += "ch.epfl.scala" % "scalafix-testkit" % "@V.stable" % Test cross CrossVersion.full,
44-
buildInfoPackage := "scalafix.tests",
45-
buildInfoObject := "BuildInfo",
36+
buildInfoPackage := "myproject.scalafix.tests",
4637
buildInfoKeys := Seq[BuildInfoKey](
4738
"inputSourceroot" ->
4839
sourceDirectory.in(testsInput, Compile).value,
@@ -76,11 +67,11 @@
7667

7768
Specify scalafix configuration inside comment at top of file like this.
7869

79-
@hl.ref(wd/"scalafix-tests"/"input"/"src"/"main"/"scala"/"test"/"VolatileLazyVal.scala")
70+
@hl.ref(wd/"scalafix-tests"/"input"/"src"/"main"/"scala"/"test"/"ExplicitUnit.scala")
8071

8172
And then testkit checks if rewritten codes match the one in output.
8273

83-
@hl.ref(wd/"scalafix-tests"/"output"/"src"/"main"/"scala"/"test"/"VolatileLazyVal.scala")
74+
@hl.ref(wd/"scalafix-tests"/"output"/"src"/"main"/"scala"/"test"/"ExplicitUnit.scala")
8475

8576
@p
8677
For a full working example, see the

Diff for: readme/src/main/scala/scalafix/Readme.scala

-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ object Readme {
1616
"""<a href="https://gitter.im/scalacenter/scalafix?utm_source=badge&amp;utm_medium=badge&amp;utm_campaign=pr-badge&amp;utm_content=badge"><img src="https://camo.githubusercontent.com/382ebf95f5b4df9275ac203229928db8c8fd5c50/68747470733a2f2f6261646765732e6769747465722e696d2f6f6c6166757270672f7363616c61666d742e737667" alt="Join the chat at https://gitter.im/scalacenter/scalafix" data-canonical-src="https://badges.gitter.im/scalacenter/scalafix.svg" style="max-width:100%;"></a>""")
1717
def bintrayRepo =
1818
"""resolvers += Resolver.bintrayRepo("scalameta", "maven")"""
19-
2019
def github: String = "https://github.com"
2120
def repo: String = "https://github.com/scalacenter/scalafix"
2221
def metaRepo: String = "https://github.com/scalameta/scalameta"

Diff for: scalafix-reflect/src/main/scala/scalafix/reflect/ScalafixCompilerDecoder.scala

+12-12
Original file line numberDiff line numberDiff line change
@@ -41,24 +41,24 @@ object ScalafixCompilerDecoder {
4141
private[this] val GitHubShorthandWithSha =
4242
"""github:([^\/]+)\/([^\/]+)\/([^\/]+)\?sha=(.+)""".r
4343

44-
private[this] def normalizedPackageName(repoName: String): String = {
45-
val packageName = repoName.replaceAll("[^a-zA-Z0-9]", "_").toLowerCase
46-
if (packageName.headOption.map(_.isDigit) == Some(true)) {
47-
s"_$packageName"
48-
} else {
49-
packageName
50-
}
51-
}
44+
private[this] val alphanumerical = "[^a-zA-Z0-9]"
45+
46+
// approximates the "format=Camel" formatter in giter8.
47+
// http://www.foundweekends.org/giter8/Combined+Pages.html#Formatting+template+fields
48+
private[this] def CamelCase(string: String): String =
49+
string.split(alphanumerical).map(_.capitalize).mkString
50+
51+
// approximates the "format=Snake" formatter in giter8.
52+
private[this] def SnakeCase(string: String): String =
53+
string.split(alphanumerical).map(_.toLowerCase).mkString("_")
5254

5355
private[this] def expandGitHubURL(org: String,
5456
repo: String,
5557
version: String,
5658
sha: String): URL = {
57-
val normVersion = version.replaceAll("[^\\d]", "_")
58-
val packageName = normalizedPackageName(repo)
59-
val fileName = s"${packageName.capitalize}_$normVersion.scala"
59+
val fileName = s"${CamelCase(repo)}_${SnakeCase(version)}.scala"
6060
new URL(
61-
s"https://github.com/$org/$repo/blob/$sha/scalafix-rewrites/src/main/scala/$packageName/scalafix/$fileName")
61+
s"https://github.com/$org/$repo/blob/$sha/scalafix/rewrites/src/main/scala/fix/$fileName")
6262
}
6363

6464
def unapply(arg: Conf.Str): Option[URL] = arg.value match {

Diff for: scalafix-tests/unit/src/test/scala/scalafix/tests/GitHubUrlRewriteSuite.scala

+33-28
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,42 @@ package scalafix
22
package tests
33

44
import scalafix.reflect.ScalafixCompilerDecoder.GitHubUrlRewrite
5-
6-
import org.scalatest.{FunSuiteLike, Matchers}
75
import metaconfig.Conf
6+
import org.scalatest.FunSuite
87

9-
class GitHubUrlRewriteSuite extends FunSuiteLike with Matchers {
10-
test("it expands a GitHub shorthand with a default sha") {
11-
Conf.Str("github:someorg/somerepo/1.2.3") match {
12-
case GitHubUrlRewrite(url) =>
13-
url.toString shouldBe "https://github.com/someorg/somerepo/blob/master/scalafix-rewrites/src/main/scala/somerepo/scalafix/Somerepo_1_2_3.scala"
8+
class GitHubUrlRewriteSuite extends FunSuite {
9+
def check(original: String, expected: String): Unit = {
10+
test(original) {
11+
Conf.Str(original) match {
12+
case GitHubUrlRewrite(obtained) =>
13+
assert(obtained.toString == expected)
14+
}
1415
}
1516
}
1617

17-
test("it expands a GitHub shorthand with a specific sha") {
18-
Conf.Str("github:someorg/somerepo/1.2.3?sha=master~1") match {
19-
case GitHubUrlRewrite(url) =>
20-
url.toString shouldBe "https://github.com/someorg/somerepo/blob/master~1/scalafix-rewrites/src/main/scala/somerepo/scalafix/Somerepo_1_2_3.scala"
21-
}
22-
}
23-
24-
test("it replaces invalid characters in package name and file name with _") {
25-
Conf.Str("github:someorg/some-repo/1.2.3") match {
26-
case GitHubUrlRewrite(url) =>
27-
url.toString shouldBe "https://github.com/someorg/some-repo/blob/master/scalafix-rewrites/src/main/scala/some_repo/scalafix/Some_repo_1_2_3.scala"
28-
}
29-
}
30-
31-
test(
32-
"it adds an underscore to the package name and to the file name if the repo name begins with a digit") {
33-
Conf.Str("github:someorg/42some-repo/1.2.3") match {
34-
case GitHubUrlRewrite(url) =>
35-
url.toString shouldBe "https://github.com/someorg/42some-repo/blob/master/scalafix-rewrites/src/main/scala/_42some_repo/scalafix/_42some_repo_1_2_3.scala"
36-
}
37-
}
18+
check(
19+
"github:someorg/somerepo/1.2.3",
20+
"https://github.com/someorg/somerepo/blob/master/scalafix/rewrites/" +
21+
"src/main/scala/fix/Somerepo_1_2_3.scala"
22+
)
23+
check(
24+
"github:someorg/somerepo/1.2.3?sha=master~1",
25+
"https://github.com/someorg/somerepo/blob/master~1/scalafix/rewrites/" +
26+
"src/main/scala/fix/Somerepo_1_2_3.scala"
27+
)
28+
check(
29+
"github:someorg/some-repo/1.2.3",
30+
"https://github.com/someorg/some-repo/blob/master/scalafix/rewrites/" +
31+
"src/main/scala/fix/SomeRepo_1_2_3.scala"
32+
)
33+
check(
34+
"github:someorg/42some-repo/1.2.3",
35+
"https://github.com/someorg/42some-repo/blob/master/scalafix/rewrites/" +
36+
// NOTE: identifiers can't start with numbers like 42. However,
37+
// giter8 doesn't support adding a prefix in case the first character
38+
// is a number: http://www.foundweekends.org/giter8/Combined+Pages.html#Formatting+template+fields
39+
// The rewrite inside the file can still be renamed to _42SomeRepo
40+
// without problem.
41+
"src/main/scala/fix/42someRepo_1_2_3.scala"
42+
)
3843
}

0 commit comments

Comments
 (0)