1 //          Copyright Basile Burg 2017.
2 // Distributed under the Boost Software License, Version 1.0.
3 //    (See accompanying file LICENSE_1_0.txt or copy at
4 //
5 module dscanner.analysis.vcall_in_ctor;
6 
7 import dscanner.analysis.base;
8 import dscanner.utils;
9 import dparse.ast, dparse.lexer;
10 import std.algorithm.searching : canFind;
11 import std.range: retro;
12 
13 /**
14  * Checks virtual calls from the constructor to methods defined in the same class.
15  *
16  * When not used carefully, virtual calls from constructors can lead to a call
17  * in a derived instance that's not yet constructed.
18  */
19 final class VcallCtorChecker : BaseAnalyzer
20 {
21     alias visit = BaseAnalyzer.visit;
22 
23     mixin AnalyzerInfo!"vcall_in_ctor";
24 
25 private:
26 
27     enum string KEY = "dscanner.vcall_ctor";
28     enum string MSG = "a virtual call inside a constructor may lead to"
29         ~ " unexpected results in the derived classes";
30 
31     // what's called in the ctor
32     Token[][] _ctorCalls;
33     // the virtual method in the classes
34     Token[][] _virtualMethods;
35 
36 
37     // The problem only happens in classes
38     bool[] _inClass = [false];
39     // The problem only happens in __ctor
40     bool[] _inCtor = [false];
41     // The problem only happens with call to virtual methods
42     bool[] _isVirtual = [true];
43     // The problem only happens with call to virtual methods
44     bool[] _isNestedFun = [false];
45     // The problem only happens in derived classes that override
46     bool[] _isFinal = [false];
47 
48     void pushVirtual(bool value)
49     {
50         _isVirtual ~= value;
51     }
52 
53     void pushInClass(bool value)
54     {
55         _inClass ~= value;
56         _ctorCalls.length += 1;
57         _virtualMethods.length += 1;
58     }
59 
60     void pushInCtor(bool value)
61     {
62         _inCtor ~= value;
63     }
64 
65     void pushNestedFunc(bool value)
66     {
67         _isNestedFun ~= value;
68     }
69 
70     void pushIsFinal(bool value)
71     {
72         _isFinal ~= value;
73     }
74 
75     void popVirtual()
76     {
77         _isVirtual.length -= 1;
78     }
79 
80     void popInClass()
81     {
82         _inClass.length -= 1;
83         _ctorCalls.length -= 1;
84         _virtualMethods.length -= 1;
85     }
86 
87     void popInCtor()
88     {
89         _inCtor.length -= 1;
90     }
91 
92     void popNestedFunc()
93     {
94         _isNestedFun.length -= 1;
95     }
96 
97     void popIsFinal()
98     {
99         _isFinal.length -= 1;
100     }
101 
102     void overwriteVirtual(bool value)
103     {
104         _isVirtual[$-1] = value;
105     }
106 
107     bool isVirtual()
108     {
109         return _isVirtual[$-1];
110     }
111 
112     bool isInClass()
113     {
114         return _inClass[$-1];
115     }
116 
117     bool isInCtor()
118     {
119         return _inCtor[$-1];
120     }
121 
122     bool isFinal()
123     {
124         return _isFinal[$-1];
125     }
126 
127     bool isInNestedFunc()
128     {
129         return _isNestedFun[$-1];
130     }
131 
132     void check()
133     {
134         foreach (call; _ctorCalls[$-1])
135             foreach (vm; _virtualMethods[$-1])
136         {
137             if (call == vm)
138             {
139                 addErrorMessage(call.line, call.column, KEY, MSG);
140                 break;
141             }
142         }
143     }
144 
145 public:
146 
147     ///
148     this(string fileName, bool skipTests = false)
149     {
150         super(fileName, null, skipTests);
151     }
152 
153     override void visit(const(ClassDeclaration) decl)
154     {
155         pushVirtual(true);
156         pushInClass(true);
157         pushNestedFunc(false);
158         decl.accept(this);
159         check();
160         popVirtual();
161         popInClass();
162         popNestedFunc();
163     }
164 
165     override void visit(const(StructDeclaration) decl)
166     {
167         pushVirtual(false);
168         pushInClass(false);
169         pushNestedFunc(false);
170         decl.accept(this);
171         check();
172         popVirtual();
173         popInClass();
174         popNestedFunc();
175     }
176 
177     override void visit(const(Constructor) ctor)
178     {
179         pushInCtor(isInClass);
180         ctor.accept(this);
181         popInCtor();
182     }
183 
184     override void visit(const(Declaration) d)
185     {
186         // "<protection>:"
187         if (d.attributeDeclaration && d.attributeDeclaration.attribute)
188         {
189             const tp = d.attributeDeclaration.attribute.attribute.type;
190             overwriteVirtual(isProtection(tp) & (tp != tok!"private"));
191         }
192 
193         // "protection {}"
194         bool pop;
195         scope(exit) if (pop)
196             popVirtual;
197 
198         const bool hasAttribs = d.attributes !is null;
199         const bool hasStatic = hasAttribs ? d.attributes.canFind!(a => a.attribute.type == tok!"static") : false;
200         const bool hasFinal = hasAttribs ? d.attributes.canFind!(a => a.attribute.type == tok!"final") : false;
201 
202         if (d.attributes) foreach (attr; d.attributes.retro)
203         {
204             if (!hasStatic &&
205                (attr.attribute == tok!"public" || attr.attribute == tok!"protected"))
206             {
207                 pushVirtual(true);
208                 pop = true;
209                 break;
210             }
211             else if (hasStatic || attr.attribute == tok!"private" || attr.attribute == tok!"package")
212             {
213                 pushVirtual(false);
214                 pop = true;
215                 break;
216             }
217         }
218 
219         // final class... final function
220         if ((d.classDeclaration || d.functionDeclaration) && hasFinal)
221             pushIsFinal(true);
222 
223         d.accept(this);
224 
225         if ((d.classDeclaration || d.functionDeclaration) && hasFinal)
226             popIsFinal;
227     }
228 
229     override void visit(const(FunctionCallExpression) exp)
230     {
231         // nested function are not virtual
232         pushNestedFunc(true);
233         exp.accept(this);
234         popNestedFunc();
235     }
236 
237     override void visit(const(UnaryExpression) exp)
238     {
239         if (isInCtor)
240         // get function identifier for a call, only for this member (so no ident chain)
241         if (const IdentifierOrTemplateInstance iot = safeAccess(exp)
242             .functionCallExpression.unaryExpression.primaryExpression.identifierOrTemplateInstance)
243         {
244             const Token t = iot.identifier;
245             if (t != tok!"")
246             {
247                 _ctorCalls[$-1] ~= t;
248             }
249         }
250         exp.accept(this);
251     }
252 
253     override void visit(const(FunctionDeclaration) d)
254     {
255         if (isInClass() && !isInNestedFunc() && !isFinal() && !d.templateParameters)
256         {
257             bool virtualOnce;
258             bool notVirtualOnce;
259 
260             const bool hasAttribs = d.attributes !is null;
261             const bool hasStatic = hasAttribs ? d.attributes.canFind!(a => a.attribute.type == tok!"static") : false;
262 
263             // handle "private", "public"... for this declaration
264             if (d.attributes) foreach (attr; d.attributes.retro)
265             {
266                 if (!hasStatic &&
267                    (attr.attribute == tok!"public" || attr.attribute == tok!"protected"))
268                 {
269                     if (!isVirtual)
270                     {
271                         virtualOnce = true;
272                         break;
273                     }
274                 }
275                 else if (hasStatic || attr.attribute == tok!"private" || attr.attribute == tok!"package")
276                 {
277                     if (isVirtual)
278                     {
279                         notVirtualOnce = true;
280                         break;
281                     }
282                 }
283             }
284 
285             if (!isVirtual && virtualOnce)
286                 _virtualMethods[$-1] ~= d.name;
287             else if (isVirtual && !virtualOnce)
288                 _virtualMethods[$-1] ~= d.name;
289 
290         }
291         d.accept(this);
292     }
293 }
294 
295 unittest
296 {
297     import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
298     import dscanner.analysis.helpers : assertAnalyzerWarnings;
299     import std.stdio : stderr;
300     import std.format : format;
301 
302     StaticAnalysisConfig sac = disabledConfig();
303     sac.vcall_in_ctor = Check.enabled;
304 
305     // fails
306     assertAnalyzerWarnings(q{
307         class Bar
308         {
309             this(){foo();} // [warn]: %s
310             private:
311             public
312             void foo(){}
313 
314         }
315     }}.format(VcallCtorChecker.MSG), sac);
316 
317     assertAnalyzerWarnings(q{
318         class Bar
319         {
320             this()
321             {
322                 foo(); // [warn]: %s
323                 foo(); // [warn]: %s
324                 bar();
325             }
326             private: void bar();
327             public{void foo(){}}
328         }
329     }}.format(VcallCtorChecker.MSG, VcallCtorChecker.MSG), sac);
330 
331     assertAnalyzerWarnings(q{
332         class Bar
333         {
334             this()
335             {
336                 foo();
337                 bar(); // [warn]: %s
338             }
339             private: public void bar();
340             public private {void foo(){}}
341         }
342     }}.format(VcallCtorChecker.MSG), sac);
343 
344     // passes
345     assertAnalyzerWarnings(q{
346         class Bar
347         {
348             this(){foo();}
349             private void foo(){}
350         }
351     }, sac);
352 
353     assertAnalyzerWarnings(q{
354         class Bar
355         {
356             this(){foo();}
357             private {void foo(){}}
358         }
359     }, sac);
360 
361     assertAnalyzerWarnings(q{
362         class Bar
363         {
364             this(){foo();}
365             private public protected private void foo(){}
366         }
367     }, sac);
368 
369     assertAnalyzerWarnings(q{
370         class Bar
371         {
372             this(){foo();}
373             final private public protected void foo(){}
374         }
375     }, sac);
376 
377     assertAnalyzerWarnings(q{
378         class Bar
379         {
380             this(){foo();}
381             final void foo(){}
382         }
383     }, sac);
384 
385     assertAnalyzerWarnings(q{
386         final class Bar
387         {
388             public:
389             this(){foo();}
390             void foo(){}
391         }
392     }, sac);
393 
394     assertAnalyzerWarnings(q{
395         class Bar
396         {
397             public:
398             this(){foo();}
399             void foo(T)(){}
400         }
401     }, sac);
402 
403     assertAnalyzerWarnings(q{
404         class Foo
405         {
406             static void nonVirtual();
407             this(){nonVirtual();}
408         }
409     }, sac);
410 
411     assertAnalyzerWarnings(q{
412         class Foo
413         {
414             package void nonVirtual();
415             this(){nonVirtual();}
416         }
417     }, sac);
418 
419     assertAnalyzerWarnings(q{
420         class C {
421             static struct S {
422             public:
423                 this(int) {
424                     foo();
425                 }
426                 void foo() {}
427             }
428         }
429     }, sac);
430 
431     import std.stdio: writeln;
432     writeln("Unittest for VcallCtorChecker passed");
433 }
434