toStringMacro.scala 8.7 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
/*
 * Copyright (c) 2021 jxnu-liguobin && contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

梦境迷离's avatar
梦境迷离 已提交
22 23 24 25 26 27 28 29 30 31
package io.github.dreamylost.macros

import scala.reflect.macros.whitebox

/**
 *
 * @author 梦境迷离
 * @since 2021/7/7
 * @version 1.0
 */
梦境迷离's avatar
梦境迷离 已提交
32
object toStringMacro {
梦境迷离's avatar
梦境迷离 已提交
33

34
  private final case class Argument(verbose: Boolean, includeInternalFields: Boolean, includeFieldNames: Boolean, callSuper: Boolean)
梦境迷离's avatar
梦境迷离 已提交
35

梦境迷离's avatar
梦境迷离 已提交
36 37
  class ToStringProcessor(override val c: whitebox.Context) extends AbstractMacroProcessor(c) {

梦境迷离's avatar
梦境迷离 已提交
38
    import c.universe._
梦境迷离's avatar
梦境迷离 已提交
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62

    override def impl(annottees: c.universe.Expr[Any]*): c.universe.Expr[Any] = {
      // extract parameters of annotation, must in order
      val arg: (Boolean, Boolean, Boolean, Boolean) = extractArgumentsTuple4 {
        case q"new toString(includeInternalFields=$bb, includeFieldNames=$cc, callSuper=$dd)" =>
          (false, evalTree(bb.asInstanceOf[Tree]), evalTree(cc.asInstanceOf[Tree]), evalTree(dd.asInstanceOf[Tree]))
        case q"new toString($aa, $bb, $cc)" =>
          (evalTree(aa.asInstanceOf[Tree]), evalTree(bb.asInstanceOf[Tree]), evalTree(cc.asInstanceOf[Tree]), false)

        case q"new toString(verbose=$aa, includeInternalFields=$bb, includeFieldNames=$cc, callSuper=$dd)" =>
          (evalTree(aa.asInstanceOf[Tree]), evalTree(bb.asInstanceOf[Tree]), evalTree(cc.asInstanceOf[Tree]), evalTree(dd.asInstanceOf[Tree]))
        case q"new toString(verbose=$aa, includeInternalFields=$bb, includeFieldNames=$cc)" =>
          (evalTree(aa.asInstanceOf[Tree]), evalTree(bb.asInstanceOf[Tree]), evalTree(cc.asInstanceOf[Tree]), false)
        case q"new toString($aa, $bb, $cc, $dd)" =>
          (evalTree(aa.asInstanceOf[Tree]), evalTree(bb.asInstanceOf[Tree]), evalTree(cc.asInstanceOf[Tree]), evalTree(dd.asInstanceOf[Tree]))

        case q"new toString(includeInternalFields=$bb, includeFieldNames=$cc)" =>
          (false, evalTree(bb.asInstanceOf[Tree]), evalTree(cc.asInstanceOf[Tree]), false)
        case q"new toString(includeInternalFields=$bb)" =>
          (false, evalTree(bb.asInstanceOf[Tree]), true, false)
        case q"new toString(includeFieldNames=$cc)" =>
          (false, true, evalTree(cc.asInstanceOf[Tree]), false)
        case q"new toString()" => (false, true, true, false)
        case _                 => c.abort(c.enclosingPosition, ErrorMessage.UNEXPECTED_PATTERN)
梦境迷离's avatar
梦境迷离 已提交
63
      }
梦境迷离's avatar
梦境迷离 已提交
64 65 66 67 68 69 70 71 72 73 74
      val argument = Argument(arg._1, arg._2, arg._3, arg._4)
      c.info(c.enclosingPosition, s"toString annottees: $annottees", force = argument.verbose)
      // Check the type of the class, which can only be defined on the ordinary class
      val annotateeClass: ClassDef = checkAndGetClassDef(annottees: _*)
      val isCase: Boolean = isCaseClass(annotateeClass)

      c.info(c.enclosingPosition, s"impl argument: $argument, isCase: $isCase", force = argument.verbose)
      val resMethod = toStringTemplateImpl(argument, annotateeClass)
      val resTree = annotateeClass match {
        case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" =>
          q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..${stats.toList.:+(resMethod)} }"
梦境迷离's avatar
梦境迷离 已提交
75 76
      }

梦境迷离's avatar
梦境迷离 已提交
77 78 79
      val res = treeResultWithCompanionObject(resTree, annottees: _*)
      printTree(argument.verbose, res)
      c.Expr[Any](res)
梦境迷离's avatar
梦境迷离 已提交
80 81
    }

梦境迷离's avatar
梦境迷离 已提交
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
    def printField(argument: Argument, lastParam: Option[String], field: Tree): Tree = {
      // Print one field as <name of the field>+"="+fieldName
      if (argument.includeFieldNames) {
        lastParam.fold(q"$field") { lp =>
          field match {
            case q"$mods var $tname: $tpt = $expr" =>
              if (tname.toString() != lp) q"""${tname.toString()}+${"="}+this.$tname+${", "}""" else q"""${tname.toString()}+${"="}+this.$tname"""
            case q"$mods val $tname: $tpt = $expr" =>
              if (tname.toString() != lp) q"""${tname.toString()}+${"="}+this.$tname+${", "}""" else q"""${tname.toString()}+${"="}+this.$tname"""
            case _ => q"$field"
          }
        }
      } else {
        lastParam.fold(q"$field") { lp =>
          field match {
            case q"$mods var $tname: $tpt = $expr" => if (tname.toString() != lp) q"""$tname+${", "}""" else q"""$tname"""
            case q"$mods val $tname: $tpt = $expr" => if (tname.toString() != lp) q"""$tname+${", "}""" else q"""$tname"""
            case _                                 => if (field.toString() != lp) q"""$field+${", "}""" else q"""$field"""
          }
        }

梦境迷离's avatar
梦境迷离 已提交
103 104
      }
    }
梦境迷离's avatar
梦境迷离 已提交
105 106 107 108 109 110 111 112

    private def toStringTemplateImpl(argument: Argument, annotateeClass: ClassDef): Tree = {
      // For a given class definition, separate the components of the class
      val (className, annotteeClassParams, superClasses, annotteeClassDefinitions) = {
        annotateeClass match {
          case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" =>
            c.info(c.enclosingPosition, s"parents: $parents", force = argument.verbose)
            (tpname, paramss.asInstanceOf[List[List[Tree]]], parents, stats.asInstanceOf[List[Tree]])
梦境迷离's avatar
梦境迷离 已提交
113
        }
梦境迷离's avatar
梦境迷离 已提交
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
      }
      // Check the type of the class, whether it already contains its own toString
      val annotteeClassFieldDefinitions = annotteeClassDefinitions.filter(p => p match {
        case _: ValDef => true
        case mem: MemberDef =>
          c.info(c.enclosingPosition, s"MemberDef:  ${mem.toString}", force = argument.verbose)
          if (mem.toString().startsWith("override def toString")) { // TODO better way
            c.abort(mem.pos, "'toString' method has already defined, please remove it or not use'@toString'")
          }
          false
        case _ => false
      })

      // For the parameters of a given constructor, separate the parameter components and extract the constructor parameters containing val and var
      val ctorParams = annotteeClassParams.flatten.map {
        case tree @ q"$mods val $tname: $tpt = $expr" => tree
        case tree @ q"$mods var $tname: $tpt = $expr" => tree
      }
      c.info(c.enclosingPosition, s"className: $className, ctorParams: ${ctorParams.toString()}, superClasses: $superClasses", force = argument.verbose)
      c.info(c.enclosingPosition, s"className: $className, fields: ${annotteeClassFieldDefinitions.toString()}", force = argument.verbose)
      val member = if (argument.includeInternalFields) ctorParams ++ annotteeClassFieldDefinitions else ctorParams
梦境迷离's avatar
梦境迷离 已提交
135

梦境迷离's avatar
梦境迷离 已提交
136 137 138
      val lastParam = member.lastOption.map {
        case v: ValDef => v.name.toTermName.decodedName.toString
        case c         => c.toString
梦境迷离's avatar
梦境迷离 已提交
139
      }
梦境迷离's avatar
梦境迷离 已提交
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
      val paramsWithName = member.foldLeft(q"${""}")((res, acc) => q"$res + ${printField(argument, lastParam, acc)}")
      //scala/bug https://github.com/scala/bug/issues/3967 not be 'Foo(i=1,j=2)' in standard library
      val toString = q"""override def toString: String = ${className.toString()} + ${"("} + $paramsWithName + ${")"}"""

      // Have super class ?
      if (argument.callSuper && superClasses.nonEmpty) {
        val superClassDef = superClasses.head match {
          case tree: Tree => Some(tree) // TODO type check better
          case _          => None
        }
        superClassDef.fold(toString)(_ => {
          val superClass = q"${"super="}"
          c.info(c.enclosingPosition, s"member: $member, superClass: $superClass, superClassDef: $superClassDef, paramsWithName: $paramsWithName", force = argument.verbose)
          q"override def toString: String = StringContext(${className.toString()} + ${"("} + $superClass, ${if (member.nonEmpty) ", " else ""}+$paramsWithName + ${")"}).s(super.toString)"
        }
        )
      } else {
        toString
梦境迷离's avatar
梦境迷离 已提交
158 159 160 161 162
      }
    }
  }

}