1 module boilerplate.accessors; 2 3 import std.traits; 4 import std.typecons: Nullable; 5 6 import boilerplate.util: DeepConst, isStatic; 7 8 struct Read 9 { 10 string visibility = "public"; 11 } 12 13 // Deprecated! See below. 14 // RefRead can not check invariants on change, so there's no point. 15 // ref property functions where the value being returned is a field of the class 16 // are entirely equivalent to public fields. 17 struct RefRead 18 { 19 string visibility = "public"; 20 } 21 22 struct ConstRead 23 { 24 string visibility = "public"; 25 } 26 27 struct Write 28 { 29 string visibility = "public"; 30 } 31 32 immutable string GenerateFieldAccessors = ` 33 import boilerplate.accessors : GenerateFieldAccessorMethods; 34 mixin GenerateFieldAccessorMethods; 35 mixin(GenerateFieldAccessorMethodsImpl); 36 `; 37 38 mixin template GenerateFieldAccessorMethods() 39 { 40 private static GenerateFieldAccessorMethodsImpl() 41 { 42 if (!__ctfe) 43 { 44 return null; 45 } 46 47 import boilerplate.accessors : Read, ConstRead, RefRead, Write, 48 GenerateReader, GenerateConstReader, GenerateRefReader, GenerateWriter; 49 50 import boilerplate.util : GenNormalMemberTuple, isStatic, udaIndex; 51 52 string result = ""; 53 54 mixin GenNormalMemberTuple; 55 56 foreach (name; NormalMemberTuple) 57 { 58 enum string fieldCode = `this.` ~ name; 59 60 mixin("alias field = " ~ fieldCode ~ ";"); 61 62 // synchronized without lock contention is basically free, so always do it 63 // TODO enable when https://issues.dlang.org/show_bug.cgi?id=18504 is fixed 64 enum synchronize = false && is(typeof(field) == class); 65 66 static if (udaIndex!(Read, __traits(getAttributes, field)) != -1) 67 { 68 enum string readerDecl = GenerateReader!(typeof(field))(name, mixin(name.isStatic), synchronize); 69 70 debug (accessors) pragma(msg, readerDecl); 71 result ~= readerDecl; 72 } 73 74 static if (udaIndex!(RefRead, __traits(getAttributes, field)) != -1) 75 { 76 result ~= `pragma(msg, "Deprecation! RefRead on ` ~ typeof(this).stringof ~ `.` ~ name 77 ~ ` makes a private field effectively public, defeating the point.");`; 78 79 enum string refReaderDecl = GenerateRefReader!(typeof(field))(name, mixin(name.isStatic)); 80 81 debug (accessors) pragma(msg, refReaderDecl); 82 result ~= refReaderDecl; 83 } 84 85 static if (udaIndex!(ConstRead, __traits(getAttributes, field)) != -1) 86 { 87 enum string constReaderDecl = GenerateConstReader!(typeof(field)) 88 (name, mixin(name.isStatic), synchronize); 89 90 debug (accessors) pragma(msg, constReaderDecl); 91 result ~= constReaderDecl; 92 } 93 94 static if (udaIndex!(Write, __traits(getAttributes, field)) != -1) 95 { 96 enum string writerDecl = GenerateWriter!(typeof(field), __traits(getAttributes, field)) 97 (name, fieldCode, mixin(name.isStatic), synchronize); 98 99 debug (accessors) pragma(msg, writerDecl); 100 result ~= writerDecl; 101 } 102 } 103 104 return result; 105 } 106 } 107 108 string getModifiers(bool isStatic) 109 { 110 return isStatic ? " static" : ""; 111 } 112 113 uint filterAttributes(T)(bool isStatic, FilterMode mode) 114 { 115 import boilerplate.util : needToDup; 116 117 uint attributes = uint.max; 118 119 if (needToDup!T) 120 { 121 attributes &= ~FunctionAttribute.nogc; 122 } 123 // Nullable.opAssign is not nogc 124 if (mode == FilterMode.Writer && isInstanceOf!(Nullable, T)) 125 { 126 attributes &= ~FunctionAttribute.nogc; 127 } 128 // TODO remove once synchronized (this) is nothrow 129 // see https://github.com/dlang/druntime/pull/2105 , https://github.com/dlang/dmd/pull/7942 130 if (is(T == class)) 131 { 132 attributes &= ~FunctionAttribute.nothrow_; 133 } 134 if (isStatic) 135 { 136 attributes &= ~FunctionAttribute.pure_; 137 } 138 return attributes; 139 } 140 141 enum FilterMode 142 { 143 Reader, 144 Writer, 145 } 146 147 string GenerateReader(T)(string name, bool fieldIsStatic, bool synchronize) 148 { 149 import boilerplate.util : needToDup; 150 import std.string : format; 151 import std.traits : Unqual; 152 153 auto example = T.init; 154 auto accessorName = accessor(name); 155 enum visibility = getVisibility!(Read, __traits(getAttributes, example)); 156 enum needToDupField = needToDup!T; 157 158 uint attributes = inferAttributes!(T, "__postblit") & 159 filterAttributes!T(fieldIsStatic, FilterMode.Reader); 160 161 string attributesString = generateAttributeString(attributes); 162 163 // for types like string where the contents are already const or value, 164 // so we can safely reassign to a non-const type 165 static if (needToDupField) 166 { 167 auto accessor_body = format!`return typeof(this.%s).init ~ this.%s;`(name, name); 168 } 169 else static if (DeepConst!(Unqual!T) && !is(Unqual!T == T)) 170 { 171 // necessitated by DMD bug https://issues.dlang.org/show_bug.cgi?id=18545 172 auto accessor_body = format!`typeof(cast() this.%s) var = this.%s; return var;`(name, name); 173 } 174 else 175 { 176 auto accessor_body = format!`return this.%s;`(name); 177 } 178 179 if (synchronize) 180 { 181 accessor_body = format!`synchronized (this) { %s} `(accessor_body); 182 } 183 184 static if (needToDupField) 185 { 186 auto modifiers = getModifiers(fieldIsStatic); 187 188 return format!("%s%s final @property auto %s() inout %s{ %s }") 189 (visibility, modifiers, accessorName, attributesString, accessor_body); 190 } 191 else if (fieldIsStatic) 192 { 193 return format!"%s static final @property auto %s() %s{ %s }" 194 (visibility, accessorName, attributesString, accessor_body); 195 } 196 else 197 { 198 return format!"%s final @property auto %s() inout %s{ %s }" 199 (visibility, accessorName, attributesString, accessor_body); 200 } 201 } 202 203 @("generates readers as expected") 204 @nogc nothrow pure @safe unittest 205 { 206 int integerValue; 207 string stringValue; 208 int[] intArrayValue; 209 const string constStringValue; 210 211 static assert(GenerateReader!int("foo", true, false) == 212 "public static final @property auto foo() " ~ 213 "@nogc nothrow @safe { return this.foo; }"); 214 static assert(GenerateReader!string("foo", true, false) == 215 "public static final @property auto foo() " ~ 216 "@nogc nothrow @safe { return this.foo; }"); 217 static assert(GenerateReader!(int[])("foo", true, false) == 218 "public static final @property auto foo() inout nothrow @safe " 219 ~ "{ return typeof(this.foo).init ~ this.foo; }"); 220 static assert(GenerateReader!(const string)("foo", true, false) == 221 "public static final @property auto foo() @nogc nothrow @safe " 222 ~ "{ typeof(cast() this.foo) var = this.foo; return var; }"); 223 } 224 225 string GenerateRefReader(T)(string name, bool isStatic) 226 { 227 import std.string : format; 228 229 auto example = T.init; 230 auto accessorName = accessor(name); 231 enum visibility = getVisibility!(RefRead, __traits(getAttributes, example)); 232 233 string attributesString; 234 if (isStatic) 235 { 236 attributesString = "@nogc nothrow @safe "; 237 } 238 else 239 { 240 attributesString = "@nogc nothrow pure @safe "; 241 } 242 243 auto modifiers = getModifiers(isStatic); 244 245 // no need to synchronize a reference read 246 return format("%s%s final @property ref auto %s() " ~ 247 "%s{ return this.%s; }", 248 visibility, modifiers, accessorName, attributesString, name); 249 } 250 251 @("generates ref readers as expected") 252 @nogc nothrow pure @safe unittest 253 { 254 static assert(GenerateRefReader!int("foo", true) == 255 "public static final @property ref auto foo() " ~ 256 "@nogc nothrow @safe { return this.foo; }"); 257 static assert(GenerateRefReader!string("foo", true) == 258 "public static final @property ref auto foo() " ~ 259 "@nogc nothrow @safe { return this.foo; }"); 260 static assert(GenerateRefReader!(int[])("foo", true) == 261 "public static final @property ref auto foo() " ~ 262 "@nogc nothrow @safe { return this.foo; }"); 263 } 264 265 string GenerateConstReader(T)(string name, bool isStatic, bool synchronize) 266 { 267 import std.string : format; 268 269 auto example = T.init; 270 auto accessorName = accessor(name); 271 enum visibility = getVisibility!(ConstRead, __traits(getAttributes, example)); 272 273 alias attributes = inferAttributes!(T, "__postblit"); 274 string attributesString = generateAttributeString(attributes); 275 276 string accessor_body = format!`return this.%s; `(name); 277 278 if (synchronize) 279 { 280 accessor_body = format!`synchronized (this) { %s} `(accessor_body); 281 } 282 283 return format("%s final @property auto %s() const %s { %s}", 284 visibility, accessorName, attributesString, accessor_body); 285 } 286 287 string GenerateWriter(T, Attributes...)(string name, string fieldCode, bool isStatic, bool synchronize) 288 { 289 import boilerplate.conditions : IsConditionAttribute, generateChecksForAttributes; 290 import boilerplate.util : needToDup; 291 import std.meta : StdMetaFilter = Filter; 292 import std.string : format; 293 294 auto example = T.init; 295 auto accessorName = accessor(name); 296 auto inputName = accessorName; 297 enum needToDupField = needToDup!T; 298 enum visibility = getVisibility!(Write, __traits(getAttributes, example)); 299 300 uint attributes = defaultFunctionAttributes & 301 filterAttributes!T(isStatic, FilterMode.Writer) & 302 inferAssignAttributes!T & 303 inferAttributes!(T, "__postblit") & 304 inferAttributes!(T, "__dtor"); 305 306 string precondition = ``; 307 308 if (auto checks = generateChecksForAttributes!(T, StdMetaFilter!(IsConditionAttribute, Attributes)) 309 (inputName, " in precondition of @Write")) 310 { 311 precondition = format!`in { import std.format : format; import std.array : empty; %s } body `(checks); 312 attributes &= ~FunctionAttribute.nogc; 313 attributes &= ~FunctionAttribute.nothrow_; 314 } 315 316 auto attributesString = generateAttributeString(attributes); 317 auto modifiers = getModifiers(isStatic); 318 319 string accessor_body = format!`this.%s = %s%s; `(name, inputName, needToDupField ? ".dup" : ""); 320 321 if (synchronize) 322 { 323 accessor_body = format!`synchronized (this) { %s} `(accessor_body); 324 } 325 326 return format("%s%s final @property void %s(typeof(%s) %s) %s%s{ %s}", 327 visibility, modifiers, accessorName, fieldCode, inputName, 328 attributesString, precondition, accessor_body); 329 } 330 331 @("generates writers as expected") 332 @nogc nothrow pure @safe unittest 333 { 334 static assert(GenerateWriter!int("foo", "integerValue", true, false) == 335 "public static final @property void foo(typeof(integerValue) foo) " ~ 336 "@nogc nothrow @safe { this.foo = foo; }"); 337 static assert(GenerateWriter!string("foo", "stringValue", true, false) == 338 "public static final @property void foo(typeof(stringValue) foo) " ~ 339 "@nogc nothrow @safe { this.foo = foo; }"); 340 static assert(GenerateWriter!(int[])("foo", "intArrayValue", true, false) == 341 "public static final @property void foo(typeof(intArrayValue) foo) " ~ 342 "nothrow @safe { this.foo = foo.dup; }"); 343 } 344 345 private enum uint defaultFunctionAttributes = 346 FunctionAttribute.nogc | 347 FunctionAttribute.safe | 348 FunctionAttribute.nothrow_ | 349 FunctionAttribute.pure_; 350 351 private template inferAttributes(T, string M) 352 { 353 uint inferAttributes() 354 { 355 uint attributes = defaultFunctionAttributes; 356 357 static if (is(T == struct)) 358 { 359 static if (hasMember!(T, M)) 360 { 361 attributes &= functionAttributes!(__traits(getMember, T, M)); 362 } 363 else 364 { 365 foreach (field; Fields!T) 366 { 367 attributes &= inferAttributes!(field, M); 368 } 369 } 370 } 371 return attributes; 372 } 373 } 374 375 private template inferAssignAttributes(T) 376 { 377 uint inferAssignAttributes() 378 { 379 uint attributes = defaultFunctionAttributes; 380 381 static if (is(T == struct)) 382 { 383 static if (hasMember!(T, "opAssign")) 384 { 385 foreach (o; __traits(getOverloads, T, "opAssign")) 386 { 387 alias params = Parameters!o; 388 static if (params.length == 1 && is(params[0] == T)) 389 { 390 attributes &= functionAttributes!o; 391 } 392 } 393 } 394 else 395 { 396 foreach (field; Fields!T) 397 { 398 attributes &= inferAssignAttributes!field; 399 } 400 } 401 } 402 return attributes; 403 } 404 } 405 406 private string generateAttributeString(uint attributes) 407 { 408 string attributesString; 409 410 if (attributes & FunctionAttribute.nogc) 411 { 412 attributesString ~= "@nogc "; 413 } 414 if (attributes & FunctionAttribute.nothrow_) 415 { 416 attributesString ~= "nothrow "; 417 } 418 if (attributes & FunctionAttribute.pure_) 419 { 420 attributesString ~= "pure "; 421 } 422 if (attributes & FunctionAttribute.safe) 423 { 424 attributesString ~= "@safe "; 425 } 426 427 return attributesString; 428 } 429 430 private string accessor(string name) @nogc nothrow pure @safe 431 { 432 import std.string : chomp, chompPrefix; 433 434 return name.chomp("_").chompPrefix("_"); 435 } 436 437 @("removes underlines from names") 438 @nogc nothrow pure @safe unittest 439 { 440 assert(accessor("foo_") == "foo"); 441 assert(accessor("_foo") == "foo"); 442 } 443 444 /** 445 * Returns a string with the value of the field "visibility" if the attributes 446 * include an UDA of type A. The default visibility is "public". 447 */ 448 template getVisibility(A, attributes...) 449 { 450 import std.string : format; 451 452 enum getVisibility = helper; 453 454 private static helper() 455 { 456 static if (!attributes.length) 457 { 458 return A.init.visibility; 459 } 460 else 461 { 462 foreach (i, uda; attributes) 463 { 464 static if (is(typeof(uda) == A)) 465 { 466 return uda.visibility; 467 } 468 else static if (is(uda == A)) 469 { 470 return A.init.visibility; 471 } 472 else static if (i == attributes.length - 1) 473 { 474 return A.init.visibility; 475 } 476 } 477 } 478 } 479 } 480 481 @("applies visibility from the uda parameter") 482 @nogc nothrow pure @safe unittest 483 { 484 @Read("public") int publicInt; 485 @Read("package") int packageInt; 486 @Read("protected") int protectedInt; 487 @Read("private") int privateInt; 488 @Read int defaultVisibleInt; 489 @Read @Write("protected") int publicReadableProtectedWritableInt; 490 491 static assert(getVisibility!(Read, __traits(getAttributes, publicInt)) == "public"); 492 static assert(getVisibility!(Read, __traits(getAttributes, packageInt)) == "package"); 493 static assert(getVisibility!(Read, __traits(getAttributes, protectedInt)) == "protected"); 494 static assert(getVisibility!(Read, __traits(getAttributes, privateInt)) == "private"); 495 static assert(getVisibility!(Read, __traits(getAttributes, defaultVisibleInt)) == "public"); 496 static assert(getVisibility!(Read, __traits(getAttributes, publicReadableProtectedWritableInt)) == "public"); 497 static assert(getVisibility!(Write, __traits(getAttributes, publicReadableProtectedWritableInt)) == "protected"); 498 } 499 500 @("creates accessors for flags") 501 nothrow pure @safe unittest 502 { 503 import std.typecons : Flag, No, Yes; 504 505 class Test 506 { 507 @Read 508 @Write 509 public Flag!"someFlag" test_ = Yes.someFlag; 510 511 mixin(GenerateFieldAccessors); 512 } 513 514 with (new Test) 515 { 516 assert(test == Yes.someFlag); 517 518 test = No.someFlag; 519 520 assert(test == No.someFlag); 521 522 static assert(is(typeof(test) == Flag!"someFlag")); 523 } 524 } 525 526 @("creates accessors for nullables") 527 nothrow pure @safe unittest 528 { 529 import std.typecons : Nullable; 530 531 class Test 532 { 533 @Read @Write 534 public Nullable!string test_ = Nullable!string("X"); 535 536 mixin(GenerateFieldAccessors); 537 } 538 539 with (new Test) 540 { 541 assert(!test.isNull); 542 assert(test.get == "X"); 543 544 static assert(is(typeof(test) == Nullable!string)); 545 } 546 } 547 548 @("does not break with const Nullable accessor") 549 nothrow pure @safe unittest 550 { 551 import std.typecons : Nullable; 552 553 class Test 554 { 555 @Read 556 private const Nullable!string test_; 557 558 mixin(GenerateFieldAccessors); 559 } 560 561 with (new Test) 562 { 563 assert(test.isNull); 564 } 565 } 566 567 @("creates non-const reader") 568 nothrow pure @safe unittest 569 { 570 class Test 571 { 572 @Read 573 int i_; 574 575 mixin(GenerateFieldAccessors); 576 } 577 578 auto mutableObject = new Test; 579 const constObject = mutableObject; 580 581 mutableObject.i_ = 42; 582 583 assert(mutableObject.i == 42); 584 585 static assert(is(typeof(mutableObject.i) == int)); 586 static assert(is(typeof(constObject.i) == const(int))); 587 } 588 589 @("creates ref reader") 590 nothrow pure @safe unittest 591 { 592 class Test 593 { 594 @RefRead 595 int i_; 596 597 mixin(GenerateFieldAccessors); 598 } 599 600 auto mutableTestObject = new Test; 601 602 mutableTestObject.i = 42; 603 604 assert(mutableTestObject.i == 42); 605 static assert(is(typeof(mutableTestObject.i) == int)); 606 } 607 608 @("creates writer") 609 nothrow pure @safe unittest 610 { 611 class Test 612 { 613 @Read @Write 614 private int i_; 615 616 mixin(GenerateFieldAccessors); 617 } 618 619 auto mutableTestObject = new Test; 620 mutableTestObject.i = 42; 621 622 assert(mutableTestObject.i == 42); 623 static assert(!__traits(compiles, mutableTestObject.i += 1)); 624 static assert(is(typeof(mutableTestObject.i) == int)); 625 } 626 627 @("checks whether hasUDA can be used for each member") 628 nothrow pure @safe unittest 629 { 630 class Test 631 { 632 alias Z = int; 633 634 @Read @Write 635 private int i_; 636 637 mixin(GenerateFieldAccessors); 638 } 639 640 auto mutableTestObject = new Test; 641 mutableTestObject.i = 42; 642 643 assert(mutableTestObject.i == 42); 644 static assert(!__traits(compiles, mutableTestObject.i += 1)); 645 } 646 647 @("returns non const for PODs and structs.") 648 nothrow pure @safe unittest 649 { 650 import std.algorithm : map, sort; 651 import std.array : array; 652 653 class C 654 { 655 @Read 656 string s_; 657 658 mixin(GenerateFieldAccessors); 659 } 660 661 C[] a = null; 662 663 static assert(__traits(compiles, a.map!(c => c.s).array.sort())); 664 } 665 666 @("functions with strings") 667 nothrow pure @safe unittest 668 { 669 class C 670 { 671 @Read @Write 672 string s_; 673 674 mixin(GenerateFieldAccessors); 675 } 676 677 with (new C) 678 { 679 s = "foo"; 680 assert(s == "foo"); 681 static assert(is(typeof(s) == string)); 682 } 683 } 684 685 @("supports user-defined accessors") 686 nothrow pure @safe unittest 687 { 688 class C 689 { 690 this() 691 { 692 str_ = "foo"; 693 } 694 695 @RefRead 696 private string str_; 697 698 public @property const(string) str() const 699 { 700 return this.str_.dup; 701 } 702 703 mixin(GenerateFieldAccessors); 704 } 705 706 with (new C) 707 { 708 str = "bar"; 709 } 710 } 711 712 @("creates accessor for locally defined types") 713 @system unittest 714 { 715 class X 716 { 717 } 718 719 class Test 720 { 721 @Read 722 public X x_; 723 724 mixin(GenerateFieldAccessors); 725 } 726 727 with (new Test) 728 { 729 x_ = new X; 730 731 assert(x == x_); 732 static assert(is(typeof(x) == X)); 733 } 734 } 735 736 @("creates const reader for simple structs") 737 nothrow pure @safe unittest 738 { 739 class Test 740 { 741 struct S 742 { 743 int i; 744 } 745 746 @Read 747 S s_; 748 749 mixin(GenerateFieldAccessors); 750 } 751 752 auto mutableObject = new Test; 753 const constObject = mutableObject; 754 755 mutableObject.s_.i = 42; 756 757 assert(constObject.s.i == 42); 758 759 static assert(is(typeof(mutableObject.s) == Test.S)); 760 static assert(is(typeof(constObject.s) == const(Test.S))); 761 } 762 763 @("returns copies when reading structs") 764 nothrow pure @safe unittest 765 { 766 class Test 767 { 768 struct S 769 { 770 int i; 771 } 772 773 @Read 774 S s_; 775 776 mixin(GenerateFieldAccessors); 777 } 778 779 auto mutableObject = new Test; 780 781 mutableObject.s.i = 42; 782 783 assert(mutableObject.s.i == int.init); 784 } 785 786 @("works with const arrays") 787 nothrow pure @safe unittest 788 { 789 class X 790 { 791 } 792 793 class C 794 { 795 @Read 796 private const(X)[] foo_; 797 798 mixin(GenerateFieldAccessors); 799 } 800 801 auto x = new X; 802 803 with (new C) 804 { 805 foo_ = [x]; 806 807 auto y = foo; 808 809 static assert(is(typeof(y) == const(X)[])); 810 static assert(is(typeof(foo) == const(X)[])); 811 } 812 } 813 814 @("has correct type of int") 815 nothrow pure @safe unittest 816 { 817 class C 818 { 819 @Read 820 private int foo_; 821 822 mixin(GenerateFieldAccessors); 823 } 824 825 with (new C) 826 { 827 static assert(is(typeof(foo) == int)); 828 } 829 } 830 831 @("works under inheritance (https://github.com/funkwerk/accessors/issues/5)") 832 @nogc nothrow pure @safe unittest 833 { 834 class A 835 { 836 @Read 837 string foo_; 838 839 mixin(GenerateFieldAccessors); 840 } 841 842 class B : A 843 { 844 @Read 845 string bar_; 846 847 mixin(GenerateFieldAccessors); 848 } 849 } 850 851 @("transfers struct attributes") 852 @nogc nothrow pure @safe unittest 853 { 854 struct S 855 { 856 this(this) 857 { 858 } 859 860 void opAssign(S s) 861 { 862 } 863 } 864 865 class A 866 { 867 @Read 868 S[] foo_; 869 870 @ConstRead 871 S bar_; 872 873 @Write 874 S baz_; 875 876 mixin(GenerateFieldAccessors); 877 } 878 } 879 880 @("returns array with mutable elements when reading") 881 nothrow pure @safe unittest 882 { 883 struct Field 884 { 885 } 886 887 struct S 888 { 889 @Read 890 Field[] foo_; 891 892 mixin(GenerateFieldAccessors); 893 } 894 895 with (S()) 896 { 897 Field[] arr = foo; 898 } 899 } 900 901 @("generates static properties for static members") 902 unittest 903 { 904 class MyStaticTest 905 { 906 @Read 907 static int stuff_ = 8; 908 909 mixin(GenerateFieldAccessors); 910 } 911 912 assert(MyStaticTest.stuff == 8); 913 } 914 915 unittest 916 { 917 struct S 918 { 919 @Read @Write 920 static int foo_ = 8; 921 922 @RefRead 923 static int bar_ = 6; 924 925 mixin(GenerateFieldAccessors); 926 } 927 928 assert(S.foo == 8); 929 static assert(is(typeof({ S.foo = 8; }))); 930 assert(S.bar == 6); 931 } 932 933 unittest 934 { 935 struct Thing 936 { 937 @Read 938 private int[] content_; 939 940 mixin(GenerateFieldAccessors); 941 } 942 943 class User 944 { 945 void helper(const int[]) 946 { 947 } 948 949 void doer(const Thing thing) 950 { 951 helper(thing.content); 952 } 953 } 954 } 955 956 @("correctly handles nullable array dupping") 957 unittest 958 { 959 class Class 960 { 961 } 962 963 struct Thing 964 { 965 @Read 966 private Class[] classes_; 967 968 mixin(GenerateFieldAccessors); 969 } 970 971 const Thing thing; 972 973 assert(thing.classes.length == 0); 974 } 975 976 @("generates invariant checks via precondition for writers") 977 unittest 978 { 979 import boilerplate.conditions : NonNull; 980 import core.exception : AssertError; 981 import std.algorithm : canFind; 982 import std.conv : to; 983 import unit_threaded.should : shouldThrow; 984 985 struct Thing 986 { 987 @Write @NonNull 988 Object object_; 989 990 mixin(GenerateFieldAccessors); 991 } 992 993 auto thing = Thing(new Object); 994 995 auto error = ({ thing.object = null; })().shouldThrow!AssertError; 996 997 assert(error.to!string.canFind("in precondition")); 998 }