Scalaでcase classのconstructorをprivateにしたときのcompanionのapplyの可視性問題と対策

以下のコードがエラーになるかどうか?という話

case class A private(x: Int)

object B {
  def f = A(2)
}

まずオプションの有無とScala versionによってややこしい。以下が一覧

Scala 2 Scala 3
no option OK NG
-Xsource:3 NG -
-source:3.0-migration - OK

Scala 2から3で変更があったが、それぞれcompiler optionの付与で、別の挙動に変更可能。

大抵Scala 3の挙動の方が望ましいはずである。

qiita.com

とはいえ、compiler optionで指定だと、全部が変わってしまうので、もう少し細かく制御したいぞ! という人のための対策

scalafixでそもそもprivate constructor自体を警告して sealed abstract case class 強制

この警告出すにはscalafix必要だが、修正後は、wartremoverやscalafixなどの外部の仕組みは必要なく、思った通りにアクセス制御可能、というメリット。

ただ、そもそも sealed abstract case class 自体が無理やりな回避策感があるし、どうせScala 3のデフォルトは勝手にprivateになるので、 あくまで一時的な対策という感じか・・・?

package fix

import scala.meta.Defn
import scala.meta.Mod
import scalafix.Patch
import scalafix.lint.Diagnostic
import scalafix.lint.LintSeverity
import scalafix.v1.SyntacticDocument
import scalafix.v1.SyntacticRule

class CaseClassPrivateConstructor extends SyntacticRule("CaseClassPrivateConstructor") {
  override def fix(implicit doc: SyntacticDocument): Patch = {
    doc.tree.collect {
      case c: Defn.Class if c.mods.exists(_.is[Mod.Case]) && c.mods.forall(!_.is[Mod.Abstract]) =>
        c.ctor.mods
          .find(_.is[Mod.Private]).map { p =>
            Patch.lint(
              Diagnostic(
                id = "",
                message =
                  "case classのconstructorをprivateにしても`-Xsource:3`を指定してないのでapplyがpublicになってしまっていて実質意味がないです。本当にprivateにしたい場合はsealed abstract case classなどを検討してください",
                position = p.pos,
                severity = LintSeverity.Warning
              )
            )
          }.asPatch
    }.asPatch
  }
}

wartremoverでapplyを検知

今さっき作った。細かい検証終わったらmergeして、後でリリースする。 applyのoverloadがある場合の検査が雑で正確ではない問題がある。

github.com

TODO: scalafixのSemanticRuleでwartremoverと同様のものを作る・・・?