Skip to content

Commit 1050dd5

Browse files
author
cchantep
committed
Check for forward reference in generated formats
1 parent 142ab39 commit 1050dd5

2 files changed

Lines changed: 101 additions & 6 deletions

File tree

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

Lines changed: 21 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)
@@ -626,7 +627,7 @@ import scala.reflect.macros.blackbox
626627

627628
case _ => c.abort(
628629
c.enclosingPosition,
629-
s"No apply function found matching unapply parameters"
630+
"No apply function found matching unapply parameters"
630631
)
631632
}
632633

@@ -662,24 +663,37 @@ import scala.reflect.macros.blackbox
662663
val defaultValue = // not applicable for 'write' only
663664
defaultValueMap.get(name).filter(_ => methodName != "write")
664665

666+
val resolvedImpl = {
667+
val implTpeName = Option(impl.tpe).fold("null")(_.toString)
668+
669+
if (implTpeName.startsWith(forwardPrefix) ||
670+
(implTpeName.startsWith("play.api.libs.json") &&
671+
!implTpeName.contains("MacroSpec"))) {
672+
impl // Avoid extra check for builtin formats
673+
} else {
674+
q"""_root_.java.util.Objects.requireNonNull($impl, "Invalid implicit resolution (forward reference?) for '" + $cn + "': " + ${implTpeName})"""
675+
}
676+
}
677+
665678
// - If we're an default value, invoke the withDefault version
666679
// - If we're an option with default value,
667680
// invoke the nullableWithDefault version
668681
(isOption, defaultValue) match {
669682
case (true, Some(v)) =>
670683
val c = TermName(s"${methodName}NullableWithDefault")
671-
q"$jspathTree.$c($v)($impl)"
684+
q"$jspathTree.$c($v)($resolvedImpl)"
672685

673-
case (true, _) =>
686+
case (true, _) => {
674687
val c = TermName(s"${methodName}Handler")
675688
q"$config.optionHandlers.$c($jspathTree)($impl)"
689+
}
676690

677691
case (false, Some(v)) =>
678692
val c = TermName(s"${methodName}WithDefault")
679-
q"$jspathTree.$c($v)($impl)"
693+
q"$jspathTree.$c($v)($resolvedImpl)"
680694

681695
case _ =>
682-
q"$jspathTree.${TermName(methodName)}($impl)"
696+
q"$jspathTree.${TermName(methodName)}($resolvedImpl)"
683697
}
684698
}.reduceLeft[Tree] { (acc, r) =>
685699
q"$acc.and($r)"
@@ -750,6 +764,7 @@ import scala.reflect.macros.blackbox
750764
case _ =>
751765
q"$json.OFormat[${atpe}](instance.reads(_), instance.writes(_))"
752766
}
767+
753768
val forwardCall =
754769
q"private val $forwardName = $forward"
755770

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 {
@@ -536,6 +574,38 @@ class MacroSpec extends WordSpec with MustMatchers
536574
formatter.writes(Obj) mustEqual jsObj
537575
formatter.reads(jsObj) mustEqual JsSuccess(Obj)
538576
}
577+
578+
"fails due to forward reference to Writes" in {
579+
implicit def writes: Writes[Lorem[Simple]] =
580+
InvalidForwardResolution.simpleLoremWrites
581+
582+
try {
583+
Json.toJson(Lorem(age = 11, ipsum = Simple(bar = "foo")))
584+
} catch {
585+
case NonFatal(npe: NullPointerException) => {
586+
val expected = "Invalid implicit resolution"
587+
npe.getMessage.take(expected.size) mustEqual expected
588+
}
589+
590+
case NonFatal(cause) => throw cause
591+
}
592+
}
593+
594+
"fails due to forward reference to Format" in {
595+
implicit def format: Format[Lorem[Simple]] =
596+
InvalidForwardResolution.simpleLoremFormat
597+
598+
try {
599+
Json.toJson(Lorem(age = 11, ipsum = Simple(bar = "foo")))
600+
} catch {
601+
case NonFatal(npe: NullPointerException) => {
602+
val expected = "Invalid implicit resolution"
603+
npe.getMessage.take(expected.size) mustEqual expected
604+
}
605+
606+
case NonFatal(cause) => throw cause
607+
}
608+
}
539609
}
540610

541611
// ---
@@ -557,6 +627,16 @@ class MacroSpec extends WordSpec with MustMatchers
557627
*/
558628
}
559629

630+
object InvalidForwardResolution {
631+
// Invalids as forward references to `simpleX`
632+
val simpleLoremReads = Json.reads[Lorem[Simple]]
633+
val simpleLoremWrites = Json.writes[Lorem[Simple]]
634+
val simpleLoremFormat = Json.format[Lorem[Simple]]
635+
636+
implicit val simpleReads: Reads[Simple] = Json.reads
637+
implicit val simpleWrites: OWrites[Simple] = Json.writes[Simple]
638+
}
639+
560640
object Foo {
561641
import shapeless.tag.@@
562642

0 commit comments

Comments
 (0)