scalafmtのはじめかた

この記事は [ウェブクルーAdvent Calendar 2020](https://qiita.com/advent-calendar/2020/webcrew) 11日目の記事です。
昨日は @tomoyuki-matsuda さんの[エンジニア採用の知識０から採用担当として　今までやってきたこと](https://qiita.com/tomoyuki-matsuda/items/76dff7602632fa1d3c24)でした。

## scalafmtとは
- scalaのソースコード整形ツール(類似ツール:prettierやrustfmtなど)
- intelliJやVSCodeなどのeditorやsbtなどから使用できる

[公式ドキュメント](https://scalameta.org/scalafmt/docs/installation.html)


## 導入方法
### intelliJの場合
- .scalafmt.confをprojectディレクトリ下に配置してポップアップで出てくるUse scalafmt formatterをクリックする
- 保存時に自動整形するにはPreferences > Editor > Code Style > ScalaからReformat on saveをチェックする

### VSCodeの場合
-  Language serverとしてMetalsを使用し.scalafmt.confを書き、settigsのScalafmt config pathにパスを入力する

### sbtの場合
- plugins.sbtに以下を追加

```plugin.sbt
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.0")
```
- sbt scalafmtAllなどで全コードを整形できる
- build.sbtにscalafmtOnCompile = trueでコンパイル時フォーマットされるが公式非推奨(undoのバッファーがぐちゃったり、コンパイル時間に影響するのでエディターの保存時フォーマットを使った方がよいらしい)

## .scalafmt.confの書き方

### とりあえずの推奨設定

```.scalafmt.conf
project.git = true 
project.excludeFilters = ["app/hoge"] 
maxColumn = 120 //初期設定だとmaxColumn=80

```

project.gitはトラッキングされているファイルのみフォーマット
project.excludeFiltersは指定したパス以下のファイルのフォーマットを無視するもの
自動生成されたコードを除いたりするための初期設定。
maxColumnは一行の上限文字数でデフォルトの80はgithub mobileやノートPCの画面にフィットするらしい
長いと後述するverticalMultilineで設定でフォーマットされる

### 自分がしている設定
```.scalafmt.conf
continuationIndent {
  callSite = 2
  defnSite = 2
  extendSite = 2
}
align = some
align {
  tokens.add = [
    { code = "=", owner = "Term.Assign" }
  ]
}
newlines {
  alwaysBeforeTopLevelStatements = true
}
verticalMultiline {
  atDefnSite = true
  arityThreshold = 2
  newlineAfterOpenParen = true
  newlineBeforeImplicitKW = true
  newlineAfterImplicitKW = true
  excludeDanglingParens = []
}
includeCurlyBraceInSelectChains = false
includeNoParensInSelectChains = false
```

- continuationIndent.callSiteは呼び出し時の引数のインデント

```.scala
function(
  arg1, // 2文字分のインデント
  arg2
)
```

- continuationIndent.defnSiteは定義時の引数インデント

```.scala
def function(
  argument1: Type1 // 2文字分のインデント
): ReturnType
```

- continuationIndent.extendSiteはextendsのインデント  
書いていないがcontinuationIndent.withSiteRelativeToExtendsでwithのインデントを設定できextends A with B のインデントはデフォルトではextendsに揃えられる

```.scala
trait Foo
  extends A
  with B
```

- align = none|some|more|mostは=や=>などを揃えられる。someの場合はcase arrowのみ揃えられる

```.scala
x match { 
  case 2  => 22
  case 22 => 222
}
```

- align.tokensは設定した字句について揃えてくれる。  
例えば`align.tokens = [{code = "=>", owner = "Case"}]`はCase文内の"=>"を揃えてくれる  
初期設定は[AlignToken.scala](https://github.com/scalameta/scalafmt/blob/master/scalafmt-core/shared/src/main/scala/org/scalafmt/config/AlignToken.scala)のdefaultを参照。
ownerは[scalameta](https://scalameta.org/docs/trees/quasiquotes.html)を見るとよい
- align.tokens.addはdefaultのtokenなどにたいして別ルールを追加するときに使われる  
置き換えをしたいときにはalign.tokensを宣言しなおすのがよい  
align.tokens系は細かい設定をしたいとき以外に使わなくて良さそう
- newlines.topLevelStatements = [after]はobjectなどのtoplevelStatementと呼ばれるものの後に(beforeなら前に)空行を入れる設定

```.scala
import org.scalafmt
package core {
  class C1 {} //beforeだとこの後に空行が入る
  object O {
    val x1 = 1
    val x2 = 2
    def A = "A"
    def B = "B"
  }
  // ここの空行がフォーマット後に入る
  class C2 {}
}
```
- verticalMultiline.arityThresholdはこれを超えた文字数の行がvertilacMultilineで設定されたフォーマットされる
- verticalMultiline.atDefnSiteはfalseだとvertivalMultilineの設定でフォーマットしない(多分)
- verticalMultiline.newlineAfterOpenParenはdef x()()の引数を改行する(多分)
- vertical.newlineBeforeImplicitKW,vertical.newlineAfterImplicitKWは(implicit xxx)のxxxの前後に空行の有無の設定
- vertical.excludeDanglingParens = []は"trait"や"class"を設定するとclass()の")"などを改行しないようになる。デフォルトでは"trait"と"class"が設定されているので")"を改行したい場合は[]を設定する必要がある

```.scala
class X(
  A:Type1,
  B:Type2 //vertical.excludeDanglingParens=["class"]だと ")"がここに配置される
)
```

- includeCurlyBraceInSelectChainsはmapなどでの中括弧で改行するかを設定する

```.scala
//true 
List(1)
  .map { x =>
    x + 2
  }
  .filter(_ > 2)

//false 
List(1).map { x =>
  x + 2
}
  .filter(_ > 2)
```

- includeNoParensInSelectChainsは()がないapplyなどで改行するかを設定できる

```.scala 
//true
List(1)
  .toIterator
  .buffered
  .map(_ + 2)
  .filter(_ > 2)

//false
List(1).toIterator.buffered
  .map(_ + 2)
  .filter(_ > 2)
```

このほかにもcommentやspace,書き換えなど(point free記法やimport a.{b,c}を分割)がある
[公式doc](https://scalameta.org/scalafmt/docs/configuration.html)

## Github Actionsによる整形
githubでコードの差分が出ないようにPR時にGithub Actionでコードフォーマットをしてみます。
javascript+prettierでのコードフォーマットを参考にして実装してみます。 
[GitHub にコード整形してもらおう - GitHub Actions でコード整形&コミット](https://qiita.com/Ouvill/items/7b6df0e9b981093b330f)

```scalafmt.yml
name: Scala PR format CI
on:
  pull_request:
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
      with:
          ref: ${{ github.head_ref }}
    - name: Set up JDK 1.8
      uses: actions/setup-java@v1
      with:
        java-version: 1.8
    - name: Run sbt format
      run: sbt scalafmtAll
    - uses: stefanzweifel/git-auto-commit-action@v3.0.0
      with:
        commit_message: Apply scalafmt Change
        ref: ${{ github.head_ref }}  
```

上記を.github/workflow以下に配置するとpull_request作成時やpull_request作成後push時にjobが走って整形後の差分をコミットしてくれます。

## まとめ
scalafmtを使ってある程度自分の好みに合わせてコードフォーマッティングができるようになります。
またローカル環境毎に異なったscalafmt.confを使っていてもGithub Actionsで差分を減らすことができます。

Webcrew2020アドベントカレンダー12日目担当は@wc-kobayashiTさんです。