2020.6.16 Taisuke Oe
前準備
//project/plugins.sbt
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.17")
//build.sbt
ThisBuild / semanticdbEnabled := true
ThisBuild / semanticdbVersion := scalafixSemanticdb.revision
.scalafix.conf を利用
// Lint
sbt:myProject> scalafix --check
// Rewrite
sbt:myProject> scalafix
引数でRuleを指定
//Rewrite
sbt:myProject> scalafix RemoveUnused
.scalafix.conf の例
//.scalafix.conf
rules = [
DisableSyntax
ProcedureSyntax
RemoveUnused
]
DisableSyntax.keywords = [
return
]
//src/main/scala/Sample.scala
import scala.util.Try
class Sample {
private val x = 1
def run() {
return
}
}
Q. 間違い?探し
このScalaのスニペットには、変更した方がいいところが幾つかあります。
分かりますか?
//src/main/scala/Sample.scala
import scala.util.Try
class Sample {
private val x = 1
def run() {
return
}
}
A. 回答
//.scalafix.conf
rules = [
DisableSyntax
ProcedureSyntax
RemoveUnused
]
DisableSyntax.keywords = [
return
]
以下をカバーする.scalafix.conf
[error] /Users/taisukeoe/Workspace/ScalafixSandbox/src/main/scala/Sample.scala:5:5: error: [DisableSyntax.return] return should be avoided, consider using if/else instead
[error] return
[error] ^^^^^^
[error] /Users/taisukeoe/Workspace/ScalafixSandbox/src/main/scala/Sample.scala:6:5: error: [DisableSyntax.return] return should be avoided, consider using if/else instead
[error] return
[error] ^^^^^^
--- /Users/taisukeoe/Workspace/ScalafixSandbox/src/main/scala/Sample.scala
+++ <expected fix>
@@ -1,8 +1,8 @@ / Compile / scalafix 1s
-import scala.util.Try
+
class Sample {
- private val x = 1
- def run() {
+
+ def run(): Unit = {
return
}
}
sbt:myProject> scalafix --check
--- /Users/taisukeoe/Workspace/ScalafixSandbox/src/main/scala/Sample.scala
+++ <expected fix>
@@ -1,8 +1,8 @@
-import scala.util.Try
+ | => refactor / Compile / scalafix 0s
class Sample {
- private val x = 1
- def run() {
+
+ def run(): Unit = {
return //scalafix:ok
}
}
[error] (refactor / Compile / scalafix) scalafix.sbt.ScalafixFailed: TestError
[error] Total time: 1 s, completed 2020/03/23 17:32:04
[error] /Users/taisukeoe/Workspace/ScalafixSandbox/src/main/scala/Sample.scala:6:5: error: [DisableSyntax.return] return should be avoided, consider using if/else instead
[error] return
[error] ^^^^^^
[error] (refactor / Compile / scalafix) scalafix.sbt.ScalafixFailed: LinterError
[error] Total time: 6 s, completed 2020/03/23 4:47:51
sbt:myProject> scalafix
// src/main/scala/Sample.scala
class Sample {
def run(): Unit = {
return
}
}
scalacenter/scalafix に含まれないRuleは、なんらかの方法で指定する必要がある
ThisBuild / scalafixDependencies += "com.github.vovapolu" %% "scaluzzi" % "0.1.8"
ThisBuild / scalafixDependencies += "com.github.liancheng" %% "organize-imports" % "0.3.1-RC2"
ThisBuild / scalafixDependencies += "com.github.vovapolu" %% "scaluzzi" % "0.1.8"
Scala 2.x -> Scala3 migration guide
library
publishされているartifactを利用
sbt:myProject> scalafix dependency:RULE@GROUP:ARTIFACT:VERSION
sbt:myProject> scalafix github:ORG/REPO/FILENAME?sha=BRANCH
scala-steward repositoryに登録されているmigration用のrulesは、自動でバージョンアップとともにRewriteするPullRequestが飛んでくる(!!)
参考: scala-steward Scalafix Migrations
libraryのupdateには対応しているが、scalaVersionのupdateには対応していない
Ruleは大別して2種
のどちらかを継承する
abstract class SyntacticRule(name: RuleName) extends Rule(name) {
def fix(implicit doc: SyntacticDocument): Patch = Patch.empty
}
abstract class SemanticRule(name: RuleName) extends Rule(name) {
def fix(implicit doc: SemanticDocument): Patch = Patch.empty
}
それぞれのfixメソッドにおいて
abstract class SyntacticRule(name: RuleName) extends Rule(name) {
def fix(implicit doc: SyntacticDocument): Patch = Patch.empty
}
abstract class SemanticRule(name: RuleName) extends Rule(name) {
def fix(implicit doc: SemanticDocument): Patch = Patch.empty
}
構文解析とは、文法に従って、文字列に対して行う次のような操作のこと
構文解析とは、文法に従って、文字列に対して行う次のような操作のこと
scala> import scala.meta._
scala> val stat = """def run(){println("run")}""".parse[Stat].get
stat: scala.meta.Stat = def run(){println("run")}
scala> val tokens = stat.tokens
tokens: scala.meta.tokens.Tokens = Tokens(, def, , run, (, ), {, println, (, "run", ), }, )
構文解析とは、文法に従って、文字列に対して行う次のような操作のこと
scala> val astString = stat.structure
astString: String = Defn.Def(Nil, Term.Name("run"), Nil, List(List()), Some(Type.Name("Unit")), Term.Block(List(Term.Apply(Term.Name("println"), List(Lit.String("run"))))))
木構造(抽象構文木 / Abstract Syntax Tree)を作る
Defn.Def( //メソッド定義
Nil, //修飾子
Term.Name("run"), //メソッド名
Nil, //型パラメータ
List(List()), //引数リスト
Some(Type.Name("Unit")), //戻り値の型
Term.Block( //メソッドボディ
List( //文のリスト
Term.Apply(
Term.Name("println"), //printlnという項に対し
List(Lit.String("run")) //"run" というStringリテラルを適用している
)
)
)
)
戻り値(`decltpe`)の字句(token)を調べると.....
戻り値の型宣言があり、かつ型宣言のtokenは空、という扱い
scala> val returnTypeTokens = stat.asInstanceOf[Defn.Def].decltpe.map(_.tokens)
returnTypeTokens: Option[scala.meta.tokens.Tokens] = Some()
余談: 細かい仕様を覚える必要はないですが......
すなわち、ある文が「ProcedureSyntaxを使ったメソッド定義」であることは、構文だけ見れば確定できる
今回のケースでいえば、以下のことは分からない。
たとえば以下のメソッドの戻り値を補うRewriteは、Syntactic APIだけでは実現不可能
def run() = println("run")
よって戻り値の型を補うExplicitResultTypesは、SemanticRuleを継承して定義されています。
SemanticDBの利用
など
ポイントは2つ
//project/plugins.sbt
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.17")
//build.sbt
ThisBuild / semanticdbEnabled := true
ThisBuild / semanticdbVersion := scalafixSemanticdb.revision
// .sbtrc
// scalafixAll は次versionで追加予定なので、それまではこんな感じ
alias execScalafix=;scalafix;Test/scalafix
alias checkScalafix=;scalafix --check;Test/scalafix --check
// もし-Xfatal-warningsをどうしても使いたければ...
alias strictCompile=;set Compile/compile/scalacOptions += "-Xfatal-warnings";compile;session clear
alias strictRecompile=;set Compile/compile/scalacOptions += "-Xfatal-warnings";~compile
Good
Bad
// project/plugins.sbt
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.17")
// project/ScalafixSettings
object ScalafixSettings {
private val unused = "-Ywarn-unused"
lazy val permanent: Seq[Setting[_]] = Seq(
scalacOptions += unused,
scalafixDependencies in ThisBuild ++= Seq(
"com.github.vovapolu" %% "scaluzzi" % "0.1.8",
"com.github.liancheng" %% "organize-imports" % "0.3.1-RC2"
),
semanticdbEnabled := true,
semanticdbVersion := scalafixSemanticdb.revision
) ++ Seq(Compile, Test).map(_ / console / scalacOptions -= unused)
}
// build.sbt
lazy val myProject = (project in file("."))
.settings(/* ... */)
lazy val refactor = project
.shadow(myProject)
.modify(RemoveXFatalWarnings)
.settings(ScalafixSettings.permanent: _*)
.light
あるプロジェクトの設定を 微妙に 変えたコピーsub projectを簡単に作れるsbt-plugin。以下の欲求から作りました。
// project/plugins.sbt
addSbtPlugin("com.taisukeoe" % "sbt-shadowyproject" % "0.1.0")
Good
Bad
1. 常に有効 が、一番落とし穴がないのでオススメ
-Xfatal-warningsを使いたい、ある程度凝ったsbt設定の使い分けをしたい、メインのコンパイル時間に影響が出た場合などは、2.でやってます。
(余談) scalafixEnable sbtコマンドで一時的にscalafixを有効にする方法もある
設定切り替えの所要時間や面倒臭さで、scalafixを使う頻度が結局下がりやすいので、scalafixを日々の開発に積極的に取り組む際はオススメしません。