Skip to content

Commit

Permalink
Add isNull macro
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanstull committed Jun 29, 2024
1 parent 2beb673 commit 9f8aebf
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 4 deletions.
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
```
<sub>* 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</sub>

Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
21 changes: 20 additions & 1 deletion src/main/scala/com/ryanstull/nullsafe/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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._
Expand Down
8 changes: 8 additions & 0 deletions src/test/scala/com/ryanstull/nullsafe/Tests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 9f8aebf

Please sign in to comment.