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