1 module boilerplate.constructor;
2 
3 version(unittest)
4 {
5     import unit_threaded.should;
6 }
7 
8 /++
9 GenerateThis is a mixin string that automatically generates a this() function, customizable with UDA.
10 +/
11 public enum string GenerateThis = `
12     import boilerplate.constructor: GenerateThisTemplate, Default, getUDADefaultOrNothing;
13     import boilerplate.util: udaIndex;
14     import std.meta : AliasSeq;
15     mixin GenerateThisTemplate;
16     mixin(typeof(this).generateThisImpl());
17 `;
18 
19 ///
20 @("creates a constructor")
21 unittest
22 {
23     class Class
24     {
25         int field;
26 
27         mixin(GenerateThis);
28     }
29 
30     auto obj = new Class(5);
31 
32     obj.field.shouldEqual(5);
33 }
34 
35 ///
36 @("calls the super constructor if it exists")
37 unittest
38 {
39     class Class
40     {
41         int field;
42 
43         mixin(GenerateThis);
44     }
45 
46     class Child : Class
47     {
48         int field2;
49 
50         mixin(GenerateThis);
51     }
52 
53     auto obj = new Child(5, 8);
54 
55     obj.field.shouldEqual(5);
56     obj.field2.shouldEqual(8);
57 }
58 
59 ///
60 @("separates fields from methods")
61 unittest
62 {
63     class Class
64     {
65         int field;
66 
67         void method() { }
68 
69         mixin(GenerateThis);
70     }
71 
72     auto obj = new Class(5);
73 
74     obj.field.shouldEqual(5);
75 }
76 
77 ///
78 @("dups arrays")
79 unittest
80 {
81     class Class
82     {
83         int[] array;
84 
85         mixin(GenerateThis);
86     }
87 
88     auto array = [2, 3, 4];
89     auto obj = new Class(array);
90 
91     array[0] = 1;
92     obj.array[0].shouldEqual(2);
93 }
94 
95 ///
96 @("dups arrays hidden behind Nullable")
97 unittest
98 {
99     import std.typecons : Nullable, nullable;
100 
101     class Class
102     {
103         Nullable!(int[]) array;
104 
105         mixin(GenerateThis);
106     }
107 
108     auto array = [2, 3, 4];
109     auto obj = new Class(array.nullable);
110 
111     array[0] = 1;
112     obj.array.get[0].shouldEqual(2);
113 
114     obj = new Class(Nullable!(int[]).init);
115     obj.array.isNull.shouldBeTrue;
116 }
117 
118 ///
119 @("uses default value for default constructor parameter")
120 unittest
121 {
122     class Class
123     {
124         @Default!5
125         int value = 5;
126 
127         mixin(GenerateThis);
128     }
129 
130     auto obj1 = new Class();
131 
132     obj1.value.shouldEqual(5);
133 
134     auto obj2 = new Class(6);
135 
136     obj2.value.shouldEqual(6);
137 }
138 
139 ///
140 @("creates no constructor for an empty struct")
141 unittest
142 {
143     struct Struct
144     {
145         mixin(GenerateThis);
146     }
147 
148     auto strct = Struct();
149 }
150 
151 ///
152 @("properly generates new default values on each call")
153 unittest
154 {
155     import std.conv : to;
156 
157     class Class
158     {
159         @Default!(() => new Object)
160         Object obj;
161 
162         mixin(GenerateThis);
163     }
164 
165     auto obj1 = new Class();
166     auto obj2 = new Class();
167 
168     (cast(void*) obj1.obj).shouldNotEqual(cast(void*) obj2.obj);
169 }
170 
171 ///
172 @("establishes the parent-child parameter order: parent explicit, child explicit, child implicit, parent implicit.")
173 unittest
174 {
175     class Parent
176     {
177         int field1;
178 
179         @Default!2
180         int field2 = 2;
181 
182         mixin(GenerateThis);
183     }
184 
185     class Child : Parent
186     {
187         int field3;
188 
189         @Default!4
190         int field4 = 4;
191 
192         mixin(GenerateThis);
193     }
194 
195     auto obj = new Child(1, 2, 3, 4);
196 
197     obj.field1.shouldEqual(1);
198     obj.field3.shouldEqual(2);
199     obj.field4.shouldEqual(3);
200     obj.field2.shouldEqual(4);
201 }
202 
203 ///
204 @("disregards static fields")
205 unittest
206 {
207     class Class
208     {
209         static int field1;
210         int field2;
211 
212         mixin(GenerateThis);
213     }
214 
215     auto obj = new Class(5);
216 
217     obj.field1.shouldEqual(0);
218     obj.field2.shouldEqual(5);
219 }
220 
221 ///
222 @("can initialize with immutable arrays")
223 unittest
224 {
225     class Class
226     {
227         immutable(Object)[] array;
228 
229         mixin(GenerateThis);
230     }
231 }
232 
233 ///
234 @("can define scope for constructor")
235 unittest
236 {
237     @(This.Private)
238     class PrivateClass
239     {
240         mixin(GenerateThis);
241     }
242 
243     @(This.Protected)
244     class ProtectedClass
245     {
246         mixin(GenerateThis);
247     }
248 
249     @(This.Package)
250     class PackageClass
251     {
252         mixin(GenerateThis);
253     }
254 
255     class PublicClass
256     {
257         mixin(GenerateThis);
258     }
259 
260     static assert(__traits(getProtection, PrivateClass.__ctor) == "private");
261     static assert(__traits(getProtection, ProtectedClass.__ctor) == "protected");
262     static assert(__traits(getProtection, PackageClass.__ctor) == "package");
263     static assert(__traits(getProtection, PublicClass.__ctor) == "public");
264 }
265 
266 ///
267 @("can use default tag with new")
268 unittest
269 {
270     class Class
271     {
272         @Default!(() => new Object)
273         Object foo;
274 
275         mixin(GenerateThis);
276     }
277 
278     ((new Class).foo !is null).shouldBeTrue;
279 }
280 
281 ///
282 @("can exclude fields from constructor")
283 unittest
284 {
285     class Class
286     {
287         @(This.Exclude)
288         int i = 5;
289 
290         mixin(GenerateThis);
291     }
292 
293     (new Class).i.shouldEqual(5);
294 }
295 
296 ///
297 @("marks duppy parameters as const when this does not prevent dupping")
298 unittest
299 {
300 
301     struct Struct
302     {
303     }
304 
305     class Class
306     {
307         Struct[] values_;
308 
309         mixin(GenerateThis);
310     }
311 
312     const Struct[] constValues;
313     auto obj = new Class(constValues);
314 }
315 
316 ///
317 @("does not include property functions in constructor list")
318 unittest
319 {
320     class Class
321     {
322         int a;
323 
324         @property int foo() const
325         {
326             return 0;
327         }
328 
329         mixin(GenerateThis);
330     }
331 
332     static assert(__traits(compiles, new Class(0)));
333     static assert(!__traits(compiles, new Class(0, 0)));
334 }
335 
336 @("declares @nogc on non-dupping constructors")
337 @nogc unittest
338 {
339     struct Struct
340     {
341         int a;
342 
343         mixin(GenerateThis);
344     }
345 
346     auto str = Struct(5);
347 }
348 
349 import std.string : format;
350 
351 enum GetSuperTypeAsString_(size_t Index) = format!`typeof(super).GeneratedConstructorTypes_[%s]`(Index);
352 
353 enum GetMemberTypeAsString_(string Member) = format!`typeof(this.%s)`(Member);
354 
355 enum SuperDefault_(size_t Index) = format!`typeof(super).GeneratedConstructorDefaults_[%s]`(Index);
356 
357 enum MemberDefault_(string Member) =
358     format!`getUDADefaultOrNothing!(__traits(getAttributes, this.%s))`(Member);
359 
360 mixin template GenerateThisTemplate()
361 {
362     private static generateThisImpl()
363     {
364         if (!__ctfe)
365         {
366             return null;
367         }
368 
369         import boilerplate.constructor : GetMemberTypeAsString_, GetSuperTypeAsString_,
370             MemberDefault_, SuperDefault_, This;
371         import boilerplate.util : GenNormalMemberTuple, bucketSort, needToDup, udaIndex;
372         import std.meta : aliasSeqOf, staticMap;
373         import std.range : array, drop, iota;
374         import std.string : endsWith, format, join;
375         import std.typecons : Nullable;
376 
377         mixin GenNormalMemberTuple;
378 
379         string result = null;
380 
381         string visibility = "public";
382 
383         foreach (uda; __traits(getAttributes, typeof(this)))
384         {
385             static if (is(typeof(uda) == This))
386             {
387                 static if (uda == This.Protected)
388                 {
389                     visibility = "protected";
390                 }
391                 static if (uda == This.Package)
392                 {
393                     visibility = "package";
394                 }
395                 static if (uda == This.Private)
396                 {
397                     visibility = "private";
398                 }
399             }
400         }
401 
402         enum MemberUseDefault(string Member)
403             = mixin(format!`udaIndex!(Default, __traits(getAttributes, this.%s)) != -1`(Member));
404 
405         static if (is(typeof(typeof(super).GeneratedConstructorFields_)))
406         {
407             enum argsPassedToSuper = typeof(super).GeneratedConstructorFields_.length;
408             enum members = typeof(super).GeneratedConstructorFields_ ~ [NormalMemberTuple];
409             enum useDefaults = typeof(super).GeneratedConstructorUseDefaults_
410                 ~ [staticMap!(MemberUseDefault, NormalMemberTuple)];
411             enum CombinedArray(alias SuperPred, alias MemberPred) = [
412                 staticMap!(SuperPred, aliasSeqOf!(typeof(super).GeneratedConstructorTypes_.length.iota)),
413                 staticMap!(MemberPred, NormalMemberTuple)
414             ];
415         }
416         else
417         {
418             enum argsPassedToSuper = 0;
419             static if (NormalMemberTuple.length > 0)
420             {
421                 enum members = [NormalMemberTuple];
422                 enum useDefaults = [staticMap!(MemberUseDefault, NormalMemberTuple)];
423                 enum CombinedArray(alias SuperPred, alias MemberPred) = [staticMap!(MemberPred, NormalMemberTuple)];
424             }
425             else
426             {
427                 enum string[] members = null;
428                 enum bool[] useDefaults = null;
429                 enum string[] CombinedArray(alias SuperPred, alias MemberPred) = null;
430             }
431         }
432 
433         enum memberTypes = CombinedArray!(GetSuperTypeAsString_, GetMemberTypeAsString_);
434         enum defaults = CombinedArray!(SuperDefault_, MemberDefault_);
435 
436         bool[] passAsConst;
437         string[] fields;
438         string[] quotedFields;
439         string[] args;
440         string[] argexprs;
441         string[] defaultAssignments;
442         string[] useDefaultsStr;
443         string[] types;
444 
445         bool anyDups = false;
446 
447         foreach (i; aliasSeqOf!(members.length.iota))
448         {
449             enum member = members[i];
450 
451             mixin(`alias Type = ` ~ memberTypes[i] ~ `;`);
452 
453             bool includeMember = false;
454 
455             enum isNullable = mixin(format!`is(%s: Template!Args, alias Template = Nullable, Args...)`(memberTypes[i]));
456 
457             static if (!isNullable)
458             {
459                 bool dupExpr = needToDup!Type;
460                 bool passExprAsConst = dupExpr && __traits(compiles, const(Type).init.dup);
461             }
462             else
463             {
464                 // unpack nullable for dup
465                 bool dupExpr = needToDup!(typeof(Type.init.get));
466                 bool passExprAsConst = dupExpr && __traits(compiles, Type(const(Type).init.get.dup));
467             }
468 
469             bool forSuper = false;
470 
471             static if (i < argsPassedToSuper)
472             {
473                 includeMember = true;
474                 forSuper = true;
475             }
476             else
477             {
478                 mixin("alias symbol = this." ~ member ~ ";");
479 
480                 static assert (is(typeof(symbol)) && !__traits(isTemplate, symbol)); /* must have a resolvable type */
481 
482                 import boilerplate.util: isStatic;
483 
484                 includeMember = true;
485 
486                 if (mixin(isStatic(member)))
487                 {
488                     includeMember = false;
489                 }
490 
491                 static if (udaIndex!(This.Exclude, __traits(getAttributes, symbol)) != -1)
492                 {
493                     includeMember = false;
494                 }
495             }
496 
497             if (!includeMember) continue;
498 
499             string paramName;
500 
501             if (member.endsWith("_"))
502             {
503                 paramName = member[0 .. $ - 1];
504             }
505             else
506             {
507                 paramName = member;
508             }
509 
510             string argexpr = paramName;
511 
512             if (dupExpr)
513             {
514                 anyDups = true;
515 
516                 static if (isNullable)
517                 {
518                     argexpr = format!`%s.isNull ? %s.init : %s(%s.get.dup)`
519                         (argexpr, memberTypes[i], memberTypes[i], argexpr);
520                 }
521                 else
522                 {
523                     argexpr = format!`%s.dup`(argexpr);
524                 }
525             }
526 
527             passAsConst ~= passExprAsConst;
528             fields ~= member;
529             quotedFields ~= `"` ~ member ~ `"`;
530             args ~= paramName;
531             argexprs ~= argexpr;
532             defaultAssignments ~= useDefaults[i] ? (` = ` ~ defaults[i]) : ``;
533             useDefaultsStr ~= useDefaults[i] ? `true` : `false`;
534             types ~= passExprAsConst ? (`const ` ~ memberTypes[i]) : memberTypes[i];
535         }
536 
537         result ~= visibility ~ ` this(`;
538 
539         int establishParameterRank(size_t i)
540         {
541             // parent explicit, our explicit, our implicit, parent implicit
542             const fieldOfParent = i < argsPassedToSuper;
543             return useDefaults[i] * 2 + (useDefaults[i] == fieldOfParent);
544         }
545 
546         foreach (k, i; fields.length.iota.array.bucketSort(&establishParameterRank))
547         {
548             auto type = types[i];
549 
550             if (k > 0)
551             {
552                 result ~= `, `;
553             }
554 
555             result ~= type ~ ` ` ~ args[i] ~ defaultAssignments[i];
556         }
557 
558         result ~= format!`) pure %snothrow @safe {`(anyDups ? `` : `@nogc `);
559 
560         static if (is(typeof(typeof(super).GeneratedConstructorFields_)))
561         {
562             result ~= `super(` ~ args[0 .. argsPassedToSuper].join(", ") ~ `);`;
563         }
564 
565         foreach (i; fields.length.iota.drop(argsPassedToSuper))
566         {
567             auto field = fields[i];
568             auto argexpr = argexprs[i];
569 
570             result ~= `this.` ~ field ~ ` = ` ~ argexpr ~ `;`;
571         }
572 
573         result ~= `}`;
574 
575         static if (!is(typeof(this) == struct))
576         {
577             // don't emit inheritance info for structs
578             result ~= `protected static enum string[] GeneratedConstructorFields_ = [`
579                 ~ quotedFields.join(`, `)
580                 ~ `];`;
581 
582             result ~= `protected static alias GeneratedConstructorTypes_ = AliasSeq!(`
583                 ~ types.join(`, `)
584                 ~ `);`;
585 
586             result ~= `protected static enum bool[] GeneratedConstructorUseDefaults_ = [`
587                 ~ useDefaultsStr.join(`, `)
588                 ~ `];`;
589 
590             result ~= `protected static alias GeneratedConstructorDefaults_ = AliasSeq!(`
591                 ~ defaults.join(`, `)
592                 ~ `);`;
593         }
594         else
595         {
596             if (fields.length == 0)
597             {
598                 // don't generate empty constructor for structs
599                 return "";
600             }
601         }
602 
603         return result;
604     }
605 }
606 
607 struct Default(alias Alias)
608 {
609     static if (__traits(compiles, Alias()))
610     {
611         @property static auto value() { return Alias(); }
612     }
613     else
614     {
615         alias value = Alias;
616     }
617 }
618 
619 enum This
620 {
621     Private,
622     Protected,
623     Package,
624     Exclude
625 }
626 
627 private size_t udaDefaultIndex(alias Symbol)()
628 {
629     enum numTraits = __traits(getAttributes, Symbol).length;
630 
631     static if (numTraits == 0)
632     {
633         return -1;
634     }
635     else
636     {
637         foreach (i, uda; __traits(getAttributes, Symbol))
638         {
639             static if (is(uda: Template!Args, alias Template = Default, Args...))
640             {
641                 return i;
642             }
643             else static if (i == numTraits - 1)
644             {
645                 return -1;
646             }
647         }
648     }
649 }
650 
651 public template getUDADefaultOrNothing(attributes...)
652 {
653     import boilerplate.util : udaIndex;
654 
655     template EnumTest()
656     {
657         enum EnumTest = attributes[udaIndex!(Default, attributes)].value;
658     }
659 
660     static if (udaIndex!(Default, attributes) == -1)
661     {
662         enum getUDADefaultOrNothing = 0;
663     }
664     else static if (__traits(compiles, EnumTest!()))
665     {
666         enum getUDADefaultOrNothing = attributes[udaIndex!(Default, attributes)].value;
667     }
668     else
669     {
670         @property static auto getUDADefaultOrNothing()
671         {
672             return attributes[udaIndex!(Default, attributes)].value;
673         }
674     }
675 }