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