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

add builder macro (#6)

* add builder macro

* add builder macro

* add builder macro
上级 a042cc9c
......@@ -9,6 +9,8 @@ scala macro and abstract syntax tree learning code.
## @toString
The `@toString` used to generate `toString` for Scala classes or a `toString` with parameter names for the case classes.
- Note
- `verbose` Whether to enable detailed log.
- `withFieldName` Whether to include the name of the field in the toString.
......@@ -58,6 +60,63 @@ val json = Json.toJson(person)
Json.fromJson[Person](json)
```
## @builder
The `@builder` used to generate builder pattern for Scala classes.
- Note
- Support `case class` / `class`.
- It can be used with `@toString`. But it needs to be put in the back.
- If there is no accompanying object, one will be generated to store the builder method.
- IDE support is not very good, a red prompt will appear, but the compilation is OK.
- Example
```scala
@builder
case class TestClass1(val i: Int = 0, var j: Int, x: String, o: Option[String] = Some(""))
val ret = TestClass1.builder().i(1).j(0).x("x").build()
assert(ret.toString == "TestClass1(1,0,x,Some())")
```
Compiler intermediate code:
```scala
object TestClass1 extends scala.AnyRef {
def <init>() = {
super.<init>();
()
};
def builder(): Builder = new Builder();
class Builder extends scala.AnyRef {
def <init>() = {
super.<init>();
()
};
private var i: Int = 0;
private var j: Int = _;
private var x: String = _;
private var o: Option[String] = Some("");
def i(i: Int): Builder = {
this.i = i;
this
};
def j(j: Int): Builder = {
this.j = j;
this
};
def x(x: String): Builder = {
this.x = x;
this
};
def o(o: Option[String]): Builder = {
this.o = o;
this
};
def build(): TestClass1 = TestClass1(i, j, x, o)
}
}
```
# How to use
Add library dependency
......
package io.github.dreamylost
import scala.annotation.{ StaticAnnotation, compileTimeOnly }
import scala.language.experimental.macros
import scala.reflect.macros.whitebox
/**
*
* @author 梦境迷离
* @since 2021/6/19
* @version 1.0
*/
@compileTimeOnly("enable macro to expand macro annotations")
final class builder extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro builderMacro.impl
}
object builderMacro {
def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
// @see https://scala-lang.org/files/archive/spec/2.13/05-classes-and-objects.html
def fieldSetMethod(c: whitebox.Context)(field: c.universe.Tree): c.universe.Tree = {
import c.universe._
field match {
case tree @ q"$mods var $tname: $tpt = $expr" =>
q"""
def $tname($tname: $tpt): Builder = {
this.$tname = $tname
this
}
"""
case tree @ q"$mods val $tname: $tpt = $expr" =>
q"""
def $tname($tname: $tpt): Builder = {
this.$tname = $tname
this
}
"""
}
}
def fieldDefinitionMethod(c: whitebox.Context)(field: c.universe.Tree): c.universe.Tree = {
import c.universe._
field match {
case tree @ q"$mods val $tname: $tpt = $expr" => q"""private var $tname: $tpt = $expr"""
case tree @ q"$mods var $tname: $tpt = $expr" => q"""private var $tname: $tpt = $expr"""
}
}
def fieldTermNameMethod(c: whitebox.Context)(field: c.universe.Tree): c.universe.Tree = {
import c.universe._
field match {
case tree @ q"$mods val $tname: $tpt = $expr" => q"""$tname"""
case tree @ q"$mods var $tname: $tpt = $expr" => q"""$tname"""
}
}
def builderTemplate(typeName: TypeName, fields: List[Tree], isCase: Boolean): c.universe.Tree = {
val termName = typeName.toTermName.toTermName
val builderFieldMethods = fields.map(f => fieldSetMethod(c)(f))
val builderFieldDefinitions = fields.map(f => fieldDefinitionMethod(c)(f))
val allFieldsTermName = fields.map(f => fieldTermNameMethod(c)(f))
q"""
def builder(): Builder = new Builder()
class Builder {
..$builderFieldDefinitions
..$builderFieldMethods
def build(): $typeName = ${if (isCase) q"$termName(..$allFieldsTermName)" else q"new $typeName(..$allFieldsTermName)"}
}
"""
}
def modifiedCompanion(compDeclOpt: Option[ModuleDef], builder: Tree, className: TypeName): c.universe.Tree = {
compDeclOpt map { compDecl =>
// Add the builder to the existing companion object
val q"object $obj extends ..$bases { ..$body }" = compDecl
val o =
q"""
object $obj extends ..$bases {
..$body
..$builder
}
"""
c.info(c.enclosingPosition, s"modifiedCompanion className: $className, exists obj: $o", force = true)
o
} getOrElse {
// Create a companion object with the builder
val o = q"object ${className.toTermName} { ..$builder }"
c.info(c.enclosingPosition, s"modifiedCompanion className: $className, new obj: $o", force = true)
o
}
}
def modifiedDeclaration(classDecl: ClassDef, compDeclOpt: Option[ModuleDef] = None): c.Expr[Nothing] = {
val (mods, className, fields) = classDecl match {
case q"$mods class $className(..$fields) extends ..$bases { ..$body }" =>
c.info(c.enclosingPosition, s"modifiedDeclaration className: $className, fields: $fields", force = true)
(mods, className, fields)
case _ => c.abort(c.enclosingPosition, s"Annotation is only supported on class. classDef: $classDecl")
}
c.info(c.enclosingPosition, s"modifiedDeclaration compDeclOpt: $compDeclOpt, fields: $fields", force = true)
className match {
case tp: TypeName =>
val builder = builderTemplate(tp, fields.asInstanceOf[List[Tree]], mods.asInstanceOf[Modifiers].hasFlag(Flag.CASE))
val compDecl = modifiedCompanion(compDeclOpt, builder, tp)
c.info(c.enclosingPosition, s"builder: $builder, compDecl: $compDecl", force = true)
// Return both the class and companion object declarations
c.Expr(
q"""
$classDecl
$compDecl
""")
}
}
c.info(c.enclosingPosition, s"builder annottees: $annottees", true)
val resTree = annottees.map(_.tree) match {
case (classDecl: ClassDef) :: Nil => modifiedDeclaration(classDecl)
case (classDecl: ClassDef) :: (compDecl: ModuleDef) :: Nil => modifiedDeclaration(classDecl, Some(compDecl))
case _ => c.abort(c.enclosingPosition, "Invalid annottee")
}
// Print the ast
c.info(
c.enclosingPosition,
"\n###### Expanded macro ######\n" + resTree.toString() + "\n###### Expanded macro ######\n",
force = true
)
resTree
}
}
......@@ -77,6 +77,8 @@ object jsonMacro {
}
c.info(c.enclosingPosition, s"json annottees: $annottees", true)
annottees.map(_.tree) match {
case (classDecl: ClassDef) :: Nil => modifiedDeclaration(classDecl)
case (classDecl: ClassDef) :: (compDecl: ModuleDef) :: Nil => modifiedDeclaration(classDecl, Some(compDecl))
......
......@@ -8,9 +8,9 @@ import scala.reflect.macros.whitebox
* toString for classes
*
* @author 梦境迷离
* @param verbose Whether to enable detailed log.
* @param verbose Whether to enable detailed log.
* @param withInternalField Whether to include the fields defined within a class.
* @param withFieldName Whether to include the name of the field in the toString.
* @param withFieldName Whether to include the name of the field in the toString.
* @since 2021/6/13
* @version 1.0
*/
......@@ -102,11 +102,13 @@ object stringMacro {
case q"new toString()" => (false, true, true)
case _ => c.abort(c.enclosingPosition, "unexpected annotation pattern!")
}
c.info(c.enclosingPosition, s"toString annottees: $annottees", true)
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 (classDecl: ClassDef) :: Nil => classDecl
case (classDecl: ClassDef) :: (compDecl: ModuleDef) :: Nil => classDecl
case _ => c.abort(c.enclosingPosition, "Unexpected annottee. Only applicable to class definitions.")
}
val isCase: Boolean = {
annotateeClass match {
......
package io.github.dreamylost
import org.scalatest.{ FlatSpec, Matchers }
/**
*
* @author 梦境迷离
* @since 2021/6/19
* @version 1.0
*/
class BuilderTest extends FlatSpec with Matchers {
"builder1" should "case class, non companion object" in {
@builder
case class TestClass1(val i: Int = 0, var j: Int, x: String, o: Option[String] = Some(""))
// field : <caseaccessor> <paramaccessor> val i: Int = 0, so default value is "_"
val ret = TestClass1.builder().i(1).j(0).x("x").build()
println(ret)
assert(ret.toString == "TestClass1(1,0,x,Some())")
}
"builder2" should "case class with companion object" in {
@builder
case class TestClass1(val i: Int = 0, var j: Int, x: String, o: Option[String] = Some(""))
object TestClass1
val ret = TestClass1.builder().i(1).j(0).x("x").build()
println(ret)
assert(ret.toString == "TestClass1(1,0,x,Some())")
}
"builder3" should "class with toString, non companion object" in {
@toString //"toString" must be before "builder"
@builder
class TestClass1(val i: Int = 0, var j: Int, x: String, o: Option[String] = Some(""))
val ret = TestClass1.builder().i(1).j(0).x("x").build()
println(ret)
assert(ret.toString == "TestClass1(i=1, j=0, x=x, o=Some())")
}
"builder4" should "class toString and companion object" in {
@toString
@builder
class TestClass1(val i: Int = 0, var j: Int, x: String, o: Option[String] = Some(""))
object TestClass1
val ret = TestClass1.builder().i(1).j(0).x("x").build()
println(ret)
assert(ret.toString == "TestClass1(i=1, j=0, x=x, o=Some())")
}
"builder5" should "case class with toString and companion object" in {
@toString
@builder
case class TestClass1(val i: Int = 0, var j: Int, x: String, o: Option[String] = Some(""))
object TestClass1
val ret = TestClass1.builder().i(1).j(0).x("x").build()
println(ret)
assert(ret.toString == "TestClass1(i=1, j=0, x=x, o=Some())")
}
"builder6" should "case class with toString, non companion object" in {
@toString
@builder
case class TestClass1(val i: Int = 0, var j: Int, x: String, o: Option[String] = Some(""))
val ret = TestClass1.builder().i(1).j(0).x("x").build()
println(ret)
assert(ret.toString == "TestClass1(i=1, j=0, x=x, o=Some())")
}
}
......@@ -11,7 +11,7 @@ import play.api.libs.json.Json
*/
class JsonTest extends FlatSpec with Matchers {
// class must be writed here
// class must be wrote here
@json
case class TestClass1(val i: Int = 0, var j: Int, x: String, o: Option[String] = Some(""))
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册