Skip to content

Commit 567ed59

Browse files
authored
Merge pull request #43 from Dwolla/downstream-rewrite-rule
2 parents 312219a + 10142a5 commit 567ed59

8 files changed

Lines changed: 179 additions & 0 deletions

File tree

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,34 @@ going back to how it used to `extend {Name}Service[Future]`.)
152152

153153
This Scalafix rule should be idempotent, so it can be rerun many times.
154154

155+
### `AdaptHigherKindedThriftCode`
156+
157+
Because the `AddCatsTaglessInstances` rewrite rule couldn't easily move the new `{Name}Service` trait up
158+
to the same level as the `{Name}Service` object, the new traits must be addressed differently. In other
159+
words, instead of finding the trait at `com.example.ThriftService`, it will now be
160+
at `com.example.ThriftService.ThriftService`.
161+
162+
The `AdaptHigherKindedThriftCode` rule exists to adapt existing code to the new location. It will
163+
find references to traits that extend `com.twitter.finagle.thrift.ThriftService` and have a type
164+
parameter of the correct shape, and add the object name before the trait name (i.e., rewriting
165+
`ThriftService` to `ThriftService.ThriftService` or `com.example.ThriftService` to
166+
`com.example.ThriftService.ThriftService`).
167+
168+
This rule is not idempotent, but it will typically only be executed once per codebase.
169+
170+
The order in which the rule is executed matters. Follow these steps:
171+
172+
1. Add Scalafix to your project by following steps 1 and 2 under "Scalafix Rule" above.
173+
2. Look at your project's sbt project graph. Because the rule is a semantic rule, it depends
174+
on the compiler being able to compile the code it will modify. This means the leaves of
175+
the project graph need to be updated before the nodes that depend on each leaf.
176+
177+
For example, run `Test/scalafix AdaptHigherKindedThriftCode` before
178+
running `Compile/scalafix AdaptHigherKindedThriftCode`.
179+
3. Only after running the `AdaptHigherKindedThriftCode` rule should you update the Scrooge
180+
and Finagle version being used in the project. Once this is updated, you can run the
181+
`AddCatsTaglessInstances` rule on the updated generated code.
182+
155183
## Artifacts
156184

157185
The Group ID for each artifact is `"com.dwolla"`. All artifacts are published to Maven Central.

build.sbt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ lazy val `scalafix-input` = (project in file("scalafix/input"))
252252
semanticdbEnabled := true,
253253
semanticdbVersion := scalafixSemanticdb.revision,
254254
)
255+
.dependsOn(`scalafix-input-dependency`)
255256
.disablePlugins(ScalafixPlugin)
256257

257258
lazy val `scalafix-output` = (project in file("scalafix/output"))
@@ -268,6 +269,7 @@ lazy val `scalafix-output` = (project in file("scalafix/output"))
268269
scalacOptions += "-nowarn",
269270
scalacOptions ~= { _.filterNot(_ == "-Xfatal-warnings") },
270271
)
272+
.dependsOn(`scalafix-output-dependency`)
271273
.disablePlugins(ScalafixPlugin)
272274

273275
lazy val `scalafix-tests` = (projectMatrix in file("scalafix/tests"))
@@ -284,6 +286,32 @@ lazy val `scalafix-tests` = (projectMatrix in file("scalafix/tests"))
284286
.dependsOn(`scalafix-rules`)
285287
.enablePlugins(ScalafixTestkitPlugin)
286288

289+
lazy val `scalafix-input-dependency` = (project in file("scalafix/input-dependency"))
290+
.settings(
291+
publish / skip := true,
292+
scalaVersion := Scala2Versions.head,
293+
libraryDependencies ++= {
294+
Seq(
295+
"com.twitter" %% "finagle-thrift" % TwitterUtilsLatestV,
296+
"org.typelevel" %% "cats-tagless-core" % CatsTaglessV,
297+
"org.typelevel" %% "cats-tagless-macros" % CatsTaglessV,
298+
)
299+
},
300+
)
301+
302+
lazy val `scalafix-output-dependency` = (project in file("scalafix/output-dependency"))
303+
.settings(
304+
publish / skip := true,
305+
scalaVersion := Scala2Versions.head,
306+
libraryDependencies ++= {
307+
Seq(
308+
"com.twitter" %% "util-core" % TwitterUtilsLatestV,
309+
"org.typelevel" %% "cats-tagless-core" % CatsTaglessV,
310+
"org.typelevel" %% "cats-tagless-macros" % CatsTaglessV,
311+
)
312+
},
313+
)
314+
287315
lazy val `async-utils-root` = (project in file("."))
288316
.aggregate(
289317
Seq(
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* this is a pared-down version of the code that used to be generated
3+
* by scrooge for a hypothetical "Foo" Thrift interface, containing
4+
* a single service named "Foo", with a method named "bar".
5+
*
6+
* It exists because we need the input for the AdaptHigherKindedThriftCode
7+
* rule to compile against the old structure. It's in a separate submodule
8+
* because we don't want any of our Scalafix rules to modify it, and we
9+
* don't want it to be available in this form when compiling the Scalafix
10+
* output module.
11+
*/
12+
13+
package example.foo
14+
15+
import com.twitter.util.Future
16+
17+
@javax.annotation.Generated(value = Array("com.twitter.scrooge.Compiler"))
18+
trait FooService[+MM[_]] extends _root_.com.twitter.finagle.thrift.ThriftService {
19+
def bar: MM[Unit]
20+
}
21+
22+
object FooService {
23+
trait MethodPerEndpoint extends FooService[Future]
24+
25+
implicit def FooServiceInReaderT[F[_]]: FooService[({type Λ0] = _root_.cats.data.ReaderT[F, FooService[F], β0]})#Λ] =
26+
_root_.cats.tagless.Derive.readerT[FooService, F]
27+
28+
implicit val FooServiceFunctorK: _root_.cats.tagless.FunctorK[FooService] = _root_.cats.tagless.Derive.functorK[FooService]
29+
30+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*rule = AdaptHigherKindedThriftCode*/
2+
package example.dwolla
3+
4+
import cats.tagless.FunctorK
5+
import example.foo.FooService
6+
7+
class UsesFooService[F[_]](val fooService: example.foo.FooService[F]) {
8+
implicitly[FunctorK[FooService]]
9+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* this is a pared-down version of the code that will be generated
3+
* by scrooge, and then modified by our AddCatsTaglessInstances rule,
4+
* for a hypothetical "Foo" Thrift interface, containing a single
5+
* service named "Foo", with a method named "bar".
6+
*/
7+
8+
package example.foo
9+
10+
import com.twitter.util.Future
11+
12+
object FooService {
13+
trait FooService[F[_]] {
14+
def bar: F[Unit]
15+
}
16+
17+
object FooService {
18+
implicit def FooServiceInReaderT[F[_]]: FooService[({type Λ0] = _root_.cats.data.ReaderT[F, FooService[F], β0]})#Λ] =
19+
_root_.cats.tagless.Derive.readerT[FooService, F]
20+
21+
implicit val FooServiceFunctorK: _root_.cats.tagless.FunctorK[FooService] = _root_.cats.tagless.Derive.functorK[FooService]
22+
23+
}
24+
25+
trait MethodPerEndpoint extends FooService[Future]
26+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package example.dwolla
2+
3+
import cats.tagless.FunctorK
4+
import example.foo.FooService
5+
6+
class UsesFooService[F[_]](val fooService: example.foo.FooService.FooService[F]) {
7+
implicitly[FunctorK[FooService.FooService]]
8+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
com.dwolla.scrooge.scalafix.AddCatsTaglessInstances
2+
com.dwolla.scrooge.scalafix.AdaptHigherKindedThriftCode
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.dwolla.scrooge.scalafix
2+
3+
import com.dwolla.scrooge.scalafix.AdaptHigherKindedThriftCode._
4+
import scalafix.v1
5+
import scalafix.v1._
6+
7+
import scala.meta._
8+
9+
class AdaptHigherKindedThriftCode extends SemanticRule("AdaptHigherKindedThriftCode") {
10+
override def fix(implicit doc: SemanticDocument): Patch =
11+
addObjectQualifierForThriftServiceTrait(doc.tree)
12+
}
13+
14+
object AdaptHigherKindedThriftCode {
15+
private def isGeneratedThriftService(t: Type.Name)
16+
(implicit doc: SemanticDocument): Boolean = {
17+
val info = t.symbol.info
18+
19+
val annotatedWithGenerated = info.exists(_.annotations.exists(_.tpe.toString() == "Generated"))
20+
21+
val anyRef = v1.Symbol("scala/AnyRef#")
22+
val thriftService = Symbol("com/twitter/finagle/thrift/ThriftService#")
23+
24+
val extendsThriftServiceAndIsHigherKinded =
25+
info
26+
.map(_.signature)
27+
.collect {
28+
29+
// class that extends AnyRef and com.twitter.finagle.thrift.ThriftService, and has a single type parameter
30+
// e.g. FooService[F[_]] extends com.twitter.finagle.thrift.ThriftService
31+
case ClassSignature(List(singleTypeParameter), List(TypeRef(NoType, `anyRef`, Nil), TypeRef(NoType, `thriftService`, Nil)), _, _) => singleTypeParameter.signature
32+
}
33+
.collect {
34+
// type parameter that has a single hole, e.g. F[_]
35+
case TypeSignature(List(_), _, _) => true
36+
}
37+
.getOrElse(false)
38+
39+
annotatedWithGenerated && extendsThriftServiceAndIsHigherKinded
40+
}
41+
42+
def addObjectQualifierForThriftServiceTrait(tree: Tree)
43+
(implicit doc: SemanticDocument): Patch =
44+
tree.collect {
45+
case t@Type.Name(name) if isGeneratedThriftService(t) =>
46+
Patch.addLeft(t, s"$name.")
47+
}
48+
.fold(Patch.empty)(_ + _)
49+
}

0 commit comments

Comments
 (0)