Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implicit conversions and type-constraint evidence (<:<) support #667

Merged
merged 9 commits into from
Jan 23, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package io.scalaland.chimney.internal.compiletime
/** All Rules implemented for this platform. */
trait StandardRules
extends derivation.transformer.rules.TransformImplicitRuleModule
with derivation.transformer.rules.TransformImplicitOuterTransformerRuleModule
with derivation.transformer.rules.TransformImplicitConversionRuleModule
with derivation.transformer.rules.TransformSubtypesRuleModule
with derivation.transformer.rules.TransformTypeConstraintRuleModule
with derivation.transformer.rules.TransformToSingletonRuleModule
with derivation.transformer.rules.TransformOptionToOptionRuleModule
with derivation.transformer.rules.TransformPartialOptionToNonOptionRuleModule
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package io.scalaland.chimney.internal.compiletime
/** All Rules implemented for this platform. */
trait StandardRules
extends derivation.transformer.rules.TransformImplicitRuleModule
with derivation.transformer.rules.TransformImplicitOuterTransformerRuleModule
with derivation.transformer.rules.TransformImplicitConversionRuleModule
with derivation.transformer.rules.TransformSubtypesRuleModule
with derivation.transformer.rules.TransformTypeConstraintRuleModule
with derivation.transformer.rules.TransformToSingletonRuleModule
with derivation.transformer.rules.TransformOptionToOptionRuleModule
with derivation.transformer.rules.TransformPartialOptionToNonOptionRuleModule
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package io.scalaland.chimney.internal.compiletime

import scala.collection.compat.Factory

import TypeAlias.<:<<

private[compiletime] trait TypesPlatform extends Types { this: DefinitionsPlatform =>

import c.universe.{internal as _, Transformer as _, *}
Expand Down Expand Up @@ -185,6 +187,12 @@ private[compiletime] trait TypesPlatform extends Types { this: DefinitionsPlatfo
object CharLiteral extends LiteralImpl[Char] with CharLiteralModule
object StringLiteral extends LiteralImpl[String] with StringLiteralModule

object <:< extends `<:<Module` {
def apply[From: Type, To: Type]: Type[From <:<< To] = weakTypeTag[From <:<< To]
def unapply[A](A: Type[A]): Option[(??, ??)] =
A.asCtor[<:<<[?, ?]].map(A0 => A0.param(0) -> A0.param(1))
}

def isTuple[A](A: Type[A]): Boolean = A.tpe.typeSymbol.fullName.startsWith("scala.Tuple")

def isSubtypeOf[A, B](A: Type[A], B: Type[B]): Boolean = A.tpe <:< B.tpe
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package io.scalaland.chimney.internal.compiletime
import scala.quoted
import scala.collection.compat.Factory

import TypeAlias.<:<<

private[compiletime] trait TypesPlatform extends Types { this: DefinitionsPlatform =>

import quotes.*, quotes.reflect.*
Expand Down Expand Up @@ -261,6 +263,14 @@ private[compiletime] trait TypesPlatform extends Types { this: DefinitionsPlatfo
object CharLiteral extends LiteralImpl[Char](CharConstant(_)) with CharLiteralModule
object StringLiteral extends LiteralImpl[String](StringConstant(_)) with StringLiteralModule

object <:< extends `<:<Module` {
def apply[From: Type, To: Type]: Type[From <:<< To] = quoted.Type.of[From <:<< To]
def unapply[A](A: Type[A]): Option[(??, ??)] = A match {
case '[<:<<[from, to]] => Some(Type[to].as_?? -> Type[to].as_??)
case _ => scala.None
}
}

def isTuple[A](A: Type[A]): Boolean = TypeRepr.of(using A).typeSymbol.fullName.startsWith("scala.Tuple")

def isSubtypeOf[A, B](A: Type[A], B: Type[B]): Boolean = TypeRepr.of(using A) <:< TypeRepr.of(using B)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package io.scalaland.chimney.internal.compiletime
import scala.collection.compat.Factory
import scala.collection.immutable.ListSet

import TypeAlias.<:<<

private[compiletime] trait Types { this: (Existentials & Results) =>

/** Platform-specific type representation (`c.WeakTypeTag[A]` in 2, `scala.quoted.Type[A]` in 3) */
Expand Down Expand Up @@ -145,6 +147,9 @@ private[compiletime] trait Types { this: (Existentials & Results) =>
val StringLiteral: StringLiteralModule
trait StringLiteralModule extends Literal[String] { this: StringLiteral.type => }

val <:< : `<:<Module`
trait `<:<Module` extends Ctor2[<:<<] { this: `<:<`.type => }

// You can `import Type.Implicits.*` in your shared code to avoid providing types manually, while avoiding conflicts
// with implicit types seen in platform-specific scopes (which would happen if those implicits were always used).
object Implicits {
Expand Down Expand Up @@ -183,6 +188,8 @@ private[compiletime] trait Types { this: (Existentials & Results) =>
implicit def MapType[K: Type, V: Type]: Type[scala.collection.Map[K, V]] = Map[K, V]
implicit def IteratorType[A: Type]: Type[Iterator[A]] = Iterator[A]
implicit def FactoryType[A: Type, C: Type]: Type[Factory[A, C]] = Factory[A, C]

implicit def `<:<Type`[From: Type, To: Type]: Type[From <:<< To] = <:<[From, To]
}

// Implementations of extension methods
Expand Down Expand Up @@ -254,3 +261,8 @@ private[compiletime] trait Types { this: (Existentials & Results) =>
def extractStringSingleton: String = Type.extractStringSingleton(tpe)
}
}
private[compiletime] object TypeAlias {
// Workaround for "type <:< is not a member of object Predef":
// apparently on 2.13/3 <:< is seen as defined in scala not scala.Predef.
type <:<<[-From, +To] = From <:< To
}
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,10 @@
weakTypeTag[runtime.TransformerFlags.PartialUnwrapsOption]
val NonAnyValWrappers: Type[runtime.TransformerFlags.NonAnyValWrappers] =
weakTypeTag[runtime.TransformerFlags.NonAnyValWrappers]
val TypeConstraintEvidence: Type[runtime.TransformerFlags.TypeConstraintEvidence] =
weakTypeTag[runtime.TransformerFlags.TypeConstraintEvidence]
val ImplicitConversions: Type[runtime.TransformerFlags.ImplicitConversions] =

Check warning on line 425 in chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala

View check run for this annotation

Codecov / codecov/patch

chimney/src/main/scala-2/io/scalaland/chimney/internal/compiletime/ChimneyTypesPlatform.scala#L425

Added line #L425 was not covered by tests
weakTypeTag[runtime.TransformerFlags.ImplicitConversions]
object ImplicitConflictResolution extends ImplicitConflictResolutionModule {
def apply[R <: dsls.ImplicitTransformerPreference: Type]
: Type[runtime.TransformerFlags.ImplicitConflictResolution[R]] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ private[compiletime] trait DerivationPlatform
PatchImplicitRule,
TransformImplicitRule,
TransformImplicitOuterTransformerRule,
TransformImplicitConversionRule,
PatchSubtypeRuleModule,
TransformTypeConstraintRule,
PatchOptionWithNonOptionRule,
PatchOptionWithOptionOptionRule,
PatchEitherWithOptionEitherRule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ trait DerivationPlatform
with datatypes.ValueClassesPlatform
with rules.TransformImplicitRuleModule
with rules.TransformImplicitOuterTransformerRuleModule
with rules.TransformImplicitConversionRuleModule
with rules.TransformSubtypesRuleModule
with rules.TransformTypeConstraintRuleModule
with rules.TransformToSingletonRuleModule
with rules.TransformOptionToOptionRuleModule
with rules.TransformPartialOptionToNonOptionRuleModule
Expand All @@ -28,7 +30,9 @@ trait DerivationPlatform
override protected val rulesAvailableForPlatform: List[Rule] = List(
TransformImplicitRule,
TransformImplicitOuterTransformerRule,
TransformImplicitConversionRule,
TransformSubtypesRule,
TransformTypeConstraintRule,
TransformToSingletonRule,
TransformOptionToOptionRule,
TransformPartialOptionToNonOptionRule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,10 @@ private[compiletime] trait ChimneyTypesPlatform extends ChimneyTypes { this: Chi
quoted.Type.of[runtime.TransformerFlags.PartialUnwrapsOption]
val NonAnyValWrappers: Type[runtime.TransformerFlags.NonAnyValWrappers] =
quoted.Type.of[runtime.TransformerFlags.NonAnyValWrappers]
val TypeConstraintEvidence: Type[runtime.TransformerFlags.TypeConstraintEvidence] =
quoted.Type.of[runtime.TransformerFlags.TypeConstraintEvidence]
val ImplicitConversions: Type[runtime.TransformerFlags.ImplicitConversions] =
quoted.Type.of[runtime.TransformerFlags.ImplicitConversions]
object ImplicitConflictResolution extends ImplicitConflictResolutionModule {
def apply[R <: dsls.ImplicitTransformerPreference: Type]
: Type[runtime.TransformerFlags.ImplicitConflictResolution[R]] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ abstract private[compiletime] class DerivationPlatform(q: scala.quoted.Quotes)
PatchImplicitRule,
TransformImplicitRule,
TransformImplicitOuterTransformerRule,
TransformImplicitConversionRule,
PatchSubtypeRuleModule,
TransformTypeConstraintRule,
PatchOptionWithNonOptionRule,
PatchOptionWithOptionOptionRule,
PatchEitherWithOptionEitherRule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ abstract private[compiletime] class DerivationPlatform(q: scala.quoted.Quotes)
with datatypes.ValueClassesPlatform
with rules.TransformImplicitRuleModule
with rules.TransformImplicitOuterTransformerRuleModule
with rules.TransformImplicitConversionRuleModule
with rules.TransformSubtypesRuleModule
with rules.TransformTypeConstraintRuleModule
with rules.TransformToSingletonRuleModule
with rules.TransformOptionToOptionRuleModule
with rules.TransformPartialOptionToNonOptionRuleModule
Expand All @@ -29,7 +31,9 @@ abstract private[compiletime] class DerivationPlatform(q: scala.quoted.Quotes)
override protected val rulesAvailableForPlatform: List[Rule] = List(
TransformImplicitRule,
TransformImplicitOuterTransformerRule,
TransformImplicitConversionRule,
TransformSubtypesRule,
TransformTypeConstraintRule,
TransformToSingletonRule,
TransformOptionToOptionRule,
TransformPartialOptionToNonOptionRule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,50 @@
def disableNonAnyValWrappers: UpdateFlag[Disable[NonAnyValWrappers, Flags]] =
disableFlag[NonAnyValWrappers]

/** Enable using [[scala.Predef.<:<]] or [[scala.Predef.=:=]] to prove that `From` is a subtype of `To`.
*
* By default, such evidence is ignored.
*
* @see
* [[https://chimney.readthedocs.io/supported-transformations/#type-evidence-based-conversions]] for more details
*
* @since 1.7.0
*/
def enableTypeConstraintEvidence: UpdateFlag[Enable[TypeConstraintEvidence, Flags]] =
enableFlag[TypeConstraintEvidence]

/** Disable using [[scala.Predef.<:<]] or [[scala.Predef.=:=]] to prove that `From` is a subtype of `To`.
*
* @see
* [[https://chimney.readthedocs.io/supported-transformations/#type-evidence-based-conversions]] for more details
*
* @since 1.7.0
*/
def disableTypeConstraintEvidence: UpdateFlag[Disable[TypeConstraintEvidence, Flags]] =
disableFlag[TypeConstraintEvidence]

Check warning on line 303 in chimney/src/main/scala/io/scalaland/chimney/dsl/TransformerTargetFlagsDsl.scala

View check run for this annotation

Codecov / codecov/patch

chimney/src/main/scala/io/scalaland/chimney/dsl/TransformerTargetFlagsDsl.scala#L302-L303

Added lines #L302 - L303 were not covered by tests

/** Enable using implicit conversions to transform one `From` into `To`.
*
* By default, implicit conversions are ignored.
*
* @see
* [[https://chimney.readthedocs.io/supported-transformations/#implicit-conversions]] for more details
*
* @since 1.7.0
*/
def enableImplicitConversions: UpdateFlag[Enable[ImplicitConversions, Flags]] =
enableFlag[ImplicitConversions]

/** Disable using implicit conversions to transform one `From` into `To`.
*
* @see
* [[https://chimney.readthedocs.io/supported-transformations/#implicit-conversions]] for more details
*
* @since 1.7.0
*/
def disableImplicitConversions: UpdateFlag[Disable[ImplicitConversions, Flags]] =
disableFlag[ImplicitConversions]

Check warning on line 325 in chimney/src/main/scala/io/scalaland/chimney/dsl/TransformerTargetFlagsDsl.scala

View check run for this annotation

Codecov / codecov/patch

chimney/src/main/scala/io/scalaland/chimney/dsl/TransformerTargetFlagsDsl.scala#L324-L325

Added lines #L324 - L325 were not covered by tests

/** Enable conflict resolution when both `Transformer` and `PartialTransformer` are available in the implicit scope.
*
* @param preference
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,8 @@ private[compiletime] trait ChimneyTypes { this: ChimneyDefinitions =>
val OptionDefaultsToNone: Type[runtime.TransformerFlags.OptionDefaultsToNone]
val PartialUnwrapsOption: Type[runtime.TransformerFlags.PartialUnwrapsOption]
val NonAnyValWrappers: Type[runtime.TransformerFlags.NonAnyValWrappers]
val TypeConstraintEvidence: Type[runtime.TransformerFlags.TypeConstraintEvidence]
val ImplicitConversions: Type[runtime.TransformerFlags.ImplicitConversions]
val ImplicitConflictResolution: ImplicitConflictResolutionModule
trait ImplicitConflictResolutionModule
extends Type.Ctor1UpperBounded[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
optionDefaultsToNone: Boolean = false,
partialUnwrapsOption: Boolean = true,
nonAnyValWrappers: Boolean = false,
typeConstraintEvidence: Boolean = false,
implicitConversions: Boolean = false,
implicitConflictResolution: Option[dsls.ImplicitTransformerPreference] = None,
optionFallbackMerge: Option[dsls.OptionFallbackMergeStrategy] = None,
eitherFallbackMerge: Option[dsls.OptionFallbackMergeStrategy] = None,
Expand Down Expand Up @@ -58,6 +60,10 @@
copy(partialUnwrapsOption = value)
} else if (Type[Flag] =:= ChimneyType.TransformerFlags.Flags.NonAnyValWrappers) {
copy(nonAnyValWrappers = value)
} else if (Type[Flag] =:= ChimneyType.TransformerFlags.Flags.TypeConstraintEvidence) {
copy(typeConstraintEvidence = value)
} else if (Type[Flag] =:= ChimneyType.TransformerFlags.Flags.ImplicitConversions) {
copy(implicitConversions = value)
} else if (Type[Flag] =:= ChimneyType.TransformerFlags.Flags.MacrosLogging) {
copy(displayMacrosLogging = value)
} else {
Expand Down Expand Up @@ -146,6 +152,8 @@
if (beanGetters) Vector("beanGetters") else Vector.empty,
if (optionDefaultsToNone) Vector("optionDefaultsToNone") else Vector.empty,
if (nonAnyValWrappers) Vector("nonAnyValWrappers") else Vector.empty,
if (typeConstraintEvidence) Vector("typeConstraintEvidence") else Vector.empty,
if (implicitConversions) Vector("implicitConversions") else Vector.empty,
implicitConflictResolution.map(r => s"ImplicitTransformerPreference=$r").toList.toVector,
optionFallbackMerge.map(s => s"optionFallbackMerge=$s").toList.toVector,
eitherFallbackMerge.map(s => s"eitherFallbackMerge=$s").toList.toVector,
Expand All @@ -169,11 +177,13 @@
case (cfg, transformerFlag"BeanSetters=$value") => cfg.copy(beanSetters = value.toBoolean)
case (cfg, transformerFlag"BeanSettersIgnoreUnmatched=$value") =>
cfg.copy(beanSettersIgnoreUnmatched = value.toBoolean)
case (cfg, transformerFlag"NonUnitBeanSetters=$value") => cfg.copy(nonUnitBeanSetters = value.toBoolean)
case (cfg, transformerFlag"BeanGetters=$value") => cfg.copy(beanGetters = value.toBoolean)
case (cfg, transformerFlag"OptionDefaultsToNone=$value") => cfg.copy(optionDefaultsToNone = value.toBoolean)
case (cfg, transformerFlag"PartialUnwrapsOption=$value") => cfg.copy(partialUnwrapsOption = value.toBoolean)
case (cfg, transformerFlag"NonAnyValWrappers=$value") => cfg.copy(nonAnyValWrappers = value.toBoolean)
case (cfg, transformerFlag"NonUnitBeanSetters=$value") => cfg.copy(nonUnitBeanSetters = value.toBoolean)
case (cfg, transformerFlag"BeanGetters=$value") => cfg.copy(beanGetters = value.toBoolean)
case (cfg, transformerFlag"OptionDefaultsToNone=$value") => cfg.copy(optionDefaultsToNone = value.toBoolean)
case (cfg, transformerFlag"PartialUnwrapsOption=$value") => cfg.copy(partialUnwrapsOption = value.toBoolean)
case (cfg, transformerFlag"NonAnyValWrappers=$value") => cfg.copy(nonAnyValWrappers = value.toBoolean)
case (cfg, transformerFlag"TypeConstraintEvidence=$value") => cfg.copy(typeConstraintEvidence = value.toBoolean)
case (cfg, transformerFlag"ImplicitConversions=$value") => cfg.copy(implicitConversions = value.toBoolean)

Check warning on line 186 in chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala

View check run for this annotation

Codecov / codecov/patch

chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala#L180-L186

Added lines #L180 - L186 were not covered by tests
case (cfg, transformerFlag"ImplicitConflictResolution=$value") =>
cfg.copy(implicitConflictResolution = value match {
case "PreferTotalTransformer" => Some(dsls.PreferTotalTransformer)
Expand Down Expand Up @@ -622,7 +632,7 @@
val implicitScopeFlags = extractTransformerFlags[ImplicitScopeFlags](TransformerFlags.global)
val allFlags = extractTransformerFlags[InstanceFlags](implicitScopeFlags)
val cfg = extractTransformerConfig[Tail](runtimeDataIdx = 0, runtimeDataStore).copy(flags = allFlags)
if (Type[InstanceFlags] =:= ChimneyType.TransformerFlags.Default) cfg else cfg.setLocalFlagsOverriden
if (wereLocalFlagsOverriden[InstanceFlags]) cfg.setLocalFlagsOverriden else cfg
}

private def extractArgumentList[Args <: runtime.ArgumentList: Type]: List[(String, ??)] =
Expand Down Expand Up @@ -781,6 +791,19 @@
// $COVERAGE-ON$
}

@scala.annotation.tailrec
private def wereLocalFlagsOverriden[Flags <: runtime.TransformerFlags: Type]: Boolean = Type[Flags] match {
case default if default =:= ChimneyType.TransformerFlags.Default => false
case ChimneyType.TransformerFlags.Enable(flag, flags) =>
import flag.Underlying as Flag, flags.Underlying as Flags2
// ImplicitConversions and TypeConstraintEvidence are excluded from check, otherwise they would be impossible
// to use with instance flags.
if (Flag =:= ChimneyType.TransformerFlags.Flags.ImplicitConversions) wereLocalFlagsOverriden[Flags2]
else if (Flag =:= ChimneyType.TransformerFlags.Flags.TypeConstraintEvidence) wereLocalFlagsOverriden[Flags2]
else true
case _ => true
}

private def extractTransformerConfig[Tail <: runtime.TransformerOverrides: Type](
runtimeDataIdx: Int,
runtimeDataStore: Expr[PatcherDefinitionCommons.RuntimeDataStore]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.scalaland.chimney.internal.compiletime.derivation.transformer.rules

import io.scalaland.chimney.internal.compiletime.DerivationResult
import io.scalaland.chimney.internal.compiletime.derivation.transformer.Derivation

private[compiletime] trait TransformImplicitConversionRuleModule { this: Derivation =>

import Type.Implicits.*

protected object TransformImplicitConversionRule extends Rule("ImplicitConversion") {

def expand[From, To](implicit ctx: TransformationContext[From, To]): DerivationResult[Rule.ExpansionResult[To]] =
if (ctx.config.areLocalFlagsAndOverridesEmpty) {
if (ctx.config.flags.implicitConversions) {
Expr.summonImplicit[From => To] match {
case Some(ev) => transformWithConversion[From, To](ev)
case None => DerivationResult.attemptNextRule

Check warning on line 17 in chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformImplicitConversionRuleModule.scala

View check run for this annotation

Codecov / codecov/patch

chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformImplicitConversionRuleModule.scala#L17

Added line #L17 was not covered by tests
}
} else DerivationResult.attemptNextRuleBecause("Implicit conversions are disabled")
} else DerivationResult.attemptNextRuleBecause("Configuration has defined overrides")

private def transformWithConversion[From, To](ev: Expr[From => To])(implicit
ctx: TransformationContext[From, To]
): DerivationResult[Rule.ExpansionResult[To]] =
// We're constructing:
// '{ ${ ev }.apply(${ src }) }
DerivationResult.expandedTotal(ev.apply(ctx.src))
}
}
Loading
Loading