From 9f8aebf2211a1696bed272744f90fc3b3584e45d Mon Sep 17 00:00:00 2001 From: ryanstull Date: Sat, 29 Jun 2024 14:13:29 -0400 Subject: [PATCH] Add isNull macro --- README.md | 21 ++++++++++++++++--- .../com/ryanstull/nullsafe/package.scala | 21 ++++++++++++++++++- .../scala/com/ryanstull/nullsafe/Tests.scala | 8 +++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f8f84d8..7bd6ddd 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Add the dependency: [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.ryanstull/scalanullsafe_2.13/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.ryanstull/scalanullsafe_2.13) ```sbt -libraryDependencies += "com.ryanstull" %% "scalanullsafe" % "1.2.6" % "provided" +libraryDependencies += "com.ryanstull" %% "scalanullsafe" % "1.3.0" % "provided" ``` * Since macros are only used at compile time, if your build tool has a way to specify compile-time-only dependencies, you can use that for this library @@ -49,15 +49,17 @@ val a2 = A(B(C(D(E("Hello"))))) ?(a2.b.c.d.e.s) //Returns "Hello" ``` -There's also a variant that returns an `Option[A]` when provided an expression of type `A`, -and another that just checks if a property is defined. +There's also a variant that returns an `Option[A]` when provided an expression of type `A`, +another that just checks if a property is defined, and it's inverse. ```scala opt(a.b.c.d.e.s) //Returns None notNull(a.b.c.d.e.s) //Returns false +isNull(a.b.c.d.e.s) //Returns true opt(a2.b.c.d.e.s) //Returns Some("Hello") notNull(a2.b.c.d.e.s) //Returns true +isNull(a2.b.c.d.e.s) //Returns false ``` ## How it works @@ -122,6 +124,19 @@ if(a != null){ } else false ``` +### `isNull` macro + +And the `isNull` macro, translating `isNull(a.b.c)` into: + +```scala +if(a != null){ + val b = a.b + if(b != null){ + b.c == null + } else true +} else true +``` + ### Safe translation All of the above work for method invocation as well as property access, and the two can be intermixed. For example: diff --git a/src/main/scala/com/ryanstull/nullsafe/package.scala b/src/main/scala/com/ryanstull/nullsafe/package.scala index 7853f4c..7904ed0 100644 --- a/src/main/scala/com/ryanstull/nullsafe/package.scala +++ b/src/main/scala/com/ryanstull/nullsafe/package.scala @@ -255,11 +255,22 @@ package object nullsafe { * * @param expr Some expression that might cause a NullPointerExpression due to method/field access on `null` * @tparam A Type of the expression - * @return `true` if the value of the expression is not null and and there wouldn't have been any NullPointerExceptions + * @return `true` if the value of the expression is not null and there wouldn't have been any NullPointerExceptions * due to method/field access on `null`, false otherwise. */ def notNull[A](expr: A): Boolean = macro notNullImpl[A] + /** + * Translates an expression that could cause a NullPointerException due to method/field access on `null` + * and adds explicit null-checks to avoid that. + * + * @param expr Some expression that might cause a NullPointerExpression due to method/field access on `null` + * @tparam A Type of the expression + * @return `true` if the value of the expression is null or there would have been any NullPointerExceptions + * due to method/field access on `null`, false otherwise. + */ + def isNull[A](expr: A): Boolean = macro isNullImpl[A] + def debugMaco[A](expr: A): A = macro debugMacoImpl[A] //Putting the implementations in an object to avoid namespace pollution. @@ -335,6 +346,14 @@ package object nullsafe { c.Expr[Boolean](result) } + def isNullImpl[A: c.WeakTypeTag](c: blackbox.Context)(expr: c.Expr[A]): c.Expr[Boolean] = { + import c.universe._ + + val tree = expr.tree + val result = rewriteToNullSafe(c)(tree)(q"true", checkLast = false, a => q"$a == null") + c.Expr[Boolean](result) + } + private def rewriteToNullSafe[A: c.WeakTypeTag](c: blackbox.Context)(tree: c.universe.Tree) (default: c.universe.Tree, checkLast: Boolean = false, finalTransform: c.universe.Tree => c.universe.Tree): c.universe.Tree = { import c.universe._ diff --git a/src/test/scala/com/ryanstull/nullsafe/Tests.scala b/src/test/scala/com/ryanstull/nullsafe/Tests.scala index 564e0d2..2646269 100644 --- a/src/test/scala/com/ryanstull/nullsafe/Tests.scala +++ b/src/test/scala/com/ryanstull/nullsafe/Tests.scala @@ -401,6 +401,14 @@ class Tests extends FlatSpec { assert(o2.doubleOpt.isEmpty) assert(o3.doubleOpt.isEmpty) } + + "isNull" should "work" in { + val a = A(B(C(null))) + val a2 = A(B(C(D(E("Test"))))) + + assert(isNull(a.b.c.d.e.s)) + assert(!isNull(a2.b.c.d.e.s)) + } } //Example of deeply nested domain object