Search code examples
scalascala-catsscala-macros

top-level class without companion can only expand either into an eponymous class or into a block consisting in eponymous companions


I am trying to use https://github.com/estatico/scala-newtype as follows:

import io.estatico.newtype.macros.newtype
import cats._
import io.databaker.env._

@newtype case class DbUrl(v: String)

@newtype case class DbUser(v: String)

@newtype case class DbPw(v: String)

final case class DbParams(url: DbUrl, user: DbUser, pw: DbPw)

trait DbConnector[F[_]] {
  def read(url: DbUrl, user: DbUser, pw: DbPw): F[DbParams]
}


object DbConnector {

  def impl[F[_] : MonadError[*[_], Throwable]](env: Environment[F])
  : DbConnector[F] =
    new LiveDbConnector[F](env)

}

and the compiler complains:

[error] ../db/DbConnector.scala:7:2: top-level class without companion can only expand either into an eponymous class or into a block consisting in eponymous companions
[error] @newtype case class DbUrl(v: String)
[error]  ^
[error] ../db/DbConnector.scala:9:2: top-level class without companion can only expand either into an eponymous class or into a block consisting in eponymous companions
[error] @newtype case class DbUser(v: String)
[error]  ^
[error] ../db/DbConnector.scala:11:2: top-level class without companion can only expand either into an eponymous class or into a block consisting in eponymous companions
[error] @newtype case class DbPw(v: String)
[error]  ^
[error] ../env/Environment.scala:8:2: top-level class without companion can only expand either into an eponymous class or into a block consisting in eponymous companions
[error] @newtype case class EnvValue(v: String)
[error]  ^
[error] ../env/Environment.scala:6:2: top-level class without companion can only expand either into an eponymous class or into a block consisting in eponymous companions
[error] @newtype case class EnvVariable(v: String)  

The content of the build.sbt:

lazy val root = (project in file("."))
  .enablePlugins(JettyPlugin)
  .settings(
    organization := "io.example",
    name := "user-svc",
    version := "0.0.1-SNAPSHOT",
    scalaVersion := "2.13.2",
    mainClass := Some("io.example.Main"),
    containerPort := 9090,
    libraryDependencies ++= Seq(
      "org.http4s" %% "http4s-servlet" % Http4sVersion,
      "org.http4s" %% "http4s-circe" % Http4sVersion,
      "org.http4s" %% "http4s-dsl" % Http4sVersion,
      "io.circe" %% "circe-generic" % CirceVersion,
      "org.scalameta" %% "munit" % MunitVersion % "test",
      "ch.qos.logback" % "logback-classic" % LogbackVersion,
      "io.estatico" %% "newtype" % NewTypeVersion,
      "javax.servlet" % "javax.servlet-api" % ServletVersion % "provided"
    ),
    addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.11.0" cross CrossVersion.full),
    addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1"),
  )

scalacOptions ++= Seq(
  "-deprecation",
  "-encoding", "UTF-8",
  "-language:higherKinds",
  "-language:postfixOps",
  "-feature",
  "-Xfatal-warnings",
  "-Ymacro-annotations"
)

What am I doing wrong?

Update

I have moved the newtype macros into package object as follows:

package object db {
  
  @newtype case class DbUrl(v: String)

  @newtype case class DbUser(v: String)

  @newtype case class DbPw(v: String)

}

but the compiler still complains:

implicit conversion method opsThis should be enabled
[error] by making the implicit value scala.language.implicitConversions visible.
[error]   @newtype case class DbUser(v: String)

Solution

  • The README.md of scala-newtype says:

    This expands into a type and companion object definition, so newtypes must be defined in an object or package object.

    Macros are allowed to expand classes into other classes with the same name and companion objects, but from what I can tell, the newtype annotation turns your case class into an object of the same name (along with a type alias like type DbUrl = DbUrl.Type). This behavior (turning a top-level annottee into a tree of some other kind) isn't allowed. If the annotation had generated a class DbUrl, and maybe an object of the same name, though, it would have been all right, but pretty much anything else won't work.

    To fix your problem, all you need to do is move this into a package object (or some other scope, as long as it isn't top-level).

    Edit: As Dmytro Mitin pointed out, the created type is not the type of DbUrl but rather something like type DbUrl = DbUrl.Type with an uppercase "T", where the definition of DbUrl.Type looks something like this (I'm just copying this from the README):

    type Base = Any { type DbUrl$newtype }
    trait Tag extends Any
    type Type <: Base with Tag