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 }