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