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