1 //          Copyright Brian Schott (Hackerpilot) 2015.
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.unmodified;
6 
7 import dscanner.analysis.base;
8 import dscanner.utils : safeAccess;
9 import dsymbol.scope_ : Scope;
10 import std.container;
11 import dparse.ast;
12 import dparse.lexer;
13 
14 /**
15  * Checks for variables that could have been declared const or immutable
16  */
17 final class UnmodifiedFinder : BaseAnalyzer
18 {
19 	alias visit = BaseAnalyzer.visit;
20 
21 	mixin AnalyzerInfo!"could_be_immutable_check";
22 
23 	///
24 	this(string fileName, const(Scope)* sc, bool skipTests = false)
25 	{
26 		super(fileName, sc, skipTests);
27 	}
28 
29 	override void visit(const Module mod)
30 	{
31 		pushScope();
32 		mod.accept(this);
33 		popScope();
34 	}
35 
36 	override void visit(const BlockStatement blockStatement)
37 	{
38 		pushScope();
39 		blockStatementDepth++;
40 		blockStatement.accept(this);
41 		blockStatementDepth--;
42 		popScope();
43 	}
44 
45 	override void visit(const StructBody structBody)
46 	{
47 		pushScope();
48 		immutable oldBlockStatementDepth = blockStatementDepth;
49 		blockStatementDepth = 0;
50 		structBody.accept(this);
51 		blockStatementDepth = oldBlockStatementDepth;
52 		popScope();
53 	}
54 
55 	override void visit(const VariableDeclaration dec)
56 	{
57 		if (dec.autoDeclaration is null && blockStatementDepth > 0
58 				&& isImmutable <= 0 && !canFindImmutable(dec))
59 		{
60 			foreach (d; dec.declarators)
61 			{
62 				if (initializedFromCast(d.initializer))
63 					continue;
64 				if (initializedFromNew(d.initializer))
65 					continue;
66 				tree[$ - 1].insert(new VariableInfo(d.name.text, d.name.line,
67 						d.name.column, isValueTypeSimple(dec.type)));
68 			}
69 		}
70 		dec.accept(this);
71 	}
72 
73 	override void visit(const AutoDeclaration autoDeclaration)
74 	{
75 		import std.algorithm : canFind;
76 
77 		if (blockStatementDepth > 0 && isImmutable <= 0
78 				&& (!autoDeclaration.storageClasses.canFind!(a => a.token == tok!"const"
79 					|| a.token == tok!"enum" || a.token == tok!"immutable")))
80 		{
81 			foreach (part; autoDeclaration.parts)
82 			{
83 				if (initializedFromCast(part.initializer))
84 					continue;
85 				if (initializedFromNew(part.initializer))
86 					continue;
87 				tree[$ - 1].insert(new VariableInfo(part.identifier.text,
88 						part.identifier.line, part.identifier.column));
89 			}
90 		}
91 		autoDeclaration.accept(this);
92 	}
93 
94 	override void visit(const AssignExpression assignExpression)
95 	{
96 		if (assignExpression.operator != tok!"")
97 		{
98 			interest++;
99 			guaranteeUse++;
100 			assignExpression.ternaryExpression.accept(this);
101 			guaranteeUse--;
102 			interest--;
103 
104 			if (assignExpression.operator == tok!"~=")
105 				interest++;
106 			assignExpression.expression.accept(this);
107 			if (assignExpression.operator == tok!"~=")
108 				interest--;
109 		}
110 		else
111 			assignExpression.accept(this);
112 	}
113 
114 	override void visit(const Declaration dec)
115 	{
116 		if (canFindImmutableOrConst(dec))
117 		{
118 			isImmutable++;
119 			dec.accept(this);
120 			isImmutable--;
121 		}
122 		else
123 			dec.accept(this);
124 	}
125 
126 	override void visit(const IdentifierChain ic)
127 	{
128 		if (ic.identifiers.length && interest > 0)
129 			variableMightBeModified(ic.identifiers[0].text);
130 		ic.accept(this);
131 	}
132 
133 	override void visit(const IdentifierOrTemplateInstance ioti)
134 	{
135 		if (ioti.identifier != tok!"" && interest > 0)
136 			variableMightBeModified(ioti.identifier.text);
137 		ioti.accept(this);
138 	}
139 
140 	mixin PartsMightModify!AsmPrimaryExp;
141 	mixin PartsMightModify!IndexExpression;
142 	mixin PartsMightModify!FunctionCallExpression;
143 	mixin PartsMightModify!NewExpression;
144 	mixin PartsMightModify!IdentifierOrTemplateChain;
145 	mixin PartsMightModify!ReturnStatement;
146 
147 	override void visit(const UnaryExpression unary)
148 	{
149 		if (unary.prefix == tok!"++" || unary.prefix == tok!"--"
150 				|| unary.suffix == tok!"++" || unary.suffix == tok!"--"
151 				|| unary.prefix == tok!"*" || unary.prefix == tok!"&")
152 		{
153 			interest++;
154 			guaranteeUse++;
155 			unary.accept(this);
156 			guaranteeUse--;
157 			interest--;
158 		}
159 		else
160 			unary.accept(this);
161 	}
162 
163 	override void visit(const ForeachStatement foreachStatement)
164 	{
165 		if (foreachStatement.low !is null)
166 		{
167 			interest++;
168 			foreachStatement.low.accept(this);
169 			interest--;
170 		}
171 		if (foreachStatement.declarationOrStatement !is null)
172 			foreachStatement.declarationOrStatement.accept(this);
173 	}
174 
175 	override void visit(const TraitsExpression)
176 	{
177 		// issue #266: Ignore unmodified variables inside of `__traits` expressions
178 	}
179 
180 	override void visit(const TypeofExpression)
181 	{
182 		// issue #270: Ignore unmodified variables inside of `typeof` expressions
183 	}
184 
185 	override void visit(const AsmStatement a)
186 	{
187 		inAsm = true;
188 		a.accept(this);
189 		inAsm = false;
190 	}
191 
192 private:
193 
194 	template PartsMightModify(T)
195 	{
196 		override void visit(const T t)
197 		{
198 			interest++;
199 			t.accept(this);
200 			interest--;
201 		}
202 	}
203 
204 	void variableMightBeModified(string name)
205 	{
206 		size_t index = tree.length - 1;
207 		auto vi = VariableInfo(name);
208 		if (guaranteeUse == 0)
209 		{
210 			auto r = tree[index].equalRange(&vi);
211 			if (!r.empty && r.front.isValueType && !inAsm)
212 				return;
213 		}
214 		while (true)
215 		{
216 			if (tree[index].removeKey(&vi) != 0 || index == 0)
217 				break;
218 			index--;
219 		}
220 	}
221 
222 	bool initializedFromNew(const Initializer initializer)
223 	{
224 		if (const UnaryExpression ue = cast(UnaryExpression) safeAccess(initializer)
225 			.nonVoidInitializer.assignExpression)
226 		{
227 			return ue.newExpression !is null;
228 		}
229 		return false;
230 	}
231 
232 	bool initializedFromCast(const Initializer initializer)
233 	{
234 		import std.typecons : scoped;
235 
236 		static class CastFinder : ASTVisitor
237 		{
238 			alias visit = ASTVisitor.visit;
239 			override void visit(const CastExpression castExpression)
240 			{
241 				foundCast = true;
242 				castExpression.accept(this);
243 			}
244 
245 			bool foundCast;
246 		}
247 
248 		if (initializer is null)
249 			return false;
250 		auto finder = scoped!CastFinder();
251 		finder.visit(initializer);
252 		return finder.foundCast;
253 	}
254 
255 	bool canFindImmutableOrConst(const Declaration dec)
256 	{
257 		import std.algorithm : canFind, map, filter;
258 
259 		return !dec.attributes.map!(a => a.attribute)
260 			.filter!(a => a == tok!"immutable" || a == tok!"const").empty;
261 	}
262 
263 	bool canFindImmutable(const VariableDeclaration dec)
264 	{
265 		import std.algorithm : canFind;
266 
267 		foreach (storageClass; dec.storageClasses)
268 		{
269 			if (storageClass.token == tok!"enum")
270 				return true;
271 		}
272 		foreach (sc; dec.storageClasses)
273 		{
274 			if (sc.token == tok!"immutable" || sc.token == tok!"const")
275 				return true;
276 		}
277 		if (dec.type !is null)
278 		{
279 			foreach (tk; dec.type.typeConstructors)
280 				if (tk == tok!"immutable" || tk == tok!"const")
281 					return true;
282 			if (dec.type.type2)
283 			{
284 				const tk = dec.type.type2.typeConstructor;
285 				if (tk == tok!"immutable" || tk == tok!"const")
286 					return true;
287 			}
288 		}
289 		return false;
290 	}
291 
292 	static struct VariableInfo
293 	{
294 		string name;
295 		size_t line;
296 		size_t column;
297 		bool isValueType;
298 	}
299 
300 	void popScope()
301 	{
302 		foreach (vi; tree[$ - 1])
303 		{
304 			immutable string errorMessage = "Variable " ~ vi.name
305 				~ " is never modified and could have been declared const or immutable.";
306 			addErrorMessage(vi.line, vi.column, "dscanner.suspicious.unmodified", errorMessage);
307 		}
308 		tree = tree[0 .. $ - 1];
309 	}
310 
311 	void pushScope()
312 	{
313 		tree ~= new RedBlackTree!(VariableInfo*, "a.name < b.name");
314 	}
315 
316 	int blockStatementDepth;
317 
318 	int interest;
319 
320 	int guaranteeUse;
321 
322 	int isImmutable;
323 
324 	bool inAsm;
325 
326 	RedBlackTree!(VariableInfo*, "a.name < b.name")[] tree;
327 }
328 
329 bool isValueTypeSimple(const Type type) pure nothrow @nogc
330 {
331 	if (type.type2 is null)
332 		return false;
333 	return type.type2.builtinType != tok!"" && type.typeSuffixes.length == 0;
334 }
335 
336 @system unittest
337 {
338 	import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
339 	import dscanner.analysis.helpers : assertAnalyzerWarnings;
340 	import std.stdio : stderr;
341 	import std.format : format;
342 
343 	StaticAnalysisConfig sac = disabledConfig();
344 	sac.could_be_immutable_check = Check.enabled;
345 
346 	// fails
347 
348 	assertAnalyzerWarnings(q{
349 		void foo(){int i = 1;} // [warn]: Variable i is never modified and could have been declared const or immutable.
350 	}, sac);
351 
352 	// pass
353 
354 	assertAnalyzerWarnings(q{
355 		void foo(){const(int) i;}
356 	}, sac);
357 
358 	assertAnalyzerWarnings(q{
359 		void foo(){immutable(int)* i;}
360 	}, sac);
361 
362 	assertAnalyzerWarnings(q{
363 		void foo(){enum i = 1;}
364 	}, sac);
365 
366 	assertAnalyzerWarnings(q{
367 		void foo(){E e = new E;}
368 	}, sac);
369 
370 	assertAnalyzerWarnings(q{
371 		void foo(){auto e = new E;}
372 	}, sac);
373 
374 	assertAnalyzerWarnings(q{
375 		void issue640()
376 		{
377 			size_t i1;
378 			new Foo(i1);
379 
380 			size_t i2;
381 			foo(i2);
382 		}
383 	}, sac);
384 }
385