scalafixのruleをまとめてライブラリにしてpublishした

本体にドキュメント書いてないので、とりあえず現時点のruleについてひたすら説明書いておきます

https://github.com/xuwei-k/scalafix-rules

AddExplicitImplicitTypes

https://github.com/xuwei-k/scalafix-rules/blob/v0.1.3/rules/src/main/scala/fix/AddExplicitImplicitTypes.scala

implicit val foo = new Foo

implicit val foo: Foo = new Foo

に書き換え。そもそもscalafix本体にもっと高機能なものがあるが、そちらはSemanticRuleで重たいため、SyntacticRuleで可能なごく単純なものだけ型をつけるもの。 これに限らず、基本的にSemanticRuleやった方が正確になる可能性があるが、重たいのであえてSyntacticRuleで頑張っているものばかりである。 以下特に言及がなければSyntacticRuleである。

CirceCodec

https://github.com/xuwei-k/scalafix-rules/blob/v0.1.3/rules/src/main/scala/fix/CirceCodec.scala

circeに @JsonCodec というmacro annotationがあるが、macro annotationはScala 3ではそのままでは動かないため、semiautoに書き換えるもの。

import io.circe.generic.JsonCodec

@JsonCodec
case class Foo(a: Int)

import io.circe.Codec
import io.circe.generic.semiauto.deriveCodec

case class Foo(a: Int)

object Foo {
  implicit val codec: Codec.AsObject[Foo] = deriveCodec[Foo]
}

になる。

型引数あるパターンはこれ書いてる時点で未対応だが、たぶん後で対応追加するかも。

対応しました

DirectoryAndPackageName

https://github.com/xuwei-k/scalafix-rules/blob/v0.1.3/rules/src/main/scala/fix/DirectoryAndPackageName.scala

現状では、書き換えは実行せずに警告出すだけ。 Javaのように、Scalaでも結局packageとdirectoryをそろえる慣習になることが多いが、それが食い違っていたら警告してくれる。

DuplicateWildcardImport

https://github.com/xuwei-k/scalafix-rules/blob/v0.1.3/rules/src/main/scala/fix/DuplicateWildcardImport.scala

import scala.util._
import scala.util.Success
import scala.util.Failure

のようなimportは、ごく一部の例外を除いてワイルドカードだけ残して

import scala.util._

と同等なので、そのように書き換えるもの。

ExplicitImplicitTypes

https://github.com/xuwei-k/scalafix-rules/blob/v0.1.3/rules/src/main/scala/fix/ExplicitImplicitTypes.scala

Scala 3に備えて、implicitなvalやdefに明示的な型が書いてないと警告するもの。 wartremoverにほぼ同等のものがあるが、こちらの方がSyntacticRuleなのでかるくて素早く実行出来るはず。

FileNameConsistent

https://github.com/xuwei-k/scalafix-rules/blob/v0.1.3/rules/src/main/scala/fix/FileNameConsistent.scala

DirectoryAndPackageNameと似たような感じで、class名とfile名が明らかに食い違っている場合に警告してくれる

LambdaParamParentheses

https://github.com/xuwei-k/scalafix-rules/blob/v0.1.3/rules/src/main/scala/fix/LambdaParamParentheses.scala

以下のようなScalaコードは、3から括弧をつけないといけないので、それを警告してくれるもの

def a: Int => Int = { x: Int => x } 

TODO: 以下のように書き換えするやつも作りたい

def a: Int => Int = { (x: Int) => x } 

NoElse

https://github.com/xuwei-k/scalafix-rules/blob/v0.1.3/rules/src/main/scala/fix/NoElse.scala

半分以上、ジョークで作ったもので、個人的にそこまで実用する予定はないが、 「elseがないifは副作用だぞ!」

みたいな話題をtwitterで見かけて、何となく作ったもの。 elseがないifを検知してくれる。

ObjectSelfType

https://github.com/xuwei-k/scalafix-rules/blob/v0.1.3/rules/src/main/scala/fix/ObjectSelfType.scala

Scala 2ではなぜかobjectに以下のように書けるが、これは有効活用できる場面が皆無?で、Scala 3ではエラーなので、それを警告する。

object A { self: B => } 

ただの object A{ self => というalias的な使い方なら、意味がある場合があるので、それは良い。あくまで型が書いてある場合。

RemoveEmptyObject

中身が空(継承もしてない)、なobjectを消す

https://github.com/xuwei-k/scalafix-rules/blob/v0.1.3/rules/src/main/scala/fix/RemoveEmptyObject.scala

RemovePureEff

https://github.com/xuwei-k/scalafix-rules/blob/v0.1.3/rules/src/main/scala/fix/RemovePureEff.scala

現状ではEffのライブライに特化していて、後で任意のMonadのpointに汎用化したいのだが、以下の時に、最初以外のやつはそのまま = すればいいので、その書き換えを行う

  for {
    a1 <- 1.pureEff[R]
    a2 <- 2.pureEff[R]
    a3 = 3.pureEff[R]
    a4 <- 4.pureEff[R]
  } yield (a1, a2, a3, a4)
  for {
    a1 <- 1.pureEff[R]
    a2 = 2
    a3 = 3.pureEff[R]
    a4 = 4
  } yield (a1, a2, a3, a4)

https://github.com/atnos-org/eff/blob/765709458b03d9d8dda65f81441d295e3475a626/shared/src/main/scala/org/atnos/eff/syntax/eff.scala#L69

RemoveSamePackageImport

https://github.com/xuwei-k/scalafix-rules/blob/v0.1.3/rules/src/main/scala/fix/RemoveSamePackageImport.scala

同じpackageのclassなどは通常importしなくても参照可能なので、そのimportを消す。

(もちろん例外的なパターンでは意味がある場合があるから、普通のscalafixのやつでは消してくれない?っぽいが)

package xxx.yyy

import xxx.yyy.ZZZ // このimport必要なくない?

Scala3ImportRewrite

https://github.com/xuwei-k/scalafix-rules/blob/v0.1.3/rules/src/main/scala/fix/Scala3ImportRewrite.scala

Scala 3に合わせて、ワイルドカードimport a.b._ から import a.b.* に変える

TODO: 逆も作りたい?

Scala3ImportWarn

Scala3ImportRewriteと同等で、書き換えせずに警告だけ出す版

https://github.com/xuwei-k/scalafix-rules/blob/v0.1.3/rules/src/main/scala/fix/Scala3ImportWarn.scala

Scala3Placeholder

https://github.com/xuwei-k/scalafix-rules/blob/v0.1.3/rules/src/main/scala/fix/Scala3Placeholder.scala

Scala 3ではplaceholderがJavaと同じように Class[?] としていきたいらしい?ので Class[_]Class[?] に書き換える

ScalaApp

scala.App は、結局Scala 3で実質非推奨なので、明示的な def main(args: Array[String]): Unit = { に書き換えるもの。

何をmainの中に残して、何を外に出すか?が難しく、割と壊れやすいので改善したい

https://github.com/xuwei-k/scalafix-rules/blob/v0.1.3/rules/src/main/scala/fix/ScalaApp.scala

ちなみに、それの検知くんは別途wartremover内部に入れました

https://docs.scala-lang.org/scala3/book/methods-main-methods.html https://github.com/wartremover/wartremover/blob/ff013fca7fbeabf527fc7451f8e8df87578716c7/core/src/main/scala/wartremover/warts/ScalaApp.scala

ScalazEitherInfix

https://github.com/xuwei-k/scalafix-rules/blob/v0.1.3/rules/src/main/scala/fix/ScalazEitherInfix.scala

scalazの \/[A, B] という記述を A \/ B に変えるだけのもの。

これは当初SemanticRuleで書いたので、今はそのままだが、別に \/ というclass名は他にないだろうし、SyntacticRuleで良い気がする。 あと、もっと汎用化したい・・・? というか、よく考えたら別にscalafmtとかでいけた気もする・・・?が、作ったのでとりあえず置いておく。

SimplifyForYield

https://github.com/xuwei-k/scalafix-rules/blob/v0.1.3/rules/src/main/scala/fix/SimplifyForYield.scala

for {
  res <- foo.bar
} yield res

というのは、通常、単に foo.bar に書き換え可能でfor式いらないので、その書き換えを行う。

ThrowableToNonFatal

https://github.com/xuwei-k/scalafix-rules/blob/v0.1.3/rules/src/main/scala/fix/ThrowableToNonFatal.scala

ThrowableでcatchしているのをNonFatalに置き換え。 import追加をscalafixに任せた都合上SemanticRule。 これは別に低レベルなテキスト置換でもいける気がするが・・・。一応import追加もあるから、その点はテキスト置換だと少し面倒か?

UnnecessarySort

https://github.com/xuwei-k/scalafix-rules/blob/v0.1.3/rules/src/main/scala/fix/UnnecessarySort.scala

  • list.sortBy(f).head -> list.minBy(f)
  • list.sortBy(f).headOption -> list.minByOption(f)
  • list.sortBy(f).last -> list.maxBy(f)
  • list.sortBy(f).lastOption -> list.maxByOption(f)

sortしてから1つだけ取り出す、という操作はやってしまいがちだが、それなら全てをsortする必要はなく、専用のメソッドが標準ライブラリのcollectionに存在するので、そのパターンを検知する。 SyntacticRuleなので、当然誤検知はあり得る。

UnnecessarySortRewriteで書き換えも可能。

UnusedConstructorParams

https://github.com/xuwei-k/scalafix-rules/blob/v0.1.3/rules/src/main/scala/fix/UnusedConstructorParams.scala

以下のような条件で検索すれば、原理上、SyntacticRuleでも、ほぼ100%?未使用なclassのコンストラクタ引数を検知できる?と思ったので、そういう原理で作ってみたもの。実際、誤検知しない気がする・・・? 現状、警告のみで、書き換えは行わない。

  • case classではない
  • val が付与されてない
  • implicit も付与されてない
  • そのclass内部のtokenを全て走査して、その名前が存在しない