0

I have a macro which enumerates class members. I would like to extend the macro so that it works recursively by enumerating inside into any class members in a form:

    object obj {
      var name = "value"
      var nested = new {
        var x = 0
      }
    }

In a runtime reflection I have used before transitioning to macros the corresponding test which works well for me is symbol.info.widen =:= typeOf[AnyRef], however this cannot work with macro, as in this can the type is not AnyRef, but its subclass (refinement).

When I print the type to the console, I get e.g.:

AnyRef{def x: Int; def x_=(x$1: Int): Unit}

When I list all base classes, I get:

List(<refinement of AnyRef>, class Object, class Any)

I cannot use a test <:< typeOf[AnyRef], as almost anything would pass such test.

How can I test for this?

Here is the reflection version of the function, working fine:

  def listMembersNested_A(m: Any): Seq[(String, Any)] = {
    import scala.reflect.runtime.currentMirror
    import scala.reflect.runtime.universe._

    val anyMirror = currentMirror.reflect(m)
    val members = currentMirror.classSymbol(m.getClass).toType.members
    val items = for {
      symbol <- members
      if symbol.isTerm && !symbol.isMethod && !symbol.isModule
    } yield {
      val field = anyMirror.reflectField(symbol.asTerm)
      symbol.name.decodedName.toString.trim -> (if (symbol.info.widen =:= typeOf[AnyRef]) {
        listMembersNested_A(field.get)
      } else {
        field.get
      })
    }
    items.toSeq
  }

Its macro counterpart (it is a materialization macro):

    def impl[O: c.WeakTypeTag](c: blackbox.Context): c.Expr[ListMembersNested[O]] = {
      import c.universe._

      val O = weakTypeOf[O]

      val dive = O.members.sorted.collect {
        case f if f.isMethod && f.asMethod.paramLists.isEmpty && f.asMethod.isGetter =>
          val fName = f.name.decodedName.toString
          if (f.info.widen =:= typeOf[AnyRef]) { /// <<<<<< this does not work
            q"$fName -> listMembersNested(t.$f)"
          } else {
            q"$fName -> t.$f"
          }
      }
      val r = q" Seq(..$dive)"
      val membersExpr = c.Expr[Seq[(String, Any)]](r)

      reify {
        new ListMembersNested[O] {
          def listMembers(t: O) = membersExpr.splice
        }
      }
    }
Suma
  • 33,181
  • 16
  • 123
  • 191
  • If I call runtime-reflection version `listMembersNested_A(obj)` then it produces `List((nested,pckg.App$obj$$anon$1@7ae42ce3), (name,value))` i.e. in your `if-else` the branch `field.get` is selected both times and the branch `listMembersNested_A(field.get)` is never selected. Is this expected behavior? – Dmytro Mitin Oct 25 '20 at 13:15
  • And your macro version `listMembers(obj)` (I defined `def listMembers[O](t: O)(implicit lmn: ListMembersNested[O]): Seq[(String, Any)] = lmn.listMembers(t)`) produces `Seq("name".$minus$greater(t.name), "nested".$minus$greater(t.nested))`. So I can't see difference between how runtime -reflection version works and how macro version works. – Dmytro Mitin Oct 25 '20 at 13:33
  • "If I call runtime-reflection version" That is really strange. When debugging the function in my project it enters the `listMembersNested_A` recursion for me and the produced result is "List((nested,List((x,0))), (name,value))" then. When I try this in Scastie, I get the same result as you do. This is both with Scala 2.12.12 – Suma Oct 25 '20 at 15:34
  • 1
    I get the behaviour I have described only when `obj` is defined as a local variable in a function. When it is a member of another object / class I get the behaviour you have described. As I was debugging it inside of a ScalaTest, it was a local variable for me. – Suma Oct 25 '20 at 16:51
  • Connected question: https://stackoverflow.com/questions/64526623/why-is-a-type-of-the-member-of-the-object-different-in-a-function – Dmytro Mitin Oct 26 '20 at 19:00

1 Answers1

2

In your macro try to replace if-else with pattern matching by types

val anyRefType = typeOf[AnyRef]

f.info match {
  case NullaryMethodType(RefinedType(List(`anyRefType`), _)) =>
    q"$fName -> listMembersNested(t.$f)"
  case _ =>
    q"$fName -> t.$f"
}
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • Great. This really works. After reading `RefinedType` ScalaDoc I though even a more specific match of `NullaryMethodType(RefinedType(List(AnyRef), _))` could be made, but somehow that no longer seems to match. – Suma Oct 26 '20 at 20:25
  • 1
    I see now. I need to match against `typeOf[AnyRef]`, not plain `AnyRef`. – Suma Oct 26 '20 at 20:38
  • @Suma If it's more convenient you can also match ``f.info.resultType match { case RefinedType(List(`anyRefType`), _) => ...`` – Dmytro Mitin Nov 30 '20 at 02:50
  • @Suma https://stackoverflow.com/questions/65062245/how-to-find-class-parameter-datatype-at-runtime-in-scala – Dmytro Mitin Nov 30 '20 at 04:08