Skip to content

Commit

Permalink
Fix #1140 by throwing a compilation error in case of 'AnyVal' and one…
Browse files Browse the repository at this point in the history
… value classes with 'CodecMakerConfig.withInlineOneValueClasses(true)' are used as leaf classes of sum types
  • Loading branch information
plokhotnyuk committed May 2, 2024
1 parent dda9ff1 commit ced2478
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 116 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -618,41 +618,6 @@ object JsonCodecMaker {
case _ => false
}

def adtLeafClasses(adtBaseTpe: Type): List[Type] = {
def collectRecursively(tpe: Type): List[Type] = {
val tpeClass = tpe.typeSymbol.asClass
val leafTpes = tpeClass.knownDirectSubclasses.toList.flatMap { s =>
val classSymbol = s.asClass
val typeParams = classSymbol.typeParams
val subTpe =
if (typeParams.isEmpty) classSymbol.toType
else {
val typeParamsAndArgs = tpeClass.typeParams.map(_.toString).zip(tpe.typeArgs).toMap
val typeArgs = typeParams.map(s => typeParamsAndArgs.getOrElse(s.toString, fail {
s"Cannot resolve generic type(s) for `${classSymbol.toType}`. Please provide a custom implicitly accessible codec for it."
}))
classSymbol.toType.substituteTypes(typeParams, typeArgs)
}
if (isSealedClass(subTpe)) collectRecursively(subTpe)
else if (isNonAbstractScalaClass(subTpe)) subTpe :: Nil
else fail(if (s.isAbstract) {
"Only sealed intermediate traits or abstract classes are supported. Please consider using of them " +
s"for ADT with base '$adtBaseTpe' or provide a custom implicitly accessible codec for the ADT base."
} else {
"Only Scala classes & objects are supported for ADT leaf classes. Please consider using of them " +
s"for ADT with base '$adtBaseTpe' or provide a custom implicitly accessible codec for the ADT base."
})
}
if (isNonAbstractScalaClass(tpe)) leafTpes :+ tpe
else leafTpes
}

val classes = collectRecursively(adtBaseTpe).distinct
if (classes.isEmpty) fail(s"Cannot find leaf classes for ADT base '$adtBaseTpe'. " +
"Please add them or provide a custom implicitly accessible codec for the ADT base.")
classes
}

def companion(tpe: Type): Symbol = {
val comp = tpe.typeSymbol.companion
if (comp.isModule) comp
Expand Down Expand Up @@ -881,6 +846,44 @@ object JsonCodecMaker {
(cfg.inlineOneValueClasses && isNonAbstractScalaClass(tpe) && !isCollection(tpe) && getClassInfo(tpe).fields.size == 1 ||
tpe.typeSymbol.isClass && tpe.typeSymbol.asClass.isDerivedValueClass)

def adtLeafClasses(adtBaseTpe: Type): List[Type] = {
def collectRecursively(tpe: Type): List[Type] = {
val tpeClass = tpe.typeSymbol.asClass
val leafTpes = tpeClass.knownDirectSubclasses.toList.flatMap { s =>
val classSymbol = s.asClass
val typeParams = classSymbol.typeParams
val subTpe =
if (typeParams.isEmpty) classSymbol.toType
else {
val typeParamsAndArgs = tpeClass.typeParams.map(_.toString).zip(tpe.typeArgs).toMap
val typeArgs = typeParams.map(s => typeParamsAndArgs.getOrElse(s.toString, fail {
s"Cannot resolve generic type(s) for `${classSymbol.toType}`. Please provide a custom implicitly accessible codec for it."
}))
classSymbol.toType.substituteTypes(typeParams, typeArgs)
}
if (isSealedClass(subTpe)) collectRecursively(subTpe)
else if (isValueClass(subTpe)) {
fail("'AnyVal' and one value classes with 'CodecMakerConfig.withInlineOneValueClasses(true)' are not " +
s"supported as leaf classes for ADT with base '$adtBaseTpe'.")
} else if (isNonAbstractScalaClass(subTpe)) subTpe :: Nil
else fail(if (s.isAbstract) {
"Only sealed intermediate traits or abstract classes are supported. Please consider using of them " +
s"for ADT with base '$adtBaseTpe' or provide a custom implicitly accessible codec for the ADT base."
} else {
"Only Scala classes & objects are supported for ADT leaf classes. Please consider using of them " +
s"for ADT with base '$adtBaseTpe' or provide a custom implicitly accessible codec for the ADT base."
})
}
if (isNonAbstractScalaClass(tpe)) leafTpes :+ tpe
else leafTpes
}

val classes = collectRecursively(adtBaseTpe).distinct
if (classes.isEmpty) fail(s"Cannot find leaf classes for ADT base '$adtBaseTpe'. " +
"Please add them or provide a custom implicitly accessible codec for the ADT base.")
classes
}

def genReadKey(types: List[Type]): Tree = {
val tpe = types.head
val implKeyCodec = findImplicitKeyCodec(types)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -755,87 +755,6 @@ object JsonCodecMaker {
def isEnumOrModuleValue(tpe: TypeRepr): Boolean = tpe.isSingleton &&
(tpe.typeSymbol.flags.is(Flags.Module) || tpe.termSymbol.flags.is(Flags.Enum))

def adtChildren(tpe: TypeRepr): Seq[TypeRepr] = { // TODO: explore yet one variant with mirrors
def resolveParentTypeArg(child: Symbol, fromNudeChildTarg: TypeRepr, parentTarg: TypeRepr,
binding: Map[String, TypeRepr]): Map[String, TypeRepr] =
if (fromNudeChildTarg.typeSymbol.isTypeParam) { // TODO: check for paramRef instead ?
val paramName = fromNudeChildTarg.typeSymbol.name
binding.get(paramName) match
case None => binding.updated(paramName, parentTarg)
case Some(oldBinding) =>
if (oldBinding =:= parentTarg) binding
else fail(s"Type parameter $paramName in class ${child.name} appeared in the constructor of " +
s"${tpe.show} two times differently, with ${oldBinding.show} and ${parentTarg.show}")
} else if (fromNudeChildTarg <:< parentTarg) binding // TODO: assure parentTag is covariant, get covariance from type parameters
else {
(fromNudeChildTarg, parentTarg) match
case (AppliedType(ctycon, ctargs), AppliedType(ptycon, ptargs)) =>
ctargs.zip(ptargs).foldLeft(resolveParentTypeArg(child, ctycon, ptycon, binding)) { (b, e) =>
resolveParentTypeArg(child, e._1, e._2, b)
}
case _ => fail(s"Failed unification of type parameters of ${tpe.show} from child $child - " +
s"${fromNudeChildTarg.show} and ${parentTarg.show}")
}

def resolveParentTypeArgs(child: Symbol, nudeChildParentTags: List[TypeRepr], parentTags: List[TypeRepr],
binding: Map[String, TypeRepr]): Map[String, TypeRepr] =
nudeChildParentTags.zip(parentTags).foldLeft(binding)((s, e) => resolveParentTypeArg(child, e._1, e._2, s))

tpe.typeSymbol.children.map { sym =>
if (sym.isType) {
if (sym.name == "<local child>") // problem - we have no other way to find this other return the name
fail(s"Local child symbols are not supported, please consider change '${tpe.show}' or implement a " +
"custom implicitly accessible codec")
val nudeSubtype = TypeIdent(sym).tpe
val tpeArgsFromChild = typeArgs(nudeSubtype.baseType(tpe.typeSymbol))
nudeSubtype.memberType(sym.primaryConstructor) match
case MethodType(_, _, resTp) => resTp
case PolyType(names, bounds, resPolyTp) =>
val targs = typeArgs(tpe)
val tpBinding = resolveParentTypeArgs(sym, tpeArgsFromChild, targs, Map.empty)
val ctArgs = names.map { name =>
tpBinding.getOrElse(name, fail(s"Type parameter $name of $sym can't be deduced from " +
s"type arguments of ${tpe.show}. Please provide a custom implicitly accessible codec for it."))
}
val polyRes = resPolyTp match
case MethodType(_, _, resTp) => resTp
case other => other // hope we have no multiple typed param lists yet.
if (ctArgs.isEmpty) polyRes
else polyRes match
case AppliedType(base, _) => base.appliedTo(ctArgs)
case AnnotatedType(AppliedType(base, _), annot) => AnnotatedType(base.appliedTo(ctArgs), annot)
case _ => polyRes.appliedTo(ctArgs)
case other => fail(s"Primary constructior for ${tpe.show} is not MethodType or PolyType but $other")
} else if (sym.isTerm) Ref(sym).tpe
else fail("Only Scala classes & objects are supported for ADT leaf classes. Please consider using of " +
s"them for ADT with base '${tpe.show}' or provide a custom implicitly accessible codec for the ADT base. " +
s"Failed symbol: $sym (fullName=${sym.fullName})\n")
}
}

def adtLeafClasses(adtBaseTpe: TypeRepr): Seq[TypeRepr] = {
def collectRecursively(tpe: TypeRepr): Seq[TypeRepr] =
val leafTpes = adtChildren(tpe).flatMap { subTpe =>
if (isEnumOrModuleValue(subTpe) || subTpe =:= TypeRepr.of[None.type]) subTpe :: Nil
else if (isSealedClass(subTpe)) collectRecursively(subTpe)
else if (isNonAbstractScalaClass(subTpe)) subTpe :: Nil
else fail(if (subTpe.typeSymbol.flags.is(Flags.Abstract) || subTpe.typeSymbol.flags.is(Flags.Trait) ) {
"Only sealed intermediate traits or abstract classes are supported. Please consider using of them " +
s"for ADT with base '${adtBaseTpe.show}' or provide a custom implicitly accessible codec for the ADT base."
} else {
"Only Scala classes & objects are supported for ADT leaf classes. Please consider using of them " +
s"for ADT with base '${adtBaseTpe.show}' or provide a custom implicitly accessible codec for the ADT base."
})
}
if (isNonAbstractScalaClass(tpe)) leafTpes :+ tpe
else leafTpes

val classes = collectRecursively(adtBaseTpe).distinct
if (classes.isEmpty) fail(s"Cannot find leaf classes for ADT base '${adtBaseTpe.show}'. " +
"Please add them or provide a custom implicitly accessible codec for the ADT base.")
classes
}

def isOption(tpe: TypeRepr, types: List[TypeRepr]): Boolean = tpe <:< TypeRepr.of[Option[_]] &&
(cfg.skipNestedOptionValues || !types.headOption.exists(_ <:< TypeRepr.of[Option[_]]))

Expand Down Expand Up @@ -1122,6 +1041,90 @@ object JsonCodecMaker {
(cfg.inlineOneValueClasses && isNonAbstractScalaClass(tpe) && !isCollection(tpe) && getClassInfo(tpe).fields.size == 1 ||
tpe <:< TypeRepr.of[AnyVal])

def adtChildren(tpe: TypeRepr): Seq[TypeRepr] = { // TODO: explore yet one variant with mirrors
def resolveParentTypeArg(child: Symbol, fromNudeChildTarg: TypeRepr, parentTarg: TypeRepr,
binding: Map[String, TypeRepr]): Map[String, TypeRepr] =
if (fromNudeChildTarg.typeSymbol.isTypeParam) { // TODO: check for paramRef instead ?
val paramName = fromNudeChildTarg.typeSymbol.name
binding.get(paramName) match
case None => binding.updated(paramName, parentTarg)
case Some(oldBinding) =>
if (oldBinding =:= parentTarg) binding
else fail(s"Type parameter $paramName in class ${child.name} appeared in the constructor of " +
s"${tpe.show} two times differently, with ${oldBinding.show} and ${parentTarg.show}")
} else if (fromNudeChildTarg <:< parentTarg) binding // TODO: assure parentTag is covariant, get covariance from type parameters
else {
(fromNudeChildTarg, parentTarg) match
case (AppliedType(ctycon, ctargs), AppliedType(ptycon, ptargs)) =>
ctargs.zip(ptargs).foldLeft(resolveParentTypeArg(child, ctycon, ptycon, binding)) { (b, e) =>
resolveParentTypeArg(child, e._1, e._2, b)
}
case _ => fail(s"Failed unification of type parameters of ${tpe.show} from child $child - " +
s"${fromNudeChildTarg.show} and ${parentTarg.show}")
}

def resolveParentTypeArgs(child: Symbol, nudeChildParentTags: List[TypeRepr], parentTags: List[TypeRepr],
binding: Map[String, TypeRepr]): Map[String, TypeRepr] =
nudeChildParentTags.zip(parentTags).foldLeft(binding)((s, e) => resolveParentTypeArg(child, e._1, e._2, s))

tpe.typeSymbol.children.map { sym =>
if (sym.isType) {
if (sym.name == "<local child>") // problem - we have no other way to find this other return the name
fail(s"Local child symbols are not supported, please consider change '${tpe.show}' or implement a " +
"custom implicitly accessible codec")
val nudeSubtype = TypeIdent(sym).tpe
val tpeArgsFromChild = typeArgs(nudeSubtype.baseType(tpe.typeSymbol))
nudeSubtype.memberType(sym.primaryConstructor) match
case MethodType(_, _, resTp) => resTp
case PolyType(names, bounds, resPolyTp) =>
val targs = typeArgs(tpe)
val tpBinding = resolveParentTypeArgs(sym, tpeArgsFromChild, targs, Map.empty)
val ctArgs = names.map { name =>
tpBinding.getOrElse(name, fail(s"Type parameter $name of $sym can't be deduced from " +
s"type arguments of ${tpe.show}. Please provide a custom implicitly accessible codec for it."))
}
val polyRes = resPolyTp match
case MethodType(_, _, resTp) => resTp
case other => other // hope we have no multiple typed param lists yet.
if (ctArgs.isEmpty) polyRes
else polyRes match
case AppliedType(base, _) => base.appliedTo(ctArgs)
case AnnotatedType(AppliedType(base, _), annot) => AnnotatedType(base.appliedTo(ctArgs), annot)
case _ => polyRes.appliedTo(ctArgs)
case other => fail(s"Primary constructior for ${tpe.show} is not MethodType or PolyType but $other")
} else if (sym.isTerm) Ref(sym).tpe
else fail("Only Scala classes & objects are supported for ADT leaf classes. Please consider using of " +
s"them for ADT with base '${tpe.show}' or provide a custom implicitly accessible codec for the ADT base. " +
s"Failed symbol: $sym (fullName=${sym.fullName})\n")
}
}

def adtLeafClasses(adtBaseTpe: TypeRepr): Seq[TypeRepr] = {
def collectRecursively(tpe: TypeRepr): Seq[TypeRepr] =
val leafTpes = adtChildren(tpe).flatMap { subTpe =>
if (isEnumOrModuleValue(subTpe) || subTpe =:= TypeRepr.of[None.type]) subTpe :: Nil
else if (isSealedClass(subTpe)) collectRecursively(subTpe)
else if (isValueClass(subTpe)) {
fail("'AnyVal' and one value classes with 'CodecMakerConfig.withInlineOneValueClasses(true)' are not " +
s"supported as leaf classes for ADT with base '${adtBaseTpe.show}'.")
} else if (isNonAbstractScalaClass(subTpe)) subTpe :: Nil
else fail(if (subTpe.typeSymbol.flags.is(Flags.Abstract) || subTpe.typeSymbol.flags.is(Flags.Trait) ) {
"Only sealed intermediate traits or abstract classes are supported. Please consider using of them " +
s"for ADT with base '${adtBaseTpe.show}' or provide a custom implicitly accessible codec for the ADT base."
} else {
"Only Scala classes & objects are supported for ADT leaf classes. Please consider using of them " +
s"for ADT with base '${adtBaseTpe.show}' or provide a custom implicitly accessible codec for the ADT base."
})
}
if (isNonAbstractScalaClass(tpe)) leafTpes :+ tpe
else leafTpes

val classes = collectRecursively(adtBaseTpe).distinct
if (classes.isEmpty) fail(s"Cannot find leaf classes for ADT base '${adtBaseTpe.show}'. " +
"Please add them or provide a custom implicitly accessible codec for the ADT base.")
classes
}

def genReadKey[T: Type](types: List[TypeRepr], in: Expr[JsonReader])(using Quotes): Expr[T] =
val tpe = types.head
val implKeyCodec = findImplicitKeyCodec(types)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2922,6 +2922,15 @@ class JsonCodecMakerSpec extends VerifyingSpec {
else "Type parameter A of class FooImpl can't be deduced from type arguments of Foo[[A >: scala.Nothing <: scala.Any] => Bar[A]]. Please provide a custom implicitly accessible codec for it."
})
}
"don't generate codecs when 'AnyVal' or one value classes with 'CodecMakerConfig.withInlineOneValueClasses(true)' are leaf types of the ADT base" in {
assert(intercept[TestFailedException](assertCompiles {
"""sealed trait X extends Any
|case class D(value: Double) extends X
|make[X](CodecMakerConfig.withInlineOneValueClasses(true))""".stripMargin
}).getMessage.contains {
"'AnyVal' and one value classes with 'CodecMakerConfig.withInlineOneValueClasses(true)' are not supported as leaf classes for ADT with base 'X'."
})
}
"don't generate codecs that cannot parse own output" in {
assert(intercept[TestFailedException](assertCompiles {
"JsonCodecMaker.make[Arrays](CodecMakerConfig.withRequireCollectionFields(true))"
Expand Down

0 comments on commit ced2478

Please sign in to comment.