自作scalafix ruleの説明その6

以下の続き

Scalaアドベントカレンダー2025の記事です

https://qiita.com/advent-calendar/2025/scala

前回書いた時から増えたものについて解説。

既存のruleの改善、改良もしてるがその説明は省略。

https://github.com/xuwei-k/scalafix-rules/compare/v0.6.11...v0.6.19

以前と同様、特に言及がなければSyntacticRuleです。

CaseClassExplicitCopy

case classにcopyという名前での明示的なメソッド定義を禁止する。

case classはそもそもデフォルトでcopyメソッドが自動生成されるが、明示的に別のcopyを定義した場合に

  • 本来自動生成される方は無かったことになるのか?
  • シグネチャ的に衝突しないなら、両方維持されるのか?
  • コンパイル時にエラーか警告になるのか?

といった挙動が、Scala 2と3で微妙に異なり、2から3へのupdate時に問題になる可能性がある。

万が一Scala 2と3で挙動が同じだったとしても明らかに分かりづらいので、いずれにせよ禁止の方がいいと思われるので。

ConstructorPrivateVal

caseがついていない普通のclassにおいて、以下の2つはほとんど違いがない

class A(x: Int)
class A(private val x: Int)

正確には

  • private 明示した方は、もちろん private
  • 何も書いてない方は private[this] 相当(こちらの方がよりスコープが狭い)

となるので、微妙に違いがある。とはいえ、その違いが必要な場合はあまり多くない。

一応、implicitなvalue classを定義する場合は明示的に private val にしたいことはある。

よって、可能な範囲で「private valが必要なさそうだなぁ」と判断したら、それを消す。

消したほうが、記述も短く、スコープも厳密にはさらに小さくなって、大抵望ましいので。

DefImplicitParamToUsingParam

Scala 2切り捨ててScala 3だけにした場合にマイグレーションするためのrule。

defのimplicit paramをusingに変更する。

つまり

- def foo(a: Int)(implicit ec: ExecutionContext): Future[Int] 
+ def foo(a: Int)(using ec: ExecutionContext): Future[Int]

という書き換え。

全てのimplicit撲滅したい場合に、順番的に最初の方にやるもの

xuwei-k.hatenablog.com

EitherToTry

EitherにはtoTryというメソッドがある。それなのにわざわざmatch式で変換してる場合にtoTryに置き換えるだけのもの

https://github.com/scala/scala/blob/41f6cfcd4b05298c59fc375c27b21eaea8e09653/src/library/scala/util/Either.scala#L436

EnumUnnecessaryExtends

Scala 3のenumで、以下のように書いた場合

enum A {
  case B extends A
  case C extends A
}

これは省略して以下のように書いても同等になる

enum A {
  case B
  case C
}

「省略可能かどうか?」を判断して、それを書き換えてくれるもの。

「省略可能かどうか?」の詳細な条件は結構ややこしくて、完璧に判断するのは面倒なので、全てのパターンで完璧に消してくれるわけではなく、明らかにバグらずに消せる単純なパターンしか消さない。

FilterHeadOption

これはSemanticRule。

wartremoverにも同様のものがある。

Seqなどを

seq.filter(f).headOption

していたら、1つしか必要ないのに全部をfilterするのは基本的に無駄である。

(filterに渡した関数に、明示的に副作用がある場合を除く)

よってそれを

seq.find(f)

に書き換える。

ForeachEntry

これもSemanticRule。

これもwartremoverに同様のものがある。

Scala 2.13からMapにforeachEntryというメソッドがあり、それが使える場合に書き換えるもの。

ここからはforeachEntry自体の説明になるが、

Mapに対してforeach呼び出した場合は

Function1[(K, V), A]

を渡すことになる。

しかし、Mapというのは内部でkeyとvalueをTuple2で保持しているとは限らないので、foreachのためだけに一時的なTuple2が生成されることになる可能性がある。

foreachEntryはforeachとは微妙に異なり

Function2[K, V, A]

を受け取ることによって、余計なTuple2生成を避けたり、引数部分で

map.foreach { case (k, v) =>
map.foreachEntry { (k, v) =>

と、(それ以上のパターンマッチをしない場合) caseを省略出来るメリットがある。

SelfTypeNamePlaceholder

Scala 2では

trait A { _ : B =>

は許可されるが、Scala 3ではエラーになる。

よってScala 3準備のためにScala 2の状態でこれを検知して禁止するためのもの。

SortedMaxMin

これはSemanticRule。

以前ほぼ同じものをSyntacticRuleで書いたことがあったが、それを忘れてSemanticRuleでも書いてしまった。

SyntacticRuleとSemanticRuleそれぞれメリット、デメリットがあるので*1、両方存在するのは問題ないが、名前に整合性がなくて微妙なので、後で名前を変えるかもしれない。

TryToEither

EitherToTryの逆。Tryにも toEither メソッドがあるが、それを知らずに?match式で変換していた場合に toEither に書き換える。

https://github.com/scala/scala/blob/41f6cfcd4b05298c59fc375c27b21eaea8e09653/src/library/scala/util/Try.scala#L181

TypeProjection

Scala 3では一部のtype projectionが禁止になるので、そのパターンScala 2で検知したい場合あるよなぁ、やってみるか〜???

https://docs.scala-lang.org/scala3/reference/dropped-features/type-projection.html

と思って作ってみたが

Scala 3で禁止されるtype projectionかどうか?」

の正確な判断は思ったより難しくて、作ってみたはいいが、中途半端過ぎるので、使い道が微妙になってしまったrule。

WildcardType

Scala 3ではワイルドカードF[_] から F[?] になる。

その書き換えをするだけのもの。

これはscalafmtで強制可能なので、scalafmtを使っているなら、このscalafix ruleに頼らず、基本的にそれを使った方が良い。

これは

  • scalafmtを導入してないproject向け
  • scalafmt導入してるのだけどversionが低くて強制書き換えがまだ使えない
  • scalafmt導入済みだが、段階的に細かく書き換える都合上、他のものは一切書き換えずに、ワイルドカードだけを狙って書き換えたい

といった特定のパターンのみに使うことを想定している

*1:速さ、簡単に実行出来るか?と正確性のトレードオフ