1 module boilerplate.conditions; 2 3 version(unittest) 4 { 5 import core.exception : AssertError; 6 import unit_threaded.should; 7 } 8 9 /++ 10 `GenerateInvariants` is a mixin string that automatically generates an `invariant{}` block 11 for each field with a condition. 12 +/ 13 public enum string GenerateInvariants = ` 14 import boilerplate.conditions : GenerateInvariantsTemplate; 15 mixin GenerateInvariantsTemplate; 16 mixin(typeof(this).generateInvariantsImpl()); 17 `; 18 19 /++ 20 When a field is marked with `@NonEmpty`, `!field.empty` is asserted. 21 +/ 22 public struct NonEmpty 23 { 24 } 25 26 /// 27 @("throws when a NonEmpty field is initialized empty") 28 unittest 29 { 30 class Class 31 { 32 @NonEmpty 33 int[] array_; 34 35 this(int[] array) 36 { 37 this.array_ = array; 38 } 39 40 mixin(GenerateInvariants); 41 } 42 43 (new Class(null)).shouldThrow!AssertError; 44 } 45 46 /// 47 @("throws when a NonEmpty field is assigned empty") 48 unittest 49 { 50 class Class 51 { 52 @NonEmpty 53 private int[] array_; 54 55 this(int[] array) 56 { 57 this.array_ = array; 58 } 59 60 public void array(int[] arrayValue) 61 { 62 this.array_ = arrayValue; 63 } 64 65 mixin(GenerateInvariants); 66 } 67 68 (new Class([2])).array(null).shouldThrow!AssertError; 69 } 70 71 /++ 72 When a field is marked with `@NonNull`, `field !is null` is asserted. 73 +/ 74 public struct NonNull 75 { 76 } 77 78 /// 79 @("throws when a NonNull field is initialized null") 80 unittest 81 { 82 class Class 83 { 84 @NonNull 85 Object obj_; 86 87 this(Object obj) 88 { 89 this.obj_ = obj; 90 } 91 92 mixin(GenerateInvariants); 93 } 94 95 (new Class(null)).shouldThrow!AssertError; 96 } 97 98 /++ 99 When a field is marked with `@AllNonNull`, `field.all!"a !is null"` is asserted. 100 +/ 101 public struct AllNonNull 102 { 103 } 104 105 /// 106 @("throws when an AllNonNull field is initialized with an array containing null") 107 unittest 108 { 109 class Class 110 { 111 @AllNonNull 112 Object[] objs; 113 114 this(Object[] objs) 115 { 116 this.objs = objs; 117 } 118 119 mixin(GenerateInvariants); 120 } 121 122 (new Class(null)).objs.shouldEqual(null); 123 (new Class([null])).shouldThrow!AssertError; 124 (new Class([new Object, null])).shouldThrow!AssertError; 125 } 126 127 /// `@AllNonNull` may be used with associative arrays. 128 @("supports AllNonNull on associative arrays") 129 unittest 130 { 131 class Class 132 { 133 @AllNonNull 134 Object[int] objs; 135 136 this(Object[int] objs) 137 { 138 this.objs = objs; 139 } 140 141 mixin(GenerateInvariants); 142 } 143 144 (new Class(null)).objs.shouldEqual(null); 145 (new Class([0: null])).shouldThrow!AssertError; 146 (new Class([0: new Object, 1: null])).shouldThrow!AssertError; 147 } 148 149 /// When used with associative arrays, `@AllNonNull` may check keys, values or both. 150 @("supports AllNonNull on associative array keys") 151 unittest 152 { 153 class Class 154 { 155 @AllNonNull 156 int[Object] objs; 157 158 this(int[Object] objs) 159 { 160 this.objs = objs; 161 } 162 163 mixin(GenerateInvariants); 164 } 165 166 (new Class(null)).objs.shouldEqual(null); 167 (new Class([null: 0])).shouldThrow!AssertError; 168 (new Class([new Object: 0, null: 1])).shouldThrow!AssertError; 169 } 170 171 /++ 172 When a field is marked with `@NonInit`, `field !is T.init` is asserted. 173 +/ 174 public struct NonInit 175 { 176 } 177 178 /// 179 @("throws when a NonInit field is initialized with T.init") 180 unittest 181 { 182 import core.time : Duration; 183 184 class Class 185 { 186 @NonInit 187 float f_; 188 189 this(float f) { this.f_ = f; } 190 191 mixin(GenerateInvariants); 192 } 193 194 (new Class(float.init)).shouldThrow!AssertError; 195 } 196 197 /++ 198 When <b>any</b> condition check is applied to a nullable field, the test applies to the value, 199 if any, contained in the field. The "null" state of the field is ignored. 200 +/ 201 @("doesn't throw when a Nullable field is null") 202 unittest 203 { 204 import std.typecons : Nullable, nullable; 205 206 class Class 207 { 208 @NonInit 209 Nullable!float f_; 210 211 this(Nullable!float f) 212 { 213 this.f_ = f; 214 } 215 216 mixin(GenerateInvariants); 217 } 218 219 (new Class(5f.nullable)).f_.isNull.shouldBeFalse; 220 (new Class(Nullable!float())).f_.isNull.shouldBeTrue; 221 (new Class(float.init.nullable)).shouldThrow!AssertError; 222 } 223 224 mixin template GenerateInvariantsTemplate() 225 { 226 private static string generateInvariantsImpl() 227 { 228 if (!__ctfe) 229 { 230 return null; 231 } 232 233 import boilerplate.conditions : IsConditionAttribute, generateChecksForAttributes; 234 import boilerplate.util : GenNormalMemberTuple; 235 import std.meta : StdMetaFilter = Filter; 236 237 string result = null; 238 239 result ~= `invariant {` ~ 240 `import std.format : format;` ~ 241 `import std.array : empty;`; 242 243 // TODO blocked by https://issues.dlang.org/show_bug.cgi?id=18504 244 // note: synchronized without lock contention is basically free 245 // IMPORTANT! Do not enable this until you have a solution for reliably detecting which attributes actually 246 // require synchronization! overzealous synchronize has the potential to lead to needless deadlocks. 247 // (consider implementing @GuardedBy) 248 enum synchronize = false; 249 250 result ~= synchronize ? `synchronized (this) {` : ``; 251 252 mixin GenNormalMemberTuple; 253 254 foreach (member; NormalMemberTuple) 255 { 256 mixin(`alias symbol = this.` ~ member ~ `;`); 257 258 static if (__traits(compiles, typeof(symbol).init)) 259 { 260 result ~= generateChecksForAttributes!(typeof(symbol), 261 StdMetaFilter!(IsConditionAttribute, __traits(getAttributes, symbol))) 262 (`this.` ~ member); 263 } 264 } 265 266 result ~= synchronize ? ` }` : ``; 267 268 result ~= ` }`; 269 270 return result; 271 } 272 } 273 274 public string generateChecksForAttributes(T, Attributes...)(string member_expression, string info = "") 275 { 276 import boilerplate.conditions : NonEmpty, NonNull; 277 import boilerplate.util : udaIndex; 278 import std.array : empty; 279 import std.string : format; 280 import std.traits : ConstOf, isAssociativeArray; 281 import std.typecons : Nullable; 282 283 enum isNullable = is(T: Template!Args, alias Template = Nullable, Args...); 284 285 static if (isNullable) 286 { 287 enum access = `%s.get`; 288 } 289 else 290 { 291 enum access = `%s`; 292 } 293 294 alias MemberType = typeof(mixin(format!access(`T.init`))); 295 296 string expression = format!access(member_expression); 297 298 enum canFormat = __traits(compiles, format(`%s`, ConstOf!MemberType.init)); 299 300 string checks; 301 302 static if (udaIndex!(NonEmpty, Attributes) != -1) 303 { 304 static if (!__traits(compiles, MemberType.init.empty())) 305 { 306 return format!`static assert(false, "Cannot call std.array.empty() on '%s'");`(expression); 307 } 308 309 static if (canFormat) 310 { 311 checks ~= format!(`assert(!%s.empty, ` 312 ~ `format("@NonEmpty: assert(!%s.empty) failed%s: %s = %%s", %s));`) 313 (expression, expression, info, expression, expression); 314 } 315 else 316 { 317 checks ~= format!`assert(!%s.empty(), "@NonEmpty: assert(!%s.empty) failed%s");` 318 (expression, expression, info); 319 } 320 } 321 322 static if (udaIndex!(NonNull, Attributes) != -1) 323 { 324 static if (__traits(compiles, MemberType.init.isNull)) 325 { 326 checks ~= format!`assert(!%s.isNull, "@NonNull: assert(!%s.isNull) failed%s");` 327 (expression, expression, info); 328 } 329 else static if (__traits(compiles, MemberType.init !is null)) 330 { 331 // Nothing good can come of printing something that is null. 332 checks ~= format!`assert(%s !is null, "@NonNull: assert(%s !is null) failed%s");` 333 (expression, expression, info); 334 } 335 else 336 { 337 return format!`static assert(false, "Cannot compare '%s' to null");`(expression); 338 } 339 } 340 341 static if (udaIndex!(NonInit, Attributes) != -1) 342 { 343 auto reference = `typeof(` ~ expression ~ `).init`; 344 345 if (!__traits(compiles, MemberType.init !is MemberType.init)) 346 { 347 return format!`static assert(false, "Cannot compare '%s' to %s.init");`(expression, MemberType.stringof); 348 } 349 350 static if (canFormat) 351 { 352 checks ~= 353 format!(`assert(%s !is %s, ` 354 ~ `format("@NonInit: assert(%s !is %s.init) failed%s: %s = %%s", %s));`) 355 (expression, reference, expression, MemberType.stringof, info, expression, expression); 356 } 357 else 358 { 359 checks ~= 360 format!`assert(%s !is %s, "@NonInit: assert(%s !is %s.init) failed%s");` 361 (expression, reference, expression, MemberType.stringof, info); 362 } 363 } 364 365 static if (udaIndex!(AllNonNull, Attributes) != -1) 366 { 367 import std.algorithm: all; 368 369 checks ~= `import std.algorithm: all;`; 370 371 static if (__traits(compiles, MemberType.init.all!"a !is null")) 372 { 373 static if (canFormat) 374 { 375 checks ~= 376 format!(`assert(%s.all!"a !is null", format(` 377 ~ `"@AllNonNull: assert(%s.all!\"a !is null\") failed%s: %s = %%s", %s));`) 378 (expression, expression, info, expression, expression); 379 } 380 else 381 { 382 checks ~= format!(`assert(%s.all!"a !is null", ` 383 ~ `"@AllNonNull: assert(%s.all!\"a !is null\") failed%s");`) 384 (expression, expression, info); 385 } 386 } 387 else static if (__traits(compiles, MemberType.init.all!"!a.isNull")) 388 { 389 static if (canFormat) 390 { 391 checks ~= 392 format!(`assert(%s.all!"!a.isNull", format(` 393 ~ `"@AllNonNull: assert(%s.all!\"!a.isNull\") failed%s: %s = %%s", %s));`) 394 (expression, expression, info, expression, expression); 395 } 396 else 397 { 398 checks ~= format!(`assert(%s.all!"!a.isNull", ` 399 ~ `"@AllNonNull: assert(%s.all!\"!a.isNull\") failed%s");`) 400 (expression, expression, info); 401 } 402 } 403 else static if (__traits(compiles, isAssociativeArray!MemberType)) 404 { 405 enum checkValues = __traits(compiles, MemberType.init.byValue.all!`a !is null`); 406 enum checkKeys = __traits(compiles, MemberType.init.byKey.all!"a !is null"); 407 408 static if (!checkKeys && !checkValues) 409 { 410 return format!(`static assert(false, "Neither key nor value of associative array ` 411 ~ `'%s' can be checked against null.");`)(expression); 412 } 413 414 static if (checkValues) 415 { 416 checks ~= 417 format!(`assert(%s.byValue.all!"a !is null", ` 418 ~ `"@AllNonNull: assert(%s.byValue.all!\"a !is null\") failed%s");`) 419 (expression, expression, info); 420 } 421 422 static if (checkKeys) 423 { 424 checks ~= 425 format!(`assert(%s.byKey.all!"a !is null", ` 426 ~ `"@AllNonNull: assert(%s.byKey.all!\"a !is null\") failed%s");`) 427 (expression, expression, info); 428 } 429 } 430 else 431 { 432 return format!`static assert(false, "Cannot compare all '%s' to null");`(expression); 433 } 434 } 435 436 if (checks.empty) 437 { 438 return null; 439 } 440 441 static if (isNullable) 442 { 443 return `if (!` ~ member_expression ~ `.isNull) {` ~ checks ~ `}`; 444 } 445 else 446 { 447 return checks; 448 } 449 } 450 451 public enum IsConditionAttribute(alias A) = __traits(isSame, A, NonEmpty) || __traits(isSame, A, NonNull) 452 || __traits(isSame, A, NonInit) || __traits(isSame, A, AllNonNull);