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