-
Notifications
You must be signed in to change notification settings - Fork 29
/
NullSafe.scala
177 lines (163 loc) · 5.61 KB
/
NullSafe.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
package com.thoughtworks.dsl.keywords
import com.thoughtworks.dsl.Dsl.{reset, shift}
import com.thoughtworks.dsl.keywords.NullSafe.?
import com.thoughtworks.dsl.keywords.NullSafe.NotNull
import scala.language.implicitConversions
import scala.language.higherKinds
import scala.annotation.compileTimeOnly
/** [[NullSafe]] is a keyword to perform `null` check.
*
* @example You can use [[NullSafe$.? ?]] annotation to represent a nullable value.
*
* {{{
* import com.thoughtworks.dsl.keywords.NullSafe._
*
* case class Tree(left: Tree @ $qmark = null, right: Tree @ $qmark = null, value: String @ $qmark = null)
*
* val root: Tree @ $qmark = Tree(
* left = Tree(
* left = Tree(value = "left-left"),
* right = Tree(value = "left-right")
* ),
* right = Tree(value = "right")
* )
* }}}
*
* A normal `.` is not null safe, when selecting `left`, `right` or `value` on a `null` value.
*
* {{{
* a[NullPointerException] should be thrownBy {
* root.right.left.right.value
* }
* }}}
*
* The above code throws an exception because `root.right.left` is `null`.
*
* The exception can be avoided by using [[?]] on a nullable value:
*
* {{{
* root.?.right.?.left.?.right.?.value should be(null)
* }}}
*
* The entire expression will be `null` if one of [[?]] is performed on a `null` value.
*
* <hr/>
*
* The boundary of a null safe operator [[?]] is the nearest enclosing expression
* whose type is annotated as `@ ?`.
*
* {{{
* ("Hello " + ("world " + root.?.right.?.left.?.value)) should be("Hello world null")
* ("Hello " + (("world " + root.?.right.?.left.?.value.?): @ $qmark)) should be("Hello null")
* (("Hello " + ("world " + root.?.right.?.left.?.value.?)): @ $qmark) should be(null)
* }}}
*
* @example The [[?]] operator usually works with Java libraries that may produce `null`.
*
* {{{
* import com.thoughtworks.dsl.keywords.NullSafe._
*
* val myMap = new java.util.HashMap[String, String]();
* ((myMap.get("key1").? + myMap.get("key2").?): @ $qmark) should be(null)
* }}}
*
* @note The [[?]] operator is only available on nullable values.
*
* A type is considered as nullable if it is a reference type,
* no matter it is annotated as `@ ?` or not.
*
* {{{
* import com.thoughtworks.dsl.keywords.NullSafe._
*
* val explicitNullable: String @ $qmark = null
* ((explicitNullable.? + " Doe") : @ $qmark) should be(null)
* }}}
*
* {{{
* val implicitNullable: String = null
* ((implicitNullable.? + " Doe") : @ $qmark) should be(null)
* }}}
*
* A type is considered as not nullable if it is a value type.
*
* {{{
* val implicitNotNullable: Int = 0
* "(implicitNotNullable.? + 42) : @ $qmark" shouldNot compile
* }}}
*
* Alternatively, a type can be considered as not nullable
* by explicitly converting it to [[com.thoughtworks.dsl.keywords.NullSafe.NotNull[A]* NotNull]].
*
* {{{
* val explicitNotNullable: NotNull[String] = NotNull("John")
* """(explicitNotNullable.? + " Doe") : @ $qmark""" shouldNot compile
* }}}
*
* @see [[NoneSafe]] for similar checks on [[scala.Option]]s.
* @author 杨博 (Yang Bo)
*
* @define qmark ?
*
*/
final case class NullSafe[A <: AnyRef](nullable: A @ ?) extends AnyVal {
@inline
final def cpsApply[Domain >: Null](handler: NotNull[A] => Domain @ ?): Domain @ ? = {
if (nullable == null) {
null
} else {
handler(NullSafe.OpaqueTypes.toNotNull(nullable))
}
}
@shift
@compileTimeOnly(
"""This method requires the compiler plugin: `addCompilerPlugin("com.thoughtworks.dsl" %% "compilerplugins-bangnotation" % "latest.release")` and must only be called inside a code block annotated as `@reset`.""")
final def ? : NotNull[A] = {
throw new IllegalAccessException(
"""This method requires the compiler plugin: `addCompilerPlugin("com.thoughtworks.dsl" %% "compilerplugins-bangnotation" % "latest.release")` and must only be called inside a code block annotated as `@reset`."""
)
}
}
object NullSafe {
private[NullSafe] trait OpaqueTypes {
type NotNull[+A] <: A
private[NullSafe] def toNotNull[A](a: A): NotNull[A]
}
private[NullSafe] val OpaqueTypes: OpaqueTypes = new OpaqueTypes {
type NotNull[+A] = A
private[NullSafe] def toNotNull[A](a: A) = a
}
/**
* @usecase type NotNull[+A] <: A
*/
type NotNull[+A] = OpaqueTypes.NotNull[A]
/** Returns `a` if `a` is not `null`.
*
* @return `a` if `a` is not `null`.
*
* {{{
* import com.thoughtworks.dsl.keywords.NullSafe._
*
* val o = new AnyRef
* NotNull(o) should be(o)
* }}}
*
* @throws java.lang.NullPointerException if `a` is `null`.
*
* {{{
* import com.thoughtworks.dsl.keywords.NullSafe._
* a[NullPointerException] should be thrownBy {
* NotNull(null)
* }
* }}}
*/
def NotNull[A](a: A): NotNull[A] = {
if (a == null) {
throw new NullPointerException
} else {
OpaqueTypes.toNotNull(a)
}
}
/** @template */
type ? = reset
implicit def implicitNullSafe[A <: AnyRef](nullable: A @ ?) = new NullSafe[A](nullable)
}