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

add `@constructor` (#37)

上级 beda0927
# scala-macro-tools [![Build](https://github.com/jxnu-liguobin/scala-macro-tools/actions/workflows/ScalaCI.yml/badge.svg)](https://github.com/jxnu-liguobin/scala-macro-tools/actions/workflows/ScalaCI.yml)
# scala-macro-tools [![Build](https://github.com/jxnu-liguobin/scala-macro-tools/actions/workflows/ScalaCI.yml/badge.svg)](https://github.com/jxnu-liguobin/scala-macro-tools/actions/workflows/ScalaCI.yml) [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.13.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22io.github.jxnu-liguobin%22%20AND%20a:%22scala-macro-tools_2.13%22)
我写该库的动机
--
......@@ -18,6 +18,14 @@
- `@synchronized`
- `@log`
- `@apply`
- `@constructor`
## 已知问题
- 不支持柯里化。
- 不支持泛型。
- `@constructor``@toString`同时使用,必须放最后。
- IDEA对宏的支持不是很好,所以会出现标红,不过编译没问题,调用结果也符合预期。
## @toString
......@@ -52,8 +60,8 @@ println(new TestClass(1, 2));
`@json`注解是向Play项目的样例类添加json format对象的最快方法。
- 说明
- 此注释启发来自[json-annotation](https://github.com/kifi/json-annotation),并做了优化,现在它可以与其他注解同时使用。
- 只有一个隐式的`val`值会被自动生成(如果伴生对象不存在的话,还会生成一个伴生对象用于存放该隐式值),此外没有其他的操作。
- 此注释启发来自[json-annotation](https://github.com/kifi/json-annotation),并做了优化,现在它可以与其他注解同时使用。
- 只有一个隐式的`val`值会被自动生成(如果伴生对象不存在的话,还会生成一个伴生对象用于存放该隐式值),此外没有其他的操作。
- 示例
......@@ -76,11 +84,8 @@ Json.fromJson[Person](json)
`@builder`注解用于为Scala类生成构造器模式。
- 说明
- 支持普通类和样例类。
- 如果该类没有伴生对象,将生成一个伴生对象来存储`builder`方法和类。
- 目前不支持主构造函数是柯里化的。
> IDEA对宏的支持不是很好,所以会出现标红,不过编译没问题,调用结果也符合预期。这意味着,目前不支持语法提示。
- 支持普通类和样例类。
- 如果该类没有伴生对象,将生成一个伴生对象来存储`builder`方法和类。
- 示例
......@@ -92,7 +97,7 @@ val ret = TestClass1.builder().i(1).j(0).x("x").build()
assert(ret.toString == "TestClass1(1,0,x,Some())")
```
宏生成的中间代码
宏生成的中间代码
```scala
object TestClass1 extends scala.AnyRef {
......@@ -136,7 +141,7 @@ object TestClass1 extends scala.AnyRef {
`@synchronized`注解是一个更方便、更灵活的用于同步方法的注解。
- 说明
- `lockedName` 指定自定义的锁对象的名称。可选,默认`this`
- `lockedName` 指定自定义的锁对象的名称。可选,默认`this`
- 支持静态方法(`object`中的函数)和实例方法(`class`中的函数)。
- 示例
......@@ -157,7 +162,7 @@ def getStr(k: Int): String = {
}
```
Compiler intermediate code:
宏生成的中间代码:
```scala
// 注意,它不会判断synchronized是否已经存在,因此如果synchronized已经存在,它将被使用两次。如下
......@@ -177,9 +182,6 @@ def getStr(k: Int): String = this.synchronized(k.$plus(""))
- `io.github.dreamylost.LogType.Log4j2` 使用 `org.apache.logging.log4j.Logger`
- `io.github.dreamylost.LogType.Slf4j` 使用 `org.slf4j.Logger`
- 支持普通类,样例类,单例对象。
> IDEA对宏的支持不是很好,所以会出现标红,不过编译没问题,调用结果也符合预期。这意味着,目前不支持语法提示。
- 示例
......@@ -197,17 +199,46 @@ def getStr(k: Int): String = this.synchronized(k.$plus(""))
- 说明
- `verbose` 指定是否开启详细编译日志。可选,默认`false`
- 仅支持在`class`上使用。
- 仅支持主构造函数。
- 目前不支持主构造函数是柯里化的。
> IDEA对宏的支持不是很好,所以会出现标红,不过编译没问题,调用结果也符合预期。这意味着,目前不支持语法提示。
- 仅支持在`class`上使用且仅支持主构造函数。
- 示例
```scala
@apply @toString class B2(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L))
println(B2(1, 2))
println(B2(1, 2, None, None)) //0.1.0,不携带字段的默认值到apply参数中,所以参数都是必传
```
## @constructor
`@constructor`注解用于为普通类生成辅助构造函数。
- 说明
- `verbose` 指定是否开启详细编译日志。可选,默认`false`
- `excludeFields` 指定是否需要排除不需要用于构造函数的`var`字段。可选,默认空(所有class内部的`var`字段都将作为构造函数的入参)。
- 仅支持在`class`上使用。
- 示例
```scala
@constructor(excludeFields = Seq("c")) //排除c字段。其中,a是val的不需要手动指定,自动排除。
class A2(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L)) {
private val a: Int = 1
var b: Int = 1 // 不携带字段的默认值到apply参数中,所以参数都是必传
protected var c: Int = _
def helloWorld: String = "hello world"
}
println(new A2(1, 2, None, None, 100))
```
宏生成的中间代码(仅构造函数部分):
```scala
def <init>(int: Int, j: Int, k: Option[String], t: Option[Long], b: Int) = {
<init>(int, j, k, t);
this.b = b
}
```
# 如何使用
......
# scala-macro-tools [![Build](https://github.com/jxnu-liguobin/scala-macro-tools/actions/workflows/ScalaCI.yml/badge.svg)](https://github.com/jxnu-liguobin/scala-macro-tools/actions/workflows/ScalaCI.yml)
# scala-macro-tools [![Build](https://github.com/jxnu-liguobin/scala-macro-tools/actions/workflows/ScalaCI.yml/badge.svg)](https://github.com/jxnu-liguobin/scala-macro-tools/actions/workflows/ScalaCI.yml) [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.13.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22io.github.jxnu-liguobin%22%20AND%20a:%22scala-macro-tools_2.13%22)
Motivation
--
......@@ -17,6 +17,16 @@ Learn Scala macro and abstract syntax tree.
- `@synchronized`
- `@log`
- `@apply`
- `@constructor`
## Known Issues
- Currying is not supported.
- Generic is not supported.
- When `@constructor` and `@toString` are used together, the former must be put last.
- IDE support is not very good, a red prompt will appear, but the compilation is OK.
## @toString
......@@ -28,7 +38,6 @@ The `@toString` used to generate `toString` for Scala classes or a `toString` wi
- `includeInternalFields` Whether to include the fields defined within a class. Not in a primary constructor, default is `true`.
- `callSuper` Whether to include the super's `toString`, default is `false`. Not support if super class is a trait.
- Support `case class` and `class`.
- Currying is not supported in constructors at present.
- Example
......@@ -52,11 +61,11 @@ println(new TestClass(1, 2));
The `@json` scala macro annotation is the quickest way to add a JSON format to your Play project's case classes.
- Note
- This annotation is drawn from [json-annotation](https://github.com/kifi/json-annotation) and have some
optimization.
- It can also be used when there are other annotations on the case classes.
- Only an implicit `val` was generated automatically(Maybe generate a companion object if it not exists), and there are no other
operations.
- This annotation is drawn from [json-annotation](https://github.com/kifi/json-annotation) and have some
optimization.
- It can also be used when there are other annotations on the case classes.
- Only an implicit `val` was generated automatically(Maybe generate a companion object if it not exists), and there are no other
operations.
- Example
```scala
......@@ -78,11 +87,8 @@ Json.fromJson[Person](json)
The `@builder` used to generate builder pattern for Scala classes.
- Note
- Support `case class` / `class`.
- If there is no companion object, one will be generated to store the `builder` class and method.
- Currying is not supported in constructors at present.
> IDE support is not very good, a red prompt will appear, but the compilation is OK. It only for the fields in the primary constructor
- Support `case class` / `class`.
- If there is no companion object, one will be generated to store the `builder` class and method.
- Example
......@@ -94,7 +100,7 @@ val ret = TestClass1.builder().i(1).j(0).x("x").build()
assert(ret.toString == "TestClass1(1,0,x,Some())")
```
Compiler intermediate code:
Compiler macro code:
```scala
object TestClass1 extends scala.AnyRef {
......@@ -138,7 +144,7 @@ object TestClass1 extends scala.AnyRef {
The `@synchronized` is a more convenient and flexible synchronous annotation.
- Note
- `lockedName` The name of the custom lock obj, default is `this`.
- `lockedName` The name of the custom lock obj, default is `this`.
- Support static and instance methods.
- Example
......@@ -159,7 +165,7 @@ def getStr(k: Int): String = {
}
```
Compiler intermediate code:
Compiler macro code:
```scala
// Note that it will not judge whether synchronized already exists, so if synchronized already exists, it will be used twice.
......@@ -178,10 +184,7 @@ The `@log` does not use mixed or wrapper, but directly uses macro to generate de
- `io.github.dreamylost.LogType.JLog` use `java.util.logging.Logger`
- `io.github.dreamylost.LogType.Log4j2` use `org.apache.logging.log4j.Logger`
- `io.github.dreamylost.LogType.Slf4j` use `org.slf4j.Logger`
- Support `class`, `case class` and `object`.
> IDE support is not very good, a red prompt will appear, but the compilation is OK. You need to provide their dependencies and configuration, please refer to the test.
- Support `class`, `case class` and `object`.
- Example
......@@ -201,9 +204,6 @@ The `@apply` used to generate `apply` method for primary construction of ordinar
- `verbose` Whether to enable detailed log.
- Only support `class`.
- Only support **primary construction**.
- Currying is not supported for constructors at present.
> IDE support is not very good, a red prompt will appear, but the compilation is OK. You need to provide their dependencies and configuration, please refer to the test.
- Example
......@@ -212,6 +212,39 @@ The `@apply` used to generate `apply` method for primary construction of ordinar
println(B2(1, 2))
```
## @constructor
The `@constructor` used to generate secondary constructor method for classes.
- Note
- `verbose` Whether to enable detailed log.
- `excludeFields` Whether to exclude the specified `var` fields, default is `Nil`.
- Only support `class`.
- Example
```scala
@constructor(excludeFields = Seq("c"))
class A2(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L)) {
private val a: Int = 1
var b: Int = 1 //The default value of the field is not carried to the apply parameter, so all parameters are required.
protected var c: Int = _
def helloWorld: String = "hello world"
}
println(new A2(1, 2, None, None, 100))
```
Compiler macro code(Only constructor def):
```scala
def <init>(int: Int, j: Int, k: Option[String], t: Option[Long], b: Int) = {
<init>(int, j, k, t);
this.b = b
}
```
# How to use
Add library dependency
......
version in ThisBuild := "0.1.0-SNAPSHOT"
version in ThisBuild := "0.0.5"
......@@ -18,4 +18,17 @@ object Main extends App {
val s = new TestClass(1, 2).toString
println(s)
@toString(includeInternalFields = false, includeFieldNames = true)
@apply
@builder class A2(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L)) {
private val a: Int = 1
var b: Int = 1
protected var c: Int = _
def helloWorld: String = "hello world"
}
println(A2(1, 2, None, None)) //use apply and toString
println(A2.builder().int(1).j(2).k(Option("hello")).t(None).build()) //use builder and toString
}
......@@ -26,14 +26,29 @@ trait MacroCommon {
* @param annottees
* @return Return ClassDef
*/
def checkAndReturnClass(c: whitebox.Context)(annottees: c.Expr[Any]*): c.universe.ClassDef = {
def checkAndGetClassDef(c: whitebox.Context)(annottees: c.Expr[Any]*): c.universe.ClassDef = {
import c.universe._
val annotateeClass: ClassDef = annottees.map(_.tree).toList match {
annottees.map(_.tree).toList match {
case (classDecl: ClassDef) :: Nil => classDecl
case (classDecl: ClassDef) :: (compDecl: ModuleDef) :: Nil => classDecl
case _ => c.abort(c.enclosingPosition, "Unexpected annottee. Only applicable to class definitions.")
}
annotateeClass
}
/**
* Get class if it exists.
*
* @param c
* @param annottees
* @return Return ClassDef without verify.
*/
def tryGetClassDef(c: whitebox.Context)(annottees: c.Expr[Any]*): Option[c.universe.ClassDef] = {
import c.universe._
annottees.map(_.tree).toList match {
case (classDecl: ClassDef) :: Nil => Some(classDecl)
case (classDecl: ClassDef) :: (compDecl: ModuleDef) :: Nil => Some(classDecl)
case _ => None
}
}
/**
......@@ -43,12 +58,35 @@ trait MacroCommon {
* @param annottees
* @return
*/
def getCompanionObject(c: whitebox.Context)(annottees: c.Expr[Any]*): Option[c.universe.ModuleDef] = {
def tryGetCompanionObject(c: whitebox.Context)(annottees: c.Expr[Any]*): Option[c.universe.ModuleDef] = {
import c.universe._
annottees.map(_.tree).toList match {
case (classDecl: ClassDef) :: Nil => None
case (classDecl: ClassDef) :: (compDecl: ModuleDef) :: Nil => Some(compDecl)
case _ => c.abort(c.enclosingPosition, "Unexpected annottee. Only applicable to class definitions.")
case (compDecl: ModuleDef) :: Nil => Some(compDecl)
case _ => None
}
}
/**
* Wrap tree result with companion object.
* @param c
* @param resTree class
* @param annottees
* @return
*/
def treeResultWithCompanionObject(c: whitebox.Context)(resTree: c.Tree, annottees: c.Expr[Any]*): c.universe.Tree = {
import c.universe._
val companionOpt = tryGetCompanionObject(c)(annottees: _*)
if (companionOpt.isEmpty) {
resTree
} else {
val q"$mods object $obj extends ..$bases { ..$body }" = companionOpt.get
val companion = q"$mods object $obj extends ..$bases { ..$body }"
q"""
$resTree
$companion
"""
}
}
......@@ -90,23 +128,46 @@ trait MacroCommon {
}
/**
* Expand the constructor and get the field TermName
* Expand the constructor and get the field TermName.
*
* @param c
* @param field
* @return
*/
def fieldTermNameMethod(c: whitebox.Context)(field: c.universe.Tree): c.universe.Tree = {
def fieldTermName(c: whitebox.Context)(field: c.universe.Tree): c.universe.TermName = {
import c.universe._
field match {
case q"$mods val $tname: $tpt = $expr" => q"""$tname"""
case q"$mods var $tname: $tpt = $expr" => q"""$tname"""
case q"$mods val $tname: $tpt = $expr" => tname.asInstanceOf[TermName]
case q"$mods var $tname: $tpt = $expr" => tname.asInstanceOf[TermName]
}
}
/**
* Expand the constructor and get the field with assign.
* @param c
* @param annotteeClassParams
* @return
*/
def fieldAssignExpr(c: whitebox.Context)(annotteeClassParams: Seq[c.Tree]): Seq[c.Tree] = {
import c.universe._
annotteeClassParams.map {
case q"$mods var $tname: $tpt = $expr" => q"$tname: $tpt" //Ignore expr
case q"$mods val $tname: $tpt = $expr" => q"$tname: $tpt"
}
}
/**
* Modify companion objects.
*
* @param c
* @param compDeclOpt
* @param codeBlock
* @param className
* @return
*/
def modifiedCompanion(c: whitebox.Context)(
compDeclOpt: Option[c.universe.ModuleDef],
apply: c.Tree, className: c.TypeName): c.universe.Tree = {
codeBlock: c.Tree, className: c.TypeName): c.universe.Tree = {
import c.universe._
compDeclOpt map { compDecl =>
val q"$mods object $obj extends ..$bases { ..$body }" = compDecl
......@@ -114,16 +175,29 @@ trait MacroCommon {
q"""
$mods object $obj extends ..$bases {
..$body
..$apply
..$codeBlock
}
"""
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} { ..$apply }"
val o = q"object ${className.toTermName} { ..$codeBlock }"
c.info(c.enclosingPosition, s"modifiedCompanion className: $className, new obj: $o", force = true)
o
}
}
/**
* Extract the internal fields of members belonging to the class, but not in primary constructor.
*
* @param c
*/
def getClassMemberValDef(c: whitebox.Context)(annotteeClassDefinitions: Seq[c.Tree]): Seq[c.Tree] = {
import c.universe._
annotteeClassDefinitions.filter(p => p match {
case _: ValDef => true
case _ => false
})
}
}
......@@ -31,23 +31,24 @@ object applyMacro extends MacroCommon {
c.info(c.enclosingPosition, s"annottees: $annottees, args: $args", force = args)
val annotateeClass: ClassDef = checkAndReturnClass(c)(annottees: _*)
val annotateeClass: ClassDef = checkAndGetClassDef(c)(annottees: _*)
val isCase: Boolean = isCaseClass(c)(annotateeClass)
c.info(c.enclosingPosition, s"impl argument: $args, isCase: $isCase", force = args)
def modifiedDeclaration(classDecl: ClassDef, compDeclOpt: Option[ModuleDef] = None): Any = {
val (className, fields) = classDecl match {
val (className, annotteeClassParams) = classDecl match {
case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends ..$bases { ..$body }" =>
c.info(c.enclosingPosition, s"modifiedDeclaration className: $tpname, paramss: $paramss", force = args)
(tpname, paramss)
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 = args)
val fieldNames = fields.asInstanceOf[List[List[Tree]]].flatten.map(f => fieldTermNameMethod(c)(f))
c.info(c.enclosingPosition, s"modifiedDeclaration compDeclOpt: $compDeclOpt, annotteeClassParams: $annotteeClassParams", force = args)
val fieldNames = annotteeClassParams.asInstanceOf[List[List[Tree]]].flatten.map(f => fieldTermName(c)(f))
val cName = className match {
case t: TypeName => t
}
val compDecl = modifiedCompanion(c)(compDeclOpt, q"""def apply(...$fields): $className = new $className(..$fieldNames)""", cName)
val annotteeClassParamsOnlyAssignExpr = fieldAssignExpr(c)(annotteeClassParams.asInstanceOf[List[List[Tree]]].flatten)
val compDecl = modifiedCompanion(c)(compDeclOpt, q"""def apply(..$annotteeClassParamsOnlyAssignExpr): $className = new $className(..$fieldNames)""", cName)
c.Expr(
q"""
$classDecl
......
......@@ -43,7 +43,7 @@ object builderMacro extends MacroCommon {
}
}
def fieldDefinitionMethod(c: whitebox.Context)(field: c.universe.Tree): c.universe.Tree = {
def fieldDefinition(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"""
......@@ -51,19 +51,11 @@ object builderMacro extends MacroCommon {
}
}
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))
val builderFieldDefinitions = fields.map(f => fieldDefinition(c)(f))
val allFieldsTermName = fields.map(f => fieldTermName(c)(f))
q"""
def builder(): Builder = new Builder()
......
package io.github.dreamylost
import scala.annotation.{ StaticAnnotation, compileTimeOnly }
import scala.language.experimental.macros
import scala.reflect.macros.whitebox
/**
* annotation to generate secondary constructor method for classes.
*
* @author 梦境迷离
* @param verbose Whether to enable detailed log.
* @param excludeFields Whether to exclude the specified var fields.
* @since 2021/7/3
* @version 1.0
*/
@compileTimeOnly("enable macro to expand macro annotations")
final class constructor(
verbose: Boolean = false,
excludeFields: Seq[String] = Nil
) extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro constructorMacro.impl
}
object constructorMacro extends MacroCommon {
def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
val args = c.prefix.tree match {
case q"new constructor(verbose=$verbose)" => (c.eval[Boolean](c.Expr(verbose)), Nil)
case q"new constructor(excludeFields=$excludeFields)" => (false, c.eval[Seq[String]](c.Expr(excludeFields)))
case q"new constructor(verbose=$verbose, excludeFields=$excludeFields)" => (c.eval[Boolean](c.Expr(verbose)), c.eval[Seq[String]](c.Expr(excludeFields)))
case q"new constructor()" => (false, Nil)
case _ => c.abort(c.enclosingPosition, "unexpected annotation pattern!")
}
val annotateeClass: ClassDef = checkAndGetClassDef(c)(annottees: _*)
val isCase: Boolean = isCaseClass(c)(annotateeClass)
if (isCase) {
c.abort(c.enclosingPosition, s"Annotation is not supported on case class. classDef: $annotateeClass")
}
c.info(c.enclosingPosition, s"annottees: $annottees, annotateeClass: $annotateeClass", args._1)
def modifiedDeclaration(classDecl: ClassDef, compDeclOpt: Option[ModuleDef] = None): Any = {
val (annotteeClassParams, annotteeClassDefinitions) = classDecl match {
case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" =>
c.info(c.enclosingPosition, s"modifiedDeclaration className: $tpname, paramss: $paramss", force = args._1)
(paramss, stats.asInstanceOf[Seq[Tree]])
case _ => c.abort(c.enclosingPosition, s"Annotation is only supported on class. classDef: $classDecl")
}
// Extract the field of the primary constructor.
val annotteeClassParamsOnlyAssignExpr = fieldAssignExpr(c)(annotteeClassParams.asInstanceOf[List[List[Tree]]].flatten)
// Extract the internal fields of members belonging to the class, but not in primary constructor.
val annotteeClassFieldDefinitions = getClassMemberValDef(c)(annotteeClassDefinitions)
val excludeFields = args._2
/**
* Extract the internal fields of members belonging to the class, but not in primary constructor and only `var`.
*/
def getClassMemberVarDefOnlyAssignExpr(): Seq[c.Tree] = {
import c.universe._
getClassMemberValDef(c)(annotteeClassDefinitions).filter(_ match {
case q"$mods var $tname: $tpt = $expr" if !excludeFields.contains(tname.asInstanceOf[TermName].decodedName.toString) => true
case _ => false
}).map {
case q"$mods var $tname: $tpt = $expr" => q"$tname: $tpt"
}
}
val annotteeClassFieldDefinitionsOnlyAssignExpr = getClassMemberVarDefOnlyAssignExpr()
if (annotteeClassFieldDefinitionsOnlyAssignExpr.isEmpty) {
c.abort(c.enclosingPosition, s"Annotation is only supported on class when the internal field (declare as 'var') is nonEmpty. classDef: $classDecl")
}
val annotteeClassFieldNames = annotteeClassFieldDefinitions.filter(_ match {
case q"$mods var $tname: $tpt = $expr" if !excludeFields.contains(tname.asInstanceOf[TermName].decodedName.toString) => true
case _ => false
}).map {
case q"$mods var $tname: $tpt = $expr" => tname.asInstanceOf[TermName]
}
c.info(c.enclosingPosition, s"modifiedDeclaration compDeclOpt: $compDeclOpt, annotteeClassParams: $annotteeClassParams", force = args._1)
// not suppport currying
val ctorFieldNames = annotteeClassParams.asInstanceOf[List[List[Tree]]].flatten.map(f => fieldTermName(c)(f))
def getConstructorTemplate(): c.universe.Tree = {
q"""
def this(..${annotteeClassParamsOnlyAssignExpr ++ annotteeClassFieldDefinitionsOnlyAssignExpr}){
this(..$ctorFieldNames)
..${annotteeClassFieldNames.map(f => q"this.$f = $f")}
}
"""
}
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.:+(getConstructorTemplate())} }"
}
c.Expr[Any](treeResultWithCompanionObject(c)(resTree, annottees: _*))
}
val resTree = handleWithImplType(c)(annottees: _*)(modifiedDeclaration)
printTree(c)(force = args._1, resTree.tree)
resTree
}
}
......@@ -107,15 +107,25 @@ object logMacro extends MacroCommon {
case (classDef @ q"$mods object $tpname extends { ..$earlydefns } with ..$parents { $self => ..$stats }") :: _ =>
LogType.getLogImpl(args._2).getTemplate(c)(tpname.asInstanceOf[TermName].decodedName.toString, isClass = false)
case _ => c.abort(c.enclosingPosition, s"Annotation is only supported on class or object.")
}
// add result into class
val resTree = annottees.map(_.tree) 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 => ..${List(logTree) ::: stats.toList} }"
val resTree = q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..${List(logTree) ::: stats.toList} }"
treeResultWithCompanionObject(c)(resTree, annottees: _*) //we should return with companion object. Even if we didn't change it.
case q"$mods object $tpname extends { ..$earlydefns } with ..$parents { $self => ..$stats }" :: _ =>
q"$mods object $tpname extends { ..$earlydefns } with ..$parents { $self => ..${List(logTree) ::: stats.toList} }"
//we should return with class def. Even if we didn't change it, but the context class was not passed in.
// val annotateeClassOpt: Option[ClassDef] = getClassDef(c)(annottees: _*)
// if(annotateeClassOpt.isEmpty){
// resTree
// } else {
// q"""
// ${annotateeClassOpt.get}
// $resTree
// """
// }
}
printTree(c)(force = args._1, resTree)
......
package io.github.dreamylost
import io.github.dreamylost.constructorMacro.treeResultWithCompanionObject
import scala.annotation.{ StaticAnnotation, compileTimeOnly }
import scala.language.experimental.macros
import scala.reflect.macros.whitebox
......@@ -101,7 +103,7 @@ object stringMacro extends MacroCommon {
case tree: Tree => Some(tree) // TODO type check better
case _ => None
}
superClassDef.fold(toString)(sc => {
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)"
......@@ -125,13 +127,15 @@ object stringMacro extends MacroCommon {
case q"new toString($aa, $bb, $cc, $dd)" => (c.eval[Boolean](c.Expr(aa)), c.eval[Boolean](c.Expr(bb)), c.eval[Boolean](c.Expr(cc)), c.eval[Boolean](c.Expr(dd)))
case q"new toString(includeInternalFields=$bb, includeFieldNames=$cc)" => (false, c.eval[Boolean](c.Expr(bb)), c.eval[Boolean](c.Expr(cc)), false)
case q"new toString(includeInternalFields=$bb)" => (false, c.eval[Boolean](c.Expr(bb)), true, false)
case q"new toString(includeFieldNames=$cc)" => (false, true, c.eval[Boolean](c.Expr(cc)), false)
case q"new toString()" => (false, true, true, false)
case _ => c.abort(c.enclosingPosition, "unexpected annotation pattern!")
}
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 = checkAndReturnClass(c)(annottees: _*)
val annotateeClass: ClassDef = checkAndGetClassDef(c)(annottees: _*)
val isCase: Boolean = isCaseClass(c)(annotateeClass)
c.info(c.enclosingPosition, s"impl argument: $argument, isCase: $isCase", force = argument.verbose)
......@@ -140,17 +144,8 @@ object stringMacro extends MacroCommon {
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)} }"
}
val companionOpt = getCompanionObject(c)(annottees: _*)
val res = if (companionOpt.isEmpty) {
resTree
} else {
val q"$mods object $obj extends ..$bases { ..$body }" = companionOpt.get
val companion = q"$mods object $obj extends ..$bases { ..$body }"
q"""
$resTree
$companion
"""
}
val res = treeResultWithCompanionObject(c)(resTree, annottees: _*)
printTree(c)(argument.verbose, res)
c.Expr[Any](res)
}
......
......@@ -19,21 +19,20 @@ class ApplyTest extends AnyFlatSpec with Matchers {
"""@toString @apply class A(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L))""" should compile
@toString
@apply class A2(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L))
println(A2(1, 2))
println(A2(1, 2, None, None))
"""@apply @toString class B(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L))""" should compile
@apply
@toString class B2(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L))
println(B2(1, 2))
println(B2(1, 2, None, None))
// exists object
"""@apply @toString class B(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L));object B3""" should compile
@apply
@toString class B3(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L))
object B3
println(B3(1, 2))
println(B3(1, 2, None, None))
}
"apply2" should "failed at class" in {
// FAILED, not support currying!!
"""@apply @toString class C(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L))(o: Int = 1)""" shouldNot compile
......
package io.github.dreamylost
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
/**
*
* @author 梦境迷离
* @since 2021/7/3
* @version 1.0
*/
class ConstructorTest extends AnyFlatSpec with Matchers {
"constructor1" should "failed at object" in {
""" @constructor
| object A2 {
| private val a: Int = 1
| var b: Int = 1
| def helloWorld: String = "hello world"
| }""".stripMargin shouldNot compile
""" @apply @toString @builder @constructor(excludeFields=Seq("c"))
| class A2(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L)) {
| private val a: Int = 1
| var b: Int = 1
| protected var c: Int = _
|
| def helloWorld: String = "hello world"
| }
| A2(1, 2, None, None).c
| """.stripMargin shouldNot compile
}
"constructor2" should "ok at class" in {
""" @constructor(verbose = true)
| class A2(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L)) {
| private val a: Int = 1
| private var b: Int = 1
| protected var c: Int = _
|
| def helloWorld: String = "hello world"
| }""".stripMargin should compile
""" @constructor
| class A2(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L)) {
| private val a: Int = 1
| var b: Int = 1
| protected var c: Int = _
|
| def helloWorld: String = "hello world"
| }""".stripMargin should compile
""" @apply @builder @toString
| class A2(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L)) {
| private val a: Int = 1
| var b: Int = 1
| protected var c: Int = _
|
| def helloWorld: String = "hello world"
| }""".stripMargin should compile
""" @apply @builder @toString @constructor
| class A2(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L)) {
| private val a: Int = 1
| var b: Int = 1
| protected var c: Int = _
|
| def helloWorld: String = "hello world"
| }""".stripMargin should compile
""" @builder @apply @toString @constructor
| class A2(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L)) {
| private val a: Int = 1
| var b: Int = 1
| protected var c: Int = _
|
| def helloWorld: String = "hello world"
| }""".stripMargin should compile
""" @builder @toString @apply @constructor
| class A2(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L)) {
| private val a: Int = 1
| var b: Int = 1
| protected var c: Int = _
|
| def helloWorld: String = "hello world"
| }""".stripMargin should compile
""" @apply @toString @builder @constructor
| class A2(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L)) {
| private val a: Int = 1
| var b: Int = 1
| protected var c: Int = _
|
| def helloWorld: String = "hello world"
| }""".stripMargin should compile
""" @apply @toString @builder @constructor(excludeFields=Seq("c"))
| class A2(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L)) {
| private val a: Int = 1
| var b: Int = 1
| protected var c: Int = _
|
| def helloWorld: String = "hello world"
| }""".stripMargin should compile
}
"constructor3" should "failed at object" in {
@apply
@toString
@builder
@constructor(excludeFields = Seq("c"))
class A2(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L)) {
private val a: Int = 1
var b: Int = 1
protected var c: Int = _
def helloWorld: String = "hello world"
}
println(A2(1, 2, None, Some(12L)))
println(A2.builder().int(1).j(2).build())
println(new A2(1, 2, None, None, 100))
}
}
......@@ -13,8 +13,8 @@ class LogTest extends AnyFlatSpec with Matchers {
"log1" should "ok on class" in {
"""@log(verbose=true) class TestClass1(val i: Int = 0, var j: Int) {
log.info("hello")
}""" should compile
log.info("hello")
}""" should compile
"""@log class TestClass2(val i: Int = 0, var j: Int)""" should compile
"""@log() class TestClass3(val i: Int = 0, var j: Int)""" should compile
......@@ -25,8 +25,8 @@ class LogTest extends AnyFlatSpec with Matchers {
"log2" should "ok on case class" in {
"""@log(verbose=true) case class TestClass1(val i: Int = 0, var j: Int) {
log.info("hello")
}""" should compile
log.info("hello")
}""" should compile
"""@log case class TestClass2(val i: Int = 0, var j: Int)""" should compile
"""@log() case class TestClass3(val i: Int = 0, var j: Int)""" should compile
......@@ -37,8 +37,8 @@ class LogTest extends AnyFlatSpec with Matchers {
"log3" should "ok on object" in {
"""@log(verbose=true) object TestClass1 {
log.info("hello")
}""" should compile
log.info("hello")
}""" should compile
"""@log object TestClass2""" should compile
"""@log() object TestClass3""" should compile
......@@ -49,8 +49,8 @@ class LogTest extends AnyFlatSpec with Matchers {
"log4 log4j2" should "ok on object" in {
"""@log(verbose=true) object TestClass1 {
log.info("hello")
}""" should compile
log.info("hello")
}""" should compile
"""@log object TestClass2""" should compile
"""@log() object TestClass3""" should compile
......@@ -61,8 +61,8 @@ class LogTest extends AnyFlatSpec with Matchers {
"log5 slf4j" should "ok on object" in {
"""@log(verbose=true) object TestClass1 {
log.info("hello")
}""" should compile
log.info("hello")
}""" should compile
"""@log object TestClass2""" should compile
"""@log() object TestClass3""" should compile
......@@ -73,8 +73,8 @@ class LogTest extends AnyFlatSpec with Matchers {
"log6 log4j2" should "ok on class" in {
"""@log(verbose=true) class TestClass1(val i: Int = 0, var j: Int) {
log.info("hello")
}""" should compile
log.info("hello")
}""" should compile
"""@log class TestClass2(val i: Int = 0, var j: Int)""" should compile
"""@log() class TestClass3(val i: Int = 0, var j: Int)""" should compile
......@@ -85,8 +85,8 @@ class LogTest extends AnyFlatSpec with Matchers {
"log7 slf4j" should "ok on class" in {
"""@log(verbose=true) class TestClass1(val i: Int = 0, var j: Int) {
log.info("hello")
}""" should compile
log.info("hello")
}""" should compile
"""@toString @builder @log class TestClass2(val i: Int = 0, var j: Int)""" should compile //Use with multiple annotations
"""@log() class TestClass3(val i: Int = 0, var j: Int)""" should compile
......@@ -96,4 +96,62 @@ class LogTest extends AnyFlatSpec with Matchers {
"""@log(verbose=true, logType=io.github.dreamylost.LogType.Slf4j) class TestClass6(val i: Int = 0, var j: Int){ log.info("hello world") }""" should compile
"""@log(io.github.dreamylost.LogType.Slf4j) class TestClass6(val i: Int = 0, var j: Int){ log.info("hello world") }""" should compile //default verbose is false
}
"log8 slf4j" should "ok on class and has object" in {
"""@log(verbose=true) class TestClass1(val i: Int = 0, var j: Int) {
log.info("hello")
}""" should compile
"""@toString @builder @log class TestClass2(val i: Int = 0, var j: Int)""" should compile //Use with multiple annotations
"""@log() class TestClass3(val i: Int = 0, var j: Int)""" should compile
"""@log(verbose=true) class TestClass4(val i: Int = 0, var j: Int)""" should compile
"""@log(logType=io.github.dreamylost.LogType.Slf4j) class TestClass5(val i: Int = 0, var j: Int)""" should compile
"""@log(verbose=true, logType=io.github.dreamylost.LogType.Slf4j) class TestClass6(val i: Int = 0, var j: Int)""" should compile
"""@log(verbose=true, logType=io.github.dreamylost.LogType.Slf4j) class TestClass6(val i: Int = 0, var j: Int){ log.info("hello world") }""" should compile
"""@log(io.github.dreamylost.LogType.Slf4j) @builder class TestClass6(val i: Int = 0, var j: Int){ log.info("hello world") }
| @log(io.github.dreamylost.LogType.Slf4j) object TestClass6 { log.info("hello world");builder() }""".stripMargin should compile //default verbose is false
}
"log9 slf4j" should "ok on class and it object" in {
"""
|@log(io.github.dreamylost.LogType.Slf4j) @builder class TestClass6(val i: Int = 0, var j: Int){ log.info("hello world") }
|@log(io.github.dreamylost.LogType.Slf4j) object TestClass6 { log.info("hello world"); builder()}
|""".stripMargin should compile
}
"log10 slf4j" should "failed on case class and it object" in {
"""
| @log(io.github.dreamylost.LogType.Slf4j)
| @builder case class TestClass6(val i: Int = 0, var j: Int) {
| log.info("hello world")
| }
| @log(logType = io.github.dreamylost.LogType.Slf4j) object TestClass6 {
| log.info("hello world"); builder()
| }
|""".stripMargin shouldNot compile //The context of class was not passed in object macro
}
"log11 slf4j" should "ok on class and it object" in {
"""
| @log(io.github.dreamylost.LogType.Slf4j)
| @builder class TestClass6(val i: Int = 0, var j: Int) {
| log.info("hello world")
| }
| @log(logType = io.github.dreamylost.LogType.Slf4j) object TestClass6 {
| log.info("hello world"); builder()
| }
|""".stripMargin should compile
"""
| @builder
| @log(io.github.dreamylost.LogType.Slf4j)
| class TestClass6(val i: Int = 0, var j: Int) {
| log.info("hello world")
| }
| @log(logType = io.github.dreamylost.LogType.Slf4j) object TestClass6 {
| log.info("hello world"); builder()
| }
|""".stripMargin should compile
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册