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

support Currying (#56)

上级 3575a35e
<component name="ArtifactManager">
<artifact name="intellij-plugin">
<output-path>$PROJECT_DIR$/intellij-plugin/target/plugin/Scala-Macro-Tools</output-path>
<root id="root">
<element id="directory" name="lib">
<element id="archive" name="intellij-plugin.jar">
<element id="module-output" name="scala-macro-tools-intellij-plugin" />
</element>
</element>
</root>
</artifact>
</component>
\ No newline at end of file
......@@ -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`上使用。
- 主构造函数存在柯里化时,内部字段被放置在柯里化的第一个括号块中。(生成的仍然是柯里化的辅助构造)
- 示例
......
......@@ -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
......
......@@ -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(
......
......@@ -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
}
}
......@@ -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
......
......@@ -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
......
......@@ -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: _*))
}
......
......@@ -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))
}
}
......@@ -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])
}
}
......@@ -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 <init>(i: Int, b: Int, c: Int)(j: Int)(k: Int) = {
* <init>(i)(j)(k);
* this.b = b;
* this.c = c
* }
*/
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册