[FIXED] Scala-Whitebox-Makro, um zu überprüfen, ob Klassenfelder vom Typ einer Fallklasse sind

Ausgabe

Ich versuche, case classaus einem Gegebenen einen zu erzeugen, der aus den Feldern case classstreift . OptionDies muss rekursiv erfolgen. Wenn also das Feld selbst ein case classist, muss Optiones auch aus seinen Feldern entfernt werden.

Bisher habe ich es geschafft, wo keine Felder sind, keine case class. Aber für die Rekursion muss ich das ClassTagfür das Feld bekommen, wenn es ein case class. Aber ich habe keine Ahnung, wie ich das machen kann. Anscheinend kann ich nur auf den Syntaxbaum vor der Typprüfung zugreifen (ich denke, es ist sinnvoll, wenn man bedenkt, dass der endgültige Quellcode noch nicht erstellt ist). Aber ich frage mich, ob es möglich ist, dies auf irgendeine Weise zu erreichen.

Hier ist mein Code und der fehlende Teil als Kommentar.

import scala.annotation.StaticAnnotation
import scala.collection.mutable
import scala.reflect.macros.blackbox.Context

import scala.language.experimental.macros
import scala.annotation.compileTimeOnly

class RemoveOptionFromFields extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro RemoveOptionFromFields.impl
}

object RemoveOptionFromFields {
  def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
    import c.universe._
 
    def modifiedClass(classDecl: ClassDef, compDeclOpt: Option[ModuleDef]) = {
      val result = classDecl match {
        case q"case class $className(..$fields) extends ..$parents { ..$body }" =>
          val fieldsWithoutOption = fields.map {
            case ValDef(mods, name, tpt, rhs) =>
              tpt.children match {
                case List(first, second) if first.toString() == "Option" =>
                  // Check if `second` is a case class?
                  // Get it's fields if so
                  val innerType = tpt.children(1)
                  ValDef(mods, name, innerType, rhs)
                case _ => 
                  ValDef(mods, name, tpt, rhs)
              }
          }

          val withOptionRemovedFromFieldsClassDecl = q"case class WithOptionRemovedFromFields(..$fieldsWithoutOption)"

          val newCompanionDecl = compDeclOpt.fold(
            q"""
            object ${className.toTermName} {
              $withOptionRemovedFromFieldsClassDecl
            }
            """
          ) {
            compDecl =>
              val q"object $obj extends ..$bases { ..$body }" = compDecl
              q"""
              object $obj extends ..$bases {
                ..$body

                $withOptionRemovedFromFieldsClassDecl
              }
              """
          }

          q"""
            $classDecl
            $newCompanionDecl
          """
      }
      c.Expr[Any](result)
    }
 
    annottees.map(_.tree) match {
      case (classDecl: ClassDef) :: Nil => modifiedClass(classDecl, None)
      case (classDecl: ClassDef) :: (compDecl: ModuleDef) :: Nil => modifiedClass(classDecl, Some(compDecl))
      case _ => c.abort(c.enclosingPosition, "This annotation only supports classes")
    }
  }
}

Lösung

Ich bin mir nicht sicher, ob ich verstehe, welche Art von Rekursion Sie benötigen. Angenommen, wir haben zwei Fallklassen: die 1. kommentierte (bezieht sich auf die 2.) und die 2. nicht kommentierte

@RemoveOptionFromFields  
case class MyClass1(mc: Option[MyClass2])  

case class MyClass2(i: Option[Int])

Was soll das Ergebnis sein?

Derzeit verwandelt sich die Anmerkung in

case class MyClass1(mc: Option[MyClass2]) 
object MyClass1 {
  case class WithOptionRemovedFromFields(mc: Class2)
} 

case class MyClass2(i: Option[Int])

Wenn das Feld selbst eine Fallklasse ist, muss Option auch aus seinen Feldern entfernt werden.

Makroannotationen können nur Klassen und ihre Begleiter umschreiben, sie können keine anderen Klassen umschreiben. In meinem Beispiel mit 2 Klassen kann die Anmerkung MyClass1und ihr Begleiter geändert werden, aber sie kann MyClass2oder ihr Begleiter nicht neu geschrieben werden. Denn das MyClass2sollte selbst kommentiert werden.

In einem Geltungsbereich werden Makroanmerkungen vor der Typprüfung dieses Geltungsbereichs expandiert. Beim Umschreiben sind die Bäume also untypisiert. Wenn Sie einige Bäume eingeben müssen (damit Sie ihre Symbole finden können), können Sie verwendenc.typecheck

Scala-Makros: Was ist der Unterschied zwischen typisierten (auch bekannt als typgeprüften) und nicht typisierten Bäumen

Um zu überprüfen, ob eine Klasse eine Fallklasse ist, die Sie verwenden könnensymbol.isClass && symbol.asClass.isCaseClass

Wie kann man überprüfen, ob ein T zur Kompilierzeit in Scala eine Fallklasse ist?

Sie brauchen kaum ClassTags.

Eine weitere Komplikation ist, wenn MyClass1und MyClass2sich im selben Bereich befinden

@RemoveOptionFromFields 
case class MyClass1(mc: Option[MyClass2])  

case class MyClass2(i: Option[Int])

Dann wird bei der Erweiterung der MyClass1Makroannotation für den Bereich noch keine Typprüfung durchgeführt, sodass es unmöglich ist, den Baum der Felddefinition zu prüfen mc: Option[MyClass2](die Klasse MyClass2ist noch nicht bekannt). Wenn sich die Klassen in unterschiedlichen Bereichen befinden, ist dies in Ordnung

{
  @RemoveOptionFromFields 
  case class MyClass1(mc: Option[MyClass2])
}  

case class MyClass2(i: Option[Int])

Dies ist eine modifizierte Version Ihres Codes (ich drucke nur die Felder der zweiten Klasse)

import scala.annotation.StaticAnnotation
import scala.reflect.macros.blackbox
import scala.language.experimental.macros
import scala.annotation.compileTimeOnly

@compileTimeOnly("enable macro annotations")
class RemoveOptionFromFields extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro RemoveOptionFromFields.impl
}

object RemoveOptionFromFields {
  def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
    import c.universe._

    def modifiedClass(classDecl: ClassDef, compDeclOpt: Option[ModuleDef]) = {
      classDecl match {
        case q"$mods class $className[..$tparams] $ctorMods(..$fields) extends { ..$earlydefns } with ..$parents { $self => ..$body }"
          if mods.hasFlag(Flag.CASE) =>
          val fieldsWithoutOption = fields.map {
            case field@q"$mods val $name: $tpt = $rhs" =>
              tpt match {
                case tq"$first[..${List(second)}]" =>
                  val firstType = c.typecheck(tq"$first", mode = c.TYPEmode, silent = true) match {
                    case EmptyTree => println(s"can't typecheck $first while expanding @RemoveOptionFromFields for $className"); NoType
                    case t => t.tpe
                  }
                  if (firstType <:< typeOf[Option[_]].typeConstructor) {
                    val secondSymbol = c.typecheck(tq"$second", mode = c.TYPEmode, silent = true) match {
                      case EmptyTree => println(s"can't typecheck $second while expanding @RemoveOptionFromFields for $className"); NoSymbol
                      case t => t.symbol
                    }
                    if (secondSymbol.isClass && secondSymbol.asClass.isCaseClass) {
                      val secondClassFields = secondSymbol.typeSignature.decls.toList.filter(s => s.isMethod && s.asMethod.isCaseAccessor)
                      secondClassFields.foreach(s =>
                        c.typecheck(q"$s", silent = true) match {
                          case EmptyTree => println(s"can't typecheck $s while expanding @RemoveOptionFromFields for $className")
                          case t => println(s"field ${t.symbol} of type ${t.tpe}, subtype of Option: ${t.tpe <:< typeOf[Option[_]]}")
                        }
                      )
                    }
                    q"$mods val $name: $second = $rhs"
                  } else field
                case _ =>
                  field
              }
          }

          val withOptionRemovedFromFieldsClassDecl = q"case class WithOptionRemovedFromFields(..$fieldsWithoutOption)"

          val newCompanionDecl = compDeclOpt.fold(
            q"""
              object ${className.toTermName} {
                $withOptionRemovedFromFieldsClassDecl
              }
            """
          ) {
            compDecl =>
              val q"$mods object $obj extends { ..$earlydefns } with ..$bases { $self => ..$body }" = compDecl
              q"""
                $mods object $obj extends { ..$earlydefns } with ..$bases { $self =>
                  ..$body

                  $withOptionRemovedFromFieldsClassDecl
                }
              """
          }

          q"""
            $classDecl
            $newCompanionDecl
          """
      }
    }

    annottees match {
      case (classDecl: ClassDef) :: Nil => modifiedClass(classDecl, None)
      case (classDecl: ClassDef) :: (compDecl: ModuleDef) :: Nil => modifiedClass(classDecl, Some(compDecl))
      case _ => c.abort(c.enclosingPosition, "This annotation only supports classes")
    }
  }
}


Beantwortet von –
Dmytro Mitin


Antwort geprüft von –
Jay B. (FixError Admin)

0 Shares:
Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like