1 module boilerplate.util;
2 
3 import std.format;
4 import std.meta;
5 import std.range : iota;
6 import std.traits;
7 
8 static if (__traits(compiles, { import config.string : toString; }))
9 {
10     import config.string : customToString = toString;
11 }
12 else
13 {
14     private void customToString(T)()
15     if (false)
16     {
17     }
18 }
19 
20 enum needToDup(T) = isArray!(T) && !DeepConst!(T);
21 
22 enum DeepConst(T) = __traits(compiles, (const T x) { T y = x; });
23 
24 @("needToDup correctly handles common types")
25 @nogc nothrow pure @safe unittest
26 {
27     int integerField;
28     int[] integerArrayField;
29 
30     static assert(!needToDup!(typeof(integerField)));
31     static assert(needToDup!(typeof(integerArrayField)));
32 }
33 
34 @("needToDup correctly handles const types")
35 @nogc nothrow pure @safe unittest
36 {
37     const(int)[] constIntegerArrayField;
38     string stringField;
39 
40     static assert(!needToDup!(typeof(constIntegerArrayField)));
41     static assert(!needToDup!(typeof(stringField)));
42 }
43 
44 @("doesn't add write-only properties to NormalMembers")
45 unittest
46 {
47     struct Test
48     {
49         @property void foo(int i) { }
50         mixin GenNormalMemberTuple;
51         static assert(is(NormalMemberTuple == AliasSeq!()),
52             "write-only properties should not appear in NormalMembers because they have no type"
53         );
54     }
55 }
56 
57 @("doesn't add read properties to NormalMembers if includeFunctions is false")
58 unittest
59 {
60     struct Test
61     {
62         @property int foo() { return 0; }
63         int bar() { return 0; }
64         mixin GenNormalMemberTuple;
65         static assert(is(NormalMemberTuple == AliasSeq!()),
66             "read properties should not appear in NormalMembers if includeFunctions is false"
67         );
68     }
69 }
70 
71 /**
72  * Generate AliasSeq of "normal" members - ie. no templates, no alias, no enum, only fields
73  * (and functions if includeFunctions is true).
74  */
75 mixin template GenNormalMemberTuple(bool includeFunctions = false)
76 {
77     import boilerplate.util : GenNormalMembersCheck, GenNormalMembersImpl;
78     import std.meta : AliasSeq;
79 
80     mixin(`alias NormalMemberTuple = ` ~ GenNormalMembersImpl([__traits(derivedMembers, typeof(this))],
81         mixin(GenNormalMembersCheck([__traits(derivedMembers, typeof(this))], includeFunctions))) ~ `;`);
82 }
83 
84 string GenNormalMembersCheck(string[] members, bool includeFunctions)
85 {
86     import std.format : format;
87     import std.string : join;
88 
89     string code = "[";
90     foreach (i, member; members)
91     {
92         if (i > 0)
93         {
94             code ~= ", "; // don't .map.join because this is compile performance critical code
95         }
96 
97         if (member != "this")
98         {
99             string check = `__traits(compiles, &typeof(this).init.` ~ member ~ `)`
100                 ~ ` && __traits(compiles, typeof(typeof(this).init.` ~ member ~ `))`;
101 
102             if (!includeFunctions)
103             {
104                 check ~= ` && !is(typeof(typeof(this).` ~ member ~ `) == function)`
105                     ~ ` && !is(typeof(&typeof(this).init.` ~ member ~ `) == delegate)`;
106             }
107 
108             code ~= check;
109         }
110         else
111         {
112             code ~= `false`;
113         }
114     }
115     code ~= "]";
116 
117     return code;
118 }
119 
120 string GenNormalMembersImpl(string[] members, bool[] compiles)
121 {
122     import std.string : join;
123 
124     string[] names;
125 
126     foreach (i, member; members)
127     {
128         if (member != "this" && compiles[i])
129         {
130             names ~= "\"" ~ member ~ "\"";
131         }
132     }
133 
134     return "AliasSeq!(" ~ names.join(", ") ~ ")";
135 }
136 
137 template getOverloadLike(Aggregate, string Name, Type)
138 {
139     alias Overloads = AliasSeq!(__traits(getOverloads, Aggregate, Name));
140     enum FunctionMatchesType(alias Fun) = is(typeof(Fun) == Type);
141     alias MatchingOverloads = Filter!(FunctionMatchesType, Overloads);
142 
143     static assert(MatchingOverloads.length == 1);
144 
145     alias getOverloadLike = MatchingOverloads[0];
146 }
147 
148 template udaIndex(alias attr, attributes...)
149 {
150     enum udaIndex = helper();
151 
152     ptrdiff_t helper()
153     {
154         if (!__ctfe)
155         {
156             return 0;
157         }
158         static if (attributes.length)
159         {
160             foreach (i, attrib; attributes)
161             {
162                 enum lastAttrib = i + 1 == attributes.length;
163 
164                 static if (__traits(isTemplate, attr))
165                 {
166                     static if (__traits(isSame, attrib, attr))
167                     {
168                         return i;
169                     }
170                     else static if (is(attrib: attr!Args, Args...))
171                     {
172                         return i;
173                     }
174                     else static if (lastAttrib)
175                     {
176                         return -1;
177                     }
178                 }
179                 else static if (__traits(compiles, is(typeof(attrib) == typeof(attr)) && attrib == attr))
180                 {
181                     static if (is(typeof(attrib) == typeof(attr)) && attrib == attr)
182                     {
183                         return i;
184                     }
185                     else static if (lastAttrib)
186                     {
187                         return -1;
188                     }
189                 }
190                 else static if (__traits(compiles, typeof(attrib)) && __traits(compiles, is(typeof(attrib) == attr)))
191                 {
192                     static if (is(typeof(attrib) == attr))
193                     {
194                         return i;
195                     }
196                     else static if (lastAttrib)
197                     {
198                         return -1;
199                     }
200                 }
201                 else static if (__traits(compiles, is(attrib == attr)))
202                 {
203                     static if (is(attrib == attr))
204                     {
205                         return i;
206                     }
207                     else static if (lastAttrib)
208                     {
209                         return -1;
210                     }
211                 }
212                 else static if (lastAttrib)
213                 {
214                     return -1;
215                 }
216             }
217         }
218         else
219         {
220             return -1;
221         }
222     }
223 }
224 
225 string isStatic(string field)
226 {
227     return `__traits(getOverloads, typeof(this), "` ~ field ~ `").length == 0`
228       ~ ` && __traits(compiles, &this.` ~ field ~ `)`;
229 }
230 
231 string isUnsafe(string field)
232 {
233     return isStatic(field) ~ ` && !__traits(compiles, () @safe { return this.` ~ field ~ `; })`;
234 }
235 
236 // a stable, simple O(n) sort optimal for a small number of sort keys
237 T[] bucketSort(T)(T[] inputArray, size_t delegate(T) rankfn)
238 {
239     import std.algorithm : joiner;
240     import std.range : array;
241 
242     T[][] buckets;
243 
244     foreach (element; inputArray)
245     {
246         auto rank = rankfn(element);
247 
248         if (rank >= buckets.length)
249         {
250             buckets.length = rank + 1;
251         }
252 
253         buckets[rank] ~= element;
254     }
255 
256     return buckets.joiner.array;
257 }
258 
259 void sinkWrite(T...)(scope void delegate(const(char)[]) sink, ref bool comma, bool escapeStrings, string fmt, T args)
260 {
261     import std.algorithm : map;
262     import std.datetime : SysTime;
263     import std.format : format, formattedWrite;
264     import std.string : join;
265     import std.typecons : Nullable;
266 
267     static if (T.length == 1) // check for optional field: single Nullable
268     {
269         const arg = args[0];
270 
271         alias PlainT = typeof(cast() arg);
272 
273         enum isNullable = is(PlainT: Nullable!Arg, Arg);
274     }
275     else
276     {
277         enum isNullable = false;
278     }
279 
280     static if (isNullable)
281     {
282         if (!arg.isNull)
283         {
284             sink.sinkWrite(comma, escapeStrings, fmt, arg.get);
285         }
286         else
287         {
288             sink.sinkWrite(comma, false, fmt, "Nullable.null");
289         }
290         return;
291     }
292     else
293     {
294         auto replaceArg(int i)()
295         if (i >= 0 && i < T.length)
296         {
297             alias PlainT = typeof(cast() args[i]);
298 
299             static if (is(PlainT == SysTime))
300             {
301                 static struct SysTimeInitWrapper
302                 {
303                     const typeof(args[i]) arg;
304 
305                     void toString(scope void delegate(const(char)[]) sink) const
306                     {
307                         if (this.arg is SysTime.init) // crashes on toString
308                         {
309                             sink("SysTime.init");
310                         }
311                         else
312                         {
313                             wrapFormatType(this.arg, false).toString(sink);
314                         }
315                     }
316                 }
317 
318                 return SysTimeInitWrapper(args[i]);
319             }
320             else
321             {
322                 return wrapFormatType(args[i], escapeStrings);
323             }
324         }
325 
326         if (comma)
327         {
328             sink(", ");
329         }
330 
331         comma = true;
332 
333         mixin(`sink.formattedWrite(fmt, ` ~ T.length.iota.map!(i => format!"replaceArg!%s"(i)).join(", ") ~ `);`);
334     }
335 }
336 
337 private auto wrapFormatType(T)(T value, bool escapeStrings)
338 {
339     import std.traits : isSomeString;
340     import std.typecons : Nullable;
341 
342     // for Nullable types, we cannot distinguish between a custom handler that takes Nullable!Arg
343     // and one that takes Arg via alias get this. So handlers that take Nullable are impossible, since we
344     // need to handle it here to avoid crashes.
345     static if (is(T: Nullable!Arg, Arg))
346     {
347         static struct NullableWrapper
348         {
349             T value;
350 
351             bool escapeStrings;
352 
353             void toString(scope void delegate(const(char)[]) sink) const
354             {
355                 if (this.value.isNull)
356                 {
357                     sink("null");
358                 }
359                 else
360                 {
361                     wrapFormatType(this.value.get, escapeStrings).toString(sink);
362                 }
363             }
364         }
365         return NullableWrapper(value, escapeStrings);
366     }
367     else static if (__traits(compiles, customToString(value, (void delegate(const(char)[])).init)))
368     {
369         static struct CustomToStringWrapper
370         {
371             T value;
372 
373             void toString(scope void delegate(const(char)[]) sink) const
374             {
375                 customToString(this.value, sink);
376             }
377         }
378         return CustomToStringWrapper(value);
379     }
380     else static if (is(T : V[K], K, V))
381     {
382         static if (isOrderingComparable!K)
383         {
384             return orderedAssociativeArray(value);
385         }
386         else
387         {
388             import std.traits : fullyQualifiedName;
389 
390             // ansi escape codes. 0: reset, 1: bold, 93: bright yellow
391             pragma(msg, "\x1b[1;93mWarning\x1b[0m: Consistent ordering of type \x1b[1m" ~ T.stringof ~ "\x1b[0m " ~
392                 "on output cannot be guaranteed.");
393             pragma(msg, "         Please implement opCmp for \x1b[1m" ~ fullyQualifiedName!K ~ "\x1b[0m.");
394 
395             return value;
396         }
397     }
398     else static if (isSomeString!T)
399     {
400         static struct QuoteStringWrapper
401         {
402             T value;
403 
404             bool escapeStrings;
405 
406             void toString(scope void delegate(const(char)[]) sink) const
407             {
408                 import std.format : formattedWrite;
409                 import std.range : only;
410 
411                 if (escapeStrings)
412                 {
413                     sink.formattedWrite!"%(%s%)"(this.value.only);
414                 }
415                 else
416                 {
417                     sink.formattedWrite!"%s"(this.value);
418                 }
419             }
420         }
421 
422         return QuoteStringWrapper(value, escapeStrings);
423     }
424     else
425     {
426         return value;
427     }
428 }
429 
430 private auto orderedAssociativeArray(T : V[K], K, V)(T associativeArray)
431 {
432     static struct OrderedAssociativeArray
433     {
434         T associativeArray;
435 
436         public void toString(scope void delegate(const(char)[]) sink) const
437         {
438             import std.algorithm : sort;
439             sink("[");
440 
441             bool comma = false;
442 
443             foreach (key; this.associativeArray.keys.sort)
444             {
445                 sink.sinkWrite(comma, true, "%s: %s", key, this.associativeArray[key]);
446             }
447             sink("]");
448         }
449     }
450 
451     return OrderedAssociativeArray(associativeArray);
452 }
453 
454 private string quote(string text)
455 {
456     import std.string : replace;
457 
458     return `"` ~ text.replace(`\`, `\\`).replace(`"`, `\"`) ~ `"`;
459 }
460 
461 private string genFormatFunctionImpl(string text)
462 {
463     import std.algorithm : findSplit;
464     import std.exception : enforce;
465     import std.format : format;
466     import std.range : empty;
467     import std.string : join;
468 
469     string[] fragments;
470 
471     string remainder = text;
472 
473     while (true)
474     {
475         auto splitLeft = remainder.findSplit("%(");
476 
477         if (splitLeft[1].empty)
478         {
479             break;
480         }
481 
482         auto splitRight = splitLeft[2].findSplit(")");
483 
484         enforce(!splitRight[1].empty, format!"Closing paren not found in '%s'"(remainder));
485         remainder = splitRight[2];
486 
487         fragments ~= quote(splitLeft[0]);
488         fragments ~= splitRight[0];
489     }
490     fragments ~= quote(remainder);
491 
492     return `string values(T)(T arg)
493     {
494         with (arg)
495         {
496             return ` ~ fragments.join(" ~ ") ~ `;
497         }
498     }`;
499 }
500 
501 public template formatNamed(string text)
502 {
503     mixin(genFormatFunctionImpl(text));
504 }
505 
506 ///
507 @("formatNamed replaces named keys with given values")
508 unittest
509 {
510     import std.typecons : tuple;
511     import unit_threaded.should : shouldEqual;
512 
513     formatNamed!("Hello %(second) World %(first)%(second)!")
514         .values(tuple!("first", "second")("3", "5"))
515         .shouldEqual("Hello 5 World 35!");
516 }
517 
518 public T[] reorder(T)(T[] source, size_t[] newOrder)
519 in
520 {
521     import std.algorithm : sort;
522     import std.range : array, iota;
523 
524     // newOrder must be a permutation of source indices
525     assert(newOrder.dup.sort.array == source.length.iota.array);
526 }
527 body
528 {
529     import std.algorithm : map;
530     import std.range : array;
531 
532     return newOrder.map!(i => source[i]).array;
533 }
534 
535 @("reorder returns reordered array")
536 unittest
537 {
538     import unit_threaded.should : shouldEqual;
539 
540     [1, 2, 3].reorder([0, 2, 1]).shouldEqual([1, 3, 2]);
541 }
542 
543 // TODO replace with Nullable once pr 19037 is merged
544 public struct Optional(T)
545 {
546     import std.typecons : Nullable;
547 
548     // workaround: types in union are not destructed
549     union DontCallDestructor { SafeUnqual!T t; }
550 
551     // workaround: types in struct are memcpied in move/moveEmplace, bypassing constness
552     struct UseMemcpyMove { DontCallDestructor u; }
553 
554     private UseMemcpyMove value = UseMemcpyMove.init;
555 
556     public bool isNull = true;
557 
558     public this(T value)
559     {
560         this.value = UseMemcpyMove(DontCallDestructor(value));
561         this.isNull = false;
562     }
563 
564     // This method should only be called from Builder.value! Builder fields are semantically write-only.
565     public inout(T) _get() inout
566     in
567     {
568         assert(!this.isNull);
569     }
570     do
571     {
572         return this.value.u.t;
573     }
574 
575     public U opAssign(U)(U value)
576     {
577         static if (is(U : Nullable!Arg, Arg))
578         {
579             // force-bypass the drug-fuelled `alias get this` idiocy by manually converting
580             // value to the strictly (const) correct type for the assign() call
581             T implConvertedValue = value;
582 
583             _assign(implConvertedValue);
584         }
585         else
586         {
587             _assign(value);
588         }
589         return value;
590     }
591 
592     private void _assign(T value)
593     {
594         import std.algorithm : move, moveEmplace;
595 
596         auto valueCopy = UseMemcpyMove(DontCallDestructor(value));
597 
598         if (this.isNull)
599         {
600             moveEmplace(valueCopy, this.value);
601 
602             this.isNull = false;
603         }
604         else
605         {
606             move(valueCopy, this.value);
607         }
608     }
609 
610     public void opOpAssign(string op, RHS)(RHS rhs)
611     if (__traits(compiles, mixin("T.init " ~ op ~ " RHS.init")))
612     {
613         if (this.isNull)
614         {
615             this = T.init;
616         }
617         mixin("this = this._get " ~ op ~ " rhs;");
618     }
619 
620     static if (is(T: Nullable!Arg, Arg))
621     {
622         private void _assign(Arg value)
623         {
624             this = T(value);
625         }
626     }
627 
628     static if (is(T == struct) && hasElaborateDestructor!T)
629     {
630         ~this()
631         {
632             if (!this.isNull)
633             {
634                 destroy(this.value.u.t);
635             }
636         }
637     }
638 }
639 
640 ///
641 unittest
642 {
643     Optional!(int[]) intArrayOptional;
644 
645     assert(intArrayOptional.isNull);
646 
647     intArrayOptional ~= 5;
648 
649     assert(!intArrayOptional.isNull);
650     assert(intArrayOptional._get == [5]);
651 
652     intArrayOptional ~= 6;
653 
654     assert(intArrayOptional._get == [5, 6]);
655 }
656 
657 ///
658 @("optional correctly supports nullable assignment to const nullable of array")
659 unittest
660 {
661     import std.typecons : Nullable;
662 
663     Optional!(const(Nullable!int)) nullableArrayOptional;
664 
665     nullableArrayOptional = Nullable!int();
666 }
667 
668 private template SafeUnqual(T)
669 {
670     static if (__traits(compiles, (T t) { Unqual!T ut = t; }))
671     {
672         alias SafeUnqual = Unqual!T;
673     }
674     else
675     {
676         alias SafeUnqual = T;
677     }
678 }
679 
680 public string removeTrailingUnderline(string name)
681 {
682     import std.string : endsWith;
683 
684     return name.endsWith("_") ? name[0 .. $ - 1] : name;
685 }
686 
687 // Remove trailing underline iff the result would not be a reserved identifier
688 public enum optionallyRemoveTrailingUnderline(string name) =
689     isReservedIdentifier!(name.removeTrailingUnderline) ? name : name.removeTrailingUnderline;
690 
691 private enum isReservedIdentifier(string identifier) = !__traits(compiles, mixin(format!q{({ int %s; })}(identifier)));
692 
693 static assert(isReservedIdentifier!"version");
694 static assert(!isReservedIdentifier!"bla");
695 
696 /**
697  * manually reimplement `move`, `moveEmplace` because the phobos implementation of
698  * `moveEmplace` is **really, really bad!** it forces a recursive template
699  * instantiation over every primitive field in a struct, causing template overflows
700  * during compilation.
701  *
702  * See:
703  *      Phobos bug https://issues.dlang.org/show_bug.cgi?id=19689
704  *      Phobos fix https://github.com/dlang/phobos/pull/6873
705  */
706 public void moveEmplace(T)(ref T source, ref T dest) @trusted
707 in (&dest !is &source)
708 {
709     import core.stdc.string : memcpy, memset;
710 
711     memcpy(&dest, &source, T.sizeof);
712     memset(&source, 0, T.sizeof);
713 }
714 
715 ///
716 public void move(T)(ref T source, ref T dest) @trusted
717 in (&dest !is &source)
718 {
719     import std.traits : hasElaborateDestructor;
720 
721     static if (hasElaborateDestructor!T)
722     {
723         dest.__xdtor();
724     }
725     moveEmplace(source, dest);
726 }