From d265f5bdb64560e406d18b9a86b3813657f9a368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=A6=E5=A2=83=E8=BF=B7=E7=A6=BB?= Date: Sat, 10 Jul 2021 00:57:18 +0800 Subject: [PATCH] support Currying (#56) --- .idea/artifacts/intellij_plugin.xml | 12 ----- README.md | 5 +- README_EN.md | 4 +- build.sbt | 4 +- .../dreamylost/macros/MacroCommon.scala | 48 +++++++++++++++++++ .../github/dreamylost/macros/applyMacro.scala | 11 +++-- .../dreamylost/macros/builderMacro.scala | 13 +++-- .../dreamylost/macros/constructorMacro.scala | 44 +++++++++++------ .../io/github/dreamylost/ApplyTest.scala | 5 ++ .../io/github/dreamylost/BuilderTest.scala | 11 +++++ .../github/dreamylost/ConstructorTest.scala | 19 ++++++++ 11 files changed, 133 insertions(+), 43 deletions(-) delete mode 100644 .idea/artifacts/intellij_plugin.xml diff --git a/.idea/artifacts/intellij_plugin.xml b/.idea/artifacts/intellij_plugin.xml deleted file mode 100644 index e6db15e..0000000 --- a/.idea/artifacts/intellij_plugin.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - $PROJECT_DIR$/intellij-plugin/target/plugin/Scala-Macro-Tools - - - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index ff6083f..d36628a 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,6 @@ ## 已知问题 -- 不支持柯里化。 - 不支持泛型。 - `@constructor`与`@toString`同时使用,必须放最后。 - IDEA对宏的支持不是很好,所以会出现标红,不过编译没问题,调用结果也符合预期。 @@ -90,6 +89,7 @@ Json.fromJson[Person](json) - 说明 - 支持普通类和样例类。 + - 仅支持对主构造函数使用。 - 如果该类没有伴生对象,将生成一个伴生对象来存储`builder`方法和类。 - 示例 @@ -215,12 +215,13 @@ println(B2(1, 2, None, None)) //0.1.0,不携带字段的默认值到apply参 ## @constructor -`@constructor`注解用于为普通类生成辅助构造函数。 +`@constructor`注解用于为普通类生成辅助构造函数。仅当类有内部字段时可用。 - 说明 - `verbose` 指定是否开启详细编译日志。可选,默认`false`。 - `excludeFields` 指定是否需要排除不需要用于构造函数的`var`字段。可选,默认空(所有class内部的`var`字段都将作为构造函数的入参)。 - 仅支持在`class`上使用。 + - 主构造函数存在柯里化时,内部字段被放置在柯里化的第一个括号块中。(生成的仍然是柯里化的辅助构造) - 示例 diff --git a/README_EN.md b/README_EN.md index 7e983e0..9fd97ba 100644 --- a/README_EN.md +++ b/README_EN.md @@ -92,6 +92,7 @@ The `@builder` used to generate builder pattern for Scala classes. - Note - Support `case class` / `class`. + - Only support for **primary constructor**. - If there is no companion object, one will be generated to store the `builder` class and method. - Example @@ -218,12 +219,13 @@ println(B2(1, 2)) ## @constructor -The `@constructor` used to generate secondary constructor method for classes. +The `@constructor` used to generate secondary constructor method for classes, only when it has inner fields. - Note - `verbose` Whether to enable detailed log. - `excludeFields` Whether to exclude the specified `var` fields, default is `Nil`. - Only support `class`. + - The inner fields are placed in the first bracket block if constructor is currying. - Example diff --git a/build.sbt b/build.sbt index 79be90d..bdb41b9 100644 --- a/build.sbt +++ b/build.sbt @@ -48,7 +48,7 @@ lazy val root = (project in file(".")) ) ).settings(Publishing.publishSettings).settings(paradise()) -lazy val `examples213` = (project in file("examples2-13")).settings(scalaVersion := scala213) +lazy val `examples2-13` = (project in file("examples2-13")).settings(scalaVersion := scala213) .settings(libraryDependencies ++= Seq( "io.github.jxnu-liguobin" %% "scala-macro-tools" % (ThisBuild / version).value, )).settings( @@ -56,7 +56,7 @@ lazy val `examples213` = (project in file("examples2-13")).settings(scalaVersion Compile / scalacOptions += "-Ymacro-annotations" ) -lazy val `examples212` = (project in file("examples2-12")).settings(scalaVersion := scala212) +lazy val `examples2-12` = (project in file("examples2-12")).settings(scalaVersion := scala212) .settings(libraryDependencies ++= Seq( "io.github.jxnu-liguobin" %% "scala-macro-tools" % (ThisBuild / version).value, )).settings( diff --git a/src/main/scala/io/github/dreamylost/macros/MacroCommon.scala b/src/main/scala/io/github/dreamylost/macros/MacroCommon.scala index 836e348..06bb163 100644 --- a/src/main/scala/io/github/dreamylost/macros/MacroCommon.scala +++ b/src/main/scala/io/github/dreamylost/macros/MacroCommon.scala @@ -236,4 +236,52 @@ trait MacroCommon { case _ => false }) } + + /** + * We generate constructor with currying, and we have to deal with the first layer of currying alone. + * + * @param typeName + * @param fieldss + * @param isCase + * @return A constructor with currying, it not contains tpt, provide for calling method. + * @example [[new TestClass12(i)(j)(k)(t)]] + */ + def getConstructorWithCurrying(c: whitebox.Context)(typeName: c.TypeName, fieldss: List[List[c.Tree]], isCase: Boolean): c.Tree = { + import c.universe._ + val allFieldsTermName = fieldss.map(f => f.map(ff => fieldTermName(c)(ff))) + // not currying + val constructor = if (fieldss.isEmpty || fieldss.size == 1) { + q"${if (isCase) q"${typeName.toTermName}(..${allFieldsTermName.flatten})" else q"new $typeName(..${allFieldsTermName.flatten})"}" + } else { + // currying + val first = allFieldsTermName.head + if (isCase) q"${typeName.toTermName}(...$first)(...${allFieldsTermName.tail})" + else q"new $typeName(...$first)(...${allFieldsTermName.tail})" + } + c.info(c.enclosingPosition, s"getConstructorWithCurrying constructor: $constructor, paramss: $fieldss", force = true) + constructor + } + + /** + * We generate apply method with currying, and we have to deal with the first layer of currying alone. + * + * @param typeName + * @param fieldss + * @return A apply method with currying. + * @example [[def apply(int: Int)(j: Int)(k: Option[String])(t: Option[Long]): B3 = new B3(int)(j)(k)(t)]] + */ + def getApplyMethodWithCurrying(c: whitebox.Context)(typeName: c.TypeName, fieldss: List[List[c.Tree]]): c.Tree = { + import c.universe._ + val allFieldsTermName = fieldss.map(f => fieldAssignExpr(c)(f)) + // not currying + val applyMethod = if (fieldss.isEmpty || fieldss.size == 1) { + q"def apply(..${allFieldsTermName.flatten}): $typeName = ${getConstructorWithCurrying(c)(typeName, fieldss, isCase = false)}" + } else { + // currying + val first = allFieldsTermName.head + q"def apply(..$first)(...${allFieldsTermName.tail}): $typeName = ${getConstructorWithCurrying(c)(typeName, fieldss, isCase = false)}" + } + c.info(c.enclosingPosition, s"getApplyWithCurrying constructor: $applyMethod, paramss: $fieldss", force = true) + applyMethod + } } diff --git a/src/main/scala/io/github/dreamylost/macros/applyMacro.scala b/src/main/scala/io/github/dreamylost/macros/applyMacro.scala index b9f1d04..ecf6e5f 100644 --- a/src/main/scala/io/github/dreamylost/macros/applyMacro.scala +++ b/src/main/scala/io/github/dreamylost/macros/applyMacro.scala @@ -23,20 +23,21 @@ object applyMacro extends MacroCommon { val isCase: Boolean = isCaseClass(c)(annotateeClass) c.info(c.enclosingPosition, s"impl argument: $args, isCase: $isCase", force = args._1) + if (isCase) c.abort(c.enclosingPosition, s"Annotation is only supported on 'case class'") + def modifiedDeclaration(classDecl: ClassDef, compDeclOpt: Option[ModuleDef] = None): Any = { val (className, annotteeClassParams) = classDecl match { case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends ..$bases { ..$body }" => c.info(c.enclosingPosition, s"modifiedDeclaration className: $tpname, paramss: $paramss", force = args._1) - (tpname, paramss) + (tpname, paramss.asInstanceOf[List[List[Tree]]]) case _ => c.abort(c.enclosingPosition, s"${ErrorMessage.ONLY_CLASS} classDef: $classDecl") } c.info(c.enclosingPosition, s"modifiedDeclaration compDeclOpt: $compDeclOpt, annotteeClassParams: $annotteeClassParams", force = args._1) - val fieldNames = annotteeClassParams.asInstanceOf[List[List[Tree]]].flatten.map(f => fieldTermName(c)(f)) - val cName = className match { + val tpName = className match { case t: TypeName => t } - val annotteeClassParamsOnlyAssignExpr = fieldAssignExpr(c)(annotteeClassParams.asInstanceOf[List[List[Tree]]].flatten) - val compDecl = modifiedCompanion(c)(compDeclOpt, q"""def apply(..$annotteeClassParamsOnlyAssignExpr): $className = new $className(..$fieldNames)""", cName) + val apply = getApplyMethodWithCurrying(c)(tpName, annotteeClassParams) + val compDecl = modifiedCompanion(c)(compDeclOpt, apply, tpName) c.Expr( q""" $classDecl diff --git a/src/main/scala/io/github/dreamylost/macros/builderMacro.scala b/src/main/scala/io/github/dreamylost/macros/builderMacro.scala index 8d9915c..debb43f 100644 --- a/src/main/scala/io/github/dreamylost/macros/builderMacro.scala +++ b/src/main/scala/io/github/dreamylost/macros/builderMacro.scala @@ -45,12 +45,11 @@ object builderMacro extends MacroCommon { } } - def builderTemplate(typeName: TypeName, fields: List[Tree], isCase: Boolean): Tree = { - val termName = typeName.toTermName + def builderTemplate(typeName: TypeName, fieldss: List[List[Tree]], isCase: Boolean): Tree = { + val fields = fieldss.flatten val builderClassName = getBuilderClassName(typeName) val builderFieldMethods = fields.map(f => fieldSetMethod(typeName, f)) val builderFieldDefinitions = fields.map(f => fieldDefinition(f)) - val allFieldsTermName = fields.map(f => fieldTermName(c)(f)) q""" def builder(): $builderClassName = new $builderClassName() @@ -60,27 +59,27 @@ object builderMacro extends MacroCommon { ..$builderFieldMethods - def build(): $typeName = ${if (isCase) q"$termName(..$allFieldsTermName)" else q"new $typeName(..$allFieldsTermName)"} + def build(): $typeName = ${getConstructorWithCurrying(c)(typeName, fieldss, isCase)} } """ } // Why use Any? The dependent type need aux-pattern in scala2. Now let's get around this. def modifiedDeclaration(classDecl: ClassDef, compDeclOpt: Option[ModuleDef] = None): Any = { - val (className, fields) = classDecl match { + val (className, fieldss) = classDecl match { // @see https://scala-lang.org/files/archive/spec/2.13/05-classes-and-objects.html case q"$mods class $tpname[..$tparams](...$paramss) extends ..$bases { ..$body }" => c.info(c.enclosingPosition, s"modifiedDeclaration className: $tpname, paramss: $paramss", force = true) (tpname, paramss) case _ => c.abort(c.enclosingPosition, s"${ErrorMessage.ONLY_CLASS} classDef: $classDecl") } - c.info(c.enclosingPosition, s"modifiedDeclaration compDeclOpt: $compDeclOpt, fields: $fields", force = true) + c.info(c.enclosingPosition, s"modifiedDeclaration compDeclOpt: $compDeclOpt, fieldss: $fieldss", force = true) val cName = className match { case t: TypeName => t } val isCase = isCaseClass(c)(classDecl) - val builder = builderTemplate(cName, fields.asInstanceOf[List[List[Tree]]].flatten, isCase) + val builder = builderTemplate(cName, fieldss, isCase) val compDecl = modifiedCompanion(c)(compDeclOpt, builder, cName) c.info(c.enclosingPosition, s"builderTree: $builder, compDecl: $compDecl", force = true) // Return both the class and companion object declarations diff --git a/src/main/scala/io/github/dreamylost/macros/constructorMacro.scala b/src/main/scala/io/github/dreamylost/macros/constructorMacro.scala index 66eb764..02cc1fd 100644 --- a/src/main/scala/io/github/dreamylost/macros/constructorMacro.scala +++ b/src/main/scala/io/github/dreamylost/macros/constructorMacro.scala @@ -35,11 +35,8 @@ object constructorMacro extends MacroCommon { case _ => c.abort(c.enclosingPosition, s"${ErrorMessage.ONLY_CLASS} classDef: $classDecl") } - // Extract the field of the primary constructor. - val annotteeClassParamsOnlyAssignExpr = fieldAssignExpr(c)(annotteeClassParams.asInstanceOf[List[List[Tree]]].flatten) - // Extract the internal fields of members belonging to the class, but not in primary constructor. - val annotteeClassFieldDefinitions = getClassMemberValDef(c)(annotteeClassDefinitions) + val classFieldDefinitions = getClassMemberValDef(c)(annotteeClassDefinitions) val excludeFields = args._2 /** @@ -55,13 +52,13 @@ object constructorMacro extends MacroCommon { } } - val annotteeClassFieldDefinitionsOnlyAssignExpr = getClassMemberVarDefOnlyAssignExpr() + val classFieldDefinitionsOnlyAssignExpr = getClassMemberVarDefOnlyAssignExpr() - if (annotteeClassFieldDefinitionsOnlyAssignExpr.isEmpty) { + if (classFieldDefinitionsOnlyAssignExpr.isEmpty) { c.abort(c.enclosingPosition, s"Annotation is only supported on class when the internal field (declare as 'var') is nonEmpty. classDef: $classDecl") } - val annotteeClassFieldNames = annotteeClassFieldDefinitions.filter(_ match { + val annotteeClassFieldNames = classFieldDefinitions.filter(_ match { case q"$mods var $tname: $tpt = $expr" if !excludeFields.contains(tname.asInstanceOf[TermName].decodedName.toString) => true case _ => false }).map { @@ -70,21 +67,40 @@ object constructorMacro extends MacroCommon { c.info(c.enclosingPosition, s"modifiedDeclaration compDeclOpt: $compDeclOpt, annotteeClassParams: $annotteeClassParams", force = args._1) - // not suppport currying - val ctorFieldNames = annotteeClassParams.asInstanceOf[List[List[Tree]]].flatten.map(f => fieldTermName(c)(f)) + // Extract the field of the primary constructor. + val ctorFieldNamess = annotteeClassParams.asInstanceOf[List[List[Tree]]] + val allFieldsTermName = ctorFieldNamess.map(f => f.map(ff => fieldTermName(c)(ff))) - def getConstructorTemplate(): c.universe.Tree = { - q""" - def this(..${annotteeClassParamsOnlyAssignExpr ++ annotteeClassFieldDefinitionsOnlyAssignExpr}){ - this(..$ctorFieldNames) + /** + * We generate this method with currying, and we have to deal with the first layer of currying alone. + */ + def getThisMethodWithCurrying(): c.Tree = { + // not currying + // Extract the field of the primary constructor. + val classParamsAssignExpr = fieldAssignExpr(c)(ctorFieldNamess.flatten) + val applyMethod = if (ctorFieldNamess.isEmpty || ctorFieldNamess.size == 1) { + q""" + def this(..${classParamsAssignExpr ++ classFieldDefinitionsOnlyAssignExpr}) = { + this(..${allFieldsTermName.flatten}) + ..${annotteeClassFieldNames.map(f => q"this.$f = $f")} + } + """ + } else { + // NOTE: currying constructor overload must be placed in the first bracket block. + val allClassParamsAssignExpr = ctorFieldNamess.map(cc => fieldAssignExpr(c)(cc)) + q""" + def this(..${allClassParamsAssignExpr.head ++ classFieldDefinitionsOnlyAssignExpr})(...${allClassParamsAssignExpr.tail}) = { + this(..${allFieldsTermName.head})(...${allFieldsTermName.tail}) ..${annotteeClassFieldNames.map(f => q"this.$f = $f")} } """ + } + applyMethod } val resTree = annotateeClass match { case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" => - q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..${stats.toList.:+(getConstructorTemplate())} }" + q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..${stats.toList.:+(getThisMethodWithCurrying())} }" } c.Expr[Any](treeResultWithCompanionObject(c)(resTree, annottees: _*)) } diff --git a/src/test/scala/io/github/dreamylost/ApplyTest.scala b/src/test/scala/io/github/dreamylost/ApplyTest.scala index 24f9d72..170438f 100644 --- a/src/test/scala/io/github/dreamylost/ApplyTest.scala +++ b/src/test/scala/io/github/dreamylost/ApplyTest.scala @@ -37,4 +37,9 @@ class ApplyTest extends AnyFlatSpec with Matchers { // FAILED, not support currying!! """@apply @toString class C(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = Some(1L))(o: Int = 1)""" shouldNot compile } + + "apply2" should "ok with currying" in { + @apply + @toString class B3(int: Int)(val j: Int)(var k: Option[String] = None)(t: Option[Long] = Some(1L)) + } } diff --git a/src/test/scala/io/github/dreamylost/BuilderTest.scala b/src/test/scala/io/github/dreamylost/BuilderTest.scala index bc847f0..a436b37 100644 --- a/src/test/scala/io/github/dreamylost/BuilderTest.scala +++ b/src/test/scala/io/github/dreamylost/BuilderTest.scala @@ -75,4 +75,15 @@ class BuilderTest extends AnyFlatSpec with Matchers { println(ret) assert(ret.toString == "TestClass1(i=1, j=0, x=x, o=Some())") } + + "builder8" should "ok on currying" in { + + @builder + case class TestClass11(val i: Int = 0)(var j: Int)(val k: Int) + (val t: Option[String]) + + @builder + class TestClass12(val i: Int = 0)(var j: Int)(val k: Int) + (val t: Option[String]) + } } diff --git a/src/test/scala/io/github/dreamylost/ConstructorTest.scala b/src/test/scala/io/github/dreamylost/ConstructorTest.scala index dc72f8a..7638e5f 100644 --- a/src/test/scala/io/github/dreamylost/ConstructorTest.scala +++ b/src/test/scala/io/github/dreamylost/ConstructorTest.scala @@ -121,5 +121,24 @@ class ConstructorTest extends AnyFlatSpec with Matchers { println(A2(1, 2, None, Some(12L))) println(A2.builder().int(1).j(2).build()) println(new A2(1, 2, None, None, 100)) + + @toString + @constructor + class TestClass12(val i: Int = 0)(var j: Int)(val k: Int) { + private val a: Int = 1 + var b: Int = 1 + protected var c: Int = _ + } + + println(new TestClass12(1, 1, 1)(2)(3)) + + /** + * def (i: Int, b: Int, c: Int)(j: Int)(k: Int) = { + * (i)(j)(k); + * this.b = b; + * this.c = c + * } + */ + } } -- GitLab