From 6d22d93df4f6438cd0b78e38c21d4e8cda921f7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=A6=E5=A2=83=E8=BF=B7=E7=A6=BB?= Date: Thu, 5 Aug 2021 22:47:07 +0800 Subject: [PATCH] Add `@jacksonEnum` and Big refactor (#93) 1. add Accessor 2. support jacksonEnum 3. big refactor --- README.md | 1 + README_CN.md | 1 + build.sbt | 3 +- docs/howToUse.md | 45 +++- docs/howToUse_en.md | 46 +++- .../io/github/dreamylost/jacksonEnum.scala | 47 ++++ .../macros/AbstractMacroProcessor.scala | 209 ++++++++++++------ .../github/dreamylost/macros/applyMacro.scala | 20 +- .../dreamylost/macros/builderMacro.scala | 41 ++-- .../dreamylost/macros/constructorMacro.scala | 28 ++- .../macros/equalsAndHashCodeMacro.scala | 83 +++---- .../dreamylost/macros/jacksonEnumMacro.scala | 108 +++++++++ .../github/dreamylost/macros/jsonMacro.scala | 22 +- .../github/dreamylost/macros/logMacro.scala | 56 +++-- .../io/github/dreamylost/macros/macros.scala | 8 +- .../dreamylost/macros/synchronizedMacro.scala | 12 +- .../dreamylost/macros/toStringMacro.scala | 52 ++--- .../github/dreamylost/JacksonEnumTest.scala | 116 ++++++++++ .../scala/io/github/dreamylost/LogTest.scala | 57 +---- 19 files changed, 663 insertions(+), 292 deletions(-) create mode 100644 src/main/scala/io/github/dreamylost/jacksonEnum.scala create mode 100644 src/main/scala/io/github/dreamylost/macros/jacksonEnumMacro.scala create mode 100644 src/test/scala/io/github/dreamylost/JacksonEnumTest.scala diff --git a/README.md b/README.md index 62820a8..49c74ca 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Learn Scala macro and abstract syntax tree. - `@apply` - `@constructor` - `@equalsAndHashCode` +- `@jacksonEnum` > Annotations involving interaction are supported in the idea plug-in (named `Scala-Macro-Tools` in Marketplace). diff --git a/README_CN.md b/README_CN.md index 2449ccc..aab4e1f 100644 --- a/README_CN.md +++ b/README_CN.md @@ -46,6 +46,7 @@ - `@apply` - `@constructor` - `@equalsAndHashCode` +- `@jacksonEnum` > 涉及到交互操作的注解在IDEA插件中都得到了支持。在插件市场中搜索`Scala-Macro-Tools`可下载。 diff --git a/build.sbt b/build.sbt index 84678b9..8802265 100644 --- a/build.sbt +++ b/build.sbt @@ -24,7 +24,8 @@ lazy val root = (project in file(".")) "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.2.9" % Test + "org.scalatest" %% "scalatest" % "3.2.9" % Test, + "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.12.4" %Test ), Compile / scalacOptions ++= { CrossVersion.partialVersion(scalaVersion.value) match { case Some((2, n)) if n <= 12 => Nil diff --git a/docs/howToUse.md b/docs/howToUse.md index 186f025..372cecc 100644 --- a/docs/howToUse.md +++ b/docs/howToUse.md @@ -145,7 +145,7 @@ def getStr(k: Int): String = this.synchronized(k.$plus("")) ## @log -`@log`注解不使用混入和包装,而是直接使用宏生成默认的log对象来操作log。 +`@log`注解不使用混入和包装,而是直接使用宏生成默认的log对象来操作log。日志库的依赖需要自己引入。 - 说明 - `verbose` 指定是否开启详细编译日志。可选,默认`false`。 @@ -155,7 +155,7 @@ def getStr(k: Int): String = this.synchronized(k.$plus("")) - `io.github.dreamylost.logs.LogType.Slf4j` 使用 `org.slf4j.Logger` - `io.github.dreamylost.logs.LogType.ScalaLoggingLazy` 基于 `scalalogging.LazyLogging` 实现,但字段被重命名为`log` - `io.github.dreamylost.logs.LogType.ScalaLoggingStrict` 基于 `scalalogging.StrictLogging`实现, 但字段被重命名为`log` - - 支持普通类,样例类,单例对象。 + - 支持普通类,单例对象。 - 示例 @@ -260,3 +260,44 @@ class Person extends scala.AnyRef { } } ``` + +## @jacksonEnum + +`@jacksonEnum`注解用于为类的主构造函数中的所有Scala枚举类型的参数提供`Jackson`序列化的支持。(jackson和jackson-scala-module依赖需要自己引入) + +- 说明 + - `verbose` 指定是否开启详细编译日志。可选,默认`false`。 + - `nonTypeRefers` 指定不需要创建`Jackson`的`TypeReference`子类的枚举类型。可选,默认`Nil`。 + - 支持`case class`和`class`。 + - 如果枚举类型存在`TypeReference`的子类,则不会生成新的子类,也不会重复添加`@JsonScalaEnumeration`注解到参数上。这主要用于解决冲突问题。 + +- 示例 + +```scala +@jacksonEnum(nonTypeRefers = Seq("EnumType")) +class B( + var enum1: EnumType.EnumType, + enum2: EnumType2.EnumType2 = EnumType2.A, + i: Int) +``` + +宏生成的中间代码: + +```scala + class EnumType2TypeRefer extends _root_.com.fasterxml.jackson.core.`type`.TypeReference[EnumType2.type] { + def () = { + super.(); + () + } + }; + class B extends scala.AnyRef { + var enum1: JacksonEnumTest.this.EnumType.EnumType = _; + @new com.fasterxml.jackson.module.scala.JsonScalaEnumeration(classOf[EnumType2TypeRefer]) private[this] val enum2: JacksonEnumTest.this.EnumType2.EnumType2 = _; + private[this] val i: Int = _; + def (enum1: JacksonEnumTest.this.EnumType.EnumType, @new com.fasterxml.jackson.module.scala.JsonScalaEnumeration(classOf[EnumType2TypeRefer]) enum2: JacksonEnumTest.this.EnumType2.EnumType2 = EnumType2.A, i: Int) = { + super.(); + () + } + }; + () +``` diff --git a/docs/howToUse_en.md b/docs/howToUse_en.md index 08a157e..99f5e14 100644 --- a/docs/howToUse_en.md +++ b/docs/howToUse_en.md @@ -147,7 +147,7 @@ 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. +The `@log` does not use mixed or wrapper, but directly uses macro to generate default log object and operate log. (Log dependency needs to be introduced) - Note - `verbose` Whether to enable detailed log. @@ -157,7 +157,7 @@ The `@log` does not use mixed or wrapper, but directly uses macro to generate de - `io.github.dreamylost.logs.LogType.Slf4j` use `org.slf4j.Logger` - `io.github.dreamylost.logs.LogType.ScalaLoggingLazy` implement by `scalalogging.LazyLogging` but field was renamed to `log` - `io.github.dreamylost.logs.LogType.ScalaLoggingStrict` implement by `scalalogging.StrictLogging` but field was renamed to `log` - - Support `class`, `case class` and `object`. + - Support `class` and `object`. - Example @@ -266,3 +266,45 @@ class Person extends scala.AnyRef { } } ``` + +## @jacksonEnum + +The `jacksonEnum` annotation is used to provide `Jackson` serialization support for all Scala enumeration type parameters in the primary constructor of the class. (jackson and jackson-scala-module dependency needs to be introduced) + +- Note + - `verbose` Whether to enable detailed log. default is `false`. + - `nonTypeRefers` Specifies the enumeration type of the `TypeReference` subclass of `Jackson` that does not need to be created. default is `Nil`. + - Support `case class` and `class`. + - If the enumeration type has subclasses of `TypeReference`, no new subclasses will be generated, + and `JsonScalaEnumeration` annotation will not be added to the parameters repeatedly. This is mainly used to solve conflict problems. + +- Example + +```scala +@jacksonEnum(nonTypeRefers = Seq("EnumType")) +class B( + var enum1: EnumType.EnumType, + enum2: EnumType2.EnumType2 = EnumType2.A, + i: Int) +``` + +Macro expansion code: + +```scala + class EnumType2TypeRefer extends _root_.com.fasterxml.jackson.core.`type`.TypeReference[EnumType2.type] { + def () = { + super.(); + () + } + }; + class B extends scala.AnyRef { + var enum1: JacksonEnumTest.this.EnumType.EnumType = _; + @new com.fasterxml.jackson.module.scala.JsonScalaEnumeration(classOf[EnumType2TypeRefer]) private[this] val enum2: JacksonEnumTest.this.EnumType2.EnumType2 = _; + private[this] val i: Int = _; + def (enum1: JacksonEnumTest.this.EnumType.EnumType, @new com.fasterxml.jackson.module.scala.JsonScalaEnumeration(classOf[EnumType2TypeRefer]) enum2: JacksonEnumTest.this.EnumType2.EnumType2 = EnumType2.A, i: Int) = { + super.(); + () + } + }; + () +``` diff --git a/src/main/scala/io/github/dreamylost/jacksonEnum.scala b/src/main/scala/io/github/dreamylost/jacksonEnum.scala new file mode 100644 index 0000000..ddfb512 --- /dev/null +++ b/src/main/scala/io/github/dreamylost/jacksonEnum.scala @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2021 jxnu-liguobin && contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package io.github.dreamylost + +import io.github.dreamylost.macros.jacksonEnumMacro + +import scala.annotation.{ compileTimeOnly, StaticAnnotation } + +/** + * annotation to generate equals and hashcode method for classes. + * + * @author 梦境迷离 + * @author choly + * + * @param verbose Whether to enable detailed log. + * @param nonTypeRefers Whether to not generate the subclass of the TypeReference for paramTypes of class. + * @since 2021/8/3 + * @version 1.0 + */ +@compileTimeOnly("enable macro to expand macro annotations") +final class jacksonEnum( + verbose: Boolean = false, + nonTypeRefers: Seq[String] = Nil +) extends StaticAnnotation { + + def macroTransform(annottees: Any*): Any = macro jacksonEnumMacro.JacksonEnumProcessor.impl + +} diff --git a/src/main/scala/io/github/dreamylost/macros/AbstractMacroProcessor.scala b/src/main/scala/io/github/dreamylost/macros/AbstractMacroProcessor.scala index fe21275..e6cbfd9 100644 --- a/src/main/scala/io/github/dreamylost/macros/AbstractMacroProcessor.scala +++ b/src/main/scala/io/github/dreamylost/macros/AbstractMacroProcessor.scala @@ -21,6 +21,8 @@ package io.github.dreamylost.macros +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter import scala.reflect.macros.whitebox /** @@ -45,7 +47,7 @@ abstract class AbstractMacroProcessor(val c: whitebox.Context) { * @return c.Expr[Any], Why use Any? The dependent type need aux-pattern in scala2. Now let's get around this. * */ - def modifiedDeclaration(classDecl: ClassDef, compDeclOpt: Option[ModuleDef] = None): Any = ??? + def createCustomExpr(classDecl: ClassDef, compDeclOpt: Option[ModuleDef] = None): Any = ??? /** * Subclasses must override the method. @@ -85,7 +87,7 @@ abstract class AbstractMacroProcessor(val c: whitebox.Context) { def printTree(force: Boolean, resTree: Tree): Unit = { c.info( c.enclosingPosition, - "\n###### Expanded macro ######\n" + resTree.toString() + "\n###### Expanded macro ######\n", + s"\n###### Time: ${ZonedDateTime.now().format(DateTimeFormatter.ISO_ZONED_DATE_TIME)} Expanded macro start ######\n" + resTree.toString() + "\n###### Expanded macro end ######\n", force = force ) } @@ -96,43 +98,27 @@ abstract class AbstractMacroProcessor(val c: whitebox.Context) { * @param annottees * @return Return ClassDef */ - def checkAndGetClassDef(annottees: Expr[Any]*): ClassDef = { + def checkGetClassDef(annottees: Seq[Expr[Any]]): ClassDef = { annottees.map(_.tree).toList match { case (classDecl: ClassDef) :: Nil => classDecl - case (classDecl: ClassDef) :: (compDecl: ModuleDef) :: Nil => classDecl + case (classDecl: ClassDef) :: (_: ModuleDef) :: Nil => classDecl case _ => c.abort(c.enclosingPosition, ErrorMessage.UNEXPECTED_PATTERN) } } /** - * Get companion object if it exists. + * Get object if it exists. * * @param annottees * @return */ - def tryGetCompanionObject(annottees: Expr[Any]*): Option[ModuleDef] = { + def getModuleDefOption(annottees: Seq[Expr[Any]]): Option[ModuleDef] = { annottees.map(_.tree).toList match { - case (classDecl: ClassDef) :: Nil => None - case (classDecl: ClassDef) :: (compDecl: ModuleDef) :: Nil => Some(compDecl) - case (compDecl: ModuleDef) :: Nil => Some(compDecl) - case _ => c.abort(c.enclosingPosition, ErrorMessage.UNEXPECTED_PATTERN) - } - } - - /** - * Wrap tree result with companion object. - * - * @param resTree class - * @param annottees - * @return - */ - def treeResultWithCompanionObject(resTree: Tree, annottees: Expr[Any]*): Tree = { - val companionOpt = tryGetCompanionObject(annottees: _*) - companionOpt.fold(resTree) { t => - q""" - $resTree - $t - """ + case (moduleDef: ModuleDef) :: Nil => Some(moduleDef) + case (_: ClassDef) :: Nil => None + case (_: ClassDef) :: (compDecl: ModuleDef) :: Nil => Some(compDecl) + case (moduleDef: ModuleDef) :: (_: ClassDef) :: Nil => Some(moduleDef) + case _ => None } } @@ -143,13 +129,11 @@ abstract class AbstractMacroProcessor(val c: whitebox.Context) { * @param modifyAction The actual processing function * @return Return the result of modifyAction */ - def handleWithImplType(annottees: Expr[Any]*) + def collectCustomExpr(annottees: Seq[Expr[Any]]) (modifyAction: (ClassDef, Option[ModuleDef]) => Any): Expr[Nothing] = { - annottees.map(_.tree) match { - case (classDecl: ClassDef) :: Nil => modifyAction(classDecl, None).asInstanceOf[Expr[Nothing]] - case (classDecl: ClassDef) :: (compDecl: ModuleDef) :: Nil => modifyAction(classDecl, Some(compDecl)).asInstanceOf[Expr[Nothing]] - case _ => c.abort(c.enclosingPosition, ErrorMessage.UNEXPECTED_PATTERN) - } + val classDef = checkGetClassDef(annottees) + val compDecl = getModuleDefOption(annottees) + modifyAction(classDef, compDecl).asInstanceOf[Expr[Nothing]] } /** @@ -162,17 +146,6 @@ abstract class AbstractMacroProcessor(val c: whitebox.Context) { annotateeClass.mods.hasFlag(Flag.CASE) } - /** - * Expand the method params and get the param Name. - * - * @param field - * @return - */ - def getMethodParamName(field: Tree): Name = { - val q"$mods val $tname: $tpt = $expr" = field - tpt.asInstanceOf[Ident].name.decodedName - } - /** * Check whether the mods of the fields has a `private[this]` or `protected[this]`, because it cannot be used out of class. * @@ -199,38 +172,27 @@ abstract class AbstractMacroProcessor(val c: whitebox.Context) { * @return {{ i: Int}} */ def getConstructorParamsNameWithType(annotteeClassParams: Seq[Tree]): Seq[Tree] = { - annotteeClassParams.map { - case v: ValDef => q"${v.name}: ${v.tpt}" - } + annotteeClassParams.map(_.asInstanceOf[ValDef]).map(v => q"${v.name}: ${v.tpt}") } /** - * Modify companion objects. + * Modify companion object or object. * * @param compDeclOpt - * @param codeBlock + * @param codeBlocks * @param className * @return */ - def modifiedCompanion( + def appendModuleBody( compDeclOpt: Option[ModuleDef], - codeBlock: Tree, className: TypeName): Tree = { - compDeclOpt map { compDecl => - val q"$mods object $obj extends ..$bases { ..$body }" = compDecl - val o = - q""" - $mods object $obj extends ..$bases { - ..$body - ..$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} { ..$codeBlock }" - c.info(c.enclosingPosition, s"modifiedCompanion className: $className, new obj: $o", force = true) - o + codeBlocks: List[Tree], className: TypeName): Tree = { + compDeclOpt.fold(q"object ${className.toTermName} { ..$codeBlocks }") { + compDecl => + c.info(c.enclosingPosition, s"appendModuleBody className: $className, exists obj: $compDecl", force = true) + val ModuleDef(mods, name, impl) = compDecl + val Template(parents, self, body) = impl + val newImpl = Template(parents, self, body ++ codeBlocks) + ModuleDef(mods, name, newImpl) } } @@ -366,4 +328,117 @@ abstract class AbstractMacroProcessor(val c: whitebox.Context) { superClasses.nonEmpty && !superClasses.forall(sc => SDKClasses.contains(sc.toString())) } + private[macros] case class ValDefAccessor( + mods: Modifiers, + name: TermName, + tpt: Tree, + rhs: Tree + ) { + + def typeName: TypeName = symbol.name.toTypeName + + def symbol: c.universe.Symbol = paramType.typeSymbol + + def paramType = c.typecheck(tq"$tpt", c.TYPEmode).tpe + } + + /** + * Retrieves the accessor fields on a class and returns a Seq of ValDefAccessor. + * + * @param params The list of params retrieved from the class + * @return An Sequence of tuples where each tuple encodes the string name and string type of a field + */ + def valDefAccessors(params: Seq[Tree]): Seq[ValDefAccessor] = { + params.map { + case ValDef(mods, name: TermName, tpt: Tree, rhs) => + ValDefAccessor(mods, name, tpt, rhs) + } + } + + /** + * Extract the necessary structure information of the class for macro programming. + * + * @param classDecl + */ + def mapToClassDeclInfo(classDecl: ClassDef): ClassDefinition = { + val q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" = classDecl + val (className, classParamss, classTypeParams) = (tpname.asInstanceOf[TypeName], paramss.asInstanceOf[List[List[Tree]]], tparams.asInstanceOf[List[Tree]]) + ClassDefinition(self.asInstanceOf[ValDef], mods.asInstanceOf[Modifiers], className, classParamss, classTypeParams, stats.asInstanceOf[List[Tree]], parents.asInstanceOf[List[Tree]]) + } + + /** + * Extract the necessary structure information of the moduleDef for macro programming. + * + * @param moduleDef + */ + def mapToModuleDeclInfo(moduleDef: ModuleDef): ClassDefinition = { + val q"$mods object $tpname extends { ..$earlydefns } with ..$parents { $self => ..$stats }" = moduleDef + ClassDefinition(self.asInstanceOf[ValDef], mods.asInstanceOf[Modifiers], tpname.asInstanceOf[TermName].toTypeName, Nil, Nil, stats.asInstanceOf[List[Tree]], parents.asInstanceOf[List[Tree]]) + } + + /** + * Generate the specified syntax tree and assign it to the tree definition itself. + * Used only when you modify the definition of the class itself. Such as add method/add field. + * + * @param classDecl + * @param classInfoAction Content body added in class definition + * @return + */ + def appendClassBody(classDecl: ClassDef, classInfoAction: ClassDefinition => List[Tree]): c.universe.ClassDef = { + val classInfo = mapToClassDeclInfo(classDecl) + val ClassDef(mods, name, tparams, impl) = classDecl + val Template(parents, self, body) = impl + ClassDef(mods, name, tparams, Template(parents, self, body ++ classInfoAction(classInfo))) + } + + // TODO fix, why cannot use ClassDef apply + def prependImplDefBody(implDef: ImplDef, classInfoAction: ClassDefinition => List[Tree]): c.universe.Tree = { + implDef match { + case classDecl: ClassDef => + val classInfo = mapToClassDeclInfo(classDecl) + val q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" = classDecl + q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..${classInfoAction(classInfo) ++ stats} }" + case moduleDef: ModuleDef => + val classInfo = mapToModuleDeclInfo(moduleDef) + val q"$mods object $tpname extends { ..$earlydefns } with ..$parents { $self => ..$stats }" = moduleDef + q"$mods object $tpname extends { ..$earlydefns } with ..$parents { $self => ..${classInfoAction(classInfo) ++ stats.toList} }" + } + } + + def appendImplDefSuper(implDef: ImplDef, classInfoAction: ClassDefinition => List[Tree]): c.universe.Tree = { + implDef match { + case classDecl: ClassDef => + val classInfo = mapToClassDeclInfo(classDecl) + val q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" = classDecl + q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..${parents ++ classInfoAction(classInfo)} { $self => ..$stats }" + case moduleDef: ModuleDef => + val classInfo = mapToModuleDeclInfo(moduleDef) + val q"$mods object $tpname extends { ..$earlydefns } with ..$parents { $self => ..$stats }" = moduleDef + q"$mods object $tpname extends { ..$earlydefns } with ..${parents.toList ++ classInfoAction(classInfo)} { $self => ..$stats }" + } + } + + /** + * Modify the method body of the method tree. + * + * @param defDef + * @param defBodyAction Method body of final result + * @return + */ + def mapToMethodDef(defDef: DefDef, defBodyAction: => Tree): c.universe.DefDef = { + val DefDef(mods, name, tparams, vparamss, tpt, rhs) = defDef + DefDef(mods, name, tparams, vparamss, tpt, defBodyAction) + } + + private[macros] case class ClassDefinition( + self: ValDef, + mods: Modifiers, + className: TypeName, + classParamss: List[List[Tree]], + classTypeParams: List[Tree], + body: List[Tree], + superClasses: List[Tree], + earlydefns: List[Tree] = Nil + ) + } diff --git a/src/main/scala/io/github/dreamylost/macros/applyMacro.scala b/src/main/scala/io/github/dreamylost/macros/applyMacro.scala index 914d14d..7153885 100644 --- a/src/main/scala/io/github/dreamylost/macros/applyMacro.scala +++ b/src/main/scala/io/github/dreamylost/macros/applyMacro.scala @@ -43,14 +43,10 @@ object applyMacro { } } - override def modifiedDeclaration(classDecl: ClassDef, compDeclOpt: Option[ModuleDef] = None): Any = { - val (className, classParamss, classTypeParams) = classDecl match { - case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends ..$bases { ..$body }" => - (tpname.asInstanceOf[TypeName], paramss.asInstanceOf[List[List[Tree]]], tparams.asInstanceOf[List[Tree]]) - case _ => c.abort(c.enclosingPosition, s"${ErrorMessage.ONLY_CLASS} classDef: $classDecl") - } - val apply = getApplyMethodWithCurrying(className, classParamss, classTypeParams) - val compDecl = modifiedCompanion(compDeclOpt, apply, className) + override def createCustomExpr(classDecl: ClassDef, compDeclOpt: Option[ModuleDef] = None): Any = { + val classDefinition = mapToClassDeclInfo(classDecl) + val apply = getApplyMethodWithCurrying(classDefinition.className, classDefinition.classParamss, classDefinition.classTypeParams) + val compDecl = appendModuleBody(compDeclOpt, List(apply), classDefinition.className) c.Expr( q""" $classDecl @@ -59,9 +55,11 @@ object applyMacro { } override def impl(annottees: Expr[Any]*): Expr[Any] = { - val annotateeClass: ClassDef = checkAndGetClassDef(annottees: _*) - if (isCaseClass(annotateeClass)) c.abort(c.enclosingPosition, ErrorMessage.ONLY_CASE_CLASS) - val resTree = handleWithImplType(annottees: _*)(modifiedDeclaration) + val annotateeClass: ClassDef = checkGetClassDef(annottees) + if (isCaseClass(annotateeClass)) { + c.abort(c.enclosingPosition, ErrorMessage.ONLY_CASE_CLASS) + } + val resTree = collectCustomExpr(annottees)(createCustomExpr) printTree(force = extractArgumentsDetail._1, resTree.tree) resTree } diff --git a/src/main/scala/io/github/dreamylost/macros/builderMacro.scala b/src/main/scala/io/github/dreamylost/macros/builderMacro.scala index 98a38ae..597754e 100644 --- a/src/main/scala/io/github/dreamylost/macros/builderMacro.scala +++ b/src/main/scala/io/github/dreamylost/macros/builderMacro.scala @@ -40,9 +40,8 @@ object builderMacro { } private def getFieldDefinition(field: Tree): Tree = { - field match { - case v: ValDef => q"private var ${v.name}: ${v.tpt} = ${v.rhs}" - } + val ValDef(mods, name, tpt, rhs) = field + q"private var $name: $tpt = $rhs" } private def getFieldSetMethod(typeName: TypeName, field: Tree, classTypeParams: List[Tree]): Tree = { @@ -56,41 +55,35 @@ object builderMacro { } """ } - field match { - case v: ValDef => valDefMapTo(v) - } + valDefMapTo(field.asInstanceOf[ValDef]) } - private def getBuilderClassAndMethod(typeName: TypeName, fieldss: List[List[Tree]], classTypeParams: List[Tree], isCase: Boolean): Tree = { + private def getBuilderClassAndMethod(typeName: TypeName, fieldss: List[List[Tree]], classTypeParams: List[Tree], isCase: Boolean): List[Tree] = { val fields = fieldss.flatten val builderClassName = getBuilderClassName(typeName) val builderFieldMethods = fields.map(f => getFieldSetMethod(typeName, f, classTypeParams)) val builderFieldDefinitions = fields.map(f => getFieldDefinition(f)) val returnTypeParams = extractClassTypeParamsTypeName(classTypeParams) - q""" - def builder[..$classTypeParams](): $builderClassName[..$returnTypeParams] = new $builderClassName() - - class $builderClassName[..$classTypeParams] { + val builderMethod = q"def builder[..$classTypeParams](): $builderClassName[..$returnTypeParams] = new $builderClassName()" + val buulderClass = + q""" + class $builderClassName[..$classTypeParams] { ..$builderFieldDefinitions ..$builderFieldMethods def build(): $typeName[..$returnTypeParams] = ${getConstructorWithCurrying(typeName, fieldss, isCase)} - } - """ + } + """ + List(builderMethod, buulderClass) } - override def modifiedDeclaration(classDecl: ClassDef, compDeclOpt: Option[ModuleDef] = None): Any = { - val (className, annotteeClassParams, classTypeParams) = classDecl match { - // @see https://scala-lang.org/files/archive/spec/2.13/05-classes-and-objects.html - case q"$mods class $tpname[..$tparams](...$paramss) extends ..$bases { ..$body }" => - (tpname.asInstanceOf[TypeName], paramss.asInstanceOf[List[List[Tree]]], tparams.asInstanceOf[List[Tree]]) - case _ => c.abort(c.enclosingPosition, s"${ErrorMessage.ONLY_CLASS} classDef: $classDecl") - } - - val builder = getBuilderClassAndMethod(className, annotteeClassParams, classTypeParams, isCaseClass(classDecl)) - val compDecl = modifiedCompanion(compDeclOpt, builder, className) + override def createCustomExpr(classDecl: ClassDef, compDeclOpt: Option[ModuleDef] = None): Any = { + val classDefinition = mapToClassDeclInfo(classDecl) + val builder = getBuilderClassAndMethod(classDefinition.className, classDefinition.classParamss, + classDefinition.classTypeParams, isCaseClass(classDecl)) + val compDecl = appendModuleBody(compDeclOpt, builder, classDefinition.className) // Return both the class and companion object declarations c.Expr( q""" @@ -100,7 +93,7 @@ object builderMacro { } override def impl(annottees: c.universe.Expr[Any]*): c.universe.Expr[Any] = { - val resTree = handleWithImplType(annottees: _*)(modifiedDeclaration) + val resTree = collectCustomExpr(annottees)(createCustomExpr) printTree(force = true, resTree.tree) resTree } diff --git a/src/main/scala/io/github/dreamylost/macros/constructorMacro.scala b/src/main/scala/io/github/dreamylost/macros/constructorMacro.scala index 7be74af..a57a942 100644 --- a/src/main/scala/io/github/dreamylost/macros/constructorMacro.scala +++ b/src/main/scala/io/github/dreamylost/macros/constructorMacro.scala @@ -98,25 +98,23 @@ object constructorMacro { applyMethod } - override 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 }" => - (paramss.asInstanceOf[List[List[Tree]]], stats.asInstanceOf[Seq[Tree]]) - case _ => c.abort(c.enclosingPosition, s"${ErrorMessage.ONLY_CLASS} classDef: $classDecl") - } - c.Expr(getThisMethodWithCurrying(annotteeClassParams, annotteeClassDefinitions)) + override def createCustomExpr(classDecl: ClassDef, compDeclOpt: Option[ModuleDef] = None): Any = { + val resTree = appendClassBody( + classDecl, + classInfo => List(getThisMethodWithCurrying(classInfo.classParamss, classInfo.body))) + c.Expr( + q""" + ${compDeclOpt.fold(EmptyTree)(x => x)} + $resTree + """) } override def impl(annottees: c.universe.Expr[Any]*): c.universe.Expr[Any] = { - val annotateeClass: ClassDef = checkAndGetClassDef(annottees: _*) - if (isCaseClass(annotateeClass)) c.abort(c.enclosingPosition, s"${ErrorMessage.ONLY_CLASS} classDef: $annotateeClass") - - val tmpTree = handleWithImplType(annottees: _*)(modifiedDeclaration) - 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.:+(tmpTree.tree)} }" + val annotateeClass: ClassDef = checkGetClassDef(annottees) + if (isCaseClass(annotateeClass)) { + c.abort(c.enclosingPosition, ErrorMessage.ONLY_CLASS) } - val res = c.Expr[Any](treeResultWithCompanionObject(resTree, annottees: _*)) + val res = collectCustomExpr(annottees)(createCustomExpr) printTree(force = extractArgumentsDetail._1, res.tree) res } diff --git a/src/main/scala/io/github/dreamylost/macros/equalsAndHashCodeMacro.scala b/src/main/scala/io/github/dreamylost/macros/equalsAndHashCodeMacro.scala index 4bf76b9..abbe10d 100644 --- a/src/main/scala/io/github/dreamylost/macros/equalsAndHashCodeMacro.scala +++ b/src/main/scala/io/github/dreamylost/macros/equalsAndHashCodeMacro.scala @@ -44,24 +44,13 @@ object equalsAndHashCodeMacro { } override def impl(annottees: c.universe.Expr[Any]*): c.universe.Expr[Any] = { - val annotateeClass: ClassDef = checkAndGetClassDef(annottees: _*) - if (isCaseClass(annotateeClass)) c.abort(c.enclosingPosition, s"${ErrorMessage.ONLY_CLASS} classDef: $annotateeClass") - - val tmpTree = handleWithImplType(annottees: _*)(modifiedDeclaration) - // return with object if it exists - val resTree = annotateeClass match { - case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" => - val originalStatus = q"{ ..$stats }" - val append = - q""" - ..$originalStatus - ..$tmpTree - """ - q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..${append} }" + val annotateeClass: ClassDef = checkGetClassDef(annottees) + if (isCaseClass(annotateeClass)) { + c.abort(c.enclosingPosition, ErrorMessage.ONLY_CLASS) } - val res = c.Expr[Any](treeResultWithCompanionObject(resTree, annottees: _*)) - printTree(force = extractArgumentsDetail._1, res.tree) - res + val resTree = collectCustomExpr(annottees)(createCustomExpr) + printTree(force = extractArgumentsDetail._1, resTree.tree) + resTree } /** @@ -76,65 +65,57 @@ object equalsAndHashCodeMacro { } // equals method - private def getEqualsMethod(className: TypeName, termNames: Seq[TermName], superClasses: Seq[Tree], annotteeClassDefinitions: Seq[Tree]): Tree = { + private def getEqualsMethod(className: TypeName, termNames: Seq[TermName], superClasses: Seq[Tree], annotteeClassDefinitions: Seq[Tree]): List[Tree] = { val existsCanEqual = getClassMemberDefDefs(annotteeClassDefinitions).exists { - case tree @ q"$mods def $tname[..$tparams](...$paramss): $tpt = $expr" if tname.asInstanceOf[TermName].decodedName.toString == "canEqual" && paramss.nonEmpty => - val params = paramss.asInstanceOf[List[List[Tree]]].flatten.map(pp => getMethodParamName(pp)) - params.exists(p => p.decodedName.toString == "Any") + case defDef: DefDef if defDef.name.decodedName.toString == "canEqual" && defDef.vparamss.nonEmpty => + val safeValDefs = valDefAccessors(defDef.vparamss.flatten) + safeValDefs.exists(_.paramType.toString == "Any") && safeValDefs.exists(_.name.decodedName.toString == "that") case _ => false } - lazy val getEqualsExpr = (termName: TermName) => { - q"this.$termName.equals(t.$termName)" - } - val equalsExprs = termNames.map(getEqualsExpr) + val equalsExprs = termNames.map(termName => q"this.$termName.equals(t.$termName)") // Make a rough judgment on whether override is needed. val modifiers = if (existsSuperClassExcludeSdkClass(superClasses)) Modifiers(Flag.OVERRIDE, typeNames.EMPTY, List()) else Modifiers(NoFlags, typeNames.EMPTY, List()) val canEqual = if (existsCanEqual) q"" else q"$modifiers def canEqual(that: Any) = that.isInstanceOf[$className]" - q""" - $canEqual - + val equalsMethod = + q""" override def equals(that: Any): Boolean = that match { case t: $className => t.canEqual(this) && Seq(..$equalsExprs).forall(f => f) && ${if (existsSuperClassExcludeSdkClass(superClasses)) q"super.equals(that)" else q"true"} case _ => false } """ + List(canEqual, equalsMethod) } private def getHashcodeMethod(termNames: Seq[TermName], superClasses: Seq[Tree]): Tree = { // we append super.hashCode by `+` // the algorithm see https://alvinalexander.com/scala/how-to-define-equals-hashcode-methods-in-scala-object-equality/ - if (!existsSuperClassExcludeSdkClass(superClasses)) { - q""" - override def hashCode(): Int = { - val state = Seq(..$termNames) - state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b) - } - """ - } else { - q""" + val superTree = q"super.hashCode" + q""" override def hashCode(): Int = { val state = Seq(..$termNames) - state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b) + super.hashCode + state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b) + ${if (existsSuperClassExcludeSdkClass(superClasses)) superTree else q"0"} } - """ - } + """ } - override def modifiedDeclaration(classDecl: ClassDef, compDeclOpt: Option[ModuleDef]): Any = { - val (className, annotteeClassParams, annotteeClassDefinitions, superClasses) = classDecl match { - case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" => - (tpname.asInstanceOf[TypeName], paramss.asInstanceOf[List[List[Tree]]], stats.asInstanceOf[Seq[Tree]], parents.asInstanceOf[Seq[Tree]]) - case _ => c.abort(c.enclosingPosition, s"${ErrorMessage.ONLY_CLASS} classDef: $classDecl") + override def createCustomExpr(classDecl: ClassDef, compDeclOpt: Option[ModuleDef]): Any = { + lazy val map = (classDefinition: ClassDefinition) => { + getClassConstructorValDefsFlatten(classDefinition.classParamss). + filter(cf => isNotLocalClassMember(cf)). + map(_.name.toTermName) ++ + getInternalFieldsTermNameExcludeLocal(classDefinition.body) } - val allFieldsTermName = getClassConstructorValDefsFlatten(annotteeClassParams).filter(cf => isNotLocalClassMember(cf)).map(_.name.toTermName) - val allTernNames = allFieldsTermName ++ getInternalFieldsTermNameExcludeLocal(annotteeClassDefinitions) - val hash = getHashcodeMethod(allTernNames, superClasses) - val equals = getEqualsMethod(className, allTernNames, superClasses, annotteeClassDefinitions) + val classDefinition = mapToClassDeclInfo(classDecl) + val res = appendClassBody(classDecl, classInfo => + getEqualsMethod(classDefinition.className, map(classInfo), classDefinition.superClasses, classDefinition.body) ++ + List(getHashcodeMethod(map(classInfo), classDefinition.superClasses)) + ) + c.Expr( q""" - ..$equals - $hash + ${compDeclOpt.fold(EmptyTree)(x => x)} + $res """) } } diff --git a/src/main/scala/io/github/dreamylost/macros/jacksonEnumMacro.scala b/src/main/scala/io/github/dreamylost/macros/jacksonEnumMacro.scala new file mode 100644 index 0000000..a6c33af --- /dev/null +++ b/src/main/scala/io/github/dreamylost/macros/jacksonEnumMacro.scala @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2021 jxnu-liguobin && contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package io.github.dreamylost.macros + +import scala.reflect.macros.whitebox + +object jacksonEnumMacro { + + class JacksonEnumProcessor(override val c: whitebox.Context) extends AbstractMacroProcessor(c) { + + import c.universe._ + + private val extractArgumentsDetail: Tuple2[Boolean, Seq[String]] = { + extractArgumentsTuple2 { + case q"new jacksonEnum(verbose=$verbose, nonTypeRefers=$nonTypeRefers)" => Tuple2(evalTree(verbose.asInstanceOf[Tree]), evalTree(nonTypeRefers.asInstanceOf[Tree])) + case q"new jacksonEnum(nonTypeRefers=$nonTypeRefers)" => Tuple2(false, evalTree(nonTypeRefers.asInstanceOf[Tree])) + case q"new jacksonEnum()" => Tuple2(false, Nil) + case _ => c.abort(c.enclosingPosition, ErrorMessage.UNEXPECTED_PATTERN) + } + } + + private def getJacksonTypeReferClasses(valDefs: List[ValDef]): Seq[Tree] = { + val safeValDefs = valDefAccessors(valDefs) + // Enum ? + safeValDefs.filter(_.symbol.name.toTermName.toString == "Value"). + map(getTypeTermName). + filter(v => !extractArgumentsDetail._2.contains(v.decodedName.toString)). + distinct. + map(c => q"""class ${TypeName(c.decodedName.toString + "TypeRefer")} extends _root_.com.fasterxml.jackson.core.`type`.TypeReference[$c.type]""") + } + + private def getTypeTermName(valDefTree: Tree): c.universe.TermName = { + val safeValDef = valDefAccessors(Seq(valDefTree)).head + getTypeTermName(safeValDef) + } + + private def getTypeTermName(accessor: ValDefAccessor): c.universe.TermName = { + val paramTypeStr = accessor.paramType.toString + TermName(paramTypeStr.split("\\.").last) + } + + private def getAnnotation(valDefTree: Tree): Tree = { + q"new com.fasterxml.jackson.module.scala.JsonScalaEnumeration(classOf[${TypeName(getTypeTermName(valDefTree).decodedName.toString + "TypeRefer")}])" + } + + private def replaceAnnotation(valDefTree: Tree): Tree = { + val safeValDef = valDefAccessors(Seq(valDefTree)).head + if (safeValDef.typeName.decodedName.toString == "Value") { + // duplication should be removed + val mods = safeValDef.mods.mapAnnotations(f => { + if (!f.toString().contains("JsonScalaEnumeration") && + !extractArgumentsDetail._2.contains(getTypeTermName(safeValDef).decodedName.toString)) f ++ List(getAnnotation(valDefTree)) else f + }) + ValDef(mods, safeValDef.name, safeValDef.tpt, safeValDef.rhs) + } else { + valDefTree + } + } + + override def impl(annottees: c.universe.Expr[Any]*): c.universe.Expr[Any] = { + val res = collectCustomExpr(annottees)(createCustomExpr) + printTree(force = extractArgumentsDetail._1, res.tree) + res + } + + override def createCustomExpr(classDecl: c.universe.ClassDef, compDeclOpt: Option[c.universe.ModuleDef]): Any = { + // return all typeReferClasses and new classDef + val classDefinition = mapToClassDeclInfo(classDecl) + val valDefs = classDefinition.classParamss.flatten.map(_.asInstanceOf[ValDef]) + val typeReferClasses = getJacksonTypeReferClasses(valDefs).distinct + val q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends ..$bases { ..$body }" = classDecl + val newFieldss = paramss.asInstanceOf[List[List[Tree]]].map(_.map(replaceAnnotation)) + val newClass = q"$mods class $tpname[..$tparams] $ctorMods(...$newFieldss) extends ..$bases { ..$body }" + val res = + q""" + ..$typeReferClasses + + $newClass + """ + + c.Expr( + q""" + ${compDeclOpt.fold(EmptyTree)(x => x)} + $res + """) + } + } +} + diff --git a/src/main/scala/io/github/dreamylost/macros/jsonMacro.scala b/src/main/scala/io/github/dreamylost/macros/jsonMacro.scala index a5d0dfc..805e87d 100644 --- a/src/main/scala/io/github/dreamylost/macros/jsonMacro.scala +++ b/src/main/scala/io/github/dreamylost/macros/jsonMacro.scala @@ -44,23 +44,19 @@ object jsonMacro { } override def impl(annottees: c.universe.Expr[Any]*): c.universe.Expr[Any] = { - val resTree = handleWithImplType(annottees: _*)(modifiedDeclaration) + val annotateeClass: ClassDef = checkGetClassDef(annottees) + if (!isCaseClass(annotateeClass)) { + c.abort(c.enclosingPosition, ErrorMessage.ONLY_CASE_CLASS) + } + val resTree = collectCustomExpr(annottees)(createCustomExpr) printTree(force = true, resTree.tree) resTree } - override def modifiedDeclaration(classDecl: c.universe.ClassDef, compDeclOpt: Option[c.universe.ModuleDef]): Any = { - val (className, fields) = classDecl match { - case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends ..$bases { ..$body }" => - if (!mods.asInstanceOf[Modifiers].hasFlag(Flag.CASE)) { - c.abort(c.enclosingPosition, ErrorMessage.ONLY_CASE_CLASS) - } else { - (tpname.asInstanceOf[TypeName], paramss.asInstanceOf[List[List[Tree]]]) - } - case _ => c.abort(c.enclosingPosition, s"${ErrorMessage.ONLY_CLASS} classDef: $classDecl") - } - val format = jsonFormatter(className, fields.flatten) - val compDecl = modifiedCompanion(compDeclOpt, format, className) + override def createCustomExpr(classDecl: c.universe.ClassDef, compDeclOpt: Option[c.universe.ModuleDef]): Any = { + val classDefinition = mapToClassDeclInfo(classDecl) + val format = jsonFormatter(classDefinition.className, classDefinition.classParamss.flatten) + val compDecl = appendModuleBody(compDeclOpt, List(format), classDefinition.className) // Return both the class and companion object declarations c.Expr( q""" diff --git a/src/main/scala/io/github/dreamylost/macros/logMacro.scala b/src/main/scala/io/github/dreamylost/macros/logMacro.scala index e298f00..f0be594 100644 --- a/src/main/scala/io/github/dreamylost/macros/logMacro.scala +++ b/src/main/scala/io/github/dreamylost/macros/logMacro.scala @@ -21,9 +21,9 @@ package io.github.dreamylost.macros -import io.github.dreamylost.{ PACKAGE, logs } -import io.github.dreamylost.logs.{ LogTransferArgument, LogType } import io.github.dreamylost.logs.LogType._ +import io.github.dreamylost.logs.{ LogTransferArgument, LogType } +import io.github.dreamylost.{ PACKAGE, logs } import scala.reflect.macros.whitebox @@ -53,45 +53,53 @@ object logMacro { private def getLogType(logType: Tree): LogType = { if (logType.children.exists(t => t.toString().contains(PACKAGE))) { - evalTree(logType.asInstanceOf[Tree]) // TODO remove asInstanceOf + evalTree(logType) } else { LogType.getLogType(logType.toString()) } } - override def impl(annottees: c.universe.Expr[Any]*): c.universe.Expr[Any] = { - val logTree = annottees.map(_.tree) match { - // Match a class, and expand, get class/object name. - case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" :: _ => - val argument = LogTransferArgument(tpname.asInstanceOf[TypeName].toTermName.decodedName.toString, isClass = true) - LogType.getLogImpl(extractArgumentsDetail._2).getTemplate(c)(argument) - case q"$mods object $tpname extends { ..$earlydefns } with ..$parents { $self => ..$stats }" :: _ => - val argument = LogTransferArgument(tpname.asInstanceOf[TermName].decodedName.toString, isClass = false) - LogType.getLogImpl(extractArgumentsDetail._2).getTemplate(c)(argument) + private def logTree(annottees: Seq[c.universe.Expr[Any]]): c.universe.Tree = { + val buildArg = (name: Name) => LogTransferArgument(name.toTermName.decodedName.toString, isClass = true) + (annottees.map(_.tree) match { + case (classDef: ClassDef) :: Nil => + LogType.getLogImpl(extractArgumentsDetail._2).getTemplate(c)(buildArg(classDef.name)) + case (moduleDef: ModuleDef) :: Nil => + LogType.getLogImpl(extractArgumentsDetail._2).getTemplate(c)(buildArg(moduleDef.name).copy(isClass = false)) + case (classDef: ClassDef) :: (_: ModuleDef) :: Nil => + LogType.getLogImpl(extractArgumentsDetail._2).getTemplate(c)(buildArg(classDef.name)) case _ => c.abort(c.enclosingPosition, ErrorMessage.ONLY_OBJECT_CLASS) - } + }).asInstanceOf[Tree] + } - // add result into class + override def impl(annottees: c.universe.Expr[Any]*): c.universe.Expr[Any] = { val resTree = annottees.map(_.tree) match { - case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" :: _ => - extractArgumentsDetail._2 match { + case (classDef: ClassDef) :: _ => + if (classDef.mods.hasFlag(Flag.CASE)) { + c.abort(c.enclosingPosition, ErrorMessage.ONLY_OBJECT_CLASS) + } + val newClass = extractArgumentsDetail._2 match { case ScalaLoggingLazy | ScalaLoggingStrict => - q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..${parents ++ Seq(logTree)} { $self => ..$stats }" - case _ => q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..${Seq(logTree) ++ stats} }" + appendImplDefSuper(checkGetClassDef(annottees), _ => List(logTree(annottees))) + case _ => + prependImplDefBody(checkGetClassDef(annottees), _ => List(logTree(annottees))) } - case q"$mods object $tpname extends { ..$earlydefns } with ..$parents { $self => ..$stats }" :: _ => + val moduleDef = getModuleDefOption(annottees) + q""" + ${if (moduleDef.isEmpty) EmptyTree else moduleDef.get} + $newClass + """ + case (_: ModuleDef) :: _ => extractArgumentsDetail._2 match { - case ScalaLoggingLazy | ScalaLoggingStrict => - q"$mods object $tpname extends { ..$earlydefns } with ..${parents ++ Seq(logTree)} { $self => ..$stats }" - case _ => q"$mods object $tpname extends { ..$earlydefns } with ..$parents { $self => ..${Seq(logTree) ++ stats} }" + case ScalaLoggingLazy | ScalaLoggingStrict => appendImplDefSuper(getModuleDefOption(annottees).get, _ => List(logTree(annottees))) + case _ => prependImplDefBody(getModuleDefOption(annottees).get, _ => List(logTree(annottees))) } // Note: If a class is annotated and it has a companion, then both are passed into the macro. // (But not vice versa - if an object is annotated and it has a companion class, only the object itself is expanded). // see https://docs.scala-lang.org/overviews/macros/annotations.html } - val res = treeResultWithCompanionObject(resTree, annottees: _*) - printTree(force = extractArgumentsDetail._1, res) + printTree(force = extractArgumentsDetail._1, resTree) c.Expr[Any](resTree) } } diff --git a/src/main/scala/io/github/dreamylost/macros/macros.scala b/src/main/scala/io/github/dreamylost/macros/macros.scala index 6cde57d..d87a7ef 100644 --- a/src/main/scala/io/github/dreamylost/macros/macros.scala +++ b/src/main/scala/io/github/dreamylost/macros/macros.scala @@ -31,10 +31,10 @@ package object macros { object ErrorMessage { // common error msg - final val ONLY_CLASS = "Annotation is only supported on class." - final val ONLY_CASE_CLASS = "Annotation is only supported on case class." - final val ONLY_OBJECT_CLASS = "Annotation is only supported on class or object." - final val UNEXPECTED_PATTERN = "Unexpected annotation pattern!" + final lazy val ONLY_CLASS = "Annotation is only supported on class." + final lazy val ONLY_CASE_CLASS = "Annotation is only supported on case class." + final lazy val ONLY_OBJECT_CLASS = "Annotation is only supported on class or object." + final lazy val UNEXPECTED_PATTERN = "Unexpected annotation pattern!" } } diff --git a/src/main/scala/io/github/dreamylost/macros/synchronizedMacro.scala b/src/main/scala/io/github/dreamylost/macros/synchronizedMacro.scala index a2756b3..3a783de 100644 --- a/src/main/scala/io/github/dreamylost/macros/synchronizedMacro.scala +++ b/src/main/scala/io/github/dreamylost/macros/synchronizedMacro.scala @@ -43,18 +43,18 @@ object synchronizedMacro { } override def impl(annottees: c.universe.Expr[Any]*): c.universe.Expr[Any] = { - val resTree = annottees map (_.tree) match { - // Match a method, and expand. - case _@ q"$modrs def $tname[..$tparams](...$paramss): $tpt = $expr" :: _ => - if (extractArgumentsDetail._2 != null) { + val resTree = annottees.map(_.tree) match { + case (defDef: DefDef) :: Nil => + val body = if (extractArgumentsDetail._2 != null) { if (extractArgumentsDetail._2 == "this") { - q"""def $tname[..$tparams](...$paramss): $tpt = ${This(TypeName(""))}.synchronized { $expr }""" + q"${This(TypeName(""))}.synchronized { ${defDef.rhs} }" } else { - q"""def $tname[..$tparams](...$paramss): $tpt = ${TermName(extractArgumentsDetail._2)}.synchronized { $expr }""" + q"${TermName(extractArgumentsDetail._2)}.synchronized { ${defDef.rhs} }" } } else { c.abort(c.enclosingPosition, "Invalid args, lockName cannot be a null!") } + mapToMethodDef(defDef, body) case _ => c.abort(c.enclosingPosition, "Invalid annotation target: not a method") } printTree(extractArgumentsDetail._1, resTree) diff --git a/src/main/scala/io/github/dreamylost/macros/toStringMacro.scala b/src/main/scala/io/github/dreamylost/macros/toStringMacro.scala index 9aaeec7..4d0c03f 100644 --- a/src/main/scala/io/github/dreamylost/macros/toStringMacro.scala +++ b/src/main/scala/io/github/dreamylost/macros/toStringMacro.scala @@ -67,21 +67,26 @@ object toStringMacro { case _ => c.abort(c.enclosingPosition, ErrorMessage.UNEXPECTED_PATTERN) } - override def impl(annottees: c.universe.Expr[Any]*): c.universe.Expr[Any] = { + override def createCustomExpr(classDecl: c.universe.ClassDef, compDeclOpt: Option[c.universe.ModuleDef]): Any = { // extract parameters of annotation, must in order - val argument = Argument(extractArgumentsDetail._1, extractArgumentsDetail._2, extractArgumentsDetail._3, extractArgumentsDetail._4) - // Check the type of the class, which can only be defined on the ordinary class - val annotateeClass: ClassDef = checkAndGetClassDef(annottees: _*) - val isCase: Boolean = isCaseClass(annotateeClass) - val resMethod = toStringTemplateImpl(argument, annotateeClass) - 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.:+(resMethod)} }" - } + val argument = Argument( + extractArgumentsDetail._1, + extractArgumentsDetail._2, + extractArgumentsDetail._3, + extractArgumentsDetail._4 + ) + val resTree = appendClassBody(classDecl, _ => List(getToStringTemplate(argument, classDecl))) + c.Expr( + q""" + ${compDeclOpt.fold(EmptyTree)(x => x)} + $resTree + """) + } - val res = treeResultWithCompanionObject(resTree, annottees: _*) - printTree(argument.verbose, res) - c.Expr[Any](res) + override def impl(annottees: c.universe.Expr[Any]*): c.universe.Expr[Any] = { + val res = collectCustomExpr(annottees)(createCustomExpr) + printTree(force = extractArgumentsDetail._1, res.tree) + res } private def printField(argument: Argument, lastParam: Option[String], field: Tree): Tree = { @@ -105,16 +110,11 @@ object toStringMacro { } } - private def toStringTemplateImpl(argument: Argument, annotateeClass: ClassDef): Tree = { + private def getToStringTemplate(argument: Argument, classDecl: ClassDef): Tree = { // For a given class definition, separate the components of the class - val (className, annotteeClassParams, superClasses, annotteeClassDefinitions) = { - annotateeClass match { - case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" => - (tpname.asInstanceOf[TypeName], paramss.asInstanceOf[List[List[Tree]]], parents, stats.asInstanceOf[List[Tree]]) - } - } + val classDefinition = mapToClassDeclInfo(classDecl) // Check the type of the class, whether it already contains its own toString - val annotteeClassFieldDefinitions = annotteeClassDefinitions.filter(p => p match { + val annotteeClassFieldDefinitions = classDefinition.body.filter(_ match { case _: ValDef => true case mem: MemberDef => if (mem.name.decodedName.toString.startsWith("toString")) { // TODO better way @@ -124,7 +124,7 @@ object toStringMacro { case _ => false }) - val ctorParams = annotteeClassParams.flatten + val ctorParams = classDefinition.classParamss.flatten val member = if (argument.includeInternalFields) ctorParams ++ annotteeClassFieldDefinitions else ctorParams val lastParam = member.lastOption.map { @@ -133,17 +133,17 @@ object toStringMacro { } val paramsWithName = member.foldLeft(q"${""}")((res, acc) => q"$res + ${printField(argument, lastParam, acc)}") //scala/bug https://github.com/scala/bug/issues/3967 not be 'Foo(i=1,j=2)' in standard library - val toString = q"""override def toString: String = ${className.toTermName.decodedName.toString} + ${"("} + $paramsWithName + ${")"}""" + val toString = q"""override def toString: String = ${classDefinition.className.toTermName.decodedName.toString} + ${"("} + $paramsWithName + ${")"}""" // Have super class ? - if (argument.callSuper && superClasses.nonEmpty) { - val superClassDef = superClasses.head match { + if (argument.callSuper && classDefinition.superClasses.nonEmpty) { + val superClassDef = classDefinition.superClasses.head match { case tree: Tree => Some(tree) // TODO type check better case _ => None } superClassDef.fold(toString)(_ => { val superClass = q"${"super="}" - q"override def toString: String = StringContext(${className.toTermName.decodedName.toString} + ${"("} + $superClass, ${if (member.nonEmpty) ", " else ""}+$paramsWithName + ${")"}).s(super.toString)" + q"override def toString: String = StringContext(${classDefinition.className.toTermName.decodedName.toString} + ${"("} + $superClass, ${if (member.nonEmpty) ", " else ""}+$paramsWithName + ${")"}).s(super.toString)" } ) } else { diff --git a/src/test/scala/io/github/dreamylost/JacksonEnumTest.scala b/src/test/scala/io/github/dreamylost/JacksonEnumTest.scala new file mode 100644 index 0000000..f2e5dec --- /dev/null +++ b/src/test/scala/io/github/dreamylost/JacksonEnumTest.scala @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2021 jxnu-liguobin && contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package io.github.dreamylost + +import com.fasterxml.jackson.module.scala.JsonScalaEnumeration +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +/** + * + * @author 梦境迷离 + * @version 1.0,2021/8/3 + */ +class JacksonEnumTest extends AnyFlatSpec with Matchers { + + object EnumType extends Enumeration { + type EnumType = Value + val A = Value(1) + val B = Value(2) + } + + object EnumType2 extends Enumeration { + type EnumType2 = Value + val A, B = Value + } + + object EnumType3 extends Enumeration { + type EnumType3 = Value + val A, B = Value + } + + "jacksonEnum1" should "ok" in { + class EnumTypeTypeRefer extends _root_.com.fasterxml.jackson.core.`type`.TypeReference[EnumType.type] + case class A( + @JsonScalaEnumeration(classOf[EnumTypeTypeRefer]) enum1: EnumType.EnumType, + enum2: EnumType.EnumType = EnumType.A + ) + } + + "jacksonEnum2" should "ok" in { + @jacksonEnum + case class A( + enum1: EnumType.EnumType, + enum2: EnumType.EnumType = EnumType.A, + i: Int) + } + + "jacksonEnum3" should "ok" in { + @jacksonEnum + case class A( + var enum1: EnumType.EnumType, + enum2: EnumType2.EnumType2 = EnumType2.A, + i: Int) + @jacksonEnum(nonTypeRefers = Seq("EnumType", "EnumType2")) // Because it has been created + class B( + var enum1: EnumType.EnumType, // No annotation will add + val enum2: EnumType2.EnumType2 = EnumType2.A, + val enum3: EnumType3.EnumType3, + i: Int) + } + + "jacksonEnum4" should "ok when duplication" in { + """ + | @jacksonEnum + | case class A( + | @JsonScalaEnumeration(classOf[EnumTypeTypeRefer]) var enum1: EnumType.EnumType, + | enum2: EnumType2.EnumType2 = EnumType2.A, + | i: Int) + |""".stripMargin should compile + + """ + | @jacksonEnum + | class A( + | @JsonScalaEnumeration(classOf[EnumTypeTypeRefer]) var enum1: EnumType.EnumType, + | enum2: EnumType2.EnumType2 = EnumType2.A, + | i: Int) + |""".stripMargin should compile + } + + "jacksonEnum5" should "failed on object" in { + """ + | @jacksonEnum + | object A() + |""".stripMargin shouldNot compile + } + + "jacksonEnum6" should "failed when input args are invalid" in { + """ + | @jacksonEnum(verbose=true, nonTypeRefers=Nil) + | class A(enum1: EnumType.EnumType) + |""".stripMargin should compile + """ + | @jacksonEnum(true) + | class B(enum1: EnumType.EnumType) + |""".stripMargin shouldNot compile + } +} diff --git a/src/test/scala/io/github/dreamylost/LogTest.scala b/src/test/scala/io/github/dreamylost/LogTest.scala index 4124347..974c46c 100644 --- a/src/test/scala/io/github/dreamylost/LogTest.scala +++ b/src/test/scala/io/github/dreamylost/LogTest.scala @@ -45,18 +45,6 @@ class LogTest extends AnyFlatSpec with Matchers { """@log(verbose=true, logType=io.github.dreamylost.logs.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.logs.LogType.JLog) case class TestClass5(val i: Int = 0, var j: Int)""" should compile - """@log(verbose=true, logType=io.github.dreamylost.logs.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") @@ -121,8 +109,8 @@ class LogTest extends AnyFlatSpec with Matchers { "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 + 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 @@ -131,7 +119,13 @@ class LogTest extends AnyFlatSpec with Matchers { """@log(verbose=true, logType=io.github.dreamylost.logs.LogType.Slf4j) class TestClass6(val i: Int = 0, var j: Int)""" should compile """@log(verbose=true, logType=io.github.dreamylost.logs.LogType.Slf4j) class TestClass6(val i: Int = 0, var j: Int){ log.info("hello world") }""" should compile """@log(logType = io.github.dreamylost.logs.LogType.Slf4j) @builder class TestClass6(val i: Int = 0, var j: Int){ log.info("hello world") } - | @log(logType = io.github.dreamylost.logs.LogType.Slf4j) object TestClass6 { log.info("hello world");builder() }""".stripMargin should compile //default verbose is false + | @log(logType = io.github.dreamylost.logs.LogType.Slf4j) object TestClass6 { log.info("hello world");builder() }""".stripMargin should compile //default verbose is false + + @log(logType = io.github.dreamylost.logs.LogType.Slf4j) + @builder class TestClass8(val i: Int = 0, var j: Int) { + log.info("hello world") + } + object TestClass8 { builder() } } "log9 slf4j" should "ok on class and it object" in { @@ -141,15 +135,7 @@ class LogTest extends AnyFlatSpec with Matchers { |""".stripMargin should compile } - "log10 slf4j" should "ok on case class and it object" in { - @log(logType = LogType.JLog) - @builder case class TestClass6_1(val i: Int = 0, var j: Int) { - log.info("hello world") - } - @log(logType = io.github.dreamylost.logs.LogType.Slf4j) object TestClass6_1 { - log.info("hello world"); - builder() - } + "log10 slf4j" should "failed on case class" in { """ | @log(verbose=false, logType = LogType.JLog) | @builder case class TestClass6_2(val i: Int = 0, var j: Int) { @@ -158,7 +144,7 @@ class LogTest extends AnyFlatSpec with Matchers { | @log(logType = io.github.dreamylost.logs.LogType.Slf4j) object TestClass6_2 { | log.info("hello world"); builder() | } - |""".stripMargin should compile + |""".stripMargin shouldNot compile } "log11 slf4j" should "ok on class and it object" in { @@ -202,7 +188,6 @@ class LogTest extends AnyFlatSpec with Matchers { log.info("") } } - "log12 slf4j" should "failed when input not in order" in { """ | import io.github.dreamylost.logs.LogType @@ -242,14 +227,6 @@ class LogTest extends AnyFlatSpec with Matchers { | log.info("hello world") | } |""".stripMargin should compile - - """ - | import io.github.dreamylost.logs.LogType - | @log(logType = LogType.ScalaLoggingLazy) - | case class TestClass5(val i: Int = 0, var j: Int) { - | log.info("hello world") - | } - |""".stripMargin should compile } "log14 scala loggging strict" should "ok when exists super class" in { @@ -281,20 +258,8 @@ class LogTest extends AnyFlatSpec with Matchers { | log.info("hello world") | } |""".stripMargin should compile - - """ - | import io.github.dreamylost.logs.LogType - | @log(logType = LogType.ScalaLoggingStrict) - | case class TestClass5(val i: Int = 0, var j: Int) extends Serializable { - | log.info("hello world") - | } - |""".stripMargin should compile } - // We must define the class outside so that the macro has been compiled before testing. - @log(logType = LogType.ScalaLoggingStrict) - @json case class TestClass1(val i: Int = 0, var j: Int, x: String, o: Option[String] = Some("")) - "log15 add @transient" should "ok" in { """ |val str = Json.toJson(TestClass1(1, 1, "hello")).toString() -- GitLab