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 //          http://www.boost.org/LICENSE_1_0.txt)
5 module dscanner.analysis.useless_initializer;
6 
7 import dscanner.analysis.base;
8 import dscanner.utils : safeAccess;
9 import containers.dynamicarray;
10 import containers.hashmap;
11 import dparse.ast;
12 import dparse.lexer;
13 import std.algorithm;
14 import std.range : empty;
15 import std.stdio;
16 
17 /*
18 Limitations:
19 	- Stuff s = Stuff.init does not work with type with postfixes`*` `[]`.
20 	- Stuff s = Stuff.init is only detected for struct within the module.
21 	- BasicType b = BasicType(v), default init used in BasicType ctor, e.g int(8).
22 */
23 
24 /**
25  * Check that detects the initializers that are
26  * not different from the implcit initializer.
27  */
28 final class UselessInitializerChecker : BaseAnalyzer
29 {
30 	alias visit = BaseAnalyzer.visit;
31 
32 private:
33 
34 	enum key = "dscanner.useless-initializer";
35 
36 	version(unittest)
37 	{
38 		enum msg = "X";
39 	}
40 	else
41 	{
42 		enum msg = `Variable %s initializer is useless because it does not differ from the default value`;
43 	}
44 
45 	static immutable intDefs = ["0", "0L", "0UL", "0uL", "0U", "0x0", "0b0"];
46 
47 	HashMap!(string, bool) _structCanBeInit;
48 	DynamicArray!(string) _structStack;
49 	DynamicArray!(bool) _inStruct;
50 	DynamicArray!(bool) _atDisabled;
51 	bool _inTest;
52 
53 public:
54 
55 	///
56 	this(string fileName, bool skipTests = false)
57 	{
58 		super(fileName, null, skipTests);
59 		_inStruct.insert(false);
60 	}
61 
62 	override void visit(const(Unittest) test)
63 	{
64 		if (skipTests)
65 			return;
66 		_inTest = true;
67 		test.accept(this);
68 		_inTest = false;
69 	}
70 
71 	override void visit(const(StructDeclaration) decl)
72 	{
73 		if (_inTest)
74 			return;
75 
76 		assert(_inStruct.length > 1);
77 
78 		const string structName = _inStruct[$-2] ?
79 			_structStack.back() ~ "." ~ decl.name.text :
80 		  decl.name.text;
81 
82 		_structStack.insert(structName);
83 		_structCanBeInit[structName] = false;
84 		_atDisabled.insert(false);
85 		decl.accept(this);
86 		_structStack.removeBack();
87 		_atDisabled.removeBack();
88 	}
89 
90 	override void visit(const(Declaration) decl)
91 	{
92 		_inStruct.insert(decl.structDeclaration !is null);
93 		decl.accept(this);
94 		if (_inStruct.length > 1 && _inStruct[$-2] && decl.constructor &&
95 			((decl.constructor.parameters && decl.constructor.parameters.parameters.length == 0) ||
96 			!decl.constructor.parameters))
97 		{
98 			_atDisabled[$-1] = decl.attributes
99 				.canFind!(a => a.atAttribute !is null && a.atAttribute.identifier.text == "disable");
100 		}
101 		_inStruct.removeBack();
102 	}
103 
104 	override void visit(const(Constructor) decl)
105 	{
106 		if (_inStruct.length > 1 && _inStruct[$-2] &&
107 			((decl.parameters && decl.parameters.parameters.length == 0) || !decl.parameters))
108 		{
109 			const bool canBeInit = !_atDisabled[$-1];
110 			_structCanBeInit[_structStack.back()] = canBeInit;
111 			if (!canBeInit)
112 				_structCanBeInit[_structStack.back()] = !decl.memberFunctionAttributes
113 					.canFind!(a => a.atAttribute !is null && a.atAttribute.identifier.text == "disable");
114 		}
115 		decl.accept(this);
116 	}
117 
118 	// issue 473, prevent to visit delegates that contain duck type checkers.
119 	override void visit(const(TypeofExpression)) {}
120 
121 	// issue 473, prevent to check expressions in __traits(compiles, ...)
122 	override void visit(const(TraitsExpression) e)
123 	{
124 		if (e.identifier.text == "compiles")
125 		{
126 			return;
127 		}
128 		else
129 		{
130 			e.accept(this);
131 		}
132 	}
133 
134 	override void visit(const(VariableDeclaration) decl)
135 	{
136 		if (!decl.type || !decl.type.type2 ||
137 			// initializer has to appear clearly in generated ddoc
138 			decl.comment !is null ||
139 			// issue 474, manifest constants HAVE to be initialized.
140 			decl.storageClasses.canFind!(a => a.token == tok!"enum"))
141 		{
142 			return;
143 		}
144 
145 		foreach (declarator; decl.declarators)
146 		{
147 			if (!declarator.initializer ||
148 				!declarator.initializer.nonVoidInitializer ||
149 				declarator.comment !is null)
150 			{
151 				continue;
152 			}
153 
154 			version(unittest)
155 			{
156 				enum warn = q{addErrorMessage(declarator.name.line,
157 					declarator.name.column, key, msg);};
158 			}
159 			else
160 			{
161 				import std.format : format;
162 				enum warn = q{addErrorMessage(declarator.name.line,
163 				declarator.name.column, key, msg.format(declarator.name.text));};
164 			}
165 
166 			// ---  Info about the declaration type --- //
167 			const bool isPtr = decl.type.typeSuffixes && decl.type.typeSuffixes
168 				.canFind!(a => a.star != tok!"");
169 			const bool isArr = decl.type.typeSuffixes && decl.type.typeSuffixes
170 				.canFind!(a => a.array);
171 
172 			bool isStr, isSzInt;
173 			Token customType;
174 
175 			if (const TypeIdentifierPart tip = safeAccess(decl).type.type2.typeIdentifierPart)
176 			{
177 				if (!tip.typeIdentifierPart)
178 				{
179 					customType = tip.identifierOrTemplateInstance.identifier;
180 					isStr = customType.text.among("string", "wstring", "dstring") != 0;
181 					isSzInt = customType.text.among("size_t", "ptrdiff_t") != 0;
182 				}
183 			}
184 
185 			// --- 'BasicType/Symbol AssignExpression' ---//
186 			const NonVoidInitializer nvi = declarator.initializer.nonVoidInitializer;
187 			const UnaryExpression ue = cast(UnaryExpression) nvi.assignExpression;
188 			if (ue && ue.primaryExpression)
189 			{
190 				const Token value = ue.primaryExpression.primary;
191 
192 				if (!isPtr && !isArr && !isStr && decl.type.type2.builtinType != tok!"")
193 				{
194 					switch(decl.type.type2.builtinType)
195 					{
196 					// check for common cases of default values
197 					case tok!"byte",    tok!"ubyte":
198 					case tok!"short",   tok!"ushort":
199 					case tok!"int",     tok!"uint":
200 					case tok!"long",    tok!"ulong":
201 					case tok!"cent",    tok!"ucent":
202 					case tok!"bool":
203 						if (intDefs.canFind(value.text) || value == tok!"false")
204 							mixin(warn);
205 						goto default;
206 					default:
207 					// check for BasicType.init
208 						if (ue.primaryExpression.basicType.type == decl.type.type2.builtinType &&
209 							ue.primaryExpression.primary.text == "init" &&
210 							!ue.primaryExpression.expression)
211 							mixin(warn);
212 					}
213 				}
214 				else if (isSzInt)
215 				{
216 					if (intDefs.canFind(value.text))
217 						mixin(warn);
218 				}
219 				else if (isPtr || isStr)
220 				{
221 					if (str(value.type) == "null")
222 						mixin(warn);
223 				}
224 				else if (isArr)
225 				{
226 					if (str(value.type) == "null")
227 						mixin(warn);
228 					else if (nvi.arrayInitializer && nvi.arrayInitializer.arrayMemberInitializations.length == 0)
229 						mixin(warn);
230 				}
231 			}
232 
233 			else if (const IdentifierOrTemplateInstance iot = safeAccess(ue)
234 				.unaryExpression.primaryExpression.identifierOrTemplateInstance)
235 			{
236 				// Symbol s = Symbol.init
237 				if (ue && customType != tok!"" && iot.identifier == customType &&
238 					ue.identifierOrTemplateInstance && ue.identifierOrTemplateInstance.identifier.text == "init")
239 				{
240 					if (customType.text in _structCanBeInit)
241 					{
242 						if  (!_structCanBeInit[customType.text])
243 							mixin(warn);
244 					}
245 				}
246 			}
247 
248 			// 'Symbol ArrayInitializer' : assumes Symbol is an array b/c of the Init
249 			else if (nvi.arrayInitializer && (isArr || isStr))
250 			{
251 				if (nvi.arrayInitializer.arrayMemberInitializations.length == 0)
252 					mixin(warn);
253 			}
254 		}
255 
256 		decl.accept(this);
257 	}
258 }
259 
260 @system unittest
261 {
262 	import dscanner.analysis.config : Check, disabledConfig, StaticAnalysisConfig;
263 	import dscanner.analysis.helpers: assertAnalyzerWarnings;
264 	import std.stdio : stderr;
265 
266 	StaticAnalysisConfig sac = disabledConfig;
267 	sac.useless_initializer = Check.enabled;
268 
269 	// fails
270 	assertAnalyzerWarnings(q{
271 		struct S {}
272 		ubyte a = 0x0;      // [warn]: X
273 		int a = 0;          // [warn]: X
274 		ulong a = 0;        // [warn]: X
275 		int* a = null;      // [warn]: X
276 		Foo* a = null;      // [warn]: X
277 		int[] a = null;     // [warn]: X
278 		int[] a = [];       // [warn]: X
279 		string a = null;    // [warn]: X
280 		string a = null;    // [warn]: X
281 		wstring a = null;   // [warn]: X
282 		dstring a = null;   // [warn]: X
283 		size_t a = 0;       // [warn]: X
284 		ptrdiff_t a = 0;    // [warn]: X
285 		string a = [];      // [warn]: X
286 		char[] a = null;    // [warn]: X
287 		int a = int.init;   // [warn]: X
288 		char a = char.init; // [warn]: X
289 		S s = S.init;       // [warn]: X
290 		bool a = false;     // [warn]: X
291 	}, sac);
292 
293 	// passes
294 	assertAnalyzerWarnings(q{
295 		struct D {@disable this();}
296 		struct E {this() @disable;}
297 		ubyte a = 0xFE;
298 		int a = 1;
299 		ulong a = 1;
300 		int* a = &a;
301 		Foo* a = &a;
302 		int[] a = &a;
303 		int[] a = [0];
304 		string a = "sdf";
305 		string a = "sdg"c;
306 		wstring a = "sdg"w;
307 		dstring a = "fgh"d;
308 		string a = q{int a;};
309 		size_t a = 1;
310 		ptrdiff_t a;
311 		ubyte a;
312 		int a;
313 		ulong a;
314 		int* a;
315 		Foo* a;
316 		int[] a;
317 		string a;
318 		wstring a;
319 		dstring a;
320 		string a = ['a'];
321 		string a = "";
322 		string a = ""c;
323 		wstring a = ""w;
324 		dstring a = ""d;
325 		string a = q{};
326 		char[] a = "ze";
327 		S s = S(0,1);
328 		S s = s.call();
329 		enum {a}
330 		enum ubyte a = 0;
331 		static assert(is(typeof((){T t = T.init;})));
332 		void foo(){__traits(compiles, (){int a = 0;}).writeln;}
333 		bool a;
334 		D d = D.init;
335 		E e = E.init;
336 		NotKnown nk = NotKnown.init;
337 	}, sac);
338 
339 	stderr.writeln("Unittest for UselessInitializerChecker passed.");
340 }
341