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