Skip to content

Commit 6030008

Browse files
committed
scalafix rule for Future.traverse
0 parents  commit 6030008

File tree

11 files changed

+191
-0
lines changed

11 files changed

+191
-0
lines changed

.gitignore

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
target/
2+
bin/
3+
lib_managed/
4+
src_managed/
5+
.settings/
6+
.classpath
7+
.project
8+
.cache
9+
*.class
10+
*.log
11+
*.iml
12+
.idea
13+
.idea_modules
14+
.bloop
15+
.metals
16+
metals.sbt
17+
.vscode
18+
.bsp

.scalafmt.conf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
version = "3.7.15"
2+
runner.dialect = scala213

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Iterable Scalafix rules
2+
3+
Repository of custom scalafix rules for Iterable projects.
4+
5+
## Usage
6+
7+
This library is not published anywhere yet, so you'll first need to build and publish locally with:
8+
```
9+
sbt publishLocal
10+
```
11+
Then you can reference the published library version in your project.
12+
13+
For example, you can run version `0.1.0` of `NoFutureTraverse` dynamically in the `sbt` console:
14+
15+
```
16+
scalafix dependency:[email protected]::scalafix-rules:0.1.0
17+
```
18+
19+
Replace the version `0.1.0` with the version you published.
20+
21+
To run the rules by default with `scalafix` or `scalafixAll`, you'll need to add a library dependency in `build.sbt`:
22+
23+
```scala
24+
ThisBuild / libraryDependencies += "com.iterable" %% "scalafix-rules" % "0.1.0" % ScalafixConfig
25+
```
26+
27+
Then you can add the rules in your `.scalafix.conf` file, e.g.:
28+
29+
```hocon
30+
rules = [
31+
# ...
32+
NoFutureTraverse # validate that Future.traverse is not used
33+
]
34+
35+
# Configuration for the NoFutureTraverse rule
36+
NoFutureTraverse {
37+
isError = true
38+
}
39+
```
40+
41+

build.sbt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
lazy val V = _root_.scalafix.sbt.BuildInfo
2+
inThisBuild(
3+
List(
4+
scalaVersion := V.scala213,
5+
crossScalaVersions := List(V.scala213),
6+
addCompilerPlugin(scalafixSemanticdb),
7+
scalacOptions += "-Yrangepos",
8+
organization := "com.iterable",
9+
homepage := Some(url("https://github.com/iterable/scalafix-rules")),
10+
licenses := List(
11+
"Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")
12+
),
13+
developers := List()
14+
)
15+
)
16+
17+
publish / skip := true
18+
19+
lazy val rules = project.settings(
20+
moduleName := "scalafix-rules",
21+
libraryDependencies += "ch.epfl.scala" %% "scalafix-core" % V.scalafixVersion
22+
)
23+
24+
lazy val input = project
25+
.settings(publish / skip := true)
26+
27+
lazy val output = project
28+
.settings(publish / skip := true)
29+
30+
lazy val tests = project
31+
.settings(
32+
libraryDependencies += "ch.epfl.scala" % "scalafix-testkit" % V.scalafixVersion % Test cross CrossVersion.full,
33+
scalafixTestkitOutputSourceDirectories :=
34+
(output / Compile / sourceDirectories).value,
35+
scalafixTestkitInputSourceDirectories :=
36+
(input / Compile / sourceDirectories).value,
37+
scalafixTestkitInputClasspath :=
38+
(input / Compile / fullClasspath).value
39+
)
40+
.settings(publish / skip := true)
41+
.dependsOn(input, rules)
42+
.enablePlugins(ScalafixTestkitPlugin)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
rule = NoFutureTraverse
3+
*/
4+
package test
5+
6+
import scala.concurrent.Future
7+
import scala.concurrent.Await
8+
import scala.concurrent.ExecutionContext.Implicits.global
9+
import scala.concurrent.duration._
10+
11+
object FutureSequenceTest {
12+
val futures = List(Future { "hello" }, Future { "world" })
13+
val result = Future.sequence(futures).map(_.mkString) // assert: NoFutureTraverse
14+
println(Await.result(result, 1.second))
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
rule = NoFutureTraverse
3+
*/
4+
package test
5+
6+
import scala.concurrent.Future
7+
import scala.concurrent.Await
8+
import scala.concurrent.ExecutionContext.Implicits.global
9+
import scala.concurrent.duration._
10+
11+
object FutureTraverseTest {
12+
val items = List("hello", "world")
13+
val result = Future.traverse(items)(item => Future(item.length)).map(_.sum) // assert: NoFutureTraverse
14+
println(Await.result(result, 1.second))
15+
}

project/build.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
sbt.version=1.10.6

project/plugins.sbt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.9.2")
2+
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.13.0")
3+
dependencyOverrides += "ch.epfl.scala" % "scalafix-interfaces" % "0.13.0"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fix.NoFutureTraverse
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package fix
2+
3+
import metaconfig.ConfDecoder
4+
import metaconfig.Configured
5+
import metaconfig.generic.Surface
6+
import scala.meta._
7+
import scalafix.lint._
8+
import scalafix.v1._
9+
10+
case class NoFutureTraverseConfig(
11+
isError: Boolean = false
12+
)
13+
object NoFutureTraverseConfig {
14+
implicit val surface: Surface[NoFutureTraverseConfig] =
15+
metaconfig.generic.deriveSurface
16+
implicit val decoder: ConfDecoder[NoFutureTraverseConfig] =
17+
metaconfig.generic.deriveDecoder(NoFutureTraverseConfig())
18+
}
19+
20+
class NoFutureTraverse(config: NoFutureTraverseConfig) extends SemanticRule("NoFutureTraverse") {
21+
22+
case class Deprecation(position: Position, name: String) extends Diagnostic {
23+
override def message = s"$name is deprecated"
24+
override def severity = if (config.isError) LintSeverity.Error else LintSeverity.Warning
25+
}
26+
27+
def this() = this(NoFutureTraverseConfig())
28+
29+
override def withConfiguration(config: Configuration): Configured[Rule] =
30+
config.conf
31+
.getOrElse("NoFutureTraverse")(this.config)
32+
.map(c => new NoFutureTraverse(c))
33+
34+
val futureTraverse = SymbolMatcher.normalized("scala.concurrent.Future.traverse")
35+
val futureSequence = SymbolMatcher.normalized("scala.concurrent.Future.sequence")
36+
37+
override def fix(implicit doc: SemanticDocument): Patch = {
38+
doc.tree.collect {
39+
case t @ Term.Apply(futureTraverse(_), _) =>
40+
Patch.lint(Deprecation(t.pos, "Future.traverse"))
41+
case t @ Term.Apply(futureSequence(_), _) =>
42+
Patch.lint(Deprecation(t.pos, "Future.sequence"))
43+
}.asPatch
44+
}
45+
}

0 commit comments

Comments
 (0)