未验证 提交 258cfa46 编写于 作者: 梦境迷离's avatar 梦境迷离 提交者: GitHub

Merge pull request #1 from bitlap/dev

support field name
......@@ -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 {
<paramaccessor> val i: Int = _;
<paramaccessor> var j: Int = _;
def <init>(i: Int = 0, j: Int) = {
super.<init>();
()
};
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 {
<paramaccessor> val i: Int = _;
<paramaccessor> var j: Int = _;
def <init>(i: Int = 0, j: Int) = {
super.<init>();
()
};
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
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 <name of the field>+"="+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)
}
......
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)")
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册