1 module boilerplate.autostring;
2 
3 import std.format : format;
4 import std.json;
5 import std.meta : Alias;
6 import std.traits : Unqual;
7 
8 version(unittest)
9 {
10     import std.conv : to;
11     import std.datetime : SysTime;
12     import unit_threaded.should;
13 }
14 
15 /++
16 GenerateToString is a mixin string that automatically generates toString functions,
17 both sink-based and classic, customizable with UDA annotations on classes, members and functions.
18 +/
19 public enum string GenerateToString = `
20     import boilerplate.autostring : GenerateToStringTemplate;
21     static import std.json;
22     mixin GenerateToStringTemplate;
23     mixin(typeof(this).generateToStringErrCheck());
24     mixin(typeof(this).generateToStringImpl());
25 `;
26 
27 /++
28 When used with objects, toString methods of type string toString() are also created.
29 +/
30 @("generates legacy toString on objects")
31 unittest
32 {
33     class Class
34     {
35         mixin(GenerateToString);
36     }
37 
38     (new Class).to!string.shouldEqual("Class()");
39     (new Class).toString.shouldEqual("Class()");
40     (new Class).toJson.shouldEqual(`{}`.parseJSON);
41 }
42 
43 /++
44 A trailing underline in member names is removed when labeling.
45 +/
46 @("removes trailing underline")
47 unittest
48 {
49     struct Struct
50     {
51         int a_;
52         mixin(GenerateToString);
53     }
54 
55     Struct.init.to!string.shouldEqual("Struct(a=0)");
56     Struct.init.toJson.shouldEqual(`{"a": 0}`.parseJSON);
57 }
58 
59 /++
60 The `@(ToString.Exclude)` tag can be used to exclude a member.
61 +/
62 @("can exclude a member")
63 unittest
64 {
65     struct Struct
66     {
67         @(ToString.Exclude)
68         int a;
69         mixin(GenerateToString);
70     }
71 
72     Struct.init.to!string.shouldEqual("Struct()");
73     Struct.init.toJson.shouldEqual(`{}`.parseJSON);
74 }
75 
76 /++
77 The `@(ToString.Optional)` tag can be used to include a member only if it's in some form "present".
78 This means non-empty for arrays, non-null for objects, non-zero for ints.
79 +/
80 @("can optionally exclude member")
81 unittest
82 {
83     import std.typecons : Nullable, nullable;
84 
85     class Class
86     {
87         mixin(GenerateToString);
88     }
89 
90     struct Test // some type that is not comparable to null or 0
91     {
92         mixin(GenerateToString);
93     }
94 
95     struct Struct
96     {
97         @(ToString.Optional)
98         int a;
99 
100         @(ToString.Optional)
101         string s;
102 
103         @(ToString.Optional)
104         Class obj;
105 
106         @(ToString.Optional)
107         Nullable!Test nullable;
108 
109         mixin(GenerateToString);
110     }
111 
112     Struct.init.to!string.shouldEqual("Struct()");
113     Struct(2, "hi", new Class, Test().nullable).to!string
114         .shouldEqual(`Struct(a=2, s="hi", obj=Class(), nullable=Test())`);
115     Struct(2, "hi", new Class, Test().nullable).toJson
116         .shouldEqual(`{"a": 2, "s": "hi", "obj": {}, "nullable": {}}`.parseJSON);
117     Struct(0, "", null, Nullable!Test()).to!string.shouldEqual("Struct()");
118     Struct(0, "", null, Nullable!Test()).toJson.shouldEqual(`{}`.parseJSON);
119 }
120 
121 /++
122 The `@(ToString.Optional)` tag can be used with a condition parameter
123 indicating when the type is to be _included._
124 +/
125 @("can pass exclusion condition to Optional")
126 unittest
127 {
128     struct Struct
129     {
130         @(ToString.Optional!(a => a > 3))
131         int i;
132 
133         mixin(GenerateToString);
134     }
135 
136     Struct.init.to!string.shouldEqual("Struct()");
137     Struct.init.toJson.shouldEqual(`{}`.parseJSON);
138     Struct(3).to!string.shouldEqual("Struct()");
139     Struct(3).toJson.shouldEqual(`{}`.parseJSON);
140     Struct(5).to!string.shouldEqual("Struct(i=5)");
141     Struct(5).toJson.shouldEqual(`{"i": 5}`.parseJSON);
142 }
143 
144 /++
145 The `@(ToString.Optional)` condition predicate
146 can also take the whole data type.
147 +/
148 @("can pass exclusion condition to Optional")
149 unittest
150 {
151     struct Struct
152     {
153         @(ToString.Optional!(self => self.include))
154         int i;
155 
156         @(ToString.Exclude)
157         bool include;
158 
159         mixin(GenerateToString);
160     }
161 
162     Struct(5, false).to!string.shouldEqual("Struct()");
163     Struct(5, false).toJson.shouldEqual(`{}`.parseJSON);
164     Struct(5, true).to!string.shouldEqual("Struct(i=5)");
165     Struct(5, true).toJson.shouldEqual(`{"i": 5}`.parseJSON);
166 }
167 
168 /++
169 The `@(ToString.Include)` tag can be used to explicitly include a member.
170 This is intended to be used on property methods.
171 +/
172 @("can include a method")
173 unittest
174 {
175     struct Struct
176     {
177         @(ToString.Include)
178         int foo() const { return 5; }
179         mixin(GenerateToString);
180     }
181 
182     Struct.init.to!string.shouldEqual("Struct(foo=5)");
183 }
184 
185 /++
186 The `@(ToString.Unlabeled)` tag will omit a field's name.
187 +/
188 @("can omit names")
189 unittest
190 {
191     struct Struct
192     {
193         @(ToString.Unlabeled)
194         int a;
195         mixin(GenerateToString);
196     }
197 
198     Struct.init.to!string.shouldEqual("Struct(0)");
199 }
200 
201 /++
202 Parent class `toString()` methods are included automatically as the first entry, except if the parent class is `Object`.
203 +/
204 @("can be used in both parent and child class")
205 unittest
206 {
207     class ParentClass { mixin(GenerateToString); }
208 
209     class ChildClass : ParentClass { mixin(GenerateToString); }
210 
211     (new ChildClass).to!string.shouldEqual("ChildClass(ParentClass())");
212 }
213 
214 @("invokes manually implemented parent toString")
215 unittest
216 {
217     class ParentClass
218     {
219         override string toString() const
220         {
221             return "Some string";
222         }
223     }
224 
225     class ChildClass : ParentClass { mixin(GenerateToString); }
226 
227     (new ChildClass).to!string.shouldEqual("ChildClass(Some string)");
228 }
229 
230 @("invokes manually implemented parent toJson")
231 unittest
232 {
233     class ParentClass
234     {
235         JSONValue toJson() const
236         {
237             return JSONValue("test");
238         }
239     }
240 
241     class ChildClass : ParentClass { mixin(GenerateToString); }
242 
243     (new ChildClass).toJson.shouldEqual(`"test"`.parseJSON);
244 }
245 
246 @("can partially override toString in child class")
247 unittest
248 {
249     class ParentClass
250     {
251         mixin(GenerateToString);
252     }
253 
254     class ChildClass : ParentClass
255     {
256         override string toString() const
257         {
258             return "Some string";
259         }
260 
261         mixin(GenerateToString);
262     }
263 
264     (new ChildClass).to!string.shouldEqual("Some string");
265 }
266 
267 @("invokes manually implemented string toString in same class")
268 unittest
269 {
270     class Class
271     {
272         override string toString() const
273         {
274             return "Some string";
275         }
276 
277         mixin(GenerateToString);
278     }
279 
280     (new Class).to!string.shouldEqual("Some string");
281 }
282 
283 @("invokes manually implemented void toString in same class")
284 unittest
285 {
286     class Class
287     {
288         void toString(scope void delegate(const(char)[]) sink) const
289         {
290             sink("Some string");
291         }
292 
293         mixin(GenerateToString);
294     }
295 
296     (new Class).to!string.shouldEqual("Some string");
297 }
298 
299 /++
300 Inclusion of parent class `toString()` can be prevented using `@(ToString.ExcludeSuper)`.
301 +/
302 @("can suppress parent class toString()")
303 unittest
304 {
305     class ParentClass { }
306 
307     @(ToString.ExcludeSuper)
308     class ChildClass : ParentClass { mixin(GenerateToString); }
309 
310     (new ChildClass).to!string.shouldEqual("ChildClass()");
311 }
312 
313 /++
314 The `@(ToString.Naked)` tag will omit the name of the type and parentheses.
315 +/
316 @("can omit the type name")
317 unittest
318 {
319     @(ToString.Naked)
320     struct Struct
321     {
322         int a;
323         mixin(GenerateToString);
324     }
325 
326     Struct.init.to!string.shouldEqual("a=0");
327 }
328 
329 /++
330 Fields with the same name (ignoring capitalization) as their type, are unlabeled by default.
331 +/
332 @("does not label fields with the same name as the type")
333 unittest
334 {
335     struct Struct1 { mixin(GenerateToString); }
336 
337     struct Struct2
338     {
339         Struct1 struct1;
340         mixin(GenerateToString);
341     }
342 
343     Struct2.init.to!string.shouldEqual("Struct2(Struct1())");
344 }
345 
346 @("does not label fields with the same name as the type, even if they're const")
347 unittest
348 {
349     struct Struct1 { mixin(GenerateToString); }
350 
351     struct Struct2
352     {
353         const Struct1 struct1;
354         mixin(GenerateToString);
355     }
356 
357     Struct2.init.to!string.shouldEqual("Struct2(Struct1())");
358 }
359 
360 @("does not label fields with the same name as the type, even if they're nullable")
361 unittest
362 {
363     import std.typecons : Nullable;
364 
365     struct Struct1 { mixin(GenerateToString); }
366 
367     struct Struct2
368     {
369         const Nullable!Struct1 struct1;
370         mixin(GenerateToString);
371     }
372 
373     Struct2(Nullable!Struct1(Struct1())).to!string.shouldEqual("Struct2(Struct1())");
374 }
375 
376 /++
377 This behavior can be prevented by explicitly tagging the field with `@(ToString.Labeled)`.
378 +/
379 @("does label fields tagged as labeled")
380 unittest
381 {
382     struct Struct1 { mixin(GenerateToString); }
383 
384     struct Struct2
385     {
386         @(ToString.Labeled)
387         Struct1 struct1;
388         mixin(GenerateToString);
389     }
390 
391     Struct2.init.to!string.shouldEqual("Struct2(struct1=Struct1())");
392 }
393 
394 /++
395 Fields of type 'SysTime' and name 'time' are unlabeled by default.
396 +/
397 @("does not label SysTime time field correctly")
398 unittest
399 {
400     struct Struct { SysTime time; mixin(GenerateToString); }
401 
402     Struct strct;
403     strct.time = SysTime.fromISOExtString("2003-02-01T11:55:00Z");
404 
405     // see unittest/config/string.d
406     strct.to!string.shouldEqual("Struct(2003-02-01T11:55:00Z)");
407 }
408 
409 /++
410 Fields named 'id' are unlabeled only if they define their own toString().
411 +/
412 @("does not label id fields with toString()")
413 unittest
414 {
415     struct IdType
416     {
417         string toString() const { return "ID"; }
418     }
419 
420     struct Struct
421     {
422         IdType id;
423         mixin(GenerateToString);
424     }
425 
426     Struct.init.to!string.shouldEqual("Struct(ID)");
427 }
428 
429 /++
430 Otherwise, they are labeled as normal.
431 +/
432 @("labels id fields without toString")
433 unittest
434 {
435     struct Struct
436     {
437         int id;
438         mixin(GenerateToString);
439     }
440 
441     Struct.init.to!string.shouldEqual("Struct(id=0)");
442 }
443 
444 /++
445 Fields that are arrays with a name that is the pluralization of the array base type are also unlabeled by default,
446 as long as the array is NonEmpty. Otherwise, there would be no way to tell what the field contains.
447 +/
448 @("does not label fields named a plural of the basetype, if the type is an array")
449 unittest
450 {
451     import boilerplate.conditions : NonEmpty;
452 
453     struct Value { mixin(GenerateToString); }
454     struct Entity { mixin(GenerateToString); }
455     struct Day { mixin(GenerateToString); }
456 
457     struct Struct
458     {
459         @NonEmpty
460         Value[] values;
461 
462         @NonEmpty
463         Entity[] entities;
464 
465         @NonEmpty
466         Day[] days;
467 
468         mixin(GenerateToString);
469     }
470 
471     auto value = Struct(
472         [Value()],
473         [Entity()],
474         [Day()]);
475 
476     value.to!string.shouldEqual("Struct([Value()], [Entity()], [Day()])");
477 }
478 
479 @("does not label fields named a plural of the basetype, if the type is a BitFlags")
480 unittest
481 {
482     import std.typecons : BitFlags;
483 
484     enum Flag
485     {
486         A = 1 << 0,
487         B = 1 << 1,
488     }
489 
490     struct Struct
491     {
492         BitFlags!Flag flags;
493 
494         mixin(GenerateToString);
495     }
496 
497     auto value = Struct(BitFlags!Flag(Flag.A, Flag.B));
498 
499     value.to!string.shouldEqual("Struct(Flag(A, B))");
500 }
501 
502 /++
503 Fields that are not NonEmpty are always labeled.
504 This is because they can be empty, in which case you can't tell what's in them from naming.
505 +/
506 @("does label fields that may be empty")
507 unittest
508 {
509     import boilerplate.conditions : NonEmpty;
510 
511     struct Value { mixin(GenerateToString); }
512 
513     struct Struct
514     {
515         Value[] values;
516 
517         mixin(GenerateToString);
518     }
519 
520     Struct(null).to!string.shouldEqual("Struct(values=[])");
521 }
522 
523 /++
524 `GenerateToString` can be combined with `GenerateFieldAccessors` without issue.
525 +/
526 @("does not collide with accessors")
527 unittest
528 {
529     struct Struct
530     {
531         import boilerplate.accessors : ConstRead, GenerateFieldAccessors;
532 
533         @ConstRead
534         private int a_;
535 
536         mixin(GenerateFieldAccessors);
537 
538         mixin(GenerateToString);
539     }
540 
541     Struct.init.to!string.shouldEqual("Struct(a=0)");
542 }
543 
544 @("supports child classes of abstract classes")
545 unittest
546 {
547     static abstract class ParentClass
548     {
549     }
550     class ChildClass : ParentClass
551     {
552         mixin(GenerateToString);
553     }
554 }
555 
556 @("supports custom toString handlers")
557 unittest
558 {
559     struct Struct
560     {
561         @ToStringHandler!(i => i ? "yes" : "no")
562         int i;
563 
564         mixin(GenerateToString);
565     }
566 
567     Struct.init.to!string.shouldEqual("Struct(i=no)");
568 }
569 
570 @("passes nullable unchanged to custom toString handlers")
571 unittest
572 {
573     import std.typecons : Nullable;
574 
575     struct Struct
576     {
577         @ToStringHandler!(ni => ni.isNull ? "no" : "yes")
578         Nullable!int ni;
579 
580         mixin(GenerateToString);
581     }
582 
583     Struct.init.to!string.shouldEqual("Struct(ni=no)");
584 }
585 
586 // see unittest.config.string
587 @("supports optional BitFlags in structs")
588 unittest
589 {
590     import std.typecons : BitFlags;
591 
592     enum Enum
593     {
594         A = 1,
595         B = 2,
596     }
597 
598     struct Struct
599     {
600         @(ToString.Optional)
601         BitFlags!Enum field;
602 
603         mixin(GenerateToString);
604     }
605 
606     Struct.init.to!string.shouldEqual("Struct()");
607 }
608 
609 version (DigitalMars)
610 {
611     @("prints hashmaps in deterministic order")
612     unittest
613     {
614         struct Struct
615         {
616             string[string] map;
617 
618             mixin(GenerateToString);
619         }
620 
621         bool foundCollision = false;
622 
623         foreach (key1; ["opstop", "opsto"])
624         {
625             enum key2 = "foo"; // collide
626 
627             const first = Struct([key1: null, key2: null]);
628             string[string] backwardsHashmap;
629 
630             backwardsHashmap[key2] = null;
631             backwardsHashmap[key1] = null;
632 
633             const second = Struct(backwardsHashmap);
634 
635             if (first.map.keys != second.map.keys)
636             {
637                 foundCollision = true;
638                 first.to!string.shouldEqual(second.to!string);
639             }
640         }
641         assert(foundCollision, "none of the listed keys caused a hash collision");
642     }
643 }
644 
645 @("applies custom formatters to types in hashmaps")
646 unittest
647 {
648     import std.datetime : SysTime;
649 
650     struct Struct
651     {
652         SysTime[string] map;
653 
654         mixin(GenerateToString);
655     }
656 
657     const expected = "2003-02-01T11:55:00Z";
658     const value = Struct(["foo": SysTime.fromISOExtString(expected)]);
659 
660     value.to!string.shouldEqual(`Struct(map=["foo": ` ~ expected ~ `])`);
661 }
662 
663 @("can format associative array of Nullable SysTime")
664 unittest
665 {
666     import std.datetime : SysTime;
667     import std.typecons : Nullable;
668 
669     struct Struct
670     {
671         Nullable!SysTime[string] map;
672 
673         mixin(GenerateToString);
674     }
675 
676     const expected = `Struct(map=["foo": null])`;
677     const value = Struct(["foo": Nullable!SysTime()]);
678 
679     value.to!string.shouldEqual(expected);
680 }
681 
682 @("can format associative array of type that cannot be sorted")
683 unittest
684 {
685     struct Struct
686     {
687         mixin(GenerateToString);
688     }
689 
690     struct Struct2
691     {
692         bool[Struct] hashmap;
693 
694         mixin(GenerateToString);
695     }
696 
697     const expected = `Struct2(hashmap=[])`;
698     const value = Struct2(null);
699 
700     value.to!string.shouldEqual(expected);
701 }
702 
703 @("labels nested types with fully qualified names")
704 unittest
705 {
706     import std.datetime : SysTime;
707     import std.typecons : Nullable;
708 
709     struct Struct
710     {
711         struct Struct2
712         {
713             mixin(GenerateToString);
714         }
715 
716         Struct2 struct2;
717 
718         mixin(GenerateToString);
719     }
720 
721     const expected = `Struct(Struct.Struct2())`;
722     const value = Struct(Struct.Struct2());
723 
724     value.to!string.shouldEqual(expected);
725 }
726 
727 @("supports fully qualified names with quotes")
728 unittest
729 {
730     struct Struct(string s)
731     {
732         struct Struct2
733         {
734             mixin(GenerateToString);
735         }
736 
737         Struct2 struct2;
738 
739         mixin(GenerateToString);
740     }
741 
742     const expected = `Struct!"foo"(Struct!"foo".Struct2())`;
743     const value = Struct!"foo"(Struct!"foo".Struct2());
744 
745     value.to!string.shouldEqual(expected);
746 }
747 
748 @("optional-always null Nullable")
749 unittest
750 {
751     import std.typecons : Nullable;
752 
753     struct Struct
754     {
755         @(ToString.Optional!(a => true))
756         Nullable!int i;
757 
758         mixin(GenerateToString);
759     }
760 
761     Struct().to!string.shouldEqual("Struct(i=Nullable.null)");
762 }
763 
764 @("force-included null Nullable")
765 unittest
766 {
767     import std.typecons : Nullable;
768 
769     struct Struct
770     {
771         @(ToString.Include)
772         Nullable!int i;
773 
774         mixin(GenerateToString);
775     }
776 
777     Struct().to!string.shouldEqual("Struct(i=Nullable.null)");
778 }
779 
780 // test for clean detection of Nullable
781 @("struct with isNull")
782 unittest
783 {
784     struct Inner
785     {
786         bool isNull() const { return false; }
787 
788         mixin(GenerateToString);
789     }
790 
791     struct Outer
792     {
793         Inner inner;
794 
795         mixin(GenerateToString);
796     }
797 
798     Outer().to!string.shouldEqual("Outer(Inner())");
799 }
800 
801 // regression
802 @("mutable struct with alias this of sink toString")
803 unittest
804 {
805     struct Inner
806     {
807         public void toString(scope void delegate(const(char)[]) sink) const
808         {
809             sink("Inner()");
810         }
811     }
812 
813     struct Outer
814     {
815         Inner inner;
816 
817         alias inner this;
818 
819         mixin(GenerateToString);
820     }
821 }
822 
823 @("immutable struct with alias this of const toString")
824 unittest
825 {
826     struct Inner
827     {
828         string toString() const { return "Inner()"; }
829     }
830 
831     immutable struct Outer
832     {
833         Inner inner;
834 
835         alias inner this;
836 
837         mixin(GenerateToString);
838     }
839 
840     Outer().to!string.shouldEqual("Outer(Inner())");
841 }
842 
843 @("class with alias to struct")
844 unittest
845 {
846     struct A
847     {
848         mixin(GenerateToString);
849     }
850 
851     class B
852     {
853         A a;
854 
855         alias a this;
856 
857         mixin(GenerateToString);
858     }
859 
860     (new B).to!string.shouldEqual("B(A())");
861 }
862 
863 mixin template GenerateToStringTemplate()
864 {
865     // this is a separate function to reduce the
866     // "warning: unreachable code" spam that is falsely created from static foreach
867     private static generateToStringErrCheck()
868     {
869         if (!__ctfe)
870         {
871             return null;
872         }
873 
874         import boilerplate.autostring : ToString, typeName;
875         import boilerplate.util : GenNormalMemberTuple;
876         import std.string : format;
877 
878         bool udaIncludeSuper;
879         bool udaExcludeSuper;
880 
881         foreach (uda; __traits(getAttributes, typeof(this)))
882         {
883             static if (is(typeof(uda) == ToString))
884             {
885                 switch (uda)
886                 {
887                     case ToString.IncludeSuper: udaIncludeSuper = true; break;
888                     case ToString.ExcludeSuper: udaExcludeSuper = true; break;
889                     default: break;
890                 }
891             }
892         }
893 
894         if (udaIncludeSuper && udaExcludeSuper)
895         {
896             return format!(`static assert(false, ` ~
897                 `"Contradictory tags on '" ~ %(%s%) ~ "': IncludeSuper and ExcludeSuper");`)
898                 ([typeName!(typeof(this))]);
899         }
900 
901         mixin GenNormalMemberTuple!true;
902 
903         foreach (member; NormalMemberTuple)
904         {
905             alias overloads = __traits(getOverloads, typeof(this), member, true);
906             static if (overloads.length > 0)
907             {
908                 alias symbol = overloads[0];
909             }
910             else
911             {
912                 mixin("alias symbol = typeof(this)." ~ member ~ ";");
913             }
914             enum error = checkAttributeConsistency!(__traits(getAttributes, symbol));
915 
916             static if (error)
917             {
918                 return format!error(member);
919             }
920         }
921 
922         return ``;
923     }
924 
925     private static generateToStringImpl()
926     {
927         if (!__ctfe)
928         {
929             return null;
930         }
931 
932         import boilerplate.autostring :
933             hasOwnStringToString, hasOwnVoidToString, isMemberUnlabeledByDefault, ToString, typeName;
934         import boilerplate.conditions : NonEmpty;
935         import boilerplate.util : GenNormalMemberTuple, udaIndex;
936         import std.json : JSONValue;
937         import std.meta : Alias;
938         import std.string : endsWith, format, split, startsWith, strip;
939         import std.traits : BaseClassesTuple, getUDAs, Unqual;
940         import std.typecons : Nullable;
941 
942         // synchronized without lock contention is basically free, so always do it
943         // TODO enable when https://issues.dlang.org/show_bug.cgi?id=18504 is fixed
944         enum synchronize = false && is(typeof(this) == class);
945 
946         const constExample = typeof(this).init;
947         auto normalExample = typeof(this).init;
948 
949         enum alreadyHaveStringToString = __traits(hasMember, typeof(this), "toString")
950             && is(typeof(normalExample.toString()) == string);
951         enum alreadyHaveUsableStringToString = alreadyHaveStringToString
952             && is(typeof(constExample.toString()) == string);
953 
954         enum alreadyHaveVoidToString = __traits(hasMember, typeof(this), "toString")
955             && is(typeof(normalExample.toString((void delegate(const(char)[])).init)) == void);
956         enum alreadyHaveUsableVoidToString = alreadyHaveVoidToString
957             && is(typeof(constExample.toString((void delegate(const(char)[])).init)) == void);
958 
959         enum isObject = is(typeof(this): Object);
960 
961         static if (isObject)
962         {
963             enum userDefinedStringToString = hasOwnStringToString!(typeof(this), typeof(super));
964             enum userDefinedVoidToString = hasOwnVoidToString!(typeof(this), typeof(super));
965             enum userDefinedToJson = hasOwnToJson!(typeof(this), typeof(super));
966         }
967         else
968         {
969             enum userDefinedStringToString = hasOwnStringToString!(typeof(this));
970             enum userDefinedVoidToString = hasOwnVoidToString!(typeof(this));
971             enum userDefinedToJson = hasOwnToJson!(typeof(this));
972         }
973 
974         enum bool generateToStringFunction = !userDefinedStringToString && !userDefinedVoidToString;
975         enum bool generateToJsonFunction = !userDefinedToJson;
976 
977         static if (generateToStringFunction || generateToJsonFunction)
978         {
979             string toStringFunction = null;
980             string toJsonFunction = null;
981 
982             bool nakedMode;
983             bool udaIncludeSuper;
984             bool udaExcludeSuper;
985 
986             foreach (uda; __traits(getAttributes, typeof(this)))
987             {
988                 static if (is(typeof(uda) == ToStringEnum))
989                 {
990                     switch (uda)
991                     {
992                         case ToString.Naked: nakedMode = true; break;
993                         case ToString.IncludeSuper: udaIncludeSuper = true; break;
994                         case ToString.ExcludeSuper: udaExcludeSuper = true; break;
995                         default: break;
996                     }
997                 }
998             }
999 
1000             string NamePlusOpenParen = typeName!(typeof(this)) ~ "(";
1001 
1002             version(AutoStringDebug)
1003             {
1004                 toStringFunction ~= format!`pragma(msg, "%s %s");`(alreadyHaveStringToString, alreadyHaveVoidToString);
1005             }
1006 
1007             static if (isObject
1008                 && is(typeof(typeof(super).init.toString((void delegate(const(char)[])).init)) == void))
1009             {
1010                 toStringFunction ~= `override `;
1011             }
1012             static if (isObject && is(typeof(super.toJson()) == JSONValue))
1013             {
1014                 toJsonFunction ~= `override `;
1015             }
1016 
1017             toStringFunction ~= `public void toString(scope void delegate(const(char)[]) sink) const {`
1018                 ~ `import boilerplate.autostring: ToStringHandler;`
1019                 ~ `import boilerplate.util: sinkWrite;`
1020                 ~ `import std.traits: getUDAs;`;
1021             toJsonFunction ~= `public std.json.JSONValue toJson() const {`
1022                 ~ `import boilerplate.util: toJsonValue;`
1023                 ~ `import std.json: JSONValue;`;
1024 
1025             static if (synchronize)
1026             {
1027                 toStringFunction ~= `synchronized (this) { `;
1028                 toJsonFunction ~= `synchronized (this) { `;
1029             }
1030 
1031             if (!nakedMode)
1032             {
1033                 toStringFunction ~= format!`sink(%(%s%));`([NamePlusOpenParen]);
1034             }
1035 
1036             bool includeSuperToString = false;
1037             bool includeSuperToJson = false;
1038 
1039             static if (isObject)
1040             {
1041                 if (alreadyHaveUsableStringToString || alreadyHaveUsableVoidToString)
1042                 {
1043                     includeSuperToString = true;
1044                 }
1045                 if (__traits(hasMember, typeof(super), "toJson"))
1046                 {
1047                     includeSuperToJson = true;
1048                 }
1049             }
1050 
1051             if (udaIncludeSuper)
1052             {
1053                 includeSuperToString = true;
1054                 includeSuperToJson = true;
1055             }
1056             else if (udaExcludeSuper)
1057             {
1058                 includeSuperToString = false;
1059                 includeSuperToJson = false;
1060             }
1061 
1062             static if (isObject)
1063             {
1064                 if (includeSuperToString)
1065                 {
1066                     static if (!alreadyHaveUsableStringToString && !alreadyHaveUsableVoidToString)
1067                     {
1068                         return `static assert(false, `
1069                             ~ `"cannot include super class in GenerateToString: `
1070                             ~ `parent class has no usable toString!");`;
1071                     }
1072                     else {
1073                         static if (alreadyHaveUsableVoidToString)
1074                         {
1075                             toStringFunction ~= `super.toString(sink);`;
1076                         }
1077                         else
1078                         {
1079                             toStringFunction ~= `sink(super.toString());`;
1080                         }
1081                         toStringFunction ~= `bool comma = true;`;
1082                     }
1083                 }
1084                 else
1085                 {
1086                     toStringFunction ~= `bool comma = false;`;
1087                 }
1088                 if (includeSuperToJson)
1089                 {
1090                     toJsonFunction ~= `JSONValue result = super.toJson;`;
1091                 }
1092                 else
1093                 {
1094                     toJsonFunction ~= `JSONValue result = JSONValue((JSONValue[string]).init);`;
1095                 }
1096             }
1097             else
1098             {
1099                 toStringFunction ~= `bool comma = false;`;
1100                 toJsonFunction ~= `JSONValue result = JSONValue((JSONValue[string]).init);`;
1101             }
1102 
1103             toStringFunction ~= `{`;
1104 
1105             mixin GenNormalMemberTuple!(true);
1106 
1107             foreach (member; NormalMemberTuple)
1108             {
1109                 alias overloads = __traits(getOverloads, typeof(this), member, true);
1110                 static if (overloads.length > 0)
1111                 {
1112                     alias symbol = overloads[0];
1113                 }
1114                 else
1115                 {
1116                     mixin("alias symbol = typeof(this)." ~ member ~ ";");
1117                 }
1118                 enum udaInclude = udaIndex!(ToString.Include, __traits(getAttributes, symbol)) != -1;
1119                 enum udaExclude = udaIndex!(ToString.Exclude, __traits(getAttributes, symbol)) != -1;
1120                 enum udaLabeled = udaIndex!(ToString.Labeled, __traits(getAttributes, symbol)) != -1;
1121                 enum udaUnlabeled = udaIndex!(ToString.Unlabeled, __traits(getAttributes, symbol)) != -1;
1122                 enum udaOptional = udaIndex!(ToString.Optional, __traits(getAttributes, symbol)) != -1;
1123                 enum udaToStringHandler = udaIndex!(ToStringHandler, __traits(getAttributes, symbol)) != -1;
1124                 enum udaNonEmpty = udaIndex!(NonEmpty, __traits(getAttributes, symbol)) != -1;
1125 
1126                 // see std.traits.isFunction!()
1127                 static if (
1128                     is(symbol == function)
1129                     || is(typeof(symbol) == function)
1130                     || (is(typeof(&symbol) U : U*) && is(U == function)))
1131                 {
1132                     enum isFunction = true;
1133                 }
1134                 else
1135                 {
1136                     enum isFunction = false;
1137                 }
1138 
1139                 enum includeOverride = udaInclude || udaOptional;
1140 
1141                 enum includeMember = (!isFunction || includeOverride) && !udaExclude;
1142 
1143                 static if (includeMember)
1144                 {
1145                     string memberName = member;
1146 
1147                     if (memberName.endsWith("_"))
1148                     {
1149                         memberName = memberName[0 .. $ - 1];
1150                     }
1151 
1152                     bool labeled = true;
1153 
1154                     static if (udaUnlabeled)
1155                     {
1156                         labeled = false;
1157                     }
1158 
1159                     if (isMemberUnlabeledByDefault!(Unqual!(typeof(symbol)))(memberName, udaNonEmpty))
1160                     {
1161                         labeled = false;
1162                     }
1163 
1164                     static if (udaLabeled)
1165                     {
1166                         labeled = true;
1167                     }
1168 
1169                     string membervalue = `this.` ~ member;
1170 
1171                     bool escapeStrings = true;
1172 
1173                     static if (udaToStringHandler)
1174                     {
1175                         alias Handlers = getUDAs!(symbol, ToStringHandler);
1176 
1177                         static assert(Handlers.length == 1);
1178 
1179                         static if (__traits(compiles, Handlers[0].Handler(typeof(symbol).init)))
1180                         {
1181                             membervalue = `getUDAs!(this.` ~ member ~ `, ToStringHandler)[0].Handler(`
1182                                 ~ membervalue
1183                                 ~ `)`;
1184 
1185                             escapeStrings = false;
1186                         }
1187                         else
1188                         {
1189                             return `static assert(false, "cannot determine how to call ToStringHandler");`;
1190                         }
1191                     }
1192 
1193                     string readMemberValue = membervalue;
1194                     string jsonMemberValue = `this.` ~ member;
1195                     string conditionalWritestmt; // formatted with sink.sinkWrite(... readMemberValue ... )
1196 
1197                     static if (udaOptional)
1198                     {
1199                         import std.array : empty;
1200 
1201                         enum optionalIndex = udaIndex!(ToString.Optional, __traits(getAttributes, symbol));
1202                         alias optionalUda = Alias!(__traits(getAttributes, symbol)[optionalIndex]);
1203 
1204                         static if (is(optionalUda == struct))
1205                         {
1206                             alias pred = Alias!(__traits(getAttributes, symbol)[optionalIndex]).condition;
1207                             static if (__traits(compiles, pred(typeof(this).init)))
1208                             {
1209                                 conditionalWritestmt = format!q{
1210                                     if (__traits(getAttributes, %s)[%s].condition(this)) { %%s }
1211                                 } (membervalue, optionalIndex);
1212                             }
1213                             else
1214                             {
1215                                 conditionalWritestmt = format!q{
1216                                     if (__traits(getAttributes, %s)[%s].condition(%s)) { %%s }
1217                                 } (membervalue, optionalIndex, membervalue);
1218                             }
1219                         }
1220                         else static if (__traits(compiles, typeof(symbol).init.isNull))
1221                         {
1222                             conditionalWritestmt = format!q{if (!%s.isNull) { %%s }}
1223                                 (membervalue);
1224 
1225                             static if (is(typeof(symbol) : Nullable!T, T))
1226                             {
1227                                 readMemberValue = membervalue ~ `.get`;
1228                                 jsonMemberValue = `this.` ~ member ~ `.get`;
1229                             }
1230                         }
1231                         else static if (__traits(compiles, typeof(symbol).init.empty))
1232                         {
1233                             conditionalWritestmt = format!q{import std.array : empty; if (!%s.empty) { %%s }}
1234                                 (membervalue);
1235                         }
1236                         else static if (__traits(compiles, typeof(symbol).init !is null))
1237                         {
1238                             conditionalWritestmt = format!q{if (%s !is null) { %%s }}
1239                                 (membervalue);
1240                         }
1241                         else static if (__traits(compiles, typeof(symbol).init != 0))
1242                         {
1243                             conditionalWritestmt = format!q{if (%s != 0) { %%s }}
1244                                 (membervalue);
1245                         }
1246                         else static if (__traits(compiles, { if (typeof(symbol).init) { } }))
1247                         {
1248                             conditionalWritestmt = format!q{if (%s) { %%s }}
1249                                 (membervalue);
1250                         }
1251                         else
1252                         {
1253                             return format!(`static assert(false, `
1254                                     ~ `"don't know how to figure out whether %s is present.");`)
1255                                 (member);
1256                         }
1257                     }
1258                     else
1259                     {
1260                         // Nullables (without handler, that aren't force-included) fall back to optional
1261                         static if (!udaToStringHandler && !udaInclude &&
1262                             __traits(compiles, typeof(symbol).init.isNull))
1263                         {
1264                             conditionalWritestmt = format!q{if (!%s.isNull) { %%s }}
1265                                 (membervalue);
1266 
1267                             static if (is(typeof(symbol) : Nullable!T, T))
1268                             {
1269                                 readMemberValue = membervalue ~ `.get`;
1270                                 jsonMemberValue = `this.` ~ member ~ `.get`;
1271                             }
1272                         }
1273                         else
1274                         {
1275                             conditionalWritestmt = q{ %s };
1276                         }
1277                     }
1278 
1279                     string writeStmt;
1280 
1281                     if (labeled)
1282                     {
1283                         writeStmt = format!`sink.sinkWrite(comma, %s, "%s=%%s", %s);`
1284                             (escapeStrings, memberName, readMemberValue);
1285                     }
1286                     else
1287                     {
1288                         writeStmt = format!`sink.sinkWrite(comma, %s, "%%s", %s);`
1289                             (escapeStrings, readMemberValue);
1290                     }
1291                     string writeJsonStmt = format!`result["%s"] = toJsonValue(%s);`
1292                         (memberName, jsonMemberValue);
1293 
1294                     toStringFunction ~= format(conditionalWritestmt, writeStmt);
1295                     toJsonFunction ~= format(conditionalWritestmt, writeJsonStmt);
1296                 }
1297             }
1298 
1299             toStringFunction ~= `} `;
1300 
1301             if (!nakedMode)
1302             {
1303                 toStringFunction ~= `sink(")");`;
1304             }
1305 
1306             static if (synchronize)
1307             {
1308                 toStringFunction ~= `} `;
1309                 toJsonFunction ~= `} `;
1310             }
1311 
1312             toStringFunction ~= `} `;
1313             toJsonFunction ~= `return result;`
1314                 ~ `} `;
1315         }
1316 
1317         static if (userDefinedStringToString && userDefinedVoidToString)
1318         {
1319             static assert(!generateToStringFunction);
1320 
1321             string toStringCode = ``; // Nothing to be done.
1322         }
1323         // if the user has defined their own string toString() in this aggregate:
1324         else static if (userDefinedStringToString)
1325         {
1326             static assert(!generateToStringFunction);
1327 
1328             // just call it.
1329             static if (alreadyHaveUsableStringToString)
1330             {
1331                 string toStringCode = `public void toString(scope void delegate(const(char)[]) sink) const {` ~
1332                     ` sink(this.toString());` ~
1333                     ` }`;
1334 
1335                 static if (isObject
1336                     && is(typeof(typeof(super).init.toString((void delegate(const(char)[])).init)) == void))
1337                 {
1338                     toStringCode = `override ` ~ toStringCode;
1339                 }
1340             }
1341             else
1342             {
1343                 string toStringCode = `static assert(false, "toString is not const in this class.");`;
1344             }
1345         }
1346         // if the user has defined their own void toString() in this aggregate:
1347         else
1348         {
1349 
1350             string toStringCode;
1351 
1352             static if (!userDefinedVoidToString)
1353             {
1354                 static assert(generateToStringFunction);
1355 
1356                 toStringCode = toStringFunction;
1357             }
1358             else
1359             {
1360                 static assert(!generateToStringFunction);
1361             }
1362 
1363             // generate fallback string toString()
1364             // that calls, specifically, *our own* toString impl.
1365             // (this is important to break cycles when a subclass implements a toString that calls super.toString)
1366             static if (isObject)
1367             {
1368                 toStringCode ~= `override `;
1369             }
1370 
1371             toStringCode ~= `public string toString() const {`
1372                 ~ `string result;`
1373                 ~ `typeof(this).toString((const(char)[] part) { result ~= part; });`
1374                 ~ `return result;`
1375             ~ `}`;
1376         }
1377         static if (userDefinedToJson)
1378         {
1379             string toJsonCode = ``;
1380         }
1381         else
1382         {
1383             string toJsonCode = toJsonFunction;
1384         }
1385         return toStringCode ~ toJsonCode;
1386     }
1387 }
1388 
1389 template checkAttributeConsistency(Attributes...)
1390 {
1391     enum checkAttributeConsistency = checkAttributeHelper();
1392 
1393     private string checkAttributeHelper()
1394     {
1395         if (!__ctfe)
1396         {
1397             return null;
1398         }
1399 
1400         import std.string : format;
1401 
1402         bool include, exclude, optional, labeled, unlabeled;
1403 
1404         foreach (uda; Attributes)
1405         {
1406             static if (is(typeof(uda) == ToStringEnum))
1407             {
1408                 switch (uda)
1409                 {
1410                     case ToString.Include: include = true; break;
1411                     case ToString.Exclude: exclude = true; break;
1412                     case ToString.Labeled: labeled = true; break;
1413                     case ToString.Unlabeled: unlabeled = true; break;
1414                     default: break;
1415                 }
1416             }
1417             else static if (is(uda == struct) && __traits(isSame, uda, ToString.Optional))
1418             {
1419                 optional = true;
1420             }
1421         }
1422 
1423         if (include && exclude)
1424         {
1425             return `static assert(false, "Contradictory tags on '%s': Include and Exclude");`;
1426         }
1427 
1428         if (include && optional)
1429         {
1430             return `static assert(false, "Redundant tags on '%s': Optional implies Include");`;
1431         }
1432 
1433         if (exclude && optional)
1434         {
1435             return `static assert(false, "Contradictory tags on '%s': Exclude and Optional");`;
1436         }
1437 
1438         if (labeled && unlabeled)
1439         {
1440             return `static assert(false, "Contradictory tags on '%s': Labeled and Unlabeled");`;
1441         }
1442 
1443         return null;
1444     }
1445 }
1446 
1447 struct ToStringHandler(alias Handler_)
1448 {
1449     alias Handler = Handler_;
1450 }
1451 
1452 enum ToStringEnum
1453 {
1454     // these go on the class
1455     Naked,
1456     IncludeSuper,
1457     ExcludeSuper,
1458 
1459     // these go on the field/method
1460     Unlabeled,
1461     Labeled,
1462     Exclude,
1463     Include,
1464 }
1465 
1466 struct ToString
1467 {
1468     static foreach (name; __traits(allMembers, ToStringEnum))
1469     {
1470         mixin(format!q{enum %s = ToStringEnum.%s;}(name, name));
1471     }
1472 
1473     static struct Optional(alias condition_)
1474     {
1475         alias condition = condition_;
1476     }
1477 }
1478 
1479 public bool isMemberUnlabeledByDefault(Type)(string field, bool attribNonEmpty)
1480 {
1481     import std.datetime : SysTime;
1482     import std.range.primitives : ElementType, isInputRange;
1483     // Types whose toString starts with the contained type
1484     import std.typecons : BitFlags, Nullable;
1485 
1486     field = field.toLower;
1487 
1488     static if (is(Type: const Nullable!BaseType, BaseType))
1489     {
1490         if (field == BaseType.stringof.toLower)
1491         {
1492             return true;
1493         }
1494     }
1495     else static if (isInputRange!Type)
1496     {
1497         alias BaseType = ElementType!Type;
1498 
1499         if (field == BaseType.stringof.toLower.pluralize && attribNonEmpty)
1500         {
1501             return true;
1502         }
1503     }
1504     else static if (is(Type: const BitFlags!BaseType, BaseType))
1505     {
1506         if (field == BaseType.stringof.toLower.pluralize)
1507         {
1508             return true;
1509         }
1510     }
1511 
1512     return field == Type.stringof.toLower
1513         || (field == "time" && is(Type == SysTime))
1514         || (field == "id" && is(typeof(Type.toString)));
1515 }
1516 
1517 private string toLower(string text)
1518 {
1519     import std.string : stdToLower = toLower;
1520 
1521     string result = null;
1522 
1523     foreach (ub; cast(immutable(ubyte)[]) text)
1524     {
1525         if (ub >= 0x80) // utf-8, non-ascii
1526         {
1527             return text.stdToLower;
1528         }
1529         if (ub >= 'A' && ub <= 'Z')
1530         {
1531             result ~= cast(char) (ub + ('a' - 'A'));
1532         }
1533         else
1534         {
1535             result ~= cast(char) ub;
1536         }
1537     }
1538     return result;
1539 }
1540 
1541 // http://code.activestate.com/recipes/82102/
1542 private string pluralize(string label)
1543 {
1544     import std.algorithm.searching : contain = canFind;
1545 
1546     string postfix = "s";
1547     if (label.length > 2)
1548     {
1549         enum vowels = "aeiou";
1550 
1551         if (label.stringEndsWith("ch") || label.stringEndsWith("sh"))
1552         {
1553             postfix = "es";
1554         }
1555         else if (auto before = label.stringEndsWith("y"))
1556         {
1557             if (!vowels.contain(label[$ - 2]))
1558             {
1559                 postfix = "ies";
1560                 label = before;
1561             }
1562         }
1563         else if (auto before = label.stringEndsWith("is"))
1564         {
1565             postfix = "es";
1566             label = before;
1567         }
1568         else if ("sxz".contain(label[$-1]))
1569         {
1570             postfix = "es"; // glasses
1571         }
1572     }
1573     return label ~ postfix;
1574 }
1575 
1576 @("has functioning pluralize()")
1577 unittest
1578 {
1579     "dog".pluralize.shouldEqual("dogs");
1580     "ash".pluralize.shouldEqual("ashes");
1581     "day".pluralize.shouldEqual("days");
1582     "entity".pluralize.shouldEqual("entities");
1583     "thesis".pluralize.shouldEqual("theses");
1584     "glass".pluralize.shouldEqual("glasses");
1585 }
1586 
1587 private string stringEndsWith(const string text, const string suffix)
1588 {
1589     import std.range : dropBack;
1590     import std.string : endsWith;
1591 
1592     if (text.endsWith(suffix))
1593     {
1594         return text.dropBack(suffix.length);
1595     }
1596     return null;
1597 }
1598 
1599 @("has functioning stringEndsWith()")
1600 unittest
1601 {
1602     "".stringEndsWith("").shouldNotBeNull;
1603     "".stringEndsWith("x").shouldBeNull;
1604     "Hello".stringEndsWith("Hello").shouldNotBeNull;
1605     "Hello".stringEndsWith("Hello").shouldEqual("");
1606     "Hello".stringEndsWith("lo").shouldEqual("Hel");
1607 }
1608 
1609 template hasOwnFunction(Aggregate, Super, string name, Ret)
1610 if (!__traits(hasMember, Aggregate, name))
1611 {
1612     enum hasOwnFunction = false;
1613 }
1614 
1615 template hasOwnFunction(Aggregate, Super, string name, Ret)
1616 if (__traits(hasMember, Aggregate, name))
1617 {
1618     import std.meta : AliasSeq, Filter;
1619     import std.traits : ReturnType, Unqual;
1620 
1621     enum FunctionMatchesType(alias Fun) = is(Unqual!(ReturnType!Fun) == Ret);
1622 
1623     alias MyFunctions = AliasSeq!(__traits(getOverloads, Aggregate, name));
1624     alias MatchingFunctions = Filter!(FunctionMatchesType, MyFunctions);
1625     enum hasFunction = MatchingFunctions.length == 1;
1626 
1627     static if (__traits(hasMember, Super, name))
1628     {
1629         alias SuperFunctions = AliasSeq!(__traits(getOverloads, Super, name));
1630     }
1631     else
1632     {
1633         alias SuperFunctions = AliasSeq!();
1634     }
1635     alias SuperMatchingFunctions = Filter!(FunctionMatchesType, SuperFunctions);
1636     enum superHasFunction = SuperMatchingFunctions.length == 1;
1637 
1638     static if (hasFunction)
1639     {
1640         static if (superHasFunction)
1641         {
1642             enum hasOwnFunction = !__traits(isSame, MatchingFunctions[0], SuperMatchingFunctions[0]);
1643         }
1644         else
1645         {
1646             enum hasOwnFunction = true;
1647         }
1648     }
1649     else
1650     {
1651         enum hasOwnFunction = false;
1652     }
1653 }
1654 
1655 /**
1656  * Find qualified name of `T` including any containing types; not including containing functions or modules.
1657  */
1658 public template typeName(T)
1659 {
1660     static if (__traits(compiles, __traits(parent, T)))
1661     {
1662         alias parent = Alias!(__traits(parent, T));
1663         enum isSame = __traits(isSame, T, parent);
1664 
1665         static if (!isSame && (
1666             is(parent == struct) || is(parent == union) || is(parent == enum) ||
1667             is(parent == class) || is(parent == interface)))
1668         {
1669             enum typeName = typeName!parent ~ "." ~ Unqual!T.stringof;
1670         }
1671         else
1672         {
1673             enum typeName = Unqual!T.stringof;
1674         }
1675     }
1676     else
1677     {
1678         enum typeName = Unqual!T.stringof;
1679     }
1680 }
1681 
1682 public alias hasOwnStringToString(T...) = hasOwn!(T, "toString", string);
1683 public alias hasOwnVoidToString(T...) = hasOwn!(T, "toString", void);
1684 public alias hasOwnToJson(T...) = hasOwn!(T, "toJson", JSONValue);
1685 
1686 private template hasOwn(Aggregate, Super, string name, Ret)
1687 if (is(Aggregate: Object))
1688 {
1689     enum hasOwn = hasOwnFunction!(Aggregate, Super, name, Ret);
1690 }
1691 
1692 private template hasOwn(Aggregate, string name, Ret)
1693 if (is(Aggregate == struct))
1694 {
1695     import std.traits : ReturnType;
1696 
1697     static if (is(ReturnType!(__traits(getMember, Aggregate.init, name)) == Ret))
1698     {
1699         enum hasOwn = !isFromAliasThis!(Aggregate, name, Ret);
1700     }
1701     else
1702     {
1703         enum hasOwn = false;
1704     }
1705 }
1706 
1707 public template isFromAliasThis(T, string member, Type)
1708 {
1709     import std.meta : AliasSeq, anySatisfy, Filter;
1710 
1711     enum FunctionMatchesType(alias Fun) = is(Unqual!(typeof(Fun)) == Type);
1712 
1713     private template isFromThatAliasThis(string field)
1714     {
1715         alias aliasMembers = AliasSeq!(__traits(getOverloads, __traits(getMember, T.init, field), member));
1716         alias ownMembers = AliasSeq!(__traits(getOverloads, T, member));
1717 
1718         enum bool isFromThatAliasThis = __traits(isSame,
1719             Filter!(FunctionMatchesType, aliasMembers),
1720             Filter!(FunctionMatchesType, ownMembers));
1721     }
1722 
1723     enum bool isFromAliasThis = anySatisfy!(isFromThatAliasThis, __traits(getAliasThis, T));
1724 }
1725 
1726 @("correctly recognizes the existence of string toString() in a class")
1727 unittest
1728 {
1729     class Class1
1730     {
1731         override string toString() { return null; }
1732         static assert(!hasOwnVoidToString!(typeof(this), typeof(super)));
1733         static assert(hasOwnStringToString!(typeof(this), typeof(super)));
1734     }
1735 
1736     class Class2
1737     {
1738         override string toString() const { return null; }
1739         static assert(!hasOwnVoidToString!(typeof(this), typeof(super)));
1740         static assert(hasOwnStringToString!(typeof(this), typeof(super)));
1741     }
1742 
1743     class Class3
1744     {
1745         void toString(scope void delegate(const(char)[]) sink) const { }
1746         override string toString() const { return null; }
1747         static assert(hasOwnVoidToString!(typeof(this), typeof(super)));
1748         static assert(hasOwnStringToString!(typeof(this), typeof(super)));
1749     }
1750 
1751     class Class4
1752     {
1753         void toString(scope void delegate(const(char)[]) sink) const { }
1754         static assert(hasOwnVoidToString!(typeof(this), typeof(super)));
1755         static assert(!hasOwnStringToString!(typeof(this), typeof(super)));
1756     }
1757 
1758     class Class5
1759     {
1760         mixin(GenerateToString);
1761     }
1762 
1763     class ChildClass1 : Class1
1764     {
1765         static assert(!hasOwnStringToString!(typeof(this), typeof(super)));
1766     }
1767 
1768     class ChildClass2 : Class2
1769     {
1770         static assert(!hasOwnStringToString!(typeof(this), typeof(super)));
1771     }
1772 
1773     class ChildClass3 : Class3
1774     {
1775         static assert(!hasOwnStringToString!(typeof(this), typeof(super)));
1776     }
1777 
1778     class ChildClass5 : Class5
1779     {
1780         static assert(!hasOwnStringToString!(typeof(this), typeof(super)));
1781     }
1782 }