1 module dmocks.qualifiers;
2 
3 import dmocks.util;
4 import std.algorithm;
5 import std.exception;
6 import std.range;
7 import std.traits;
8 
9 /// Factory for qualifier matches
10 /// specifies match that exactly matches passed method T
11 QualifierMatch qualifierMatch(alias T)()
12 {
13     auto q = QualifierMatch();
14     auto quals = qualifiers!T;
15     foreach (string name; quals)
16     {
17         q._qualifiers[name] = true;
18     }
19     foreach (string name; validQualifiers.filter!(a => a !in q._qualifiers))
20     {
21         q._qualifiers[name] = false;
22     }
23     debug enforceQualifierMatch(q._qualifiers);
24     return q;
25 }
26 
27 /// Factory for qualifier match objects
28 /// Let's you create exact pattern for matching methods by qualifiers
29 /// Params:
30 ///  quals - map specifying matching condition
31 ///  - true - matches to method with qualifier
32 ///  - false - matches to method without qualifier
33 ///  - not included - qualifier is ignored in matching (optional)
34 QualifierMatch qualifierMatch(bool[string] quals)
35 {
36     enforceQualifierMatch(quals);
37     auto q = QualifierMatch();
38     q._qualifiers = quals;
39     return q;
40 }
41 
42 /// Helper function getting qualifiers string array
43 string[] qualifiers(alias T)()
44 {
45     return getFunctionAttributes!(T)() ~ getMethodAttributes!(T)();
46 }
47 
48 string formatQualifiers(alias T)()
49 {
50     return qualifiers!T.join(" ");
51 }
52 
53 ///
54 unittest
55 {
56     class A
57     {
58         int a;
59         int make() const shared @property
60         {
61             return a;
62         }
63 
64         int makePure() inout pure @safe
65         {
66             return a;
67         }
68 
69         int makeImut() immutable nothrow @trusted
70         {
71             return a;
72         }
73 
74         ref int makeRef()
75         {
76             return a;
77         }
78     }
79 
80     auto aimut = new immutable(A);
81     auto aconst = new const shared(A);
82     auto amut = new A;
83     //  assert(qualifiers!(aimut.makeImut)().sort().array() == [Qual!"immutable", Qual!"nothrow", Qual!"@trusted"].sort);
84     //  assert(qualifiers!(aconst.makePure)().sort().array() == [Qual!"@safe", Qual!"inout", Qual!"pure"].sort);
85     //  assert(qualifiers!(aconst.make)().sort().array() == [Qual!"@property", Qual!"@system", Qual!"const", Qual!"shared"].sort);
86     assert(qualifiers!(amut.makeRef)().sort().array() == [Qual!"@system", Qual!"ref"]);
87     //  assert(qualifierMatch!(aconst.make).matches([Qual!"@property", Qual!"@system", Qual!"const", Qual!"shared"].sort));
88     //  assert(!qualifierMatch!(aconst.make).matches([Qual!"@property", Qual!"@system", Qual!"shared"].sort));
89     //  assert(!qualifierMatch!(aconst.make).matches([Qual!"@property", Qual!"ref", Qual!"@system", Qual!"shared"].sort));
90     //  assert(!qualifierMatch!(aconst.make).matches(["property", Qual!"@system", Qual!"shared"].sort));
91 }
92 
93 private string[] getFunctionAttributes(alias T)()
94 {
95     import std.array;
96 
97     enum attributes = functionAttributes!(typeof(&T));
98     auto ret = appender!(string[]);
99     static if ((attributes & FunctionAttribute.nothrow_) != 0)
100     {
101         ret.put("nothrow");
102     }
103     static if ((attributes & FunctionAttribute.pure_) != 0)
104     {
105         ret.put("pure");
106     }
107     static if ((attributes & FunctionAttribute.ref_) != 0)
108     {
109         ret.put("ref");
110     }
111     static if ((attributes & FunctionAttribute.property) != 0)
112     {
113         ret.put("@property");
114     }
115     static if ((attributes & FunctionAttribute.trusted) != 0)
116     {
117         ret.put("@trusted");
118     }
119     static if ((attributes & FunctionAttribute.safe) != 0)
120     {
121         ret.put("@safe");
122     }
123     static if ((attributes & FunctionAttribute.safe) == 0 && (attributes & FunctionAttribute.trusted) == 0)
124     {
125         ret.put("@system");
126     }
127     return ret.data;
128 }
129 
130 private string[] getMethodAttributes(alias T)()
131 {
132     alias FunctionTypeOf!T TYPE;
133     import std.array;
134 
135     auto ret = appender!(string[]);
136     static if (is(TYPE == const))
137     {
138         ret.put("const");
139     }
140     static if (is(TYPE == immutable))
141     {
142         ret.put("immutable");
143     }
144     static if (is(TYPE == shared))
145     {
146         ret.put("shared");
147     }
148     static if (is(TYPE == inout))
149     {
150         ret.put("inout");
151     }
152     return ret.data;
153 }
154 
155 /// checks if qualifiers is a unique set of valid qualifiers
156 public void enforceQualifierNames(string[] qualifiers)
157 {
158     enforce!(MocksSetupException)(qualifiers.uniq.array == qualifiers,
159             "Qualifiers: given qualifiers are not unique: " ~ qualifiers.join(" "));
160 
161     // bad perf, but data is small
162     foreach (string q; qualifiers)
163     {
164         enforce!(MocksSetupException)(validQualifiers.canFind(q), "Qualifiers: found invalid qualifier: " ~ q);
165     }
166 }
167 
168 private immutable string[] validQualifiers = sort(["const", "shared", "immutable", "nothrow", "pure", "ref", "@property", "@trusted", "@safe", "inout", "@system"])
169     .array;
170 
171 /// validates qualifier name
172 template Qual(string val)
173 {
174     static assert(validQualifiers.canFind(val), "Incorrect qualifier name");
175     enum Qual = val;
176 }
177 
178 ///
179 unittest
180 {
181     static assert(__traits(compiles, Qual!"const"));
182     static assert(!__traits(compiles, Qual!"consta"));
183     enforceQualifierNames([Qual!"const", Qual!"@property"]);
184     assertThrown!(MocksSetupException)(enforceQualifierNames([Qual!"const", Qual!"const", Qual!"@property"]));
185     assertThrown!(MocksSetupException)(enforceQualifierNames(["consta", Qual!"@property"]));
186 }
187 
188 /// check if qualifier match is correctly formulated
189 void enforceQualifierMatch(bool[string] qualifiers)
190 {
191     enforceQualifierNames(qualifiers.keys);
192     bool testBothSet(string first, string second)()
193     {
194         return Qual!first in qualifiers && Qual!second in qualifiers && qualifiers[Qual!first] && qualifiers[Qual!second];
195     }
196 
197     bool testThreeForbidden(string first, string second, string third)()
198     {
199         return Qual!first in qualifiers && Qual!second && Qual!second in qualifiers && !qualifiers[Qual!first] && !qualifiers[Qual!second] && !qualifiers[Qual!third];
200     }
201 
202     void enforceBothNotSet(string first, string second)()
203     {
204         enforce!(MocksSetupException)(!testBothSet!(first, second),
205                 "Qualifiers: cannot require both " ~ first ~ " and " ~ second);
206     }
207 
208     void enforceThreeNotSet(string first, string second, string third)()
209     {
210         enforce!(MocksSetupException)(!testThreeForbidden!(first, second, third),
211                 "Qualifiers: cannot forbid all " ~ first ~ ", " ~ second ~ " and " ~ third);
212     }
213 
214     enforceBothNotSet!("@trusted", "@safe");
215     enforceBothNotSet!("@system", "@trusted");
216     enforceBothNotSet!("@system", "@safe");
217     enforceBothNotSet!("const", "immutable");
218     enforceBothNotSet!("const", "inout");
219     enforceBothNotSet!("immutable", "inout");
220     enforceThreeNotSet!("@system", "@safe", "@trusted");
221 }
222 
223 /++
224 + type that allows you to specify which qualifiers are required in a match
225 + stores required and forbidden qualifiers
226 +/
227 struct QualifierMatch
228 {
229     private bool[string] _qualifiers;
230 
231     ///
232     string toString() const
233     {
234         auto opt = validQualifiers.dup.filter!((a) => a !in _qualifiers)().join(" ");
235         return _qualifiers.keys.filter!((a) => _qualifiers[a])().join(" ") ~
236             (opt.length != 0 ? " (optional: " ~ opt ~ ")" : "");
237     }
238 
239     string diffToString(string[] against) const
240     {
241         if (matches(against)) return toString();
242         else return yellow(toString());
243     }
244 
245     /// returns true if all required qualifiers are present and all forbidden are absent in against array
246     bool matches(string[] against) const
247     {
248         debugLog("QualifierMatch: match against: " ~ against.join(" "));
249         debugLog("state: " ~ toString());
250         foreach (string searched; against)
251         {
252             const(bool)* found = searched in _qualifiers;
253             if (found is null)
254                 continue;
255             if (!(*found))
256                 return false;
257         }
258 
259         foreach (string key, const(bool) val; _qualifiers)
260         {
261             if (!val)
262                 continue;
263             if (!against.canFind(key))
264                 return false;
265         }
266         return true;
267     }
268 }