diff --git a/README.md b/README.md index 8ca343aada3c9cab3c5482f7da6e8fa668fa7383..7eaddf235470d0710ad8db62dd01646f52baa2d0 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ Learn Scala macro and abstract syntax tree. # Environment -- It is compiled in Java 8, 11 -- It is compiled in Scala 2.11.x ~ 2.13.x +- Compile passed in Java 8、11 +- Compile passed in Scala 2.11.12、2.12.14、2.13.6 # Features @@ -33,6 +33,7 @@ Learn Scala macro and abstract syntax tree. - `@equalsAndHashCode` - `@jacksonEnum` - `@elapsed` +- `@JavaCompatible` > The intellij plugin named `Scala-Macro-Tools` in marketplace. diff --git a/README_CN.md b/README_CN.md index 7bba100c52acfa0afdb8f948e495f3a042e7c925..7cdf73a52f0f044e1e4e279df95816bddab57cf9 100644 --- a/README_CN.md +++ b/README_CN.md @@ -18,8 +18,8 @@ # 环境 -- 使用 Java 8, 11 编译通过 -- 使用 Scala 2.11.x ~ 2.13.x 编译通过 +- Java 8、11 编译通过 +- Scala 2.11.12、2.12.14、2.13.6 编译通过 # 功能 @@ -33,6 +33,7 @@ - `@equalsAndHashCode` - `@jacksonEnum` - `@elapsed` +- `@JavaCompatible` > Intellij插件 `Scala-Macro-Tools`。 diff --git a/src/main/scala/io/github/dreamylost/JavaCompatible.scala b/src/main/scala/io/github/dreamylost/JavaCompatible.scala new file mode 100644 index 0000000000000000000000000000000000000000..30cba325918e68d39834eb17e09cbdfd1636d367 --- /dev/null +++ b/src/main/scala/io/github/dreamylost/JavaCompatible.scala @@ -0,0 +1,42 @@ +/* + * 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 scala.annotation.{ compileTimeOnly, StaticAnnotation } +import io.github.dreamylost.macros.javaCompatibleMacro + +/** + * annotation to generate non-parameter constructor and get/set method for case classes. + * Fields marked `private[this]` in curry are not supported ! + * + * @author 梦境迷离 + * @param verbose Whether to enable detailed log. + * @since 2021/11/23 + * @version 1.0 + */ +@compileTimeOnly("enable macro to expand macro annotations") +final class JavaCompatible( + verbose: Boolean = false +) extends StaticAnnotation { + + def macroTransform(annottees: Any*): Any = macro javaCompatibleMacro.JavaCompatibleProcessor.impl +} diff --git a/src/main/scala/io/github/dreamylost/macros/javaCompatibleMacro.scala b/src/main/scala/io/github/dreamylost/macros/javaCompatibleMacro.scala new file mode 100644 index 0000000000000000000000000000000000000000..d5a5fc73dfcc9c7fa4540ca4f190c8bc3fbab2ce --- /dev/null +++ b/src/main/scala/io/github/dreamylost/macros/javaCompatibleMacro.scala @@ -0,0 +1,112 @@ +/* + * 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 + +/** + * + * @author 梦境迷离 + * @since 2021/11/23 + * @version 1.0 + */ +object javaCompatibleMacro { + + class JavaCompatibleProcessor(override val c: whitebox.Context) extends AbstractMacroProcessor(c) { + + import c.universe._ + + /** + * We generate this method with currying, and we have to deal with the first layer of currying alone. + */ + private def getNoArgsContrWithCurrying(annotteeClassParams: List[List[Tree]], annotteeClassDefinitions: Seq[Tree]): Tree = { + if (annotteeClassDefinitions.exists(f => !isNotLocalClassMember(f))) { + c.info(c.enclosingPosition, s"The params of 'private[this]' exists in class constructor", verbose) + } + annotteeClassDefinitions.foreach { + case defDef: DefDef if defDef.name.decodedName.toString == "this" && defDef.vparamss.isEmpty => + c.abort(defDef.pos, "Non-parameter constructor method has already defined, please remove it or not use'@JavaCompatible'") + case _ => + } + + val defaultParameters = annotteeClassParams.map(valDefAccessors).map(params => params.map(param => { + param.paramType match { + case t if t <:< typeOf[Int] => q"0" + case t if t <:< typeOf[Byte] => q"0" + case t if t <:< typeOf[Double] => q"0D" + case t if t <:< typeOf[Float] => q"0F" + case t if t <:< typeOf[Short] => q"0" + case t if t <:< typeOf[Long] => q"0L" + case t if t <:< typeOf[Char] => q"63.toChar" // default char is ? + case t if t <:< typeOf[Boolean] => q"false" + case _ => q"null" + } + })) + if (annotteeClassParams.isEmpty || annotteeClassParams.size == 1) { + q""" + def this() = { + this(..${defaultParameters.flatten}) + } + """ + } else { + q""" + def this() = { + this(..${defaultParameters.head})(...${defaultParameters.tail}) + } + """ + } + } + + private def replaceAnnotation(valDefTree: Tree): Tree = { + val safeValDef = valDefAccessors(Seq(valDefTree)).head + val mods = safeValDef.mods.mapAnnotations(f => { + if (!f.toString().contains("BeanProperty")) f ++ List(q"new _root_.scala.beans.BeanProperty") else f + }) + ValDef(mods, safeValDef.name, safeValDef.tpt, safeValDef.rhs) + } + + private def getClassWithBeanProperty(classDecl: ClassDef): Tree = { + val q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends ..$bases { ..$body }" = classDecl + val newFieldss = paramss.asInstanceOf[List[List[Tree]]].map(_.map(replaceAnnotation)) + q"$mods class $tpname[..$tparams] $ctorMods(...$newFieldss) extends ..$bases { ..$body }" + } + + override def createCustomExpr(classDecl: ClassDef, compDeclOpt: Option[ModuleDef] = None): Any = { + val tmpClassDefTree = appendClassBody(classDecl, classInfo => List(getNoArgsContrWithCurrying(classInfo.classParamss, classInfo.body))) + val rest = getClassWithBeanProperty(tmpClassDefTree) + c.Expr( + q""" + ${compDeclOpt.fold(EmptyTree)(x => x)} + $rest + """) + } + + 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) + } + } + } + +} diff --git a/src/test/scala/io/github/dreamylost/JavaCompatibleTest.scala b/src/test/scala/io/github/dreamylost/JavaCompatibleTest.scala new file mode 100644 index 0000000000000000000000000000000000000000..98d37db3fc014e2fa93bf7a6d74a019e0ead0d5f --- /dev/null +++ b/src/test/scala/io/github/dreamylost/JavaCompatibleTest.scala @@ -0,0 +1,112 @@ +/* + * 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 + +/** + * + * @author 梦境迷离 + * @since 2021/11/23 + * @version 1.0 + */ +class JavaCompatibleTest extends AnyFlatSpec with Matchers { + + "JavaCompatible1" should "ok" in { + """ + | @JavaCompatible + | case class A(a: Int, b: Short, c: Byte, d: Double, e: Float, f: Long, g: Char, h: Boolean) + | val t = new A() + | assert(t.a == 0 && t.g == '?') + |""".stripMargin should compile + } + + "JavaCompatible2" should "ok" in { + """ + | @JavaCompatible + | case class A(a: Int, b: Short, c: Byte, d: Double)(val e: Float, val f: Long)(val g: Char, val h: Boolean) + | val t = new A() + | assert(t.a == 0 && t.g == '?') + |""".stripMargin should compile + } + + "JavaCompatible3" should "failed" in { + """ + | @JavaCompatible + | case class A(a: Int, b: Short, c: Byte, d: Double)(val e: Float, val f: Long)(g: Char, h: Boolean) + | val t = new A() + | assert(t.a == 0 && t.g == '?') + |""".stripMargin shouldNot compile + + """ + | @JavaCompatible + | class A(val a: Int, val b: Short) + | val t = new A() + | assert(t.a == 0) + |""".stripMargin shouldNot compile + } + + "JavaCompatible4" should "ok" in { + @JavaCompatible + case class A(a: Int, b: Short, c: Byte, d: Double, e: Float, f: Long, g: Char, h: Boolean, i: String) + val t = new A() + assert(t.a == 0 && t.g == '?') + } + + "JavaCompatible5" should "ok" in { + import scala.beans.BeanProperty + @JavaCompatible + case class A(@BeanProperty a: Int, b: Short, c: Byte, d: Double, e: Float, f: Long, g: Char, h: Boolean, i: String) + val t = new A() + assert(t.a == 0 && t.g == '?') + } + + "JavaCompatible6" should "ok when exists @BeanProperty" in { + import scala.beans.BeanProperty + @JavaCompatible + case class A(@BeanProperty a: Int, b: Short, c: Byte, d: Double, e: Float, f: Long, g: Char, h: Boolean, i: String) + val t = new A() + assert(t.getA == 0) + assert(t.getB == 0) + } + + "JavaCompatible7" should "ok when exists super" in { + import scala.beans.BeanProperty + class B(@BeanProperty val name: String, @BeanProperty val id: Int) + @JavaCompatible + case class A(a: Int, b: Short, override val name: String, override val id: Int) extends B(name, id) + val t = new A() + assert(t.getA == 0) + assert(t.getB == 0) + } + + // Why this code compile failed but test in """ """.stripMargin will pass? + "JavaCompatible8" should "fail when exists super but not use @BeanProperty" in { + """ + | class B(val name: String, val id: Int) + | @JavaCompatible + | case class A(a: Int, b: Short, override val name: String, override val id: Int) extends B(name, id) + | val t = new A() + |""".stripMargin should compile + } +}