1 module boilerplate.autostring;
2 
3 version(unittest)
4 {
5     import std.conv : to;
6     import std.datetime : SysTime;
7     import unit_threaded.should;
8 }
9 
10 /++
11 GenerateToString is a mixin string that automatically generates toString functions,
12 both sink-based and classic, customizable with UDA annotations on classes, members and functions.
13 +/
14 public enum string GenerateToString = `
15     import boilerplate.autostring : GenerateToStringTemplate;
16     mixin GenerateToStringTemplate;
17     mixin(typeof(this).generateToStringErrCheck());
18     mixin(typeof(this).generateToStringImpl());
19 `;
20 
21 /++
22 When used with objects, toString methods of type string toString() are also created.
23 +/
24 @("generates legacy toString on objects")
25 unittest
26 {
27     class Class
28     {
29         mixin(GenerateToString);
30     }
31 
32     (new Class).to!string.shouldEqual("Class()");
33     (new Class).toString.shouldEqual("Class()");
34 }
35 
36 /++
37 A trailing underline in member names is removed when labeling.
38 +/
39 @("removes trailing underline")
40 unittest
41 {
42     struct Struct
43     {
44         int a_;
45         mixin(GenerateToString);
46     }
47 
48     Struct.init.to!string.shouldEqual("Struct(a=0)");
49 }
50 
51 /++
52 The `@(ToString.Exclude)` tag can be used to exclude a member.
53 +/
54 @("can exclude a member")
55 unittest
56 {
57     struct Struct
58     {
59         @(ToString.Exclude)
60         int a;
61         mixin(GenerateToString);
62     }
63 
64     Struct.init.to!string.shouldEqual("Struct()");
65 }
66 
67 /++
68 The `@(ToString.Optional)` tag can be used to include a member only if it's in some form "present".
69 This means non-empty for arrays, non-null for objects, non-zero for ints.
70 +/
71 @("can optionally exclude member")
72 unittest
73 {
74     class Class
75     {
76         mixin(GenerateToString);
77     }
78 
79     struct Struct
80     {
81         @(ToString.Optional)
82         int a;
83         @(ToString.Optional)
84         string s;
85         @(ToString.Optional)
86         Class obj;
87         mixin(GenerateToString);
88     }
89 
90     Struct.init.to!string.shouldEqual("Struct()");
91     Struct(2, "hi", new Class).to!string.shouldEqual("Struct(a=2, s=hi, obj=Class())");
92     Struct(0, "", null).to!string.shouldEqual("Struct()");
93 }
94 
95 /++
96 The `@(ToString.Include)` tag can be used to explicitly include a member.
97 This is intended to be used on property methods.
98 +/
99 @("can include a method")
100 unittest
101 {
102     struct Struct
103     {
104         @(ToString.Include)
105         int foo() const { return 5; }
106         mixin(GenerateToString);
107     }
108 
109     Struct.init.to!string.shouldEqual("Struct(foo=5)");
110 }
111 
112 /++
113 The `@(ToString.Unlabeled)` tag will omit a field's name.
114 +/
115 @("can omit names")
116 unittest
117 {
118     struct Struct
119     {
120         @(ToString.Unlabeled)
121         int a;
122         mixin(GenerateToString);
123     }
124 
125     Struct.init.to!string.shouldEqual("Struct(0)");
126 }
127 
128 /++
129 Parent class `toString()` methods are included automatically as the first entry, except if the parent class is `Object`.
130 +/
131 @("can be used in both parent and child class")
132 unittest
133 {
134     class ParentClass { mixin(GenerateToString); }
135 
136     class ChildClass : ParentClass { mixin(GenerateToString); }
137 
138     (new ChildClass).to!string.shouldEqual("ChildClass(ParentClass())");
139 }
140 
141 @("invokes manually implemented parent toString")
142 unittest
143 {
144     class ParentClass
145     {
146         override string toString() const
147         {
148             return "Some string";
149         }
150     }
151 
152     class ChildClass : ParentClass { mixin(GenerateToString); }
153 
154     (new ChildClass).to!string.shouldEqual("ChildClass(Some string)");
155 }
156 
157 @("can partially override toString in child class")
158 unittest
159 {
160     class ParentClass
161     {
162         mixin(GenerateToString);
163     }
164 
165     class ChildClass : ParentClass
166     {
167         override string toString() const
168         {
169             return "Some string";
170         }
171 
172         mixin(GenerateToString);
173     }
174 
175     (new ChildClass).to!string.shouldEqual("Some string");
176 }
177 
178 @("invokes manually implemented string toString in same class")
179 unittest
180 {
181     class Class
182     {
183         override string toString() const
184         {
185             return "Some string";
186         }
187 
188         mixin(GenerateToString);
189     }
190 
191     (new Class).to!string.shouldEqual("Some string");
192 }
193 
194 @("invokes manually implemented void toString in same class")
195 unittest
196 {
197     class Class
198     {
199         void toString(scope void delegate(const(char)[]) sink) const
200         {
201             sink("Some string");
202         }
203 
204         mixin(GenerateToString);
205     }
206 
207     (new Class).to!string.shouldEqual("Some string");
208 }
209 
210 /++
211 Inclusion of parent class `toString()` can be prevented using `@(ToString.ExcludeSuper)`.
212 +/
213 @("can suppress parent class toString()")
214 unittest
215 {
216     class ParentClass { }
217 
218     @(ToString.ExcludeSuper)
219     class ChildClass : ParentClass { mixin(GenerateToString); }
220 
221     (new ChildClass).to!string.shouldEqual("ChildClass()");
222 }
223 
224 /++
225 The `@(ToString.Naked)` tag will omit the name of the type and parentheses.
226 +/
227 @("can omit the type name")
228 unittest
229 {
230     @(ToString.Naked)
231     struct Struct
232     {
233         int a;
234         mixin(GenerateToString);
235     }
236 
237     Struct.init.to!string.shouldEqual("a=0");
238 }
239 
240 /++
241 Fields with the same name (ignoring capitalization) as their type, are unlabeled by default.
242 +/
243 @("does not label fields with the same name as the type")
244 unittest
245 {
246     struct Struct1 { mixin(GenerateToString); }
247 
248     struct Struct2
249     {
250         Struct1 struct1;
251         mixin(GenerateToString);
252     }
253 
254     Struct2.init.to!string.shouldEqual("Struct2(Struct1())");
255 }
256 
257 @("does not label fields with the same name as the type, even if they're const")
258 unittest
259 {
260     struct Struct1 { mixin(GenerateToString); }
261 
262     struct Struct2
263     {
264         const Struct1 struct1;
265         mixin(GenerateToString);
266     }
267 
268     Struct2.init.to!string.shouldEqual("Struct2(Struct1())");
269 }
270 
271 /++
272 This behavior can be prevented by explicitly tagging the field with `@(ToString.Labeled)`.
273 +/
274 @("does label fields tagged as labeled")
275 unittest
276 {
277     struct Struct1 { mixin(GenerateToString); }
278 
279     struct Struct2
280     {
281         @(ToString.Labeled)
282         Struct1 struct1;
283         mixin(GenerateToString);
284     }
285 
286     Struct2.init.to!string.shouldEqual("Struct2(struct1=Struct1())");
287 }
288 
289 /++
290 Fields of type 'SysTime' and name 'time' are unlabeled by default.
291 +/
292 @("does not label SysTime time field correctly")
293 unittest
294 {
295     struct Struct { SysTime time; mixin(GenerateToString); }
296 
297     Struct strct;
298     strct.time = SysTime.fromISOExtString("2003-02-01T11:55:00Z");
299 
300     // see unittest/config/string.d
301     strct.to!string.shouldEqual("Struct(2003-02-01T11:55:00Z)");
302 }
303 
304 /++
305 Fields named 'id' are unlabeled only if they define their own toString().
306 +/
307 @("does not label id fields with toString()")
308 unittest
309 {
310     struct IdType
311     {
312         string toString() const { return "ID"; }
313     }
314 
315     struct Struct
316     {
317         IdType id;
318         mixin(GenerateToString);
319     }
320 
321     Struct.init.to!string.shouldEqual("Struct(ID)");
322 }
323 
324 /++
325 Otherwise, they are labeled as normal.
326 +/
327 @("labels id fields without toString")
328 unittest
329 {
330     struct Struct
331     {
332         int id;
333         mixin(GenerateToString);
334     }
335 
336     Struct.init.to!string.shouldEqual("Struct(id=0)");
337 }
338 
339 /++
340 Fields that are arrays with a name that is the pluralization of the array base type are also unlabeled by default.
341 +/
342 @("does not label fields with the same name as the type")
343 unittest
344 {
345     struct SomeValue { mixin(GenerateToString); }
346     struct Entity { mixin(GenerateToString); }
347     struct Day { mixin(GenerateToString); }
348 
349     struct Struct
350     {
351         SomeValue[] someValues;
352         Entity[] entities;
353         Day[] days;
354         mixin(GenerateToString);
355     }
356 
357     Struct.init.to!string.shouldEqual("Struct([], [], [])");
358 }
359 
360 /++
361 `GenerateToString` can be combined with `GenerateFieldAccessors` without issue.
362 +/
363 @("does not collide with accessors")
364 unittest
365 {
366     struct Struct
367     {
368         import boilerplate.accessors : GenerateFieldAccessors, ConstRead;
369 
370         @ConstRead
371         private int a_;
372 
373         mixin(GenerateFieldAccessors);
374 
375         mixin(GenerateToString);
376     }
377 
378     Struct.init.to!string.shouldEqual("Struct(a=0)");
379 }
380 
381 @("supports child classes of abstract classes")
382 unittest
383 {
384     static abstract class ParentClass
385     {
386     }
387     class ChildClass : ParentClass
388     {
389         mixin(GenerateToString);
390     }
391 }
392 
393 @("supports custom toString handlers")
394 unittest
395 {
396     struct Struct
397     {
398         @ToStringHandler!(i => i ? "yes" : "no")
399         int i;
400 
401         mixin(GenerateToString);
402     }
403 
404     Struct.init.to!string.shouldEqual("Struct(i=no)");
405 }
406 
407 @("passes nullable unchanged to custom toString handlers")
408 unittest
409 {
410     import std.typecons : Nullable;
411 
412     struct Struct
413     {
414         @ToStringHandler!(ni => ni.isNull ? "no" : "yes")
415         Nullable!int ni;
416 
417         mixin(GenerateToString);
418     }
419 
420     Struct.init.to!string.shouldEqual("Struct(ni=no)");
421 }
422 
423 mixin template GenerateToStringTemplate()
424 {
425 
426     // this is a separate function to reduce the
427     // "warning: unreachable code" spam that is falsely created from static foreach
428     private static generateToStringErrCheck()
429     {
430         if (!__ctfe)
431         {
432             return null;
433         }
434 
435         import boilerplate.autostring : ToString;
436         import boilerplate.util : GenNormalMemberTuple;
437         import std.string : format;
438 
439         bool udaIncludeSuper;
440         bool udaExcludeSuper;
441 
442         foreach (uda; __traits(getAttributes, typeof(this)))
443         {
444             static if (is(typeof(uda) == ToString))
445             {
446                 switch (uda)
447                 {
448                     case ToString.IncludeSuper: udaIncludeSuper = true; break;
449                     case ToString.ExcludeSuper: udaExcludeSuper = true; break;
450                     default: break;
451                 }
452             }
453         }
454 
455         if (udaIncludeSuper && udaExcludeSuper)
456         {
457             return format!`static assert(false, "Contradictory tags on '%s': IncludeSuper and ExcludeSuper");`
458                 (typeof(this).stringof);
459         }
460 
461         mixin GenNormalMemberTuple!true;
462 
463         foreach (member; NormalMemberTuple)
464         {
465             enum error = checkAttributeConsistency!(__traits(getAttributes, __traits(getMember, typeof(this), member)));
466 
467             static if (error)
468             {
469                 return format!error(member);
470             }
471         }
472 
473         return ``;
474     }
475 
476     private static generateToStringImpl()
477     {
478         if (!__ctfe)
479         {
480             return null;
481         }
482 
483         import std.string : endsWith, format, split, startsWith, strip;
484         import std.traits : BaseClassesTuple, Unqual, getUDAs;
485         import boilerplate.autostring : ToString, isMemberUnlabeledByDefault;
486         import boilerplate.util : GenNormalMemberTuple, udaIndex;
487 
488         // synchronized without lock contention is basically free, so always do it
489         // TODO enable when https://issues.dlang.org/show_bug.cgi?id=18504 is fixed
490         enum synchronize = false && is(typeof(this) == class);
491 
492         const constExample = typeof(this).init;
493         auto normalExample = typeof(this).init;
494 
495         enum alreadyHaveStringToString = __traits(hasMember, typeof(this), "toString")
496             && is(typeof(normalExample.toString()) == string);
497         enum alreadyHaveUsableStringToString = alreadyHaveStringToString
498             && is(typeof(constExample.toString()) == string);
499 
500         enum alreadyHaveVoidToString = __traits(hasMember, typeof(this), "toString")
501             && is(typeof(normalExample.toString((void delegate(const(char)[])).init)) == void);
502         enum alreadyHaveUsableVoidToString = alreadyHaveVoidToString
503             && is(typeof(constExample.toString((void delegate(const(char)[])).init)) == void);
504 
505         enum isObject = is(typeof(this): Object);
506 
507         static if (isObject)
508         {
509             enum userDefinedStringToString = hasOwnStringToString!(typeof(this), typeof(super));
510             enum userDefinedVoidToString = hasOwnVoidToString!(typeof(this), typeof(super));
511         }
512         else
513         {
514             enum userDefinedStringToString = alreadyHaveStringToString;
515             enum userDefinedVoidToString = alreadyHaveVoidToString;
516         }
517 
518         static if (userDefinedStringToString && userDefinedVoidToString)
519         {
520             string result = ``; // Nothing to be done.
521         }
522         // if the user has defined their own string toString() in this aggregate:
523         else static if (userDefinedStringToString)
524         {
525             // just call it.
526             static if (alreadyHaveUsableStringToString)
527             {
528                 string result = `public void toString(scope void delegate(const(char)[]) sink) const {` ~
529                     ` sink(this.toString());` ~
530                     ` }`;
531 
532                 static if (isObject
533                     && is(typeof(typeof(super).init.toString((void delegate(const(char)[])).init)) == void))
534                 {
535                     result = `override ` ~ result;
536                 }
537             }
538             else
539             {
540                 string result = `static assert(false, "toString is not const in this class.");`;
541             }
542         }
543         // if the user has defined their own void toString() in this aggregate:
544         else
545         {
546             string result = null;
547 
548             static if (!userDefinedVoidToString)
549             {
550                 bool nakedMode;
551                 bool udaIncludeSuper;
552                 bool udaExcludeSuper;
553 
554                 foreach (uda; __traits(getAttributes, typeof(this)))
555                 {
556                     static if (is(typeof(uda) == ToString))
557                     {
558                         switch (uda)
559                         {
560                             case ToString.Naked: nakedMode = true; break;
561                             case ToString.IncludeSuper: udaIncludeSuper = true; break;
562                             case ToString.ExcludeSuper: udaExcludeSuper = true; break;
563                             default: break;
564                         }
565                     }
566                 }
567 
568                 string NamePlusOpenParen = Unqual!(typeof(this)).stringof ~ "(";
569 
570                 version(AutoStringDebug)
571                 {
572                     result ~= format!`pragma(msg, "%s %s");`(alreadyHaveStringToString, alreadyHaveVoidToString);
573                 }
574 
575                 static if (isObject && alreadyHaveVoidToString) result ~= `override `;
576 
577                 result ~= `public void toString(scope void delegate(const(char)[]) sink) const {`
578                     ~ `import boilerplate.autostring: ToStringHandler;`
579                     ~ `import boilerplate.util: sinkWrite;`
580                     ~ `import std.traits: getUDAs;`;
581 
582                 static if (synchronize)
583                 {
584                     result ~= `synchronized (this) { `;
585                 }
586 
587                 if (!nakedMode)
588                 {
589                     result ~= `sink("` ~ NamePlusOpenParen ~ `");`;
590                 }
591 
592                 bool includeSuper = false;
593 
594                 static if (isObject)
595                 {
596                     if (alreadyHaveUsableStringToString || alreadyHaveUsableVoidToString)
597                     {
598                         includeSuper = true;
599                     }
600                 }
601 
602                 if (udaIncludeSuper)
603                 {
604                     includeSuper = true;
605                 }
606                 else if (udaExcludeSuper)
607                 {
608                     includeSuper = false;
609                 }
610 
611                 static if (isObject)
612                 {
613                     if (includeSuper)
614                     {
615                         static if (!alreadyHaveUsableStringToString && !alreadyHaveUsableVoidToString)
616                         {
617                             return `static assert(false, `
618                                 ~ `"cannot include super class in GenerateToString: `
619                                 ~ `parent class has no usable toString!");`;
620                         }
621                         else {
622                             static if (alreadyHaveUsableVoidToString)
623                             {
624                                 result ~= `super.toString(sink);`;
625                             }
626                             else
627                             {
628                                 result ~= `sink(super.toString());`;
629                             }
630                             result ~= `bool comma = true;`;
631                         }
632                     }
633                     else
634                     {
635                         result ~= `bool comma = false;`;
636                     }
637                 }
638                 else
639                 {
640                     result ~= `bool comma = false;`;
641                 }
642 
643                 result ~= `{`;
644 
645                 mixin GenNormalMemberTuple!(true);
646 
647                 foreach (member; NormalMemberTuple)
648                 {
649                     mixin("alias symbol = this." ~ member ~ ";");
650 
651                     enum udaInclude = udaIndex!(ToString.Include, __traits(getAttributes, symbol)) != -1;
652                     enum udaExclude = udaIndex!(ToString.Exclude, __traits(getAttributes, symbol)) != -1;
653                     enum udaLabeled = udaIndex!(ToString.Labeled, __traits(getAttributes, symbol)) != -1;
654                     enum udaUnlabeled = udaIndex!(ToString.Unlabeled, __traits(getAttributes, symbol)) != -1;
655                     enum udaOptional = udaIndex!(ToString.Optional, __traits(getAttributes, symbol)) != -1;
656                     enum udaToStringHandler = udaIndex!(ToStringHandler, __traits(getAttributes, symbol)) != -1;
657 
658                     // see std.traits.isFunction!()
659                     static if (is(symbol == function) || is(typeof(symbol) == function)
660                         || is(typeof(&symbol) U : U*) && is(U == function))
661                     {
662                         enum isFunction = true;
663                     }
664                     else
665                     {
666                         enum isFunction = false;
667                     }
668 
669                     enum includeOverride = udaInclude || udaOptional;
670 
671                     enum includeMember = (!isFunction || includeOverride) && !udaExclude;
672 
673                     static if (includeMember)
674                     {
675                         string memberName = member;
676 
677                         if (memberName.endsWith("_"))
678                         {
679                             memberName = memberName[0 .. $ - 1];
680                         }
681 
682                         bool labeled = true;
683 
684                         static if (udaUnlabeled)
685                         {
686                             labeled = false;
687                         }
688 
689                         if (isMemberUnlabeledByDefault!(Unqual!(typeof(symbol)))(memberName))
690                         {
691                             labeled = false;
692                         }
693 
694                         static if (udaLabeled)
695                         {
696                             labeled = true;
697                         }
698 
699                         string membervalue = `this.` ~ member;
700 
701                         static if (udaToStringHandler)
702                         {
703                             alias Handlers = getUDAs!(symbol, ToStringHandler);
704 
705                             static assert(Handlers.length == 1);
706 
707                             static if (__traits(compiles, Handlers[0].Handler(typeof(symbol).init)))
708                             {
709                                 membervalue = `getUDAs!(this.` ~ member ~ `, ToStringHandler)[0].Handler(`
710                                     ~ membervalue
711                                     ~ `)`;
712                             }
713                             else
714                             {
715                                 return `static assert(false, "cannot determine how to call ToStringHandler");`;
716                             }
717                         }
718 
719                         string writestmt;
720 
721                         if (labeled)
722                         {
723                             writestmt = format!`sink.sinkWrite(comma, "%s=%%s", %s);`
724                                 (memberName, membervalue);
725                         }
726                         else
727                         {
728                             writestmt = format!`sink.sinkWrite(comma, "%%s", %s);`(membervalue);
729                         }
730 
731                         static if (udaOptional)
732                         {
733                             import std.array : empty;
734 
735                             static if (__traits(compiles, typeof(symbol).init.empty))
736                             {
737                                 result ~= format!`import std.array : empty; if (!%s.empty) { %s }`
738                                     (membervalue, writestmt);
739                             }
740                             else static if (__traits(compiles, typeof(symbol).init !is null))
741                             {
742                                 result ~= format!`if (%s !is null) { %s }`
743                                     (membervalue, writestmt);
744                             }
745                             else static if (__traits(compiles, typeof(symbol).init != 0))
746                             {
747                                 result ~= format!`if (%s != 0) { %s }`
748                                     (membervalue, writestmt);
749                             }
750                             else
751                             {
752                                 return format!(`static assert(false, `
753                                         ~ `"don't know how to figure out whether %s is present.");`)
754                                     (member);
755                             }
756                         }
757                         else
758                         {
759                             result ~= writestmt;
760                         }
761                     }
762                 }
763 
764                 result ~= `} `;
765 
766                 if (!nakedMode)
767                 {
768                     result ~= `sink(")");`;
769                 }
770 
771                 static if (synchronize)
772                 {
773                     result ~= `} `;
774                 }
775 
776                 result ~= `} `;
777             }
778 
779             // generate fallback string toString()
780             // that calls, specifically, *our own* toString impl.
781             // (this is important to break cycles when a subclass implements a toString that calls super.toString)
782             static if (isObject)
783             {
784                 result ~= `override `;
785             }
786 
787             result ~= `public string toString() const {`
788                 ~ `string result;`
789                 ~ `typeof(this).toString((const(char)[] part) { result ~= part; });`
790                 ~ `return result;`
791             ~ `}`;
792         }
793         return result;
794     }
795 }
796 
797 template checkAttributeConsistency(Attributes...)
798 {
799     enum checkAttributeConsistency = checkAttributeHelper();
800 
801     private string checkAttributeHelper()
802     {
803         if (!__ctfe)
804         {
805             return null;
806         }
807 
808         import std.string : format;
809 
810         bool include, exclude, optional, labeled, unlabeled;
811 
812         foreach (uda; Attributes)
813         {
814             static if (is(typeof(uda) == ToString))
815             {
816                 switch (uda)
817                 {
818                     case ToString.Include: include = true; break;
819                     case ToString.Exclude: exclude = true; break;
820                     case ToString.Optional: optional = true; break;
821                     case ToString.Labeled: labeled = true; break;
822                     case ToString.Unlabeled: unlabeled = true; break;
823                     default: break;
824                 }
825             }
826         }
827 
828         if (include && exclude)
829         {
830             return `static assert(false, "Contradictory tags on '%s': Include and Exclude");`;
831         }
832 
833         if (include && optional)
834         {
835             return `static assert(false, "Redundant tags on '%s': Optional implies Include");`;
836         }
837 
838         if (exclude && optional)
839         {
840             return `static assert(false, "Contradictory tags on '%s': Exclude and Optional");`;
841         }
842 
843         if (labeled && unlabeled)
844         {
845             return `static assert(false, "Contradictory tags on '%s': Labeled and Unlabeled");`;
846         }
847 
848         return null;
849     }
850 }
851 
852 struct ToStringHandler(alias Handler_)
853 {
854     alias Handler = Handler_;
855 }
856 
857 enum ToString
858 {
859     // these go on the class
860     Naked,
861     IncludeSuper,
862     ExcludeSuper,
863 
864     // these go on the field/method
865     Unlabeled,
866     Labeled,
867     Exclude,
868     Include,
869     Optional,
870 }
871 
872 public bool isMemberUnlabeledByDefault(Type)(string field)
873 {
874     import std.string : toLower;
875     import std.range.primitives : ElementType, isInputRange;
876 
877     static if (isInputRange!Type)
878     {
879         alias BaseType = ElementType!Type;
880 
881         if (field.toLower == BaseType.stringof.toLower.pluralize)
882         {
883             return true;
884         }
885     }
886 
887     return field.toLower == Type.stringof.toLower
888         || field.toLower == "time" && Type.stringof == "SysTime"
889         || field.toLower == "id" && is(typeof(Type.toString));
890 }
891 
892 // http://code.activestate.com/recipes/82102/
893 private string pluralize(string label)
894 {
895     import std.algorithm.searching : contain = canFind;
896 
897     string postfix = "s";
898     if (label.length > 2)
899     {
900         enum vowels = "aeiou";
901 
902         if (label.stringEndsWith("ch") || label.stringEndsWith("sh"))
903         {
904             postfix = "es";
905         }
906         else if (auto before = label.stringEndsWith("y"))
907         {
908             if (!vowels.contain(label[$ - 2]))
909             {
910                 postfix = "ies";
911                 label = before;
912             }
913         }
914         else if (auto before = label.stringEndsWith("is"))
915         {
916             postfix = "es";
917             label = before;
918         }
919         else if ("sxz".contain(label[$-1]))
920         {
921             postfix = "es"; // glasses
922         }
923     }
924     return label ~ postfix;
925 }
926 
927 @("has functioning pluralize()")
928 unittest
929 {
930     "dog".pluralize.shouldEqual("dogs");
931     "ash".pluralize.shouldEqual("ashes");
932     "day".pluralize.shouldEqual("days");
933     "entity".pluralize.shouldEqual("entities");
934     "thesis".pluralize.shouldEqual("theses");
935     "glass".pluralize.shouldEqual("glasses");
936 }
937 
938 private string stringEndsWith(const string text, const string suffix)
939 {
940     import std.range : dropBack;
941     import std.string : endsWith;
942 
943     if (text.endsWith(suffix))
944     {
945         return text.dropBack(suffix.length);
946     }
947     return null;
948 }
949 
950 @("has functioning stringEndsWith()")
951 unittest
952 {
953     "".stringEndsWith("").shouldNotBeNull;
954     "".stringEndsWith("x").shouldBeNull;
955     "Hello".stringEndsWith("Hello").shouldNotBeNull;
956     "Hello".stringEndsWith("Hello").shouldEqual("");
957     "Hello".stringEndsWith("lo").shouldEqual("Hel");
958 }
959 
960 template hasOwnFunction(Aggregate, Super, string Name, Type)
961 {
962     import std.meta : AliasSeq, Filter;
963     import std.traits : Unqual;
964     enum FunctionMatchesType(alias Fun) = is(Unqual!(typeof(Fun)) == Type);
965 
966     alias MyFunctions = AliasSeq!(__traits(getOverloads, Aggregate, Name));
967     alias MatchingFunctions = Filter!(FunctionMatchesType, MyFunctions);
968     enum hasFunction = MatchingFunctions.length == 1;
969 
970     alias SuperFunctions = AliasSeq!(__traits(getOverloads, Super, Name));
971     alias SuperMatchingFunctions = Filter!(FunctionMatchesType, SuperFunctions);
972     enum superHasFunction = SuperMatchingFunctions.length == 1;
973 
974     static if (hasFunction)
975     {
976         static if (superHasFunction)
977         {
978             enum hasOwnFunction = !__traits(isSame, MatchingFunctions[0], SuperMatchingFunctions[0]);
979         }
980         else
981         {
982             enum hasOwnFunction = true;
983         }
984     }
985     else
986     {
987         enum hasOwnFunction = false;
988     }
989 }
990 
991 private final abstract class StringToStringSample {
992     override string toString();
993 }
994 
995 private final abstract class VoidToStringSample {
996     void toString(scope void delegate(const(char)[]) sink);
997 }
998 
999 enum hasOwnStringToString(Aggregate, Super)
1000     = hasOwnFunction!(Aggregate, Super, "toString", typeof(StringToStringSample.toString));
1001 
1002 enum hasOwnVoidToString(Aggregate, Super)
1003     = hasOwnFunction!(Aggregate, Super, "toString", typeof(VoidToStringSample.toString));
1004 
1005 @("correctly recognizes the existence of string toString() in a class")
1006 unittest
1007 {
1008     class Class1
1009     {
1010         override string toString() { return null; }
1011         static assert(!hasOwnVoidToString!(typeof(this), typeof(super)));
1012         static assert(hasOwnStringToString!(typeof(this), typeof(super)));
1013     }
1014 
1015     class Class2
1016     {
1017         override string toString() const { return null; }
1018         static assert(!hasOwnVoidToString!(typeof(this), typeof(super)));
1019         static assert(hasOwnStringToString!(typeof(this), typeof(super)));
1020     }
1021 
1022     class Class3
1023     {
1024         void toString(scope void delegate(const(char)[]) sink) const { }
1025         override string toString() const { return null; }
1026         static assert(hasOwnVoidToString!(typeof(this), typeof(super)));
1027         static assert(hasOwnStringToString!(typeof(this), typeof(super)));
1028     }
1029 
1030     class Class4
1031     {
1032         void toString(scope void delegate(const(char)[]) sink) const { }
1033         static assert(hasOwnVoidToString!(typeof(this), typeof(super)));
1034         static assert(!hasOwnStringToString!(typeof(this), typeof(super)));
1035     }
1036 
1037     class Class5
1038     {
1039         mixin(GenerateToString);
1040     }
1041 
1042     class ChildClass1 : Class1
1043     {
1044         static assert(!hasOwnStringToString!(typeof(this), typeof(super)));
1045     }
1046 
1047     class ChildClass2 : Class2
1048     {
1049         static assert(!hasOwnStringToString!(typeof(this), typeof(super)));
1050     }
1051 
1052     class ChildClass3 : Class3
1053     {
1054         static assert(!hasOwnStringToString!(typeof(this), typeof(super)));
1055     }
1056 
1057     class ChildClass5 : Class5
1058     {
1059         static assert(!hasOwnStringToString!(typeof(this), typeof(super)));
1060     }
1061 }