diff --git a/README.md b/README.md index 8a20f3f2..62333813 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ The `@builder` used to generate builder pattern for Scala classes. - Note - Support `case class` / `class`. - - It can be used with `@toString`. But it needs to be put in the back. + - It can be used with `@toString`. But `@builder` needs to be put in the back. - If there is no companion object, one will be generated to store the `builder` method and `Builder` class. - IDE support is not very good, a red prompt will appear, but the compilation is OK. @@ -122,6 +122,42 @@ object TestClass1 extends scala.AnyRef { } ``` +## @synchronized + +The `@synchronized` is a more convenient and flexible synchronous annotation. + +- Note + - `lockedName` The name of custom lock obj. + - Support static and instance methods. + - It can customize the lock object, and the default is `this`. + +- Example + +```scala + +private final val obj = new Object + +@synchronized(lockedName = "obj") // The default is this. If you fill in a non existent field name, the compilation will fail. +def getStr3(k: Int): String = { + k + "" +} + +// or +@synchronized //use this +def getStr(k: Int): String = { + k + "" +} +``` + +Compiler intermediate code: + +```scala +// Note that it will not judge whether synchronized already exists, so if synchronized already exists, it will be used twice. +// For example `def getStr(k: Int): String = this.synchronized(this.synchronized(k.$plus(""))) +// It is not sure whether it will be optimized at the bytecode level. +def getStr(k: Int): String = this.synchronized(k.$plus("")) +``` + # How to use Add library dependency diff --git a/src/main/scala/io/github/dreamylost/synchronized.scala b/src/main/scala/io/github/dreamylost/synchronized.scala new file mode 100644 index 00000000..fccb2c8d --- /dev/null +++ b/src/main/scala/io/github/dreamylost/synchronized.scala @@ -0,0 +1,58 @@ +package io.github.dreamylost + +import scala.annotation.{ StaticAnnotation, compileTimeOnly } +import scala.language.experimental.macros +import scala.reflect.macros.whitebox + +/** + * + * @author 梦境迷离 + * @param lockedName The name of custom lock obj. + * @param verbose Whether to enable detailed log. + * @since 2021/6/24 + * @version 1.0 + */ +@compileTimeOnly("enable macro to expand macro annotations") +final class synchronized( + verbose: Boolean = false, + lockedName: String = "this" +) extends StaticAnnotation { + def macroTransform(annottees: Any*): Any = macro synchronizedMacro.impl +} + +object synchronizedMacro { + def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { + import c.universe._ + + val args: (Boolean, String) = c.prefix.tree match { + case q"new synchronized(verbose=$verbose, lockedName=$lock)" => (c.eval[Boolean](c.Expr(verbose)), c.eval[String](c.Expr(lock))) + case q"new synchronized(lockedName=$lock)" => (false, c.eval[String](c.Expr(lock))) + case q"new synchronized()" => (false, "this") + case _ => c.abort(c.enclosingPosition, "unexpected annotation pattern!") + } + + c.info(c.enclosingPosition, s"annottees: $annottees", force = args._1) + + val resTree = annottees map (_.tree) match { + // Match a method, and expand. + case _@ q"$modrs def $tname[..$tparams](...$paramss): $tpt = $expr" :: _ => + if (args._2 != null) { + if (args._2 == "this") { + q"""def $tname[..$tparams](...$paramss): $tpt = ${This(TypeName(""))}.synchronized { $expr }""" + } else { + q"""def $tname[..$tparams](...$paramss): $tpt = ${TermName(args._2)}.synchronized { $expr }""" + } + } else { + c.abort(c.enclosingPosition, "Invalid args, lockName cannot be a null!") + } + case _ => c.abort(c.enclosingPosition, "Invalid annotation target: not a method") + } + // Print the ast + c.info( + c.enclosingPosition, + "\n###### Expanded macro ######\n" + resTree.toString() + "\n###### Expanded macro ######\n", + force = args._1 + ) + c.Expr[Any](resTree) + } +} diff --git a/src/test/scala/io/github/dreamylost/Synchronized.scala b/src/test/scala/io/github/dreamylost/Synchronized.scala new file mode 100644 index 00000000..2a2a9e23 --- /dev/null +++ b/src/test/scala/io/github/dreamylost/Synchronized.scala @@ -0,0 +1,70 @@ +package io.github.dreamylost + +import org.scalatest.{ FlatSpec, Matchers } + +/** + * + * @author 梦境迷离 + * @since 2021/6/24 + * @version 1.0 + */ +class Synchronized extends FlatSpec with Matchers { + + "synchronized1" should "is ok at class" in { + @synchronized + def getStr(k: Int): String = { + k + "" + } + """@synchronized + def getStr(k: Int): String = { + k + "" + } + """ should compile + + @synchronized + def getStr2(k: Int): String = { + k + "" + } + """@synchronized + def getStr2(k: Int) = { + k + "" + } + """ should compile + } + + "synchronized2" should "is ok by custom obj" in { + + val obj = new Object + + @synchronized(lockedName = "obj") + def getStr3(k: Int): String = { + k + "" + } + """ + @synchronized(lockedName = "obj") + def getStr3(k: Int) = { + k + "" + } + """ should compile + + object TestObject { + // def getStr(k: Int): String = this.synchronized(k.$plus("")) + // def getStr(k: Int): String = this.synchronized(this.synchronized(k.$plus(""))) + @synchronized + def getStr(k: Int): String = { + this.synchronized(k + "") + } + } + + """ + @synchronized(lockedName = "obj") + class A + """ shouldNot compile + + """ + @synchronized(lockedName = "obj") + val s = "1" + """ shouldNot compile + } + +}