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