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

Support `@elapsed` (#94)

* 1.support `@elapsed`
  - method Future
  - method Non-Future
  - println to log
  - println to console
上级 3de43041
......@@ -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)**
......
......@@ -47,8 +47,9 @@
- `@constructor`
- `@equalsAndHashCode`
- `@jacksonEnum`
- `@elapsed`
> 涉及到交互操作的注解在IDEA插件中都得到了支持。在插件市场中搜索`Scala-Macro-Tools`可下载
> Intellij插件 `Scala-Macro-Tools`
[各个注解的说明](./docs/howToUse.md)
......
......@@ -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
......
......@@ -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)
}
```
## @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)
}
```
/*
* 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)
}
}
/*
* 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
}
......@@ -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
......
......@@ -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 => {
......
......@@ -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)
}
}
}
......@@ -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
}
}
......@@ -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
}
}
}
......@@ -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
}
}
/*
* 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)
}
}
}
......@@ -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)
}
......
......@@ -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
......
......@@ -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 = {
......
......@@ -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!"
}
}
......@@ -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 <name of the field>+"="+fieldName
if (argument.includeFieldNames) {
......
/*
* 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
}
}
......@@ -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
}
}
......@@ -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
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册