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