From 8bb25e25c6012ffc8ec21f1fa0c72507f2bb9b30 Mon Sep 17 00:00:00 2001 From: Kacper Korban Date: Fri, 7 Apr 2023 12:42:50 +0200 Subject: [PATCH] Scala 3: Only include first parameter list of constructor when applying copy closes softwaremill#138 --- .../quicklens/QuicklensMacros.scala | 18 +++++++--- .../quicklens/SecondParamListTest.scala | 36 +++++++++++++++++++ 2 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 quicklens/src/test/scala/com/softwaremill/quicklens/SecondParamListTest.scala diff --git a/quicklens/src/main/scala-3/com/softwaremill/quicklens/QuicklensMacros.scala b/quicklens/src/main/scala-3/com/softwaremill/quicklens/QuicklensMacros.scala index d949640..c7974fa 100644 --- a/quicklens/src/main/scala-3/com/softwaremill/quicklens/QuicklensMacros.scala +++ b/quicklens/src/main/scala-3/com/softwaremill/quicklens/QuicklensMacros.scala @@ -48,9 +48,10 @@ object QuicklensMacros { def toPathModifyFromFocus[S: Type, A: Type](obj: Expr[S], focus: Expr[S => A])(using Quotes): Expr[PathModify[S, A]] = toPathModify(obj, modifyImpl(obj, Seq(focus))) - private def modifyImpl[S: Type, A: Type](obj: Expr[S], focuses: Seq[Expr[S => A]])(using - Quotes - ): Expr[(A => A) => S] = { + private def modifyImpl[S: Type, A: Type]( + obj: Expr[S], + focuses: Seq[Expr[S => A]] + )(using Quotes): Expr[(A => A) => S] = { import quotes.reflect.* def unsupportedShapeInfo(tree: Tree) = @@ -209,8 +210,9 @@ object QuicklensMacros { case AppliedType(_, typeParams) => Some(typeParams) case _ => None } - - val fieldsIdxs = 1.to(objSymbol.primaryConstructor.paramSymss.flatten.filter(_.isTerm).length) + val constructorTree: DefDef = objSymbol.primaryConstructor.tree.asInstanceOf[DefDef] + val firstParamListLength: Int = constructorTree.termParamss.headOption.map(_.params).toList.flatten.length + val fieldsIdxs = 1.to(firstParamListLength) val args = fieldsIdxs.map { i => val defaultMethod = obj.select(symbolMethodByNameUnsafe(objSymbol, "copy$default$" + i.toString)) argsMap.getOrElse( @@ -219,6 +221,11 @@ object QuicklensMacros { ) }.toList + if constructorTree.termParamss.drop(1).exists(_.params.exists(!_.symbol.flags.is(Flags.Implicit))) then + report.errorAndAbort( + s"Implementation limitation: Only the first parameter list of the modified case classes can be non-implicit." + ) + typeParams match { // if the object's type is parametrised, we need to call .copy with the same type parameters case Some(typeParams) => Apply(TypeApply(Select(obj, copy), typeParams.map(Inferred(_))), args) @@ -335,6 +342,7 @@ object QuicklensMacros { val res: (Expr[A => A] => Expr[S]) = (mod: Expr[A => A]) => Typed(mapToCopy(Symbol.spliceOwner, mod, obj.asTerm, pathTree), TypeTree.of[S]).asExpr.asInstanceOf[Expr[S]] + to(res) } } diff --git a/quicklens/src/test/scala/com/softwaremill/quicklens/SecondParamListTest.scala b/quicklens/src/test/scala/com/softwaremill/quicklens/SecondParamListTest.scala new file mode 100644 index 0000000..9cca121 --- /dev/null +++ b/quicklens/src/test/scala/com/softwaremill/quicklens/SecondParamListTest.scala @@ -0,0 +1,36 @@ +package com.softwaremill.quicklens + +import com.softwaremill.quicklens.TestData._ +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class SecondParamListTest extends AnyFlatSpec with Matchers { + it should "modify an object with second implicit param list" in { + import com.softwaremill.quicklens._ + + case class State(inside: Boolean)(implicit d: Double) + + val d: Double = 1.0 + + val state1 = State(true)(d) + + implicit val dd: Double = d + val state2 = state1.modify(_.inside).setTo(true) + + state1 should be(state2) + } + + it should "should give a meaningful error for an object with more than one non-implicit param list" in { + import com.softwaremill.quicklens._ + + case class State(inside: Boolean)(d: Double) + + val d: Double = 1.0 + + val state1 = State(true)(d) + + implicit val dd: Double = d + + assertDoesNotCompile("state1.modify(_.inside).setTo(true)") + } +}