1 module boilerplate.conditions;
2 
3 version(unittest)
4 {
5     import core.exception : AssertError;
6     import unit_threaded.should;
7 }
8 
9 /++
10 `GenerateInvariants` is a mixin string that automatically generates an `invariant{}` block
11 for each field with a condition.
12 +/
13 public enum string GenerateInvariants = `
14     import boilerplate.conditions : GenerateInvariantsTemplate;
15     mixin GenerateInvariantsTemplate;
16     mixin(typeof(this).generateInvariantsImpl());
17 `;
18 
19 /++
20 When a field is marked with `@NonEmpty`, `!field.empty` is asserted.
21 +/
22 public struct NonEmpty
23 {
24 }
25 
26 ///
27 @("throws when a NonEmpty field is initialized empty")
28 unittest
29 {
30     class Class
31     {
32         @NonEmpty
33         int[] array_;
34 
35         this(int[] array)
36         {
37             this.array_ = array;
38         }
39 
40         mixin(GenerateInvariants);
41     }
42 
43     (new Class(null)).shouldThrow!AssertError;
44 }
45 
46 ///
47 @("throws when a NonEmpty field is assigned empty")
48 unittest
49 {
50     class Class
51     {
52         @NonEmpty
53         private int[] array_;
54 
55         this(int[] array)
56         {
57             this.array_ = array;
58         }
59 
60         public void array(int[] arrayValue)
61         {
62             this.array_ = arrayValue;
63         }
64 
65         mixin(GenerateInvariants);
66     }
67 
68     (new Class([2])).array(null).shouldThrow!AssertError;
69 }
70 
71 /++
72 When a field is marked with `@NonNull`, `field !is null` is asserted.
73 +/
74 public struct NonNull
75 {
76 }
77 
78 ///
79 @("throws when a NonNull field is initialized null")
80 unittest
81 {
82     class Class
83     {
84         @NonNull
85         Object obj_;
86 
87         this(Object obj)
88         {
89             this.obj_ = obj;
90         }
91 
92         mixin(GenerateInvariants);
93     }
94 
95     (new Class(null)).shouldThrow!AssertError;
96 }
97 
98 /++
99 When a field is marked with `@AllNonNull`, `field.all!"a !is null"` is asserted.
100 +/
101 public struct AllNonNull
102 {
103 }
104 
105 ///
106 @("throws when an AllNonNull field is initialized with an array containing null")
107 unittest
108 {
109     class Class
110     {
111         @AllNonNull
112         Object[] objs;
113 
114         this(Object[] objs)
115         {
116             this.objs = objs;
117         }
118 
119         mixin(GenerateInvariants);
120     }
121 
122     (new Class(null)).objs.shouldEqual(null);
123     (new Class([null])).shouldThrow!AssertError;
124     (new Class([new Object, null])).shouldThrow!AssertError;
125 }
126 
127 /// `@AllNonNull` may be used with associative arrays.
128 @("supports AllNonNull on associative arrays")
129 unittest
130 {
131     class Class
132     {
133         @AllNonNull
134         Object[int] objs;
135 
136         this(Object[int] objs)
137         {
138             this.objs = objs;
139         }
140 
141         mixin(GenerateInvariants);
142     }
143 
144     (new Class(null)).objs.shouldEqual(null);
145     (new Class([0: null])).shouldThrow!AssertError;
146     (new Class([0: new Object, 1: null])).shouldThrow!AssertError;
147 }
148 
149 /// When used with associative arrays, `@AllNonNull` may check keys, values or both.
150 @("supports AllNonNull on associative array keys")
151 unittest
152 {
153     class Class
154     {
155         @AllNonNull
156         int[Object] objs;
157 
158         this(int[Object] objs)
159         {
160             this.objs = objs;
161         }
162 
163         mixin(GenerateInvariants);
164     }
165 
166     (new Class(null)).objs.shouldEqual(null);
167     (new Class([null: 0])).shouldThrow!AssertError;
168     (new Class([new Object: 0, null: 1])).shouldThrow!AssertError;
169 }
170 
171 /++
172 When a field is marked with `@NonInit`, `field !is T.init` is asserted.
173 +/
174 public struct NonInit
175 {
176 }
177 
178 ///
179 @("throws when a NonInit field is initialized with T.init")
180 unittest
181 {
182     import core.time : Duration;
183 
184     class Class
185     {
186         @NonInit
187         float f_;
188 
189         this(float f) { this.f_ = f; }
190 
191         mixin(GenerateInvariants);
192     }
193 
194     (new Class(float.init)).shouldThrow!AssertError;
195 }
196 
197 /++
198 When <b>any</b> condition check is applied to a nullable field, the test applies to the value,
199 if any, contained in the field. The "null" state of the field is ignored.
200 +/
201 @("doesn't throw when a Nullable field is null")
202 unittest
203 {
204     import std.typecons : Nullable, nullable;
205 
206     class Class
207     {
208         @NonInit
209         Nullable!float f_;
210 
211         this(Nullable!float f)
212         {
213             this.f_ = f;
214         }
215 
216         mixin(GenerateInvariants);
217     }
218 
219     (new Class(5f.nullable)).f_.isNull.shouldBeFalse;
220     (new Class(Nullable!float())).f_.isNull.shouldBeTrue;
221     (new Class(float.init.nullable)).shouldThrow!AssertError;
222 }
223 
224 mixin template GenerateInvariantsTemplate()
225 {
226     private static string generateInvariantsImpl()
227     {
228         if (!__ctfe)
229         {
230             return null;
231         }
232 
233         import boilerplate.conditions : IsConditionAttribute, generateChecksForAttributes;
234         import boilerplate.util : GenNormalMemberTuple;
235         import std.meta : StdMetaFilter = Filter;
236 
237         string result = null;
238 
239         result ~= `invariant {` ~
240             `import std.format : format;` ~
241             `import std.array : empty;`;
242 
243         // TODO blocked by https://issues.dlang.org/show_bug.cgi?id=18504
244         // note: synchronized without lock contention is basically free
245         // IMPORTANT! Do not enable this until you have a solution for reliably detecting which attributes actually
246         // require synchronization! overzealous synchronize has the potential to lead to needless deadlocks.
247         // (consider implementing @GuardedBy)
248         enum synchronize = false;
249 
250         result ~= synchronize ? `synchronized (this) {` : ``;
251 
252         mixin GenNormalMemberTuple;
253 
254         foreach (member; NormalMemberTuple)
255         {
256             mixin(`alias symbol = this.` ~ member ~ `;`);
257 
258             static if (__traits(compiles, typeof(symbol).init))
259             {
260                 result ~= generateChecksForAttributes!(typeof(symbol),
261                         StdMetaFilter!(IsConditionAttribute, __traits(getAttributes, symbol)))
262                     (`this.` ~ member);
263             }
264         }
265 
266         result ~= synchronize ? ` }` : ``;
267 
268         result ~= ` }`;
269 
270         return result;
271     }
272 }
273 
274 public string generateChecksForAttributes(T, Attributes...)(string member_expression, string info = "")
275 {
276     import boilerplate.conditions : NonEmpty, NonNull;
277     import boilerplate.util : udaIndex;
278     import std.array : empty;
279     import std.string : format;
280     import std.traits : ConstOf, isAssociativeArray;
281     import std.typecons : Nullable;
282 
283     enum isNullable = is(T: Template!Args, alias Template = Nullable, Args...);
284 
285     static if (isNullable)
286     {
287         enum access = `%s.get`;
288     }
289     else
290     {
291         enum access = `%s`;
292     }
293 
294     alias MemberType = typeof(mixin(format!access(`T.init`)));
295 
296     string expression = format!access(member_expression);
297 
298     enum canFormat = __traits(compiles, format(`%s`, ConstOf!MemberType.init));
299 
300     string checks;
301 
302     static if (udaIndex!(NonEmpty, Attributes) != -1)
303     {
304         static if (!__traits(compiles, MemberType.init.empty()))
305         {
306             return format!`static assert(false, "Cannot call std.array.empty() on '%s'");`(expression);
307         }
308 
309         static if (canFormat)
310         {
311             checks ~= format!(`assert(!%s.empty, `
312                 ~ `format("@NonEmpty: assert(!%s.empty) failed%s: %s = %%s", %s));`)
313                 (expression, expression, info, expression, expression);
314         }
315         else
316         {
317             checks ~= format!`assert(!%s.empty(), "@NonEmpty: assert(!%s.empty) failed%s");`
318                 (expression, expression, info);
319         }
320     }
321 
322     static if (udaIndex!(NonNull, Attributes) != -1)
323     {
324         static if (__traits(compiles, MemberType.init.isNull))
325         {
326             checks ~= format!`assert(!%s.isNull, "@NonNull: assert(!%s.isNull) failed%s");`
327                 (expression, expression, info);
328         }
329         else static if (__traits(compiles, MemberType.init !is null))
330         {
331             // Nothing good can come of printing something that is null.
332             checks ~= format!`assert(%s !is null, "@NonNull: assert(%s !is null) failed%s");`
333                 (expression, expression, info);
334         }
335         else
336         {
337             return format!`static assert(false, "Cannot compare '%s' to null");`(expression);
338         }
339     }
340 
341     static if (udaIndex!(NonInit, Attributes) != -1)
342     {
343         auto reference = `typeof(` ~ expression ~ `).init`;
344 
345         if (!__traits(compiles, MemberType.init !is MemberType.init))
346         {
347             return format!`static assert(false, "Cannot compare '%s' to %s.init");`(expression, MemberType.stringof);
348         }
349 
350         static if (canFormat)
351         {
352             checks ~=
353                 format!(`assert(%s !is %s, `
354                     ~ `format("@NonInit: assert(%s !is %s.init) failed%s: %s = %%s", %s));`)
355                     (expression, reference, expression, MemberType.stringof, info, expression, expression);
356         }
357         else
358         {
359             checks ~=
360                 format!`assert(%s !is %s, "@NonInit: assert(%s !is %s.init) failed%s");`
361                     (expression, reference, expression, MemberType.stringof, info);
362         }
363     }
364 
365     static if (udaIndex!(AllNonNull, Attributes) != -1)
366     {
367         import std.algorithm: all;
368 
369         checks ~= `import std.algorithm: all;`;
370 
371         static if (__traits(compiles, MemberType.init.all!"a !is null"))
372         {
373             static if (canFormat)
374             {
375                 checks ~=
376                     format!(`assert(%s.all!"a !is null", format(`
377                         ~ `"@AllNonNull: assert(%s.all!\"a !is null\") failed%s: %s = %%s", %s));`)
378                         (expression, expression, info, expression, expression);
379             }
380             else
381             {
382                 checks ~= format!(`assert(%s.all!"a !is null", `
383                     ~ `"@AllNonNull: assert(%s.all!\"a !is null\") failed%s");`)
384                     (expression, expression, info);
385             }
386         }
387         else static if (__traits(compiles, MemberType.init.all!"!a.isNull"))
388         {
389             static if (canFormat)
390             {
391                 checks ~=
392                     format!(`assert(%s.all!"!a.isNull", format(`
393                         ~ `"@AllNonNull: assert(%s.all!\"!a.isNull\") failed%s: %s = %%s", %s));`)
394                         (expression, expression, info, expression, expression);
395             }
396             else
397             {
398                 checks ~= format!(`assert(%s.all!"!a.isNull", `
399                     ~ `"@AllNonNull: assert(%s.all!\"!a.isNull\") failed%s");`)
400                     (expression, expression, info);
401             }
402         }
403         else static if (__traits(compiles, isAssociativeArray!MemberType))
404         {
405             enum checkValues = __traits(compiles, MemberType.init.byValue.all!`a !is null`);
406             enum checkKeys = __traits(compiles, MemberType.init.byKey.all!"a !is null");
407 
408             static if (!checkKeys && !checkValues)
409             {
410                 return format!(`static assert(false, "Neither key nor value of associative array `
411                     ~ `'%s' can be checked against null.");`)(expression);
412             }
413 
414             static if (checkValues)
415             {
416                 checks ~=
417                     format!(`assert(%s.byValue.all!"a !is null", `
418                         ~ `"@AllNonNull: assert(%s.byValue.all!\"a !is null\") failed%s");`)
419                         (expression, expression, info);
420             }
421 
422             static if (checkKeys)
423             {
424                 checks ~=
425                     format!(`assert(%s.byKey.all!"a !is null", `
426                         ~ `"@AllNonNull: assert(%s.byKey.all!\"a !is null\") failed%s");`)
427                         (expression, expression, info);
428             }
429         }
430         else
431         {
432             return format!`static assert(false, "Cannot compare all '%s' to null");`(expression);
433         }
434     }
435 
436     if (checks.empty)
437     {
438         return null;
439     }
440 
441     static if (isNullable)
442     {
443         return `if (!` ~ member_expression ~ `.isNull) {` ~ checks ~ `}`;
444     }
445     else
446     {
447         return checks;
448     }
449 }
450 
451 public enum IsConditionAttribute(alias A) = __traits(isSame, A, NonEmpty) || __traits(isSame, A, NonNull)
452     || __traits(isSame, A, NonInit) || __traits(isSame, A, AllNonNull);