2020.3.23 Taisuke Oe
前準備
//project/plugins.sbt
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.12")//build.sbt
scalacOptions ++= Seq("-Yrangepos", "-Ywarn-unused")
addCompilerPlugin(scalafixSemanticdb).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:51sbt:myProject> scalafix// src/main/scala/Sample.scala
class Sample {
  def run(): Unit = {
    return
  }
}scalacenter/scalafix に含まれないRuleは、なんらかの方法で指定する必要がある
scalafixDependencies in ThisBuild += "com.github.vovapolu" %% "scaluzzi" % "0.1.4"publishされているartifactを利用
sbt:myProject> scalafix dependency:RULE@GROUP:ARTIFACT:VERSIONsbt:myProject> scalafix github:ORG/REPO/FILENAME?sha=BRANCHscala-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の利用
など
SemanticDB生成設定をどうするか
3通りの方法を考えてみましょう
// project/plugins.sbt
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.12")
// build.sbt
scalacOptions ++= Seq("-Yrangepos", "-Ywarn-unused")
addCompilerPlugin(scalafixSemanticdb)
// .sbtrc
alias execScalafix=;scalafix;Test/scalafix
alias checkScalafix=;scalafix --check;Test/scalafix --check
alias strictCompile=;set Compile/compile/scalacOptions += "-Xfatal-warnings";compile
alias strictRecompile=;set Compile/compile/scalacOptions += "-Xfatal-warnings";~compileGood
Bad
// project/plugins.sbt
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.12")
// build.sbt
scalacOptions += "-Ywarn-unused"// .sbtrc
alias execScalafix=;scalafixEnable;scalafix;Test/scalafix
alias checkScalafix=;scalafixEnable;scalafix --check;Test/scalafix --check
alias strictCompile=;set Compile/compile/scalacOptions += "-Xfatal-warnings";compile
alias strictRecompile=;set Compile/compile/scalacOptions += "-Xfatal-warnings";~compile`scalafixEnable` は、SemanticDB系の設定を一時的に追加するsbtコマンド
Good
Bad
// project/plugins.sbt
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.12")
// project/ScalafixSettings
object ScalafixSettings {
  private val unused = "-Ywarn-unused"
  lazy val adhoc: Seq[Setting[_]] = Seq(
    scalacOptions += unused,
    //Add external Scalafix Rules here.
    scalafixDependencies in ThisBuild ++= Seq(
      "com.github.vovapolu" %% "scaluzzi" % "0.1.4",
      "com.nequissimus" %% "sort-imports" % "0.3.2"
    )
  ) ++ Seq(Compile, Test).map(_ / console / scalacOptions -= unused)
  lazy val permanentlyAdded: Seq[Setting[_]] =
    adhoc ++ Seq(
      scalacOptions += "-Yrangepos",
      addCompilerPlugin(scalafixSemanticdb)
    )
}// build.sbt
lazy val myProject = (project in file("."))
  .settings(common)
  .settings(ScalafixSettings.adhoc)
//For refactoring-purpose, semanticdb-scalac compiler plugin is always enabled
lazy val refactor = project
  .settings(common)
  .settings(
    for {
      cfg <- Seq(Compile, Test)
      ky <- Seq(
        sourceDirectory,
        resourceDirectory
      ) // You may need to include managed or unmanaged directories
    } yield cfg / ky := (myProject / cfg / ky).value
  )
  .settings(ScalafixSettings.permanentlyAdded)Good
Bad
私見だが、 1 > 3 >> 2の順で検討するのが良いと思う
リファクタリング目的で日常的に使うなら、sbt-scalafixがオススメです。