diff --git a/tests/neg-macros/annot-codegen.check b/tests/neg-macros/annot-codegen.check new file mode 100644 index 000000000000..83f844f6131f --- /dev/null +++ b/tests/neg-macros/annot-codegen.check @@ -0,0 +1,35 @@ + +-- Error: tests/neg-macros/annot-codegen/Test_2.scala:7:4 -------------------------------------------------------------- +7 | new Bar("", two, 1, 1.0) // error: body needs to be replaced + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | Replace the underline code by: + | data.generated[Bar]() +-- Error: tests/neg-macros/annot-codegen/Test_2.scala:9:6 -------------------------------------------------------------- +9 | def withThree(three: Int): Bar // error: body needs to be data.generate[Bar]() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Replace the underline code by: + | def withThree(three: scala.Int): Bar = data.generated[Bar]() +-- Error: tests/neg-macros/annot-codegen/Test_2.scala:1:80 ------------------------------------------------------------- +1 |@data class Bar(val one: String, val two: Int, val three: Int, val four: Double): // error: additional methods needed + | ^ + | @data requires the following additional method(s): + | + | def withFour(four: scala.Double): Bar = data.generated[Bar]() +-- Error: tests/neg-macros/annot-codegen/Test_2.scala:12:32 ------------------------------------------------------------ +12 | def withOne(one: T): Baz[T] = ??? // error + | ^^^ + | Replace the underline code by: + | data.generated[Baz[Baz.this.T]]() +-- Error: tests/neg-macros/annot-codegen/Test_2.scala:13:6 ------------------------------------------------------------- +13 | def withTwo(two: Int): Baz[T] // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Replace the underline code by: + | def withTwo(two: scala.Int): Baz[Baz.this.T] = data.generated[Baz[Baz.this.T]]() +-- Error: tests/neg-macros/annot-codegen/Test_2.scala:14:38 ------------------------------------------------------------ +14 | def withThree(three: Int): Baz[T] = data.generated[Baz[T]]() // error // FIXME: should be supported + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | constructor Baz in class Baz does not take parameters +-- Error: tests/neg-macros/annot-codegen/Test_2.scala:15:36 ------------------------------------------------------------ +15 | def withFour(four: Int): Baz[T] = data.generated[Nothing]() // error // FIXME: wrong error + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + | constructor Baz in class Baz does not take parameters diff --git a/tests/neg-macros/annot-codegen/Macro_1.scala b/tests/neg-macros/annot-codegen/Macro_1.scala new file mode 100644 index 000000000000..18e75c185752 --- /dev/null +++ b/tests/neg-macros/annot-codegen/Macro_1.scala @@ -0,0 +1,87 @@ +import scala.annotation.{experimental, MacroAnnotation} +import scala.quoted.* +import scala.collection.mutable.ArrayBuffer + +@experimental +class data extends MacroAnnotation: + import data.* + def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] = + import quotes.reflect.* + tree match + case cdef: ClassDef => + val classPatches = ArrayBuffer[String]() + + val cls = tree.symbol + val typeParams = cdef.body.map(_.symbol).filter(_.isType) + val clsTpe = + if typeParams.isEmpty then cls.typeRef + else AppliedType(cls.typeRef, typeParams.map(_.typeRef)) + val expectedBody = + clsTpe.asType match + case '[t] => '{ data.generated[t]() } + + val params = paramNames(cls) + for param <- params do + val withParam = With(param) + val paramType = cls.declaredField(param).info + val existingOpt = + cdef.body.find(stat => + val paramss = stat.symbol.paramSymss + stat.symbol.name == withParam + && paramss.size == 1 && paramss(0).size == 1 + && paramss(0)(0).name == param // FIXME: if the parameter name is incorrect, propose rewriting it + && paramss(0)(0).info == paramType // FIXME: if the parameter type changed, propose rewriting it + ) + existingOpt match + case Some(tree: DefDef) => + tree.rhs match + case Some(rhs) => + if !rhs.asExpr.matches(expectedBody) then + report.error(s"Replace the underline code by:\n${expectedBody.show}", rhs.pos) + case _ => + report.error(s"Replace the underline code by:\n${tree.show} = ${expectedBody.show}", tree.pos) + case _ => + // The method is not present + classPatches += + s"def $withParam($param: ${paramType.show}): ${clsTpe.show} = ${expectedBody.show}" + + + val ctr = cdef.constructor + val endPos = Position(ctr.pos.sourceFile, ctr.pos.end, ctr.pos.end) + // TODO: if the class has no existing body, we also need to add braces or ':' + if classPatches.nonEmpty then + report.error("@data requires the following additional method(s):\n\n" + + classPatches.mkString("\n"), endPos) + case _ => + report.error("Annotation only supports `class`") + List(tree) + +object data: + // TODO: Is T necessary? Could this be transparent? + inline def generated[T](): T = ${generatedImpl[T]} + private def generatedImpl[T: Type](using Quotes): Expr[T] = + import quotes.reflect.* + val meth = Symbol.spliceOwner.owner + val cls = meth.owner + val params = paramNames(cls) + meth.name match + case With(paramName) => + val localParam = meth.paramSymss(0)(0) + val body = // FIXME handle type parameters + Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), + params.map(p => if p == paramName then Ref(localParam) else Ref(cls.declaredField(p)))) + body.asExprOf[T] + case _ => + report.errorAndAbort("@data.generated used in invalid context") + + def paramNames(using Quotes)(cls: quotes.reflect.Symbol): List[String] = + cls.primaryConstructor.paramSymss.dropWhile(_.headOption.exists(_.isType)).head.map(_.name) + +private object With: + def apply(paramName: String): String = + s"with${paramName.head.toUpper}${paramName.tail}" + def unapply(methodName: String): Option[String] = methodName match + case s"with$rest" => + Some(s"${rest.head.toLower}${rest.tail}") + case _ => + None diff --git a/tests/neg-macros/annot-codegen/Test_2.scala b/tests/neg-macros/annot-codegen/Test_2.scala new file mode 100644 index 000000000000..8181d80da85e --- /dev/null +++ b/tests/neg-macros/annot-codegen/Test_2.scala @@ -0,0 +1,15 @@ +@data class Bar(val one: String, val two: Int, val three: Int, val four: Double): // error: additional methods needed + // This definition is OK and will be left as is + def withOne(one: String): Bar = + data.generated[Bar]() + + def withTwo(two: Int): Bar = + new Bar("", two, 1, 1.0) // error: body needs to be replaced + + def withThree(three: Int): Bar // error: body needs to be data.generate[Bar]() + +@data class Baz[T](val one: T, val two: Int, val three: Int, val four: Int): + def withOne(one: T): Baz[T] = ??? // error + def withTwo(two: Int): Baz[T] // error + def withThree(three: Int): Baz[T] = data.generated[Baz[T]]() // error // FIXME: should be supported + def withFour(four: Int): Baz[T] = data.generated[Nothing]() // error // FIXME: wrong error