diff --git a/README.md b/README.md index f492ed8cd4520cf10e0176691bd80a527ae680b7..cd91b482afdecb657b516d682d42194fae2b32b0 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,36 @@ # 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) -Motivation +我写该库的动机 -- -Learn Scala macro and abstract syntax tree. +学习Scala宏编程(macro)和抽象语法树(ast)。 -> The project is currently experimental +> 本项目目前处于实验阶段 -# Features +[中文说明](./README.md)|[English](./README_EN.md) + + +# 功能 + +- `@toString` +- `@json` +- `@builder` +- `@synchronized` +- `@log` +- `@apply` ## @toString -The `@toString` used to generate `toString` for Scala classes or a `toString` with parameter names for the case classes. +`@toString`注解用于为Scala类生成`toString`方法。 -- Note - - `verbose` Whether to enable detailed log. - - `includeFieldNames` Whether to include the names of the field in the `toString`. - - `includeInternalFields` Whether to include the fields defined within a class. Not in a primary constructor. - - `callSuper` Whether to include the super's `toString`. Not support if super class is a trait. - - Support `case class` and `class`. +- 说明 + - `verbose` 指定是否开启详细编译日志。可选,默认`false`。 + - `includeFieldNames` 指定是否在`toString`中包含字段的名称。可选,默认`true`。 + - `includeInternalFields` 指定是否包含类内部定义的字段。它们不是在主构造函数中。可选,默认`true`。 + - `callSuper` 指定是否包含`super`的`toString`方法值。如果超级类是一种特质,则不支持。可选,默认`false`。 + - 支持普通类和样例类。 -- Example +- 示例 ```scala class TestClass(val i: Int = 0, var j: Int) { @@ -32,28 +42,26 @@ class TestClass(val i: Int = 0, var j: Int) { println(new TestClass(1, 2)); ``` -|includeInternalFields / includeFieldNames| false |true -| --------------------------------- | ---------------------------------- |----------------------------------| -|false|```TestClass(1, 2)``` |```TestClass(i=0, j=2)```| -|true|```TestClass(1, 2, 0, hello, world)```|```TestClass(i=1, j=2, y=0, z=hello, x=world)```| +| includeInternalFields / includeFieldNames | false | true | +| ----------------------------------------- | -------------------------------------- | ------------------------------------------------ | +| false | ```TestClass(1, 2)``` | ```TestClass(i=0, j=2)``` | +| true | ```TestClass(1, 2, 0, hello, world)``` | ```TestClass(i=1, j=2, y=0, z=hello, x=world)``` | ## @json -The `@json` scala macro annotation is the quickest way to add a JSON format to your Play project's case classes. +`@json`注解是向Play项目的样例类添加json format对象的最快方法。 -- 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. -- Example +- 说明 + - 此注释启发来自[json-annotation](https://github.com/kifi/json-annotation),并做了优化,现在它可以与其他注解同时使用。 + - 只有一个隐式的`val`值会被自动生成(如果伴生对象不存在的话,还会生成一个伴生对象用于存放该隐式值),此外没有其他的操作。 + +- 示例 ```scala @json case class Person(name: String, age: Int) ``` -You can now serialize/deserialize your objects using Play's convenience methods: +现在,您可以使用Play的转化方法序列化或反序列化对象: ```scala import play.api.libs.json._ @@ -65,15 +73,16 @@ Json.fromJson[Person](json) ## @builder -The `@builder` used to generate builder pattern for Scala classes. +`@builder`注解用于为Scala类生成构造器模式。 -- Note - - Support `case class` / `class`. - - It can be used with `@toString`. But `@builder` needs to be put in the back. - - If there is no companion object, one will be generated to store the `builder` method and `Builder` class. - - IDE support is not very good, a red prompt will appear, but the compilation is OK. +- 说明 + - 支持普通类和样例类。 + - 如果该类没有伴生对象,将生成一个伴生对象来存储`builder`方法和类。 + - 目前不支持主构造函数是柯里化的。 + +> IDEA对宏的支持不是很好,所以会出现标红,不过编译没问题,调用结果也符合预期。这意味着,目前不支持语法提示。 -- Example +- 示例 ```scala @builder @@ -83,7 +92,7 @@ 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 { @@ -124,26 +133,25 @@ object TestClass1 extends scala.AnyRef { ## @synchronized -The `@synchronized` is a more convenient and flexible synchronous annotation. +`@synchronized`注解是一个更方便、更灵活的用于同步方法的注解。 -- Note - - `lockedName` The name of custom lock obj. - - Support static and instance methods. - - It can customize the lock object, and the default is `this`. +- 说明 + - `lockedName` 指定自定义的锁对象的名称。可选,默认`this`。 + - 支持静态方法(`object`中的函数)和实例方法(`class`中的函数)。 -- Example +- 示例 ```scala private final val obj = new Object -@synchronized(lockedName = "obj") // The default is this. If you fill in a non existent field name, the compilation will fail. +@synchronized(lockedName = "obj") // 如果您填写一个不存在的字段名,编译将失败。 def getStr3(k: Int): String = { k + "" } -// or -@synchronized //use this +// 或者 +@synchronized //使用 this 作为锁对象 def getStr(k: Int): String = { k + "" } @@ -152,43 +160,88 @@ def getStr(k: Int): String = { Compiler intermediate code: ```scala -// Note that it will not judge whether synchronized already exists, so if synchronized already exists, it will be used twice. -// For example `def getStr(k: Int): String = this.synchronized(this.synchronized(k.$plus(""))) -// It is not sure whether it will be optimized at the bytecode level. +// 注意,它不会判断synchronized是否已经存在,因此如果synchronized已经存在,它将被使用两次。如下 +// `def getStr(k: Int): String = this.synchronized(this.synchronized(k.$plus(""))) +// 目前还不确定是否在字节码级别会被优化。 def getStr(k: Int): String = this.synchronized(k.$plus("")) ``` -# How to use +## @log + +`@log`注解不使用混入和包装,而是直接使用宏生成默认的log对象来操作log。 + +- 说明 + - `verbose` 指定是否开启详细编译日志。可选,默认`false`。 + - `logType` 指定需要生成的`log`的类型。可选,默认`JLog` + - `io.github.dreamylost.LogType.JLog` 使用 `java.util.logging.Logger` + - `io.github.dreamylost.LogType.Log4j2` 使用 `org.apache.logging.log4j.Logger` + - `io.github.dreamylost.LogType.Slf4j` 使用 `org.slf4j.Logger` + - 支持普通类,样例类,单例对象。 + + +> IDEA对宏的支持不是很好,所以会出现标红,不过编译没问题,调用结果也符合预期。这意味着,目前不支持语法提示。 + +- 示例 + +```scala +@log(verbose = true) class TestClass1(val i: Int = 0, var j: Int) { + log.info("hello") +} + +@log(verbose=true, logType=io.github.dreamylost.LogType.Slf4j) class TestClass6(val i: Int = 0, var j: Int){ log.info("hello world") } +``` + +## @apply + +`@apply`注解用于为普通类的主构造函数生成`apply`方法。 + +- 说明 + - `verbose` 指定是否开启详细编译日志。可选,默认`false`。 + - 仅支持在`class`上使用。 + - 仅支持主构造函数。 + - 目前不支持主构造函数是柯里化的。 + +> IDEA对宏的支持不是很好,所以会出现标红,不过编译没问题,调用结果也符合预期。这意味着,目前不支持语法提示。 + +- 示例 + +```scala +@apply @toString class B2(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L)) +println(B2(1, 2)) +``` + +# 如何使用 + +添加库依赖,在sbt中 -Add library dependency +> 在gradle,maven中,通常`scala-macro-tools`被替换为`scala-macro-tools_2.12`这种。其中,`2.12`表示Scala版本号。 ```scala "io.github.jxnu-liguobin" %% "scala-macro-tools" % "" ``` -The artefacts have been uploaded to Maven Central. +该库已发布到maven中央仓库,请使用最新版本。 -| Library Version | Scala 2.11 | Scala 2.12 | Scala 2.13 | -|---------|------------|------------|------------| -| 0.0.5 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.5)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.5/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.5)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.5/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.5)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.5/jar) | -| 0.0.4 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.4)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.4/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.4)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.4/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.4)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.4/jar) | -| 0.0.3 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.3)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.3/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.3)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.3/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.3)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.3/jar) | -| 0.0.2 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.2)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.2/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.2)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.2/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.2)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.2/jar) | -| 0.0.1 |-|-| [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.1)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.1/jar) | +| Library Version | Scala 2.11 | Scala 2.12 | Scala 2.13 | +| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 0.0.5 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.5)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.5/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.5)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.5/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.5)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.5/jar) | +| 0.0.4 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.4)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.4/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.4)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.4/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.4)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.4/jar) | +| 0.0.3 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.3)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.3/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.3)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.3/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.3)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.3/jar) | +| 0.0.2 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.2)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.2/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.2)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.2/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.2)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.2/jar) | +| 0.0.1 | - | - | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.1)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.1/jar) | -Importing the library into your build system (e.g gradle, sbt), is not enough. You need to follow an extra step. +仅将本库导入构建系统(例如gradle、sbt)是不够的。你需要多走一步。 -| Scala 2.11 | Scala 2.12 | Scala 2.13 | -|------------|-------------|------------| -| Import macro paradise plugin | Import macro paradise plugin | Enable compiler flag `-Ymacro-annotations` required | +| Scala 2.11 | Scala 2.12 | Scala 2.13 | +| ------------------------ | ------------------------ | ------------------------------------- | +| 导入 macro paradise 插件 | 导入 macro paradise 插件 | 开启 编译器标记 `-Ymacro-annotations` | ```scala addCompilerPlugin("org.scalamacros" % "paradise_" % "") ``` -Where `` must be the full scala version. For example 2.12.13, and not 2.12. +``必须是Scala版本号的完整编号,如`2.12.13`,而不是`2.12`。 -If that doesn't work, google for alternatives. +如果这不起作用,可以谷歌寻找替代品。 -In version scala`2.13.x`, the functionality of macro paradise has been included in the scala compiler directly. However, -you must still enable the compiler flag `-Ymacro-annotations`. +在`scala 2.13.x`版本中,macro paradise的功能直接包含在scala编译器中。然而,仍然必须启用编译器标志`-Ymacro annotations`。 diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 0000000000000000000000000000000000000000..479fb0d008192c086ab133498be6c81dcbe2d128 --- /dev/null +++ b/README_EN.md @@ -0,0 +1,248 @@ +# 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) + +Motivation +-- + +Learn Scala macro and abstract syntax tree. + +> The project is currently experimental + +[中文说明](./README.md)|[English](./README_EN.md) + +# Features + +- `@toString` +- `@json` +- `@builder` +- `@synchronized` +- `@log` +- `@apply` + +## @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. + - `includeFieldNames` Whether to include the names of the field in the `toString`, default is `true`. + - `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 + +```scala +class TestClass(val i: Int = 0, var j: Int) { + val y: Int = 0 + var z: String = "hello" + var x: String = "world" +} + +println(new TestClass(1, 2)); +``` + +| includeInternalFields / includeFieldNames | false | true | +| ----------------------------------------- | -------------------------------------- | ------------------------------------------------ | +| false | ```TestClass(1, 2)``` | ```TestClass(i=0, j=2)``` | +| true | ```TestClass(1, 2, 0, hello, world)``` | ```TestClass(i=1, j=2, y=0, z=hello, x=world)``` | + +## @json + +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. +- Example + +```scala +@json case class Person(name: String, age: Int) +``` + +You can now serialize/deserialize your objects using Play's convenience methods: + +```scala +import play.api.libs.json._ + +val person = Person("Victor Hugo", 46) +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`. + - 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 + +- 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 () = { + super.(); + () + }; + def builder(): Builder = new Builder(); + class Builder extends scala.AnyRef { + def () = { + super.(); + () + }; + 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) + } +} +``` + +## @synchronized + +The `@synchronized` is a more convenient and flexible synchronous annotation. + +- Note + - `lockedName` The name of the custom lock obj, default is `this`. + - Support static and instance methods. + +- Example + +```scala + +private final val obj = new Object + +@synchronized(lockedName = "obj") // The default is this. If you fill in a non existent field name, the compilation will fail. +def getStr3(k: Int): String = { + k + "" +} + +// or +@synchronized //use this +def getStr(k: Int): String = { + k + "" +} +``` + +Compiler intermediate code: + +```scala +// Note that it will not judge whether synchronized already exists, so if synchronized already exists, it will be used twice. +// For example `def getStr(k: Int): String = this.synchronized(this.synchronized(k.$plus(""))) +// It is not sure whether it will be optimized at the bytecode level. +def getStr(k: Int): String = this.synchronized(k.$plus("")) +``` + +## @log + +The `@log` does not use mixed or wrapper, but directly uses macro to generate default log object and operate log. + +- Note + - `verbose` Whether to enable detailed log. + - `logType` Specifies the type of `log` that needs to be generated, default is `io.github.dreamylost.LogType.JLog`. + - `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. + +- Example + +```scala +@log(verbose = true) class TestClass1(val i: Int = 0, var j: Int) { + log.info("hello") +} + +@log(verbose=true, logType=io.github.dreamylost.LogType.Slf4j) class TestClass6(val i: Int = 0, var j: Int){ log.info("hello world") } +``` + +## @apply + +The `@apply` used to generate `apply` method for primary construction of ordinary classes. + +- Note + - `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 + +```scala +@apply @toString class B2(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L)) +println(B2(1, 2)) +``` + +# How to use + +Add library dependency + +```scala +"io.github.jxnu-liguobin" %% "scala-macro-tools" % "" +``` + +The artefacts have been uploaded to Maven Central. + +| Library Version | Scala 2.11 | Scala 2.12 | Scala 2.13 | +| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 0.0.5 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.5)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.5/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.5)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.5/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.5)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.5/jar) | +| 0.0.4 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.4)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.4/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.4)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.4/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.4)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.4/jar) | +| 0.0.3 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.3)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.3/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.3)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.3/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.3)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.3/jar) | +| 0.0.2 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.2)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.11/0.0.2/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.2)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.12/0.0.2/jar) | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.2)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.2/jar) | +| 0.0.1 | - | - | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.1)](https://search.maven.org/artifact/io.github.jxnu-liguobin/scala-macro-tools_2.13/0.0.1/jar) | + +Importing the library into your build system (e.g gradle, sbt), is not enough. You need to follow an extra step. + +| Scala 2.11 | Scala 2.12 | Scala 2.13 | +| ---------------------------- | ---------------------------- | --------------------------------------------------- | +| Import macro paradise plugin | Import macro paradise plugin | Enable compiler flag `-Ymacro-annotations` required | + +```scala +addCompilerPlugin("org.scalamacros" % "paradise_" % "") +``` + +Where `` must be the full scala version. For example 2.12.13, and not 2.12. + +If that doesn't work, google for alternatives. + +In version scala`2.13.x`, the functionality of macro paradise has been included in the scala compiler directly. However, +you must still enable the compiler flag `-Ymacro-annotations`. diff --git a/build.sbt b/build.sbt index a6653eaaf2dd0f1b8c056fc8d0eb8e3abea397b0..b354a99d1dedd2b4b9f0bdd4b7fe3a03a66d83e8 100644 --- a/build.sbt +++ b/build.sbt @@ -15,6 +15,9 @@ lazy val root = (project in file(".")) libraryDependencies ++= Seq( "org.scala-lang" % "scala-compiler" % scalaVersion.value, "org.scala-lang" % "scala-reflect" % scalaVersion.value, + "org.apache.logging.log4j" % "log4j-api" % "2.14.1" % Test, + "org.apache.logging.log4j" % "log4j-core" % "2.14.1" % Test, + "org.apache.logging.log4j" % "log4j-slf4j-impl" % "2.14.1" % Test, "com.typesafe.play" %% "play-json" % "2.7.4" % Test, "org.scalatest" %% "scalatest" % "3.0.9" % Test ), Compile / scalacOptions ++= { diff --git a/src/main/scala/io/github/dreamylost/MacroCommon.scala b/src/main/scala/io/github/dreamylost/MacroCommon.scala new file mode 100644 index 0000000000000000000000000000000000000000..485afe79a0d6ce5e57f7e3c33c8406b49ea1eed5 --- /dev/null +++ b/src/main/scala/io/github/dreamylost/MacroCommon.scala @@ -0,0 +1,129 @@ +package io.github.dreamylost + +import scala.reflect.macros.whitebox + +/** + * Common methods + * + * @author 梦境迷离 + * @since 2021/6/28 + * @version 1.0 + */ +trait MacroCommon { + + def printTree(c: whitebox.Context)(force: Boolean, resTree: c.Tree): Unit = { + c.info( + c.enclosingPosition, + "\n###### Expanded macro ######\n" + resTree.toString() + "\n###### Expanded macro ######\n", + force = force + ) + } + + /** + * Check the class and its companion object, and return the class definition. + * + * @param c + * @param annottees + * @return Return ClassDef + */ + def checkAndReturnClass(c: whitebox.Context)(annottees: c.Expr[Any]*): c.universe.ClassDef = { + import c.universe._ + val annotateeClass: ClassDef = 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 companion object if it exists. + * + * @param c + * @param annottees + * @return + */ + def getCompanionObject(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.") + } + } + + /** + * Modify the associated object itself according to whether there is an associated object. + * + * @param c + * @param annottees + * @param modifyAction The dependent type need aux-pattern in scala2. Now let's get around this. + * @return Return the result of modifyAction + */ + def handleWithImplType(c: whitebox.Context)(annottees: c.Expr[Any]*) + (modifyAction: (c.universe.ClassDef, Option[c.universe.ModuleDef]) => Any): c.Expr[Nothing] = { + import c.universe._ + annottees.map(_.tree) match { + case (classDecl: ClassDef) :: Nil => modifyAction(classDecl, None).asInstanceOf[c.Expr[Nothing]] + case (classDecl: ClassDef) :: (compDecl: ModuleDef) :: Nil => modifyAction(classDecl, Some(compDecl)).asInstanceOf[c.Expr[Nothing]] + case _ => c.abort(c.enclosingPosition, "Invalid annottee") + } + } + + /** + * Expand the class and check whether the class is a case class. + * + * @param c + * @param annotateeClass classDef + * @return Return true if it is a case class + */ + def isCaseClass(c: whitebox.Context)(annotateeClass: c.universe.ClassDef): Boolean = { + import c.universe._ + annotateeClass match { + case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" => + if (mods.asInstanceOf[Modifiers].hasFlag(Flag.CASE)) { + c.info(c.enclosingPosition, "Annotation is used on 'case class'.", force = true) + true + } else false + case _ => c.abort(c.enclosingPosition, s"Annotation is only supported on class. classDef: $annotateeClass") + } + } + + /** + * 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 = { + import c.universe._ + field match { + case q"$mods val $tname: $tpt = $expr" => q"""$tname""" + case q"$mods var $tname: $tpt = $expr" => q"""$tname""" + } + } + + def modifiedCompanion(c: whitebox.Context)( + compDeclOpt: Option[c.universe.ModuleDef], + apply: c.Tree, className: c.TypeName): c.universe.Tree = { + import c.universe._ + compDeclOpt map { compDecl => + val q"$mods object $obj extends ..$bases { ..$body }" = compDecl + val o = + q""" + $mods object $obj extends ..$bases { + ..$body + ..$apply + } + """ + 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 }" + c.info(c.enclosingPosition, s"modifiedCompanion className: $className, new obj: $o", force = true) + o + } + } +} diff --git a/src/main/scala/io/github/dreamylost/apply.scala b/src/main/scala/io/github/dreamylost/apply.scala new file mode 100644 index 0000000000000000000000000000000000000000..c2562c0b118b7e099c7b8aff8e4c09f033d2058c --- /dev/null +++ b/src/main/scala/io/github/dreamylost/apply.scala @@ -0,0 +1,64 @@ +package io.github.dreamylost + +import scala.annotation.{ StaticAnnotation, compileTimeOnly } +import scala.language.experimental.macros +import scala.reflect.macros.whitebox + +/** + * annotation to generate apply method for primary construction of ordinary classes. + * + * @author 梦境迷离 + * @param verbose Whether to enable detailed log. + * @since 2021/6/30 + * @version 1.0 + */ +@compileTimeOnly("enable macro to expand macro annotations") +final class apply( + verbose: Boolean = false +) extends StaticAnnotation { + def macroTransform(annottees: Any*): Any = macro applyMacro.impl + +} + +object applyMacro extends MacroCommon { + def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { + import c.universe._ + val args: Boolean = c.prefix.tree match { + case q"new apply(verbose=$verbose)" => c.eval[Boolean](c.Expr(verbose)) + case q"new apply()" => false + case _ => c.abort(c.enclosingPosition, "unexpected annotation pattern!") + } + + c.info(c.enclosingPosition, s"annottees: $annottees, args: $args", force = args) + + val annotateeClass: ClassDef = checkAndReturnClass(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 { + 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)) + val cName = className match { + case t: TypeName => t + } + val compDecl = modifiedCompanion(c)(compDeclOpt, q"""def apply(...$fields): $className = new $className(..$fieldNames)""", cName) + c.Expr( + q""" + $classDecl + $compDecl + """) + } + + val resTree = handleWithImplType(c)(annottees: _*)(modifiedDeclaration) + printTree(c)(force = args, resTree.tree) + + resTree + + } +} diff --git a/src/main/scala/io/github/dreamylost/builder.scala b/src/main/scala/io/github/dreamylost/builder.scala index 088ecb12e2e196cff610c518be7d8d2ae4cc2478..d34ff0cbb8ebb07a1c3455f2c4f133c3737fcc72 100644 --- a/src/main/scala/io/github/dreamylost/builder.scala +++ b/src/main/scala/io/github/dreamylost/builder.scala @@ -5,6 +5,7 @@ import scala.language.experimental.macros import scala.reflect.macros.whitebox /** + * annotation to generate builder pattern for classes. * * @author 梦境迷离 * @since 2021/6/19 @@ -16,7 +17,7 @@ final class builder extends StaticAnnotation { } -object builderMacro { +object builderMacro extends MacroCommon { def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { import c.universe._ @@ -77,64 +78,37 @@ object builderMacro { """ } - 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) + // The dependent type need aux-pattern in scala2. Now let's get around this. + def modifiedDeclaration(classDecl: ClassDef, compDeclOpt: Option[ModuleDef] = None): Any = { + val (className, fields) = classDecl match { + case q"$mods class $tpname[..$tparams](...$paramss) extends ..$bases { ..$body }" => + c.info(c.enclosingPosition, s"modifiedDeclaration className: $tpname, paramss: $paramss", force = true) + (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 = 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""" + + val cName = className match { + case t: TypeName => t + } + val isCase = isCaseClass(c)(classDecl) + val builder = builderTemplate(cName, fields.asInstanceOf[List[List[Tree]]].flatten, isCase) + val compDecl = modifiedCompanion(c)(compDeclOpt, builder, cName) + 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) + c.info(c.enclosingPosition, s"builder annottees: $annottees", force = 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") - } + val resTree = handleWithImplType(c)(annottees: _*)(modifiedDeclaration) + printTree(c)(force = true, resTree.tree) - // Print the ast - c.info( - c.enclosingPosition, - "\n###### Expanded macro ######\n" + resTree.toString() + "\n###### Expanded macro ######\n", - force = true - ) resTree } } diff --git a/src/main/scala/io/github/dreamylost/json.scala b/src/main/scala/io/github/dreamylost/json.scala index 30d01273d1764540c7d43246fd939878111a5dd5..432eadf63a85e2a5fb35a51fbdff7b8e08b5f29d 100644 --- a/src/main/scala/io/github/dreamylost/json.scala +++ b/src/main/scala/io/github/dreamylost/json.scala @@ -5,7 +5,7 @@ import scala.language.experimental.macros import scala.reflect.macros.whitebox /** - * annotation for case classes + * annotation to generate play-json implicit object for case classes. * * @author 梦境迷离 * @since 2021/6/13 @@ -16,7 +16,7 @@ final class json extends StaticAnnotation { def macroTransform(annottees: Any*): Any = macro jsonMacro.impl } -object jsonMacro { +object jsonMacro extends MacroCommon { def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { import c.universe._ @@ -29,60 +29,38 @@ object jsonMacro { } } - def modifiedCompanion(compDeclOpt: Option[ModuleDef], format: Tree, className: TypeName): c.universe.Tree = { - compDeclOpt map { compDecl => - // Add the formatter to the existing companion object - val q"object $obj extends ..$bases { ..$body }" = compDecl - val o = - q""" - object $obj extends ..$bases { - ..$body - $format - } - """ - c.info(c.enclosingPosition, s"modifiedCompanion className: $className, exists obj: $o", force = true) - o - } getOrElse { - // Create a companion object with the formatter - val o = q"object ${className.toTermName} { $format }" - 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] = { + // The dependent type need aux-pattern in scala2. Now let's get around this. + def modifiedDeclaration(classDecl: ClassDef, compDeclOpt: Option[ModuleDef] = None): Any = { val (className, fields) = classDecl match { - case q"$mods class $className(..$fields) extends ..$bases { ..$body }" => + case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends ..$bases { ..$body }" => if (!mods.asInstanceOf[Modifiers].hasFlag(Flag.CASE)) { c.abort(c.enclosingPosition, s"Annotation is only supported on case class. classDef: $classDecl, mods: $mods") } else { - c.info(c.enclosingPosition, s"modifiedDeclaration className: $className, fields: $fields", force = true) - (className, fields) + c.info(c.enclosingPosition, s"modifiedDeclaration className: $tpname, paramss: $paramss", force = true) + (tpname, paramss) } case _ => c.abort(c.enclosingPosition, s"Annotation is only supported on case class. classDef: $classDecl") } c.info(c.enclosingPosition, s"modifiedDeclaration className: $className, fields: $fields", force = true) - className match { - case t: TypeName => - val format = jsonFormatter(t, fields.asInstanceOf[List[Tree]]) - val compDecl = modifiedCompanion(compDeclOpt, format, t) - c.info(c.enclosingPosition, s"format: $format, compDecl: $compDecl", force = true) - // Return both the class and companion object declarations - c.Expr( - q""" + val cName = className match { + case t: TypeName => t + } + val format = jsonFormatter(cName, fields.asInstanceOf[List[List[Tree]]].flatten) + val compDecl = modifiedCompanion(c)(compDeclOpt, format, cName) + c.info(c.enclosingPosition, s"format: $format, compDecl: $compDecl", force = true) + // Return both the class and companion object declarations + c.Expr( + q""" $classDecl $compDecl """) - } } - c.info(c.enclosingPosition, s"json annottees: $annottees", true) + c.info(c.enclosingPosition, s"json annottees: $annottees", force = true) + val resTree = handleWithImplType(c)(annottees: _*)(modifiedDeclaration) + printTree(c)(force = true, resTree.tree) - 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") - } + resTree } } diff --git a/src/main/scala/io/github/dreamylost/log.scala b/src/main/scala/io/github/dreamylost/log.scala new file mode 100644 index 0000000000000000000000000000000000000000..54d87de0ec3a9cab8bda22e2fc5c7110a340e94d --- /dev/null +++ b/src/main/scala/io/github/dreamylost/log.scala @@ -0,0 +1,124 @@ +package io.github.dreamylost + +import io.github.dreamylost.LogType.LogType + +import scala.annotation.{ StaticAnnotation, compileTimeOnly } +import scala.language.experimental.macros +import scala.reflect.macros.whitebox + +/** + * annotation to generate log. + * + * @author 梦境迷离 + * @param verbose Whether to enable detailed log. + * @param logType Specifies the type of `log` that needs to be generated + * @since 2021/6/28 + * @version 1.0 + */ +@compileTimeOnly("enable macro to expand macro annotations") +final class log( + verbose: Boolean = false, + logType: LogType.LogType = LogType.JLog +) extends StaticAnnotation { + def macroTransform(annottees: Any*): Any = macro logMacro.impl +} + +sealed trait BaseLog { + val typ: LogType + + def getTemplate(c: whitebox.Context)(t: String, isClass: Boolean): c.Tree +} + +object LogType extends Enumeration { + type LogType = Value + val JLog, Log4j2, Slf4j = Value + + private lazy val types = Map( + JLog -> JLogImpl, + Log4j2 -> Log4J2Impl, + Slf4j -> Slf4jImpl + ) + + def getLogImpl(logType: LogType): BaseLog = { + types.getOrElse(logType, default = throw new Exception(s"Not support log: $logType")) + } + +} + +object JLogImpl extends BaseLog { + override val typ: LogType = LogType.JLog + + override def getTemplate(c: whitebox.Context)(t: String, isClass: Boolean): c.Tree = { + import c.universe._ + if (isClass) { + q"""private final val log: java.util.logging.Logger = java.util.logging.Logger.getLogger(classOf[${TypeName(t)}].getName)""" + } else { + q"""private final val log: java.util.logging.Logger = java.util.logging.Logger.getLogger(${TermName(t)}.getClass.getName)""" + } + } + +} + +object Log4J2Impl extends BaseLog { + override val typ: LogType = LogType.Log4j2 + + override def getTemplate(c: whitebox.Context)(t: String, isClass: Boolean): c.Tree = { + import c.universe._ + if (isClass) { + q"""private final val log: org.apache.logging.log4j.Logger = org.apache.logging.log4j.LogManager.getLogger(classOf[${TypeName(t)}].getName)""" + } else { + q"""private final val log: org.apache.logging.log4j.Logger = org.apache.logging.log4j.LogManager.getLogger(${TermName(t)}.getClass.getName)""" + } + } + +} + +object Slf4jImpl extends BaseLog { + override val typ: LogType = LogType.Slf4j + + override def getTemplate(c: whitebox.Context)(t: String, isClass: Boolean): c.Tree = { + import c.universe._ + if (isClass) { + q"""private final val log: org.slf4j.Logger = org.slf4j.LoggerFactory.getLogger(classOf[${TypeName(t)}])""" + } else { + q"""private final val log: org.slf4j.Logger = org.slf4j.LoggerFactory.getLogger(${TermName(t)}.getClass)""" + } + } +} + +object logMacro extends MacroCommon { + def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { + import c.universe._ + val args: (Boolean, LogType) = c.prefix.tree match { + case q"new log(logType=$logType)" => (false, c.eval[LogType](c.Expr(logType))) + case q"new log(verbose=$verbose)" => (c.eval[Boolean](c.Expr(verbose)), LogType.JLog) + case q"new log($logType)" => (false, c.eval[LogType](c.Expr(logType))) + case q"new log(verbose=$verbose, logType=$logType)" => (c.eval[Boolean](c.Expr(verbose)), c.eval[LogType](c.Expr(logType))) + case q"new log()" => (false, LogType.JLog) + case _ => c.abort(c.enclosingPosition, "unexpected annotation pattern!") + } + + c.info(c.enclosingPosition, s"annottees: $annottees, args: $args", force = args._1) + + val logTree = annottees.map(_.tree) match { + // Match a class, and expand, get class/object name. + case (classDef @ q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }") :: _ => + LogType.getLogImpl(args._2).getTemplate(c)(tpname.asInstanceOf[TypeName].toTermName.decodedName.toString, isClass = true) + 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} }" + case q"$mods object $tpname extends { ..$earlydefns } with ..$parents { $self => ..$stats }" :: _ => + q"$mods object $tpname extends { ..$earlydefns } with ..$parents { $self => ..${List(logTree) ::: stats.toList} }" + } + + printTree(c)(force = args._1, resTree) + c.Expr[Any](resTree) + } +} diff --git a/src/main/scala/io/github/dreamylost/synchronized.scala b/src/main/scala/io/github/dreamylost/synchronized.scala index fccb2c8d0eb77fd7fc321a0f845265220b09dc3d..0c3b555210bf7c1ca4df4579f8c37126ff72d653 100644 --- a/src/main/scala/io/github/dreamylost/synchronized.scala +++ b/src/main/scala/io/github/dreamylost/synchronized.scala @@ -5,6 +5,7 @@ import scala.language.experimental.macros import scala.reflect.macros.whitebox /** + * annotation to generate synchronized for methods. * * @author 梦境迷离 * @param lockedName The name of custom lock obj. @@ -20,7 +21,7 @@ final class synchronized( def macroTransform(annottees: Any*): Any = macro synchronizedMacro.impl } -object synchronizedMacro { +object synchronizedMacro extends MacroCommon { def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { import c.universe._ @@ -47,12 +48,7 @@ object synchronizedMacro { } case _ => c.abort(c.enclosingPosition, "Invalid annotation target: not a method") } - // Print the ast - c.info( - c.enclosingPosition, - "\n###### Expanded macro ######\n" + resTree.toString() + "\n###### Expanded macro ######\n", - force = args._1 - ) + printTree(c)(args._1, resTree) c.Expr[Any](resTree) } } diff --git a/src/main/scala/io/github/dreamylost/toString.scala b/src/main/scala/io/github/dreamylost/toString.scala index eff6672138393fef437270a4c5d9791e66066020..3af5c130ba1a92296fc7d37058b4992546d7722b 100644 --- a/src/main/scala/io/github/dreamylost/toString.scala +++ b/src/main/scala/io/github/dreamylost/toString.scala @@ -5,7 +5,7 @@ import scala.language.experimental.macros import scala.reflect.macros.whitebox /** - * toString for classes + * annotation to generate toString for classes. * * @author 梦境迷离 * @param verbose Whether to enable detailed log. @@ -29,7 +29,7 @@ final class toString( final case class Argument(verbose: Boolean, includeInternalFields: Boolean, includeFieldNames: Boolean, callSuper: Boolean) -object stringMacro { +object stringMacro extends MacroCommon { def printField(c: whitebox.Context)(argument: Argument, lastParam: Option[String], field: c.universe.Tree): c.universe.Tree = { import c.universe._ @@ -71,7 +71,7 @@ object stringMacro { case _: ValDef => true case mem: MemberDef => c.info(c.enclosingPosition, s"MemberDef: ${mem.toString}", force = argument.verbose) - if (mem.toString().startsWith("override def toString")) { + 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 @@ -131,20 +131,8 @@ object stringMacro { 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 = 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.") - } - val isCase: Boolean = { - annotateeClass match { - case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" => - if (mods.asInstanceOf[Modifiers].hasFlag(Flag.CASE)) { - c.warning(c.enclosingPosition, "'toString' annotation is used on 'case class'.") - true - } else false - } - } + val annotateeClass: ClassDef = checkAndReturnClass(c)(annottees: _*) + val isCase: Boolean = isCaseClass(c)(annotateeClass) c.info(c.enclosingPosition, s"impl argument: $argument, isCase: $isCase", force = argument.verbose) val resMethod = toStringTemplateImpl(c)(argument, annotateeClass) @@ -152,12 +140,18 @@ object stringMacro { 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)} }" } - // Print the ast - c.info( - c.enclosingPosition, - "\n###### Expanded macro ######\n" + resTree.toString() + "\n###### Expanded macro ######\n", - force = argument.verbose - ) - c.Expr[Any](resTree) + 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 + """ + } + printTree(c)(argument.verbose, res) + c.Expr[Any](res) } } diff --git a/src/test/resources/log4j2.xml b/src/test/resources/log4j2.xml new file mode 100644 index 0000000000000000000000000000000000000000..8c854aeeae220f9490580e77d5de24e3c425a3cf --- /dev/null +++ b/src/test/resources/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/test/scala/io/github/dreamylost/ApplyTest.scala b/src/test/scala/io/github/dreamylost/ApplyTest.scala new file mode 100644 index 0000000000000000000000000000000000000000..36d1569c498ef25f60078468a368c2d324df670a --- /dev/null +++ b/src/test/scala/io/github/dreamylost/ApplyTest.scala @@ -0,0 +1,40 @@ +package io.github.dreamylost + +import org.scalatest.{ FlatSpec, Matchers } + +/** + * + * @author 梦境迷离 + * @since 2021/6/30 + * @version 1.0 + */ +class ApplyTest extends FlatSpec with Matchers { + + "apply1" should "ok at class" in { + // int: Int => private[this] val int: Int = _; + // val j: Int => val j: Int = _; + // apply => def apply(int: Int, j: Int, k: Option[String] = None, t: Option[Long] = Some(1L)): A = new A(int, j, k, t) + + """@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)) + + """@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)) + + // 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)) + } + + "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 + } +} diff --git a/src/test/scala/io/github/dreamylost/BuilderTest.scala b/src/test/scala/io/github/dreamylost/BuilderTest.scala index b70e2bdcd0c57dfc10658925d5d0493096cdb1ac..c67b6ac25ed6b4d2ada6539b8d748c8cd475ee7d 100644 --- a/src/test/scala/io/github/dreamylost/BuilderTest.scala +++ b/src/test/scala/io/github/dreamylost/BuilderTest.scala @@ -19,7 +19,6 @@ class BuilderTest extends FlatSpec with Matchers { 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("")) @@ -67,4 +66,13 @@ class BuilderTest extends FlatSpec with Matchers { assert(ret.toString == "TestClass1(i=1, j=0, x=x, o=Some())") } + "builder7" should "case class with toString and companion object not in order" in { + @builder + @toString //failed when companion object exists, fix in 0.0.6 + 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())") + } } diff --git a/src/test/scala/io/github/dreamylost/JsonTest.scala b/src/test/scala/io/github/dreamylost/JsonTest.scala index 844c9b58e81d571c50b92bf55352c62e2ba6c7e2..857a2fb1fa087c02fe8fb8667e99ce80368d4e6e 100644 --- a/src/test/scala/io/github/dreamylost/JsonTest.scala +++ b/src/test/scala/io/github/dreamylost/JsonTest.scala @@ -12,6 +12,7 @@ import play.api.libs.json.Json class JsonTest extends FlatSpec with Matchers { // class must be wrote here + @json case class TestClass1(val i: Int = 0, var j: Int, x: String, o: Option[String] = Some("")) diff --git a/src/test/scala/io/github/dreamylost/LogMain.scala b/src/test/scala/io/github/dreamylost/LogMain.scala new file mode 100644 index 0000000000000000000000000000000000000000..9793f750c91ae3bded7fc7f3326f204cbffa2dce --- /dev/null +++ b/src/test/scala/io/github/dreamylost/LogMain.scala @@ -0,0 +1,22 @@ +package io.github.dreamylost + +/** + * + * @author 梦境迷离 + * @since 2021/6/29 + * @version 1.0 + */ +object LogMain extends App { + + private final val log: java.util.logging.Logger = java.util.logging.Logger.getLogger(LogMain.getClass.getName) + + // object is not type + private final val log2: org.apache.logging.log4j.Logger = org.apache.logging.log4j.LogManager.getLogger(LogMain.getClass.getName) + + private final val log3: org.slf4j.Logger = org.slf4j.LoggerFactory.getLogger(LogMain.getClass) + + log.info("hello1") + log2.info("hello2") + log3.info("hello3") + +} diff --git a/src/test/scala/io/github/dreamylost/LogTest.scala b/src/test/scala/io/github/dreamylost/LogTest.scala new file mode 100644 index 0000000000000000000000000000000000000000..7eaef4b396c4f6bee63ae1c85c115448244c3089 --- /dev/null +++ b/src/test/scala/io/github/dreamylost/LogTest.scala @@ -0,0 +1,98 @@ +package io.github.dreamylost + +import org.scalatest.{ FlatSpec, Matchers } + +/** + * + * @author 梦境迷离 + * @since 2021/6/28 + * @version 1.0 + */ +class LogTest extends FlatSpec 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 class TestClass2(val i: Int = 0, var j: Int)""" should compile + """@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.JLog) class TestClass5(val i: Int = 0, var j: Int)""" should compile + """@log(verbose=true, logType=io.github.dreamylost.LogType.JLog) class TestClass6(val i: Int = 0, var j: Int)""" should compile + } + + "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 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 + """@log(verbose=true) case class TestClass4(val i: Int = 0, var j: Int)""" should compile + """@log(logType=io.github.dreamylost.LogType.JLog) case class TestClass5(val i: Int = 0, var j: Int)""" should compile + """@log(verbose=true, logType=io.github.dreamylost.LogType.JLog) case class TestClass6(val i: Int = 0, var j: Int)""" should compile + } + + "log3" should "ok on object" in { + """@log(verbose=true) object TestClass1 { + log.info("hello") + }""" should compile + + """@log object TestClass2""" should compile + """@log() object TestClass3""" should compile + """@log(verbose=true) object TestClass4""" should compile + """@log(logType=io.github.dreamylost.LogType.JLog) object TestClass5""" should compile + """@log(verbose=true, logType=io.github.dreamylost.LogType.JLog) object TestClass6""" should compile + } + + "log4 log4j2" should "ok on object" in { + """@log(verbose=true) object TestClass1 { + log.info("hello") + }""" should compile + + """@log object TestClass2""" should compile + """@log() object TestClass3""" should compile + """@log(verbose=true) object TestClass4""" should compile + """@log(logType=io.github.dreamylost.LogType.Log4j2) object TestClass5""" should compile + """@log(verbose=true, logType=io.github.dreamylost.LogType.Log4j2) object TestClass6""" should compile + } + + "log5 slf4j" should "ok on object" in { + """@log(verbose=true) object TestClass1 { + log.info("hello") + }""" should compile + + """@log object TestClass2""" should compile + """@log() object TestClass3""" should compile + """@log(verbose=true) object TestClass4""" should compile + """@log(logType=io.github.dreamylost.LogType.Slf4j) object TestClass5""" should compile + """@log(verbose=true, logType=io.github.dreamylost.LogType.Slf4j) object TestClass6""" should compile + } + + "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 class TestClass2(val i: Int = 0, var j: Int)""" should compile + """@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.Log4j2) class TestClass5(val i: Int = 0, var j: Int)""" should compile + """@log(verbose=true, logType=io.github.dreamylost.LogType.Log4j2) class TestClass6(val i: Int = 0, var j: Int)""" should compile + } + + "log7 slf4j" should "ok on class" 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) class TestClass6(val i: Int = 0, var j: Int){ log.info("hello world") }""" should compile //default verbose is false + } +} diff --git a/src/test/scala/io/github/dreamylost/Synchronized.scala b/src/test/scala/io/github/dreamylost/SynchronizedTest.scala similarity index 95% rename from src/test/scala/io/github/dreamylost/Synchronized.scala rename to src/test/scala/io/github/dreamylost/SynchronizedTest.scala index 2a2a9e23683ca9d631840a599e21798572f76e3d..ea2e7b2949b4f1dbd12ce48e23ab8712ebf9480e 100644 --- a/src/test/scala/io/github/dreamylost/Synchronized.scala +++ b/src/test/scala/io/github/dreamylost/SynchronizedTest.scala @@ -8,7 +8,7 @@ import org.scalatest.{ FlatSpec, Matchers } * @since 2021/6/24 * @version 1.0 */ -class Synchronized extends FlatSpec with Matchers { +class SynchronizedTest extends FlatSpec with Matchers { "synchronized1" should "is ok at class" in { @synchronized diff --git a/src/test/scala/io/github/dreamylost/ToStringTest.scala b/src/test/scala/io/github/dreamylost/ToStringTest.scala index 0b59eebaa9b8b18ea5e698fc638351fdd6b283d4..3760a65d8d04f3de4c2fa3980083663a835540a4 100644 --- a/src/test/scala/io/github/dreamylost/ToStringTest.scala +++ b/src/test/scala/io/github/dreamylost/ToStringTest.scala @@ -9,7 +9,6 @@ import org.scalatest.{ FlatSpec, Matchers } * @version 1.0 */ class ToStringTest extends FlatSpec with Matchers { - "toString1" should "not contains internal field" in { @toString(false, false, false) class TestClass(val i: Int = 0, var j: Int) { @@ -260,4 +259,5 @@ class ToStringTest extends FlatSpec with Matchers { // Because not support if super class is a trait assert(s5.startsWith("TestClass5(super=io.github.dreamylost.ToStringTes") && s5.endsWith("1)")) } + }