softwaremill / magnolia

Easy, fast, transparent generic derivation of typeclass instances
https://softwaremill.com/open-source/
Apache License 2.0
754 stars 116 forks source link

[bug] type arguments can't be determined from Any #472

Open kciesielski opened 1 year ago

kciesielski commented 1 year ago

In 1.3.1, Magnolia usages in Tapir generate warnings like:

[warn] /xxx/src/core/impl.scala: the type test for Option[com.softwaremill.Endpoints.UserId]
 cannot be checked at runtime because its type arguments can't be determined from Any

I have added a workaround with suppression in https://github.com/softwaremill/magnolia/pull/473, but a proper fix may be needed in the future.

Detailed problem description

When deriving default values for case class members, Magnolia checks if there's a user-defined default, otherwise it falls back to a default based on field's type. For example, for case class Example(field: Int) it would derive a default of Example(0). However, there's an issue when dealing with generic classes with generic field types, where user defined default exists:

case class Example[A](field: Option[A] = Some("A"))
val defaultVal = HasDefault.derived[Example[Int]].defaultValue.right.get
// defaultVal == Example(Some("A")), wrong! Should be Example(None)
val fieldVal: Int = defaultVal.field.get // compiles, but throws a ClassCastException

Consequences

The Example(Some("A")) default value is incorrect, because object's type is Example[Int]. This would throw a ClassCastException on resolving the field's wrapped value. Using generic types with default values like this doesn't seem like a common scenario, so severity of this issue is very low.

Cause

In core.paramsFromMaps default values for case class fields are passed in an argument of type defaults: Map[String, Option[() => Any]]. These Any values are unsafely casted to type parameters in the safeCast method, which may cause casting for example an Option[String] to an Option[Int]. Instead, for a default param that doesn't match the type, a None should be returned, causing a fallback type's proper default value.

Possible solution

A proper resolution might require enriching defaults: Map[String, Option[() => Any]] with type information, which, compared to actual type (in the method called p) would allow to detect a mismatch, causing a fallback to None. This requires nontrivial changes to the macro which generates defaults.