Search code examples
scalascala-macrosscala-2.11

How to check if some T is a case class at compile time in Scala?


I have the following macro:

package macros

import scala.reflect.macros.blackbox.Context

object CompileTimeAssertions {
  def mustBeCaseClass[T]: Unit =
    macro CompileTimeAssertionsImpl.mustBeCaseClass[T]
}

object CompileTimeAssertionsImpl {
  def mustBeCaseClass[T: c.WeakTypeTag](c: Context): c.Expr[Unit] = {
    import c.universe._
    val symbol = c.weakTypeTag[T].tpe.typeSymbol
    if (!symbol.isClass || !symbol.asClass.isCaseClass) {
      c.error(c.enclosingPosition, s"${symbol.fullName} must be a case class")
    }
    reify(Unit)
  }
}

It works when generics aren't involved, but fails when they are:

import macros.CompileTimeAssertions._
import org.scalatest.{Matchers, WordSpec}

case class ACaseClass(foo: String, bar: String)

class NotACaseClass(baz: String)

class MacroSpec extends WordSpec with Matchers {
  "the mustBeCaseClass macro" should {
    "compile when passed a case class" in {
      mustBeCaseClass[ACaseClass]
    }

    "not compile when passed a vanilla class" in {
//      mustBeCaseClass[NotACaseClass] // fails to compile as expected.
    }

    "compile when working with generics" in {
//      class CaseClassContainer[T] { mustBeCaseClass[T] } // fails to compile.
//      new CaseClassContainer[ACaseClass]
    }
  }
}

The compiler error is mine:

MacroSpec.CaseClassContainer.T must be a case class

I'd like to find out what T is when the CaseClassContainer is instantiated. Is that even possible? If it is can you provide an example?

Thanks in advance.


Solution

  • Thanks to Eugene and Travis' advice I was able to solve this problem with type classes. Here's the solution:

    package macros
    
    import scala.reflect.macros.blackbox.Context
    
    trait IsCaseClass[T]
    
    object IsCaseClass {
      implicit def isCaseClass[T]: IsCaseClass[T] =
        macro IsCaseClassImpl.isCaseClass[T]
    }
    
    object IsCaseClassImpl {
      def isCaseClass[T]
          (c: Context)
          (implicit T: c.WeakTypeTag[T]): c.Expr[IsCaseClass[T]] = {
        import c.universe._
        val symbol = c.weakTypeTag[T].tpe.typeSymbol
        if (!symbol.isClass || !symbol.asClass.isCaseClass) {
          c.abort(c.enclosingPosition, s"${symbol.fullName} must be a case class")
        } else {
          c.Expr[IsCaseClass[T]](q"_root_.macros.IsCaseClassImpl[$T]()")
        }
      }
    }
    
    case class IsCaseClassImpl[T]() extends IsCaseClass[T]
    

    And here is the usage:

    import macros.IsCaseClass
    import org.scalatest.{Matchers, WordSpec}
    
    case class ACaseClass(foo: String, bar: String)
    
    class NotACaseClass(baz: String)
    
    class CaseClassContainer[T: IsCaseClass]
    
    class MacroSpec extends WordSpec with Matchers {
      "the code" should {
        "compile" in {
          new CaseClassContainer[ACaseClass]
        }
    
        "not compile" in {
    //      new CaseClassContainer[NotACaseClass]
        }
      }
    }
    

    Worth noting the use of abort instead of error. Abort returns Nothing whereas error returns Unit. The latter was fine when the macro wasn't returning anything.