scalalandio / chimney

Scala library for boilerplate-free, type-safe data transformations
https://chimney.readthedocs.io
Apache License 2.0
1.15k stars 91 forks source link

Chimney >= 0.8.0 can't transform case class with annotated parameter #562

Closed little-inferno closed 2 months ago

little-inferno commented 2 months ago

Checklist

Describe the bug

Compilation fail on new Chimney version if case class parameter have annotation and parameters have default value

import io.scalaland.chimney.dsl._

import scala.annotation.StaticAnnotation

case class Kek(param1: Option[Int] = None, param2: List[String] = Nil, param3: Option[String] = None)

final case class annot() extends StaticAnnotation

case class Lol(
  param1: Option[String] = None,
  param2: List[String] = Nil,
  @annot() param3: Option[String] = None,
  param4: Option[String] = None
)

object Test120 extends App {
  val kek = Kek(Some(1), "2" :: "3" :: Nil, Some("4"))

  println(
    kek
      .into[Lol]
      .withFieldComputed(_.param1, _.param1.map(_.toString))
      .enableDefaultValues
      .enableOptionDefaultsToNone
      .transform
  )
}

Reproduction

I created a repository to reproduce: https://github.com/little-inferno/chimney-error

Project chimney075 and chimney120 contain the same code but use a different version of Chimney, the first one works, the second one doesn’t

Expected behavior

I expected that Chimney can transform case classes with annotated parameters

Actual behavior

Use chimney 0.7.5 - success

Use chimney 1.2.0 -

[error] /Users/d.zasypkin/projects/chimney-error/chimney120/src/main/scala/Test.scala:25:8: Chimney can't derive transformation from Kek to Lol
[error]
[error]   macro expansion thrown exception!: java.lang.AssertionError: Expected that Lol's constructor parameter `value param1` would have default value: attempted `apply$default$1` and `$lessinit$greater$default$1`, found: Scope{
[error]   def <init>: <?>;
[error]   final override def toString: <?>;
[error]   case def apply: <?>;
[error]   case def unapply: <?>;
[error]   def <init>$default$1 : <?>;
[error]   def <init>$default$2 : <?>;
[error]   def <init>$default$3 : <?>;
[error]   def <init>$default$4 : <?>
[error] }:
[error]     io.scalaland.chimney.internal.compiletime.Results.assertionFailed(Results.scala:12)
[error]     io.scalaland.chimney.internal.compiletime.Results.assertionFailed$(Results.scala:12)
[error]     io.scalaland.chimney.internal.compiletime.derivation.transformer.TransformerMacros.assertionFailed(TransformerMacros.scala:10)
[error]     io.scalaland.chimney.internal.compiletime.datatypes.ProductTypesPlatform$ProductType$$anonfun$1.$anonfun$applyOrElse$1(ProductTypesPlatform.scala:137)
[error]     scala.Option.getOrElse(Option.scala:201)
[error]     io.scalaland.chimney.internal.compiletime.datatypes.ProductTypesPlatform$ProductType$$anonfun$1.applyOrElse(ProductTypesPlatform.scala:136)
[error]     io.scalaland.chimney.internal.compiletime.datatypes.ProductTypesPlatform$ProductType$$anonfun$1.applyOrElse(ProductTypesPlatform.scala:125)
[error]     scala.collection.immutable.List.collect(List.scala:268)
[error]     io.scalaland.chimney.internal.compiletime.datatypes.ProductTypesPlatform$ProductType$.parseConstructor(ProductTypesPlatform.scala:125)
[error]     io.scalaland.chimney.internal.compiletime.datatypes.ProductTypesPlatform$ProductType$.parseConstructor(ProductTypesPlatform.scala:12)
[error]     ...
[error] Consult https://chimney.readthedocs.io for usage examples.
[error]
[error]       .transform
[error]        ^
[error] one error found
[error] (Compile / compileIncremental) Compilation failed

Which Chimney version do you use

Chimney <= 0.7.5 works great

Chimney >= 0.8.0 give an error

Which platform do you use

If you checked JVM

Temurin-17.0.11+9

MateuszKubuszok commented 2 months ago

Couldn't reproduce in Scastie:

Then the reproduction from https://github.com/little-inferno/chimney-error worked.

However, commenting out

//ThisBuild / scalacOptions += "-Ymacro-annotations"

make the code work again. I am not sure what is the cause, but presence of "-Ymacro-annotations" changes what macros sees internally, I suspect that we might have a workaround for such cases which did not survive the rewrite as it's difficult to have a regression test for something as unpredictable as compiler flag changing the data available to the macro. I might take a look at this somewhere in the next week if I'll have the time.

little-inferno commented 2 months ago

Yes, it seems that using -Ymacro-annotations change decoded name from

apply$default$idx

to

<init>$default$idx

And this matching give an error https://github.com/scalalandio/chimney/blob/2643676b3e89469038c2ec7e354411fc8b9b3319/chimney-macro-commons/src/main/scala-2/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypesPlatform.scala#L129-L139

I wrote dirty quickfix and it fix compilation errors in https://github.com/little-inferno/chimney-error

MateuszKubuszok commented 2 months ago

I have something local, but I'd need to add regression tests to make sure it works and would keep working.

MateuszKubuszok commented 2 months ago

My local fix was quite similar to your - I'd add a test for default value in a class which is not case class, and maybe something with -Ymacro-annotations. I suspect that missing tpe.typeSymbol.typeSingature was the issue, since there was an unfixed bug in Scala 2 macros when sometimes type symbol is not initialized until this method is called (even if it's completely not needed). But I haven't tested this PR at all.

MateuszKubuszok commented 2 months ago

As far as I tested the fix I merged worked. I tested with local snapshot, once https://github.com/scalalandio/chimney/actions/runs/9761346064/job/26942175116 passed you might check as well with a Sonatype OSS shapshots.

little-inferno commented 2 months ago

Great, there are no more errors with the default parameters 🎉

MateuszKubuszok commented 2 months ago

Released in 1.3.0