Skip to content

Commit 40554be

Browse files
author
cchantep
committed
Check for forward reference in generated formats
1 parent 1b04827 commit 40554be

2 files changed

Lines changed: 100 additions & 6 deletions

File tree

play-json/shared/src/main/scala/play/api/libs/json/JsMacroImpl.scala

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ import scala.reflect.macros.blackbox
112112
)
113113

114114
val optTpeCtor = typeOf[Option[_]].typeConstructor
115-
val forwardName = TermName(c.freshName("forward"))
115+
val forwardPrefix = "play_jsmacro"
116+
val forwardName = TermName(c.freshName(forwardPrefix))
116117

117118
// MacroOptions
118119
val options = config.actualType.member(TypeName("Opts")).asType.toTypeIn(config.actualType)
@@ -634,7 +635,7 @@ import scala.reflect.macros.blackbox
634635

635636
case _ => c.abort(
636637
c.enclosingPosition,
637-
s"No apply function found matching unapply parameters"
638+
"No apply function found matching unapply parameters"
638639
)
639640
}
640641

@@ -670,24 +671,36 @@ import scala.reflect.macros.blackbox
670671
val defaultValue = // not applicable for 'write' only
671672
defaultValueMap.get(name).filter(_ => methodName != "write")
672673

674+
val resolvedImpl = {
675+
val implTpeName = Option(impl.tpe).fold("null")(_.toString)
676+
677+
if (implTpeName.startsWith(forwardPrefix) ||
678+
(implTpeName.startsWith("play.api.libs.json") &&
679+
!implTpeName.contains("MacroSpec"))) {
680+
impl // Avoid extra check for builtin formats
681+
} else {
682+
q"""_root_.java.util.Objects.requireNonNull($impl, "Invalid implicit resolution (forward reference?) for '" + $cn + "': " + ${implTpeName})"""
683+
}
684+
}
685+
673686
// - If we're an default value, invoke the withDefault version
674687
// - If we're an option with default value,
675688
// invoke the nullableWithDefault version
676689
(isOption, defaultValue) match {
677690
case (true, Some(v)) =>
678691
val c = TermName(s"${methodName}NullableWithDefault")
679-
q"$jspathTree.$c($v)($impl)"
692+
q"$jspathTree.$c($v)($resolvedImpl)"
680693

681694
case (true, _) =>
682695
val c = TermName(s"${methodName}Nullable")
683-
q"$jspathTree.$c($impl)"
696+
q"$jspathTree.$c($resolvedImpl)"
684697

685698
case (false, Some(v)) =>
686699
val c = TermName(s"${methodName}WithDefault")
687-
q"$jspathTree.$c($v)($impl)"
700+
q"$jspathTree.$c($v)($resolvedImpl)"
688701

689702
case _ =>
690-
q"$jspathTree.${TermName(methodName)}($impl)"
703+
q"$jspathTree.${TermName(methodName)}($resolvedImpl)"
691704
}
692705
}.reduceLeft[Tree] { (acc, r) =>
693706
q"$acc.and($r)"
@@ -758,6 +771,7 @@ import scala.reflect.macros.blackbox
758771
case _ =>
759772
q"$json.OFormat[${atpe}](instance.reads(_), instance.writes(_))"
760773
}
774+
761775
val forwardCall =
762776
q"private val $forwardName = $forward"
763777

play-json/shared/src/test/scala/play/api/libs/json/MacroSpec.scala

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
package play.api.libs.json
66

7+
import scala.util.control.NonFatal
8+
79
object TestFormats {
810
implicit def eitherReads[A: Reads, B: Reads] = Reads[Either[A, B]] { js =>
911
implicitly[Reads[A]].reads(js) match {
@@ -142,6 +144,42 @@ class MacroSpec extends WordSpec with MustMatchers
142144
jsOptional.validate[Family].get mustEqual optional
143145
}
144146
}
147+
148+
"fails due to forward reference to Reads" in {
149+
implicit def reads: Reads[Lorem[Simple]] =
150+
InvalidForwardResolution.simpleLoremReads
151+
152+
val jsLorem = Json.obj("age" -> 11, "ipsum" -> Json.obj("bar" -> "foo"))
153+
154+
try {
155+
jsLorem.validate[Lorem[Simple]]
156+
} catch {
157+
case NonFatal(npe: NullPointerException) => {
158+
val expected = "Invalid implicit resolution"
159+
npe.getMessage.take(expected.size) mustEqual expected
160+
}
161+
162+
case NonFatal(cause) => throw cause
163+
}
164+
}
165+
166+
"fails due to forward reference to Format" in {
167+
implicit def format: Format[Lorem[Simple]] =
168+
InvalidForwardResolution.simpleLoremFormat
169+
170+
val jsLorem = Json.obj("age" -> 11, "ipsum" -> Json.obj("bar" -> "foo"))
171+
172+
try {
173+
jsLorem.validate[Lorem[Simple]]
174+
} catch {
175+
case NonFatal(npe: NullPointerException) => {
176+
val expected = "Invalid implicit resolution"
177+
npe.getMessage.take(expected.size) mustEqual expected
178+
}
179+
180+
case NonFatal(cause) => throw cause
181+
}
182+
}
145183
}
146184

147185
"Writes" should {
@@ -523,6 +561,38 @@ class MacroSpec extends WordSpec with MustMatchers
523561
jsOptional.validate(Json.reads[Optional]).
524562
get mustEqual (optional)
525563
}
564+
565+
"fails due to forward reference to Writes" in {
566+
implicit def writes: Writes[Lorem[Simple]] =
567+
InvalidForwardResolution.simpleLoremWrites
568+
569+
try {
570+
Json.toJson(Lorem(age = 11, ipsum = Simple(bar = "foo")))
571+
} catch {
572+
case NonFatal(npe: NullPointerException) => {
573+
val expected = "Invalid implicit resolution"
574+
npe.getMessage.take(expected.size) mustEqual expected
575+
}
576+
577+
case NonFatal(cause) => throw cause
578+
}
579+
}
580+
581+
"fails due to forward reference to Format" in {
582+
implicit def format: Format[Lorem[Simple]] =
583+
InvalidForwardResolution.simpleLoremFormat
584+
585+
try {
586+
Json.toJson(Lorem(age = 11, ipsum = Simple(bar = "foo")))
587+
} catch {
588+
case NonFatal(npe: NullPointerException) => {
589+
val expected = "Invalid implicit resolution"
590+
npe.getMessage.take(expected.size) mustEqual expected
591+
}
592+
593+
case NonFatal(cause) => throw cause
594+
}
595+
}
526596
}
527597

528598
// ---
@@ -544,6 +614,16 @@ class MacroSpec extends WordSpec with MustMatchers
544614
*/
545615
}
546616

617+
object InvalidForwardResolution {
618+
// Invalids as forward references to `simpleX`
619+
val simpleLoremReads = Json.reads[Lorem[Simple]]
620+
val simpleLoremWrites = Json.writes[Lorem[Simple]]
621+
val simpleLoremFormat = Json.format[Lorem[Simple]]
622+
623+
implicit val simpleReads: Reads[Simple] = Json.reads
624+
implicit val simpleWrites: OWrites[Simple] = Json.writes[Simple]
625+
}
626+
547627
object Foo {
548628
import shapeless.tag.@@
549629

0 commit comments

Comments
 (0)