diff --git a/README.md b/README.md index 49c74ca7822f4df137b6d041d277f6666113ee63..716e832cb74a7093d9ac7bba18f88070e918713a 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,9 @@ Learn Scala macro and abstract syntax tree. - `@constructor` - `@equalsAndHashCode` - `@jacksonEnum` +- `@elapsed` -> Annotations involving interaction are supported in the idea plug-in (named `Scala-Macro-Tools` in Marketplace). +> The intellij plugin named `Scala-Macro-Tools` in marketplace. **[Description of each annotation](./docs/howToUse_en.md)** diff --git a/README_CN.md b/README_CN.md index aab4e1f6ac83ef2609f58e22b2b9e28951a91a10..15f524a531157f26b3265301467dff134d6a8d09 100644 --- a/README_CN.md +++ b/README_CN.md @@ -47,8 +47,9 @@ - `@constructor` - `@equalsAndHashCode` - `@jacksonEnum` +- `@elapsed` -> 涉及到交互操作的注解在IDEA插件中都得到了支持。在插件市场中搜索`Scala-Macro-Tools`可下载。 +> Intellij插件 `Scala-Macro-Tools`。 [各个注解的说明](./docs/howToUse.md) diff --git a/build.sbt b/build.sbt index 8802265da6dde9be0362b344c0fea3bbcfcbde58..aa4f43d327aba2d779a522021679ac52b971021e 100644 --- a/build.sbt +++ b/build.sbt @@ -7,7 +7,7 @@ organization := "io.github.jxnu-liguobin" lazy val scala212 = "2.12.14" lazy val scala211 = "2.11.12" lazy val scala213 = "2.13.6" -lazy val lastVersionForExamples = "0.2.0" +lazy val lastVersionForExamples = "0.3.0" scalaVersion := scala213 diff --git a/docs/howToUse.md b/docs/howToUse.md index 372cecc19e2d44edfa52101d88b1631d0e8a087e..4e337e07260cf820af61b79a7dade5bff594005f 100644 --- a/docs/howToUse.md +++ b/docs/howToUse.md @@ -301,3 +301,32 @@ class B( }; () ``` + +## @elapsed + +`@elapsed`注解用于计算方法的执行耗时 + +- 说明 + - `limit` 执行耗时超过该值则打印日志或输出到控制台。 + - 方法的所有者作用域内有`slf4j`的`org.slf4j.Logger`对象,则使用该对象,否则使用`println`。 + - `logLevel` 指定打印的日志级别。 + - 支持方法的返回类型为`Future[_]`。 + - 使用`map`实现。 + - 支持方法的返回类型的不是`Future`。 + - 使用`try finally`实现。 + - 仅能在非抽象方法上使用该注解。 + +- 示例 + +```scala +class A { + // Duration和TimeUnit必须是全类名 + @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.WARN) + def helloScala1(t: String): Future[String] = { + Future(t)(scala.concurrent.ExecutionContext.Implicits.global) + } + + @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.INFO) + def helloScala2: String = Await.result(helloScala1("world"), Duration.Inf) +} +``` diff --git a/docs/howToUse_en.md b/docs/howToUse_en.md index 99f5e149e17203a5a2e43756b7314c061e69005b..5110454ee32763b2bd8bbe28228a188d6307ecd0 100644 --- a/docs/howToUse_en.md +++ b/docs/howToUse_en.md @@ -1,6 +1,6 @@ ## @toString -The `@toString` used to generate `toString` for Scala classes or a `toString` with parameter names for the case classes. +The `@toString` annotation is used to generate `toString` for Scala classes or a `toString` with parameter names for the case classes. - Note - `verbose` Whether to enable detailed log. @@ -28,7 +28,7 @@ println(new TestClass(1, 2)); ## @json -The `@json` scala macro annotation is the quickest way to add a JSON format to your Play project's case classes. +The `@json` 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 @@ -54,7 +54,7 @@ Json.fromJson[Person](json) ## @builder -The `@builder` used to generate builder pattern for Scala classes. +The `@builder` annotation is used to generate builder pattern for Scala classes. - Note - Support `case class` / `class`. @@ -112,7 +112,7 @@ object TestClass1 extends scala.AnyRef { ## @synchronized -The `@synchronized` is a more convenient and flexible synchronous annotation. +The `@synchronized` annotation is a more convenient and flexible synchronous annotation. - Note - `lockedName` The name of the custom lock obj, default is `this`. @@ -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. (Log dependency needs to be introduced) +The `@log` annotation 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. @@ -174,7 +174,7 @@ class TestClass6(val i: Int = 0, var j: Int) { ## @apply -The `@apply` used to generate `apply` method for primary construction of ordinary classes. +The `@apply` annotation is used to generate `apply` method for primary construction of ordinary classes. - Note - `verbose` Whether to enable detailed log. @@ -190,7 +190,7 @@ println(B2(1, 2)) ## @constructor -The `@constructor` used to generate secondary constructor method for classes, only when it has internal fields. +The `@constructor` annotation is used to generate secondary constructor method for classes, only when it has internal fields. - Note - `verbose` Whether to enable detailed log. @@ -269,7 +269,7 @@ 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) +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`. @@ -308,3 +308,32 @@ Macro expansion code: }; () ``` + +## @elapsed + +The `@elapsed` annotation is used to calculate the execution time of the method. + +- Note + - `limit` The log will be printed or output to the console if the execution time exceeds this value. + - If there is an `org.slf4j.Logger` object of `slf4j` in the owner scope of the method, this object is used; otherwise, `println` is used. + - `logLevel` Specifies the log level to print. + - The return type of supported method is not `Future[_]`. + - Use `map` to implement. + - The return type of the supported method is not `Future`. + - Use `try finally` to implement. + - Annotation is only supported use on non-abstract method. + +- Example + +```scala +class A { + // Duration and TimeUnit must Full class name + @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.WARN) + def helloScala1(t: String): Future[String] = { + Future(t)(scala.concurrent.ExecutionContext.Implicits.global) + } + + @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.INFO) + def helloScala2: String = Await.result(helloScala1("world"), Duration.Inf) +} +``` diff --git a/src/main/scala/io/github/dreamylost/LogLevel.scala b/src/main/scala/io/github/dreamylost/LogLevel.scala new file mode 100644 index 0000000000000000000000000000000000000000..312da171e02ce6ece173963737f4e34c465f5ca8 --- /dev/null +++ b/src/main/scala/io/github/dreamylost/LogLevel.scala @@ -0,0 +1,45 @@ +/* + * 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 + +/** + * Log Level for elapsed + * + * @author 梦境迷离 + * @since 2021/8/7 + * @version 1.0 + */ +object LogLevel extends Enumeration { + + type LogLevel = Value + val INFO, WARN, DEBUG = Value + + private[dreamylost] def getLogLevel(shortType: String): LogLevel = { + val tpe1 = s"$PACKAGE.elapsed.$shortType" //LogLevel.INFO + val tpe2 = s"$PACKAGE.elapsed.LogLevel.$shortType" // INFO + val v = LogLevel.values.find(p => { + s"$PACKAGE.elapsed.LogLevel.${p.toString}" == tpe1 || + s"$PACKAGE.elapsed.LogLevel.${p.toString}" == tpe2 || s"$PACKAGE.elapsed.LogLevel.${p.toString}" == shortType + }).get.toString + LogLevel.withName(v) + } +} diff --git a/src/main/scala/io/github/dreamylost/elapsed.scala b/src/main/scala/io/github/dreamylost/elapsed.scala new file mode 100644 index 0000000000000000000000000000000000000000..b23e54321daf716d382f85dcf66fbc3b94767512 --- /dev/null +++ b/src/main/scala/io/github/dreamylost/elapsed.scala @@ -0,0 +1,44 @@ +/* + * 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 LogLevel.LogLevel +import io.github.dreamylost.macros.elapsedMacro + +import scala.annotation.{ StaticAnnotation, compileTimeOnly } +import scala.concurrent.duration.Duration + +/** + * annotation to record method cost time. + * + * @author 梦境迷离 + * @param limit Time consuming condition to trigger log + * @param logLevel Log Level + * @since 2021/8/7 + * @version 1.0 + */ +@compileTimeOnly("enable macro to expand macro annotations") +final class elapsed(limit: Duration, logLevel: LogLevel) extends StaticAnnotation { + + def macroTransform(annottees: Any*): Any = macro elapsedMacro.ElapsedProcessor.impl + +} diff --git a/src/main/scala/io/github/dreamylost/logs/BaseLog.scala b/src/main/scala/io/github/dreamylost/logs/BaseLog.scala index 39f19b21e9946456b67dc683acd611e988e5e6e8..c392cd48f118474645e68fe4b0dedb17195c5584 100644 --- a/src/main/scala/io/github/dreamylost/logs/BaseLog.scala +++ b/src/main/scala/io/github/dreamylost/logs/BaseLog.scala @@ -26,6 +26,7 @@ import io.github.dreamylost.logs.LogType.LogType import scala.reflect.macros.whitebox trait BaseLog { + val typ: LogType def getTemplate(c: whitebox.Context)(logTransferArgument: LogTransferArgument): c.Tree diff --git a/src/main/scala/io/github/dreamylost/logs/LogType.scala b/src/main/scala/io/github/dreamylost/logs/LogType.scala index 9258409cbe7e2556b751b2a32bb943d98c8f2fcb..cfc3134c62822ae2163ae73eb1ace0d207c87b9e 100644 --- a/src/main/scala/io/github/dreamylost/logs/LogType.scala +++ b/src/main/scala/io/github/dreamylost/logs/LogType.scala @@ -37,11 +37,11 @@ object LogType extends Enumeration { ScalaLoggingLazy -> ScalaLoggingLazyImpl ) - def getLogImpl(logType: LogType): BaseLog = { + private[dreamylost] def getLogImpl(logType: LogType): BaseLog = { types.getOrElse(logType, default = throw new Exception(s"Not support log type: $logType")) } - def getLogType(shortType: String): LogType = { + private[dreamylost] def getLogType(shortType: String): LogType = { val tpe1 = s"$PACKAGE.logs.$shortType" //LogType.JLog val tpe2 = s"$PACKAGE.logs.LogType.$shortType" // JLog val v = LogType.values.find(p => { diff --git a/src/main/scala/io/github/dreamylost/macros/AbstractMacroProcessor.scala b/src/main/scala/io/github/dreamylost/macros/AbstractMacroProcessor.scala index e6cbfd9a0c4a548f3796339b2110f1b8fbac187d..b363ccb5dd0341a10a357f0bfed57cf73faa56cc 100644 --- a/src/main/scala/io/github/dreamylost/macros/AbstractMacroProcessor.scala +++ b/src/main/scala/io/github/dreamylost/macros/AbstractMacroProcessor.scala @@ -23,6 +23,7 @@ package io.github.dreamylost.macros import java.time.ZonedDateTime import java.time.format.DateTimeFormatter +import scala.annotation.tailrec import scala.reflect.macros.whitebox /** @@ -35,12 +36,12 @@ abstract class AbstractMacroProcessor(val c: whitebox.Context) { import c.universe._ - protected lazy val SDKClasses = Set("java.lang.Object", "scala.AnyRef") + protected final lazy val SDKClasses = Set("java.lang.Object", "scala.AnyRef") + + protected val verbose: Boolean = false /** * Subclasses should override the method and return the final result abstract syntax tree, or an abstract syntax tree close to the final result. - * When the macro implementation is very simple, we don't need to use this method, so we don't need to implement it. - * When there are many macro input parameters, we will not use this method temporarily because we need to pass parameters. * * @param classDecl * @param compDeclOpt @@ -50,15 +51,27 @@ abstract class AbstractMacroProcessor(val c: whitebox.Context) { def createCustomExpr(classDecl: ClassDef, compDeclOpt: Option[ModuleDef] = None): Any = ??? /** - * Subclasses must override the method. + * Subclasses should override this method if it cannot use `createCustomExpr` method. * * @param annottees * @return Macro expanded final syntax tree. */ - def impl(annottees: Expr[Any]*): Expr[Any] + def impl(annottees: Expr[Any]*): Expr[Any] = { + checkAnnottees(annottees) + val resTree = collectCustomExpr(annottees)(createCustomExpr) + printTree(force = verbose, resTree.tree) + resTree + } + + /** + * Check the input tree of the annotation. + * + * @param annottees + */ + def checkAnnottees(annottees: Seq[Expr[Any]]): Unit = {} /** - * Eval tree. + * Eval the input tree. * * @param tree * @tparam T @@ -441,4 +454,22 @@ abstract class AbstractMacroProcessor(val c: whitebox.Context) { earlydefns: List[Tree] = Nil ) + def findNameOnEnclosingClass(t: Name): Option[TermName] = { + @tailrec + def doFind(trees: List[Tree]): Option[TermName] = trees match { + case Nil => None + case tree :: tail => tree match { + case ValDef(_, name, tpt, _) => + if (c.typecheck(tq"$tpt", c.TYPEmode).tpe.toString == t.decodedName.toString) //TODO better + Some(name.toTermName) else None + case _ => doFind(tail) + } + } + + c.enclosingClass match { + case ClassDef(_, _, _, Template(_, _, body)) => doFind(body) + case ModuleDef(_, _, Template(_, _, body)) => doFind(body) + } + } + } diff --git a/src/main/scala/io/github/dreamylost/macros/applyMacro.scala b/src/main/scala/io/github/dreamylost/macros/applyMacro.scala index 71538850cf8fc977adc9dd5ca7a077639392154d..bc93bdbb625a304f302d8aa0634c63b99e7e77f9 100644 --- a/src/main/scala/io/github/dreamylost/macros/applyMacro.scala +++ b/src/main/scala/io/github/dreamylost/macros/applyMacro.scala @@ -54,15 +54,15 @@ object applyMacro { """) } - override def impl(annottees: Expr[Any]*): Expr[Any] = { + override def checkAnnottees(annottees: Seq[c.universe.Expr[Any]]): Unit = { + super.checkAnnottees(annottees) 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 } + + override protected val verbose: Boolean = extractArgumentsDetail._1 } } diff --git a/src/main/scala/io/github/dreamylost/macros/builderMacro.scala b/src/main/scala/io/github/dreamylost/macros/builderMacro.scala index 597754e642c40b6fd8d007e357713f51a53bf608..786f12b8265e0a6da6eadfdc615a00c5244a1c8b 100644 --- a/src/main/scala/io/github/dreamylost/macros/builderMacro.scala +++ b/src/main/scala/io/github/dreamylost/macros/builderMacro.scala @@ -40,7 +40,7 @@ object builderMacro { } private def getFieldDefinition(field: Tree): Tree = { - val ValDef(mods, name, tpt, rhs) = field + val ValDef(_, name, tpt, rhs) = field q"private var $name: $tpt = $rhs" } @@ -67,15 +67,15 @@ object builderMacro { val builderMethod = q"def builder[..$classTypeParams](): $builderClassName[..$returnTypeParams] = new $builderClassName()" val buulderClass = q""" - class $builderClassName[..$classTypeParams] { + class $builderClassName[..$classTypeParams] { - ..$builderFieldDefinitions + ..$builderFieldDefinitions - ..$builderFieldMethods + ..$builderFieldMethods - def build(): $typeName[..$returnTypeParams] = ${getConstructorWithCurrying(typeName, fieldss, isCase)} - } - """ + def build(): $typeName[..$returnTypeParams] = ${getConstructorWithCurrying(typeName, fieldss, isCase)} + } + """ List(builderMethod, buulderClass) } @@ -91,12 +91,6 @@ object builderMacro { $compDecl """) } - - override def impl(annottees: c.universe.Expr[Any]*): c.universe.Expr[Any] = { - 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 a57a942c687986790c5d417bc2805865e753ff9f..fde13e0e1a9ff999887234e9f91f3eb5d4d29edd 100644 --- a/src/main/scala/io/github/dreamylost/macros/constructorMacro.scala +++ b/src/main/scala/io/github/dreamylost/macros/constructorMacro.scala @@ -84,7 +84,7 @@ object constructorMacro { this(..${allFieldsTermName.flatten}) ..${annotteeClassFieldNames.map(f => q"this.$f = $f")} } - """ + """ } else { // NOTE: currying constructor overload must be placed in the first bracket block. val allClassCtorParamsNameWithType = annotteeClassParams.map(cc => getConstructorParamsNameWithType(cc)) @@ -109,15 +109,14 @@ object constructorMacro { """) } - override def impl(annottees: c.universe.Expr[Any]*): c.universe.Expr[Any] = { + override def checkAnnottees(annottees: Seq[c.universe.Expr[Any]]): Unit = { + super.checkAnnottees(annottees) val annotateeClass: ClassDef = checkGetClassDef(annottees) if (isCaseClass(annotateeClass)) { c.abort(c.enclosingPosition, ErrorMessage.ONLY_CLASS) } - val res = collectCustomExpr(annottees)(createCustomExpr) - printTree(force = extractArgumentsDetail._1, res.tree) - res } + override val verbose: Boolean = extractArgumentsDetail._1 } } diff --git a/src/main/scala/io/github/dreamylost/macros/elapsedMacro.scala b/src/main/scala/io/github/dreamylost/macros/elapsedMacro.scala new file mode 100644 index 0000000000000000000000000000000000000000..6181e5cd46f035c0a1f51ef2326983325987a24f --- /dev/null +++ b/src/main/scala/io/github/dreamylost/macros/elapsedMacro.scala @@ -0,0 +1,179 @@ +/* + * 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 io.github.dreamylost.LogLevel.LogLevel +import io.github.dreamylost.{ LogLevel, PACKAGE } + +import scala.concurrent.duration._ +import scala.reflect.macros.whitebox + +/** + * 1.Annotation is only supported use on non-abstract method. + * 2.For methods that are not future, `try finally` is used to implement the timing of the method. + * 3.For methods that are Futures, `Future map` is used to implement the timing of the method. + * + * @author 梦境迷离 + * @since 2021/8/7 + * @version 1.0 + */ +object elapsedMacro { + + class ElapsedProcessor(override val c: whitebox.Context) extends AbstractMacroProcessor(c) { + + import c.universe._ + + private lazy val start: c.universe.TermName = TermName("$elapsedBegin") + private lazy val valDef: c.universe.TermName = TermName("$elapsed") + + private def getLogLevel(logLevel: Tree): LogLevel = { + if (logLevel.children.exists(t => t.toString().contains(PACKAGE))) { + evalTree(logLevel) + } else { + LogLevel.getLogLevel(logLevel.toString()) + } + } + + private val extractArgumentsDetail: (Duration, LogLevel) = extractArgumentsTuple2 { + case q"new elapsed(limit=$limit, logLevel=$logLevel)" => (evalTree(limit.asInstanceOf[Tree]), getLogLevel(logLevel.asInstanceOf[Tree])) + case _ => c.abort(c.enclosingPosition, ErrorMessage.UNEXPECTED_PATTERN) + } + + private def getStartExpr: c.universe.Tree = { + q"""val $start = _root_.scala.concurrent.duration.Duration.fromNanos(System.nanoTime())""" + } + + private def getLog(methodName: TermName, logBy: Tree): c.universe.Tree = { + // CI will fail when use lambda. + implicit val durationApply: c.universe.Liftable[Duration] = new Liftable[Duration] { + override def apply(value: Duration): c.universe.Tree = q"${value._1}" + } + q""" + val $valDef = _root_.scala.concurrent.duration.Duration.fromNanos(System.nanoTime()) - $start + if ($valDef._1 >= ${extractArgumentsDetail._1}) $logBy(StringContext("slow invoked: [", "] elapsed [", "]").s(${methodName.toString}, $valDef.toMillis)) + """ + } + + private def getPrintlnLog(methodName: TermName): c.universe.Tree = { + val log = findNameOnEnclosingClass(TypeName("org.slf4j.Logger")) + if (log.isEmpty) { // if there is no slf4j log, print it to the console + getLog(methodName, q"_root_.scala.Predef.println") + } else { + extractArgumentsDetail._2 match { + case LogLevel.INFO => getLog(methodName, q"${log.get}.info") + case LogLevel.DEBUG => getLog(methodName, q"${log.get}.debug") + case LogLevel.WARN => getLog(methodName, q"${log.get}.warn") + } + } + } + + private def mapToNewMethod(defDef: DefDef, defDefMap: DefDef => Tree): c.universe.DefDef = { + if (defDef.rhs.isEmpty) { + c.abort(c.enclosingPosition, "Annotation is only supported use on non-abstract method.") + } + + if (defDef.rhs.children.size < 2) { + c.abort(c.enclosingPosition, "The method returned directly by an expression is not supported.") + } + mapToMethodDef(defDef, defDefMap.apply(defDef)) + } + + private def getNewMethodWithFuture(defDef: DefDef): DefDef = { + mapToNewMethod(defDef, defDef => { + q""" + $getStartExpr + val resFuture = ${defDef.rhs} + resFuture.map { res => ..${getPrintlnLog(defDef.name)} ; res }(_root_.scala.concurrent.ExecutionContext.Implicits.global) + """ + }) + } + + // There may be a half-way exit, rather than the one whose last expression is exit. + // Unreliable function!!! + // private def returnEarly(defDef: DefDef, trees: Tree*): List[Tree] = { + // val ifElseMatch = (f: If) => { + // if (f.elsep.nonEmpty) { + // if (f.elsep.children.nonEmpty && f.elsep.children.size > 1) { + // If(f.cond, f.thenp, q"..${returnEarly(defDef, f.elsep.children: _*)}") + // } else { + // If(f.cond, f.thenp, q"..${returnEarly(defDef, f.elsep)}") + // } + // } else { + // f //no test + // } + // } + // if (trees.isEmpty) return Nil + // trees.map { + // case r: Return => + // q""" + // ..${getPrintlnLog(defDef.name)} + // $r + // """ + // case f: If => //support if return + // c.info(c.enclosingPosition, s"returnEarly: thenp: ${f.thenp}, children: ${f.thenp.children}, cond: ${f.cond}", force = true) + // c.info(c.enclosingPosition, s"returnEarly: elsep: ${f.elsep}, children: ${f.elsep.children}, cond: ${f.cond}", force = true) + // if (f.thenp.nonEmpty) { + // if (f.thenp.children.nonEmpty && f.thenp.children.size > 1) { + // val ifTree = If(f.cond, q"..${returnEarly(defDef, f.thenp.children: _*)}", f.elsep) + // ifElseMatch(ifTree) + // } else { + // val ifTree = If(f.cond, q"..${returnEarly(defDef, f.thenp)}", f.elsep) + // ifElseMatch(ifTree) + // } + // } else { + // ifElseMatch(f) //no test + // } + // case t => + // // TODO support for/while/switch + // c.info(c.enclosingPosition, s"returnEarly: not support expr: $t", force = true) + // t + // }.toList + // } + + private def getNewMethod(defDef: DefDef): DefDef = { + mapToNewMethod(defDef, defDef => { + q""" + $getStartExpr + ${Try(defDef.rhs, Nil, getPrintlnLog(defDef.name))} + """ + }) + } + + override def impl(annottees: c.universe.Expr[Any]*): c.universe.Expr[Any] = { + val resTree = annottees.map(_.tree) match { + case (defDef @ DefDef(_, _, _, _, tpt, _)) :: Nil => + if (tpt.isEmpty) { + c.abort(c.enclosingPosition, "The return type of the method is not specified!") + } + val tp = c.typecheck(tq"$tpt", c.TYPEmode).tpe + if (tp <:< typeOf[scala.concurrent.Future[_]]) { + getNewMethodWithFuture(defDef) + } else { + getNewMethod(defDef) + } + } + printTree(force = true, resTree) + c.Expr[Any](resTree) + } + } + +} diff --git a/src/main/scala/io/github/dreamylost/macros/equalsAndHashCodeMacro.scala b/src/main/scala/io/github/dreamylost/macros/equalsAndHashCodeMacro.scala index abbe10d1ab343e7104a93195754f3eca96a6ae79..e40ed327725bb5563ed22eeb88f910052296a14f 100644 --- a/src/main/scala/io/github/dreamylost/macros/equalsAndHashCodeMacro.scala +++ b/src/main/scala/io/github/dreamylost/macros/equalsAndHashCodeMacro.scala @@ -43,16 +43,16 @@ object equalsAndHashCodeMacro { case _ => c.abort(c.enclosingPosition, ErrorMessage.UNEXPECTED_PATTERN) } - override def impl(annottees: c.universe.Expr[Any]*): c.universe.Expr[Any] = { + override def checkAnnottees(annottees: Seq[c.universe.Expr[Any]]): Unit = { + super.checkAnnottees(annottees) val annotateeClass: ClassDef = checkGetClassDef(annottees) if (isCaseClass(annotateeClass)) { c.abort(c.enclosingPosition, ErrorMessage.ONLY_CLASS) } - val resTree = collectCustomExpr(annottees)(createCustomExpr) - printTree(force = extractArgumentsDetail._1, resTree.tree) - resTree } + override val verbose: Boolean = extractArgumentsDetail._1 + /** * Extract the internal fields of members belonging to the class. */ @@ -78,12 +78,12 @@ object equalsAndHashCodeMacro { val canEqual = if (existsCanEqual) q"" else q"$modifiers def canEqual(that: Any) = that.isInstanceOf[$className]" 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 - } - """ + 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) } diff --git a/src/main/scala/io/github/dreamylost/macros/jacksonEnumMacro.scala b/src/main/scala/io/github/dreamylost/macros/jacksonEnumMacro.scala index a6c33afc04cc066d48be94db05dd0d6f04c19464..37ab01b11bc26b725a644ae3a8b9e5d0e32423d0 100644 --- a/src/main/scala/io/github/dreamylost/macros/jacksonEnumMacro.scala +++ b/src/main/scala/io/github/dreamylost/macros/jacksonEnumMacro.scala @@ -76,11 +76,7 @@ object jacksonEnumMacro { } } - 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 val verbose: Boolean = extractArgumentsDetail._1 override def createCustomExpr(classDecl: c.universe.ClassDef, compDeclOpt: Option[c.universe.ModuleDef]): Any = { // return all typeReferClasses and new classDef diff --git a/src/main/scala/io/github/dreamylost/macros/jsonMacro.scala b/src/main/scala/io/github/dreamylost/macros/jsonMacro.scala index 805e87dc543d48877be8226a2995cf4bb14ca1b9..6227c607ba0c28c6399c082c7de41a7302e528d2 100644 --- a/src/main/scala/io/github/dreamylost/macros/jsonMacro.scala +++ b/src/main/scala/io/github/dreamylost/macros/jsonMacro.scala @@ -43,14 +43,12 @@ object jsonMacro { } } - override def impl(annottees: c.universe.Expr[Any]*): c.universe.Expr[Any] = { + override def checkAnnottees(annottees: Seq[c.universe.Expr[Any]]): Unit = { + super.checkAnnottees(annottees) 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 createCustomExpr(classDecl: c.universe.ClassDef, compDeclOpt: Option[c.universe.ModuleDef]): Any = { diff --git a/src/main/scala/io/github/dreamylost/macros/macros.scala b/src/main/scala/io/github/dreamylost/macros/macros.scala index d87a7ef506e8f1ca84da72b98a40215205005041..81dcee8f402506a93e9c7cb9c96a9751aefa654c 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 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!" + final lazy val ONLY_CLASS: String = "Annotation is only supported on class." + final lazy val ONLY_CASE_CLASS: String = "Annotation is only supported on case class." + final lazy val ONLY_OBJECT_CLASS: String = "Annotation is only supported on class or object." + final lazy val UNEXPECTED_PATTERN: String = "Unexpected annotation pattern!" } } diff --git a/src/main/scala/io/github/dreamylost/macros/toStringMacro.scala b/src/main/scala/io/github/dreamylost/macros/toStringMacro.scala index 4d0c03fa31984367b93e94b14979072805215b6f..67bc4b948f6c6ac70ef66d664b394c6b6092c763 100644 --- a/src/main/scala/io/github/dreamylost/macros/toStringMacro.scala +++ b/src/main/scala/io/github/dreamylost/macros/toStringMacro.scala @@ -67,6 +67,8 @@ object toStringMacro { case _ => c.abort(c.enclosingPosition, ErrorMessage.UNEXPECTED_PATTERN) } + override val verbose: Boolean = extractArgumentsDetail._1 + override def createCustomExpr(classDecl: c.universe.ClassDef, compDeclOpt: Option[c.universe.ModuleDef]): Any = { // extract parameters of annotation, must in order val argument = Argument( @@ -83,12 +85,6 @@ object toStringMacro { """) } - 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 = { // Print one field as +"="+fieldName if (argument.includeFieldNames) { diff --git a/src/test/scala/io/github/dreamylost/ElapsedTest.scala b/src/test/scala/io/github/dreamylost/ElapsedTest.scala new file mode 100644 index 0000000000000000000000000000000000000000..093c56d0011546d03dc55973e033ace3e8784fc2 --- /dev/null +++ b/src/test/scala/io/github/dreamylost/ElapsedTest.scala @@ -0,0 +1,354 @@ +/* + * 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 org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import scala.concurrent.Future + +/** + * + * @author 梦境迷离 + * @since 2021/8/7 + * @version 1.0 + */ +class ElapsedTest extends AnyFlatSpec with Matchers { + + "elapsed1" should "failed, not calculate anything, the return type is not specified" in { + //Duration and TimeUnit must Full class name + """ + | class A { + | @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.INFO) + | def i = ??? + | } + | + | class B { + | @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.WARN) + | def j = ??? + | } + | + | class C { + | @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.DEBUG) + | def j = ??? + | } + | + | class D { + | @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.INFO) + | def i:String = ??? + | } + | val a = new A() + | val b = new B() + | val c = new C() + | val d = new D() + |""".stripMargin shouldNot compile + } + + "elapsed2" should "ok, get the returnType of the method " in { + //Duration and TimeUnit must Full class name + """ + |class A { + | @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.NANOSECONDS), logLevel = io.github.dreamylost.LogLevel.INFO) + | def helloWorld: String = { + | println("hello world") + | "hello" + | } + | + | @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.INFO) + | def helloScala: String = { + | Thread.sleep(2000) + | println("hello world") + | "hello" + | } + |} + | val a = new A + | a.helloWorld + | a.helloScala + |""".stripMargin should compile + } + + "elapsed3" should "ok" in { + //Duration and TimeUnit must Full class name + """ + | class A { + | @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.NANOSECONDS), logLevel = io.github.dreamylost.LogLevel.INFO) + | def helloWorld: String = { + | println("") ; println(""); "" + | } + | + | @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.INFO) + | def helloScala1: String = { println("") ; println(""); ""} + | + | @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.INFO) + | def helloScala2: String = { println("") ; println(""); "" } + | + | @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.INFO) + | def helloScala3: String = { + | val s = "hello" + | val x = "world" + | return s + | } + | } + | val a = new A() + |""".stripMargin should compile + } + + "elapsed4" should "ok, return early" in { + //Duration and TimeUnit must Full class name + """ + | class A { + | @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.INFO) + | def helloScala1: String = { + | val s = "hello" + | if (s == "hello") { + | return "world" + | } + | val x = "world" + | return s + | } + | } + | + | val a = new A + | a.helloScala1 + | + | class B { + | @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.INFO) + | def helloScala11: String = { + | val s = "hello" + | if (s == "hello") { + | return "world" + "wooo" + | } + | val x = "world" + | return s + | } + | } + | + | val b = new B() + |""".stripMargin should compile + } + + "elapsed5" should "ok, return Future" in { + class A { + + private final val log3: org.slf4j.Logger = org.slf4j.LoggerFactory.getLogger(classOf[A]) + + @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.INFO) + def helloScala1: Future[String] = { + Thread.sleep(1000) + Future.successful("hello world") + } + + @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.DEBUG) + def helloScala2: Future[String] = { + Thread.sleep(2000) + Future { + "hello world" + }(scala.concurrent.ExecutionContext.Implicits.global) + } + + @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.WARN) + def helloScala3: Future[String] = Future { + "hello world" + }(scala.concurrent.ExecutionContext.Implicits.global) + } + } + + "elapsed6" should "failed, not support when only has one expr" in { + class B { + + import scala.concurrent.Await + import scala.concurrent.duration.Duration + + @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.WARN) + def helloScala(t: String): Future[String] = { + Future(t)(scala.concurrent.ExecutionContext.Implicits.global) + } + + @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.WARN) + def helloScala11(t: String): Future[String] = Future(t)(scala.concurrent.ExecutionContext.Implicits.global) + + @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.INFO) + def helloScala2: String = { + val s = Future("")(scala.concurrent.ExecutionContext.Implicits.global) + Await.result(helloScala("world"), Duration.Inf) + } + } + } + + "elapsed7" should "ok at object but has runTime Error" in { //Why? + """ + | object A { + | private final val log1: org.slf4j.Logger = org.slf4j.LoggerFactory.getLogger(A.getClass) + | + | @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.INFO) + | def helloScala1: Future[String] = { + | Thread.sleep(1000) + | Future.successful("hello world") + | } + | + | @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.DEBUG) + | def helloScala2: Future[String] = { + | Thread.sleep(2000) + | Future { + | "hello world" + | }(scala.concurrent.ExecutionContext.Implicits.global) + | } + | + | @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.WARN) + | def helloScala3: Future[String] = Future { + | "hello world" + | }(scala.concurrent.ExecutionContext.Implicits.global) + | } + |""".stripMargin should compile + } + + "elapsed8" should "ok at input args" in { + @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = LogLevel.WARN) + def helloScala1: String = { + println("") + println("") + "hello" + } + import io.github.dreamylost.LogLevel.WARN + @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = WARN) + def helloScala2: String = { + println("") + println("") + "hello" + } + + @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.WARN) + def helloScala3: String = { + println("") + println("") + "hello" + } + } + + "elapsed9" should "failed at input args" in { + """ + |@elapsed(logLevel = io.github.dreamylost.LogLevel.WARN, limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS)) + | def helloScala1: String = { + | println("") + | println("") + | "hello" + | } + |""".stripMargin shouldNot compile //args not in order + } + "elapsed10" should "multi-return" in { + class A { + + @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.INFO) + def j: Int = { + var i = 1 + if (i == 1) { + val h = 0 + var l = 0 + if (j == 0) { + val h = 0 + var l = 0 + return 1 + } else { + val j = 0 + return 0 + } + } else { + i + } + i + } + + @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.INFO) + def k: Unit = { + var i = 1 + if (i == 1) { + val i = 0 + val k = 0 + if (i == 0) { + 1 + } else { + 2 + } + } else { + val u = 0 + 0 + } + + var k = 1 + if (k == 1) { + val i = 0 + val k = 0 + if (i == 0) { + return () + } else { + return () + } + } else { + val u = 0 + return u + } + 1 + } + + @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.INFO) + def l: Int = { + val i = 0 + for (i <- Seq(1)) { + if (i == 1) { + return 1 //not support + } + } + 0 + } + + @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.INFO) + def m: Int = { + var i = 1 + if (i == 1) { + } else { + val u = 0 + return 0 + } + + if (i == 1) { + return 1 + } else { + val u = 0 + return 0 + } + + 1 + } + + } + } + + "elapsed11" should "failed at abstract method" in { + """ + |abstract class A { + | @elapsed(limit = scala.concurrent.duration.Duration(1, java.util.concurrent.TimeUnit.SECONDS), logLevel = io.github.dreamylost.LogLevel.WARN) + | def hello:String + | } + |""".stripMargin shouldNot compile + } + +} diff --git a/src/test/scala/io/github/dreamylost/EqualsAndHashCodeTest.scala b/src/test/scala/io/github/dreamylost/EqualsAndHashCodeTest.scala index bbca4075b7972fa4c0f1ed875d4ef25ee3d70252..d9258d5d9a67139539c5fbca57152a3d5a285fce 100644 --- a/src/test/scala/io/github/dreamylost/EqualsAndHashCodeTest.scala +++ b/src/test/scala/io/github/dreamylost/EqualsAndHashCodeTest.scala @@ -215,4 +215,13 @@ class EqualsAndHashCodeTest extends AnyFlatSpec with Matchers { |""".stripMargin shouldNot compile } + "equals7" should "failed on case class" in { + """ + | @equalsAndHashCode(excludeFields = Nil) + | case class Employee4(name: String, age: Int, var role: String) extends Person(name, age) { + | val i = 0 + | def hello: String = ??? + | } + |""".stripMargin shouldNot compile + } } diff --git a/src/test/scala/io/github/dreamylost/JsonTest.scala b/src/test/scala/io/github/dreamylost/JsonTest.scala index fefd1c3138667825073b0b71ba60768eecca630a..f3212450f3644e463905de6f928f72392bc35f80 100644 --- a/src/test/scala/io/github/dreamylost/JsonTest.scala +++ b/src/test/scala/io/github/dreamylost/JsonTest.scala @@ -66,4 +66,12 @@ class JsonTest extends AnyFlatSpec with Matchers { println(ret) assert(ret == "{\n \"i\" : 1,\n \"j\" : 2,\n \"x\" : \"\",\n \"o\" : \"\"\n}") } + + "json4" should "failed on normal class" in { + """ + | @json + | @SerialVersionUID(1L) + | class TestClass3(val i: Int = 0, var j: Int, x: String, o: Option[String] = Some("")) + |""".stripMargin shouldNot compile + } }