diff --git a/README.md b/README.md index 932dfee8e69c55c5ea60dd99311069db86eaad14..091204df0d3a75502f22c081ce96bfad4625d104 100644 --- a/README.md +++ b/README.md @@ -4,59 +4,47 @@ scala macro and abstract syntax tree learning code. # @toString -- NOTE - - Automatically ignore when use on `case` class. - - Contains constructor parameters which have `val`/`var` modifier and class internal fields. - - The existing custom `toString` method will fail to compile. +- Argument + - `verbose` Whether to enable detailed log. + - `withFieldName` Whether to include the name of the field in the toString. + - `containsCtorParams` Whether to include the fields of the primary constructor. - source code1 + ```scala -@toString(isContainsCtorParams = true) class TestClass(val i: Int = 0, var j: Int) { val y: Int = 0 var z: String = "hello" var x: String = "world" } -// result of scalac -class TestClass extends scala.AnyRef { - val i: Int = _; - var j: Int = _; - def (i: Int = 0, j: Int) = { - super.(); - () - }; - val y: Int = 0; - var z: String = "hello"; - var x: String = "world"; - override def toString(): String = scala.collection.immutable.List(i, j, y, z, x).toString.replace("List", "TestClass") // a crude way, TODO refactor it. -} -//println(new TestClass(1, 2)) -//TestClass(1, 2, 0, hello, world) +case class TestClass2(i: Int = 0, var j: Int) // No method body, only have primary constructor. ``` -- source code2 -```scala -@toString -class TestClass(val i: Int = 0, var j: Int) { - val y: Int = 0 - var z: String = "hello" - var x: String = "world" -} -// result of scalac -class TestClass extends scala.AnyRef { - val i: Int = _; - var j: Int = _; - def (i: Int = 0, j: Int) = { - super.(); - () - }; - val y: Int = 0; - var z: String = "hello"; - var x: String = "world"; - override def toString(): String = scala.collection.immutable.List(y, z, x).toString.replace("List", "TestClass") // a crude way, TODO refactor it. -} -//println(new TestClass(1, 2)) -//TestClass(0, hello, world) +- when withFieldName=false containsCtorParams=false + +``` +println(new TestClass(1, 2)) +TestClass(0, hello, world) ``` +- when withFieldName=false containsCtorParams=true + +``` +println(new TestClass(1, 2)) +TestClass(1, 2, 0, hello, world) +``` + +- when withFieldName=true containsCtorParams=false + +``` +println(new TestClass(1, 2)) +TestClass(y=0, z=hello, x=world) +``` + +- when withFieldName=true containsCtorParams=true + +``` +println(new TestClass(1, 2)) +TestClass(i=1, j=2, y=0, z=hello, x=world) +``` \ No newline at end of file diff --git a/src/main/scala/io/github/liguobin/toString.scala b/src/main/scala/io/github/liguobin/toString.scala index d9bea853afaa7c607cd5856e61dbf30418176185..7bfc9d2e838e7cb1966a443336d9774e27004e4a 100644 --- a/src/main/scala/io/github/liguobin/toString.scala +++ b/src/main/scala/io/github/liguobin/toString.scala @@ -1,6 +1,6 @@ package io.github.liguobin -import scala.annotation.{ StaticAnnotation, compileTimeOnly } +import scala.annotation.{StaticAnnotation, compileTimeOnly} import scala.language.experimental.macros import scala.reflect.macros.whitebox @@ -8,110 +8,128 @@ import scala.reflect.macros.whitebox * toString for class * * @author 梦境迷离 - * @param verbose Whether to enable detailed log - * @param isContainsCtorParams Whether to include the fields in the constructor + * @param verbose Whether to enable detailed log. + * @param containsCtorParams Whether to include the fields of the primary constructor. + * @param withFieldName Whether to include the name of the field in the toString. * @since 2021/6/13 * @version 1.0 */ @compileTimeOnly("enable macro to expand macro annotations") -class toString(verbose: Boolean = false, isContainsCtorParams: Boolean = false) extends StaticAnnotation { +class toString( + verbose: Boolean = false, + containsCtorParams: Boolean = true, + withFieldName: Boolean = true + ) extends StaticAnnotation { def macroTransform(annottees: Any*): Any = macro stringMacro.impl } +case class Argument(verbose: Boolean, containsCtorParams: Boolean, withFieldName: Boolean) + object stringMacro { - def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { + def printField(c: whitebox.Context)(argument: Argument, lastParam: Option[String], field: c.universe.Tree): c.universe.Tree = { import c.universe._ - - // extract 'isVerbose' parameters of annotation - val isVerbose = c.prefix.tree match { - case Apply(_, q"verbose = $foo" :: Nil) => - foo match { - case Literal(Constant(verbose: Boolean)) => verbose - case _ => - c.warning( - c.enclosingPosition, - "The value provided for 'verbose' must be a constant (true or false) and not an expression (e.g. 2 == 1 + 1). Verbose set to false." - ) - false + // Print one field as +"="+fieldName + if (argument.withFieldName) { + lastParam.fold(q"$field") { lp => + field match { + case tree@q"$mods var $tname: $tpt = $expr" => + if (tname.toString() != lp) q"""${tname.toString()}+${"="}+this.$tname+${", "}""" else q"""${tname.toString()}+${"="}+this.$tname""" + case tree@q"$mods val $tname: $tpt = $expr" => + if (tname.toString() != lp) q"""${tname.toString()}+${"="}+this.$tname+${", "}""" else q"""${tname.toString()}+${"="}+this.$tname""" + case _ => q"$field" } - case _ => false + } + } else { + lastParam.fold(q"$field") { lp => + field match { + case tree@q"$mods var $tname: $tpt = $expr" => if (tname.toString() != lp) q"""$tname+${", "}""" else q"""$tname""" + case tree@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""" + } + } + } + } - // extract 'containsCtorParams' parameters of annotation - val containsCtorParams = c.prefix.tree match { - case Apply(_, q"isContainsCtorParams = $foo" :: Nil) => - foo match { - case Literal(Constant(isContainsCtorParams: Boolean)) => isContainsCtorParams - case _ => - c.warning( - c.enclosingPosition, - "The value provided for 'isContainsCtorParams' must be a constant (true or false) and not an expression (e.g. 2 == 1 + 1). isContainsCtorParams set to false." - ) - false + private def toStringTemplateImpl(c: whitebox.Context)(argument: Argument, annotateeClass: c.universe.ClassDef): c.universe.Tree = { + import c.universe._ + // For a given class definition, separate the components of the class + val (className, annotteeClassParams, annotteeClassDefinitions) = { + annotateeClass match { + case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" => + (tpname, paramss, stats) + } + } + // Check the type of the class, whether it already contains its own toString + val annotteeClassFieldDefinitions = annotteeClassDefinitions.asInstanceOf[List[Tree]].filter(p => p match { + case _: ValDef => true + case mem: MemberDef => + c.info(c.enclosingPosition, s"MemberDef: ${mem.toString}", true) + if (mem.toString().startsWith("override def toString")) { + c.abort(mem.pos, "'toString' method has already defined, please remove it or not use'@toString'") } + false + case m: DefDef => + 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.asInstanceOf[List[List[Tree]]].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()}", force = true) + c.info(c.enclosingPosition, s"className: $className, fields: ${annotteeClassFieldDefinitions.toString()}", force = true) + val member = if (argument.containsCtorParams) ctorParams ++ annotteeClassFieldDefinitions else annotteeClassFieldDefinitions + val lastParam = member.lastOption.map { + case v: ValDef => v.name.toTermName.decodedName.toString + case c => c.toString + } + val paramsWithName = member.foldLeft(q"${""}")((res, acc) => q"$res + ${printField(c)(argument, lastParam, acc)}") + //scala/bug https://github.com/scala/bug/issues/3967 not be 'Foo(i=1,j=2)' in standard library + q"""override def toString: String = ${className.toString()} + ${"("} + $paramsWithName + ${")"}""" + } + + def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { + import c.universe._ + // extract parameters of annotation + // extract 'isVerbose' parameters of annotation + val arg = c.prefix.tree match { + case q"new toString($aa, $bb, $cc)" => (c.eval[Boolean](c.Expr(aa)), c.eval[Boolean](c.Expr(bb)), c.eval[Boolean](c.Expr(cc))) + case _ => c.abort(c.enclosingPosition, "unexpected annotation pattern!") + } + val argument = Argument(arg._1, arg._2, arg._3) // Check the type of the class, which can only be defined on the ordinary class val annotateeClass: ClassDef = annottees.map(_.tree).toList match { case (claz: ClassDef) :: Nil => claz - case _ => c.abort(c.enclosingPosition, "Unexpected annottee. Only applicable to class definitions.") + case _ => c.abort(c.enclosingPosition, "Unexpected annottee. Only applicable to class definitions.") } - - // For a given class definition, separate the components of the class - val (isCase, className, annotteeClassParams, annotteeClassDefinitions) = { + val isCase: Boolean = { annotateeClass match { case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" => - val isCase = if (mods.asInstanceOf[Modifiers].hasFlag(Flag.CASE)) { - c.warning( - c.enclosingPosition, - "'toString' annotation is used on 'case class'. Ignore") + if (mods.asInstanceOf[Modifiers].hasFlag(Flag.CASE)) { + c.warning(c.enclosingPosition, "'toString' annotation is used on 'case class'.") true } else false - (isCase, tpname, paramss, stats) } } - // Check the type of the class, whether it already contains its own toString - annotteeClassDefinitions.asInstanceOf[List[Tree]].map { - case v: ValDef => true - case m: MemberDef => c.abort(m.pos, "'toString' method has already defined, please remove it or not use'@toString'") - } - - // Extract the fields in the class definition - val fields = annotteeClassDefinitions.asInstanceOf[List[Tree]].map { - case v: ValDef => v.name - } - - // For the parameters of a given constructor, separate the parameter components and extract the constructor parameters containing val and var - val ctorParams = annotteeClassParams.asInstanceOf[List[List[Tree]]].flatten.map { - case tree @ q"$mods val $tname: $tpt = $expr" => TermName.apply(tname.toString()) - case tree @ q"$mods var $tname: $tpt = $expr" => TermName.apply(tname.toString()) - } - - val member = if (containsCtorParams) ctorParams ++ fields else fields - - // Generate toString method TODO refactor code - val method = - q""" - override def toString(): String = ($member).toString.replace("List", ${className.toString()}) - """ + c.info(c.enclosingPosition, s"impl argument: $argument, isCase: $isCase", true) + val resMethod = toStringTemplateImpl(c)(argument, annotateeClass) val resTree = annotateeClass match { case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" => - if (!isCase) { - q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..${stats.toList.:+(method)} }" - } else { - 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)} }" } // Print the ast c.info( c.enclosingPosition, "\n###### Expanded macro ######\n" + resTree.toString() + "\n###### Expanded macro ######\n", - force = isVerbose + force = argument.verbose ) c.Expr[Any](resTree) } diff --git a/src/test/scala/io/github/liguobin/ToStringTest.scala b/src/test/scala/io/github/liguobin/ToStringTest.scala index acbcf40191e6c31dabda51ccb13b48ac5a53a569..811ce3c200033bca99e2b20e1d39d929d2b23c4a 100644 --- a/src/test/scala/io/github/liguobin/ToStringTest.scala +++ b/src/test/scala/io/github/liguobin/ToStringTest.scala @@ -1,6 +1,6 @@ package io.github.liguobin -import org.scalatest.{ FlatSpec, Matchers } +import org.scalatest.{FlatSpec, Matchers} /** * @@ -10,8 +10,8 @@ import org.scalatest.{ FlatSpec, Matchers } */ class ToStringTest extends FlatSpec with Matchers { - "toString1" should "contains ContainsCtorParams" in { - @toString + "toString1" should "not contains constructors parameters" in { + @toString(false, false, false) class TestClass(val i: Int = 0, var j: Int) { val y: Int = 0 var z: String = "hello" @@ -22,8 +22,32 @@ class ToStringTest extends FlatSpec with Matchers { assert(s == "TestClass(0, hello, world)") } - "toString2" should "contains ContainsCtorParams" in { - @toString(isContainsCtorParams = true) + "toString2" should "contains constructors parameters" in { + @toString(true, true, true) + class TestClass(val i: Int = 0, var j: Int) { + val y: Int = 0 + var z: String = "hello" + var x: String = "world" + } + val s = new TestClass(1, 2).toString + println(s) + assert(s == "TestClass(i=1, j=2, y=0, z=hello, x=world)") + } + + "toString3" should "not contains constructors parameters but with name" in { + @toString(true, false, true) + class TestClass(val i: Int = 0, var j: Int) { + val y: Int = 0 + var z: String = "hello" + var x: String = "world" + } + val s = new TestClass(1, 2).toString + println(s) + assert(s == "TestClass(y=0, z=hello, x=world)") + } + + "toString4" should "contains constructors parameters but without name" in { + @toString(true, true, false) class TestClass(val i: Int = 0, var j: Int) { val y: Int = 0 var z: String = "hello" @@ -34,25 +58,135 @@ class ToStringTest extends FlatSpec with Matchers { assert(s == "TestClass(1, 2, 0, hello, world)") } - // "toString3" should "failed when toString already defined" in { - // @toString(isContainsCtorParams = true) - // class TestClass(val i: Int = 0, var j: Int) { - // val y: Int = 0 - // var z: String = "hello" - // var x: String = "world" - // - // override def toString = s"TestClass($y, $z, $x, $i, $j)" - // } - // val s = new TestClass(1, 2).toString - // println(s) - // assert(s == "TestClass(1, 2, 0, hello world macro, hello world)") - // } - - "toString4" should "case class" in { - @toString(isContainsCtorParams = true) - case class TestClass(val i: Int = 0, var j: Int) + "toString5" should "case class without name" in { + @toString(true, false, false) + case class TestClass(i: Int = 0, var j: Int) { + val y: Int = 0 + var z: String = "hello" + var x: String = "world" + } + val s = TestClass(1, 2).toString + println(s) + assert(s == "TestClass(0, hello, world)") + } + + "toString6" should "case class with name" in { + @toString(true, false, true) + case class TestClass(i: Int = 0, var j: Int) { + val y: Int = 0 + var z: String = "hello" + var x: String = "world" + } + case class TestClass2(i: Int = 0, var j: Int) // No method body, use default toString val s = TestClass(1, 2).toString + val s2 = TestClass2(1, 2).toString println(s) - assert(s == "TestClass(1,2)") + println(s2) + + assert(s == "TestClass(y=0, z=hello, x=world)") + assert(s2 == "TestClass2(1,2)") + } + + "toString7" should "case class with name" in { + @toString(true, true, true) + case class TestClass(i: Int = 0, var j: Int) { + val y: Int = 0 + var z: String = "hello" + var x: String = "world" + } + val s = TestClass(1, 2).toString + println(s) + assert(s == "TestClass(i=1, j=2, y=0, z=hello, x=world)") + } + + "toString8" should "case class with name and itself" in { + @toString(true, true, true) + case class TestClass(i: Int = 0, var j: Int, k: TestClass) { + val y: Int = 0 + var z: String = "hello" + var x: String = "world" + } + val s = TestClass(1, 2, TestClass(1, 2, null)).toString + println(s) + assert(s == "TestClass(i=1, j=2, k=TestClass(i=1, j=2, k=null, y=0, z=hello, x=world), y=0, z=hello, x=world)") + } + + "toString9" should "case class with name and itself2" in { + @toString(true, true, true) + case class TestClass(i: Int = 0, var j: Int) { + val y: Int = 0 + var z: String = "hello" + var x: String = "world" + val t: TestClass = null // if not null, will error + } + val s = TestClass(1, 2).toString + println(s) + assert(s == "TestClass(i=1, j=2, y=0, z=hello, x=world, t=null)") + } + + "toString10" should "case class with name and itself3" in { + @toString(true, true, true) + case class TestClass(i: Int = 0, var j: Int, k: TestClass) { + val y: Int = 0 + var z: String = "hello" + var x: String = "world" + } + val s = TestClass(1, 2, TestClass(1, 2, TestClass(1, 3, null))).toString + println(s) + assert(s == "TestClass(i=1, j=2, k=TestClass(i=1, j=2, k=TestClass(i=1, j=3, k=null, y=0, z=hello, x=world), y=0, z=hello, x=world), y=0, z=hello, x=world)") + } + + "toString11" should "class with name and code block contains method" in { + @toString(true, true, true) + class TestClass(i: Int = 0, var j: Int) { + def helloWorld: String = i + "" + + println(helloWorld) + + // override def toString = s"TestClass(j=$j, i=$i)" // scalac override def toString = StringContext("TestClass(j=", ", i=", ")").s(j, i) + } + val s = new TestClass(1, 2).toString + println(s) + assert(s == "TestClass(i=1, j=2)") + } + + "toString12" should "class with name and not code block" in { + @toString(true, false, true) + class TestClass(i: Int = 0, var j: Int) + val s = new TestClass(1, 2).toString + println(s) + assert(s == "TestClass()") + + @toString(true, true, true) + class TestClass2(i: Int = 1, var j: Int = 2) + val s2 = new TestClass2(1, 2).toString + println(s2) + assert(s2 == "TestClass2(i=1, j=2)") + + @toString(true, true, false) + class TestClass3(i: Int = 1, var j: Int = 3) + val s3 = new TestClass3(1, 2).toString + println(s3) + assert(s3 == "TestClass3(1, 2)") + } + + "toString13" should "case class with name and not code block" in { + @toString(true, true, false) + case class TestClass(i: Int = 1, var j: Int = 3) + val s = TestClass(1, 2).toString + println(s) + assert(s == "TestClass(1, 2)") + + @toString(true, false, false) + case class TestClass2(i: Int = 1, var j: Int = 3) + val s2 = TestClass2(1, 2).toString + println(s2) + assert(s2 == "TestClass2()") + + @toString(true, true, true) + case class TestClass3(i: Int = 1, var j: Int = 3) + val s3 = TestClass3(1, 2).toString + println(s3) + assert(s3 == "TestClass3(i=1, j=2)") } }