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 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!IdentifierOrTemplateChain;
142 	mixin PartsMightModify!ReturnStatement;
143 
144 	override void visit(const UnaryExpression unary)
145 	{
146 		if (unary.prefix == tok!"++" || unary.prefix == tok!"--"
147 				|| unary.suffix == tok!"++" || unary.suffix == tok!"--"
148 				|| unary.prefix == tok!"*" || unary.prefix == tok!"&")
149 		{
150 			interest++;
151 			guaranteeUse++;
152 			unary.accept(this);
153 			guaranteeUse--;
154 			interest--;
155 		}
156 		else
157 			unary.accept(this);
158 	}
159 
160 	override void visit(const ForeachStatement foreachStatement)
161 	{
162 		if (foreachStatement.low !is null)
163 		{
164 			interest++;
165 			foreachStatement.low.accept(this);
166 			interest--;
167 		}
168 		if (foreachStatement.declarationOrStatement !is null)
169 			foreachStatement.declarationOrStatement.accept(this);
170 	}
171 
172 	override void visit(const TraitsExpression)
173 	{
174 		// issue #266: Ignore unmodified variables inside of `__traits` expressions
175 	}
176 
177 	override void visit(const TypeofExpression)
178 	{
179 		// issue #270: Ignore unmodified variables inside of `typeof` expressions
180 	}
181 
182 	override void visit(const AsmStatement a)
183 	{
184 		inAsm = true;
185 		a.accept(this);
186 		inAsm = false;
187 	}
188 
189 private:
190 
191 	template PartsMightModify(T)
192 	{
193 		override void visit(const T t)
194 		{
195 			interest++;
196 			t.accept(this);
197 			interest--;
198 		}
199 	}
200 
201 	void variableMightBeModified(string name)
202 	{
203 		size_t index = tree.length - 1;
204 		auto vi = VariableInfo(name);
205 		if (guaranteeUse == 0)
206 		{
207 			auto r = tree[index].equalRange(&vi);
208 			if (!r.empty && r.front.isValueType && !inAsm)
209 				return;
210 		}
211 		while (true)
212 		{
213 			if (tree[index].removeKey(&vi) != 0 || index == 0)
214 				break;
215 			index--;
216 		}
217 	}
218 
219 	bool initializedFromNew(const Initializer initializer)
220 	{
221 		if (const UnaryExpression ue = cast(UnaryExpression) safeAccess(initializer)
222 			.nonVoidInitializer.assignExpression)
223 		{
224 			return ue.newExpression !is null;
225 		}
226 		return false;
227 	}
228 
229 	bool initializedFromCast(const Initializer initializer)
230 	{
231 		import std.typecons : scoped;
232 
233 		static class CastFinder : ASTVisitor
234 		{
235 			alias visit = ASTVisitor.visit;
236 			override void visit(const CastExpression castExpression)
237 			{
238 				foundCast = true;
239 				castExpression.accept(this);
240 			}
241 
242 			bool foundCast;
243 		}
244 
245 		if (initializer is null)
246 			return false;
247 		auto finder = scoped!CastFinder();
248 		finder.visit(initializer);
249 		return finder.foundCast;
250 	}
251 
252 	bool canFindImmutableOrConst(const Declaration dec)
253 	{
254 		import std.algorithm : canFind, map, filter;
255 
256 		return !dec.attributes.map!(a => a.attribute)
257 			.filter!(a => a == tok!"immutable" || a == tok!"const").empty;
258 	}
259 
260 	bool canFindImmutable(const VariableDeclaration dec)
261 	{
262 		import std.algorithm : canFind;
263 
264 		foreach (storageClass; dec.storageClasses)
265 		{
266 			if (storageClass.token == tok!"enum")
267 				return true;
268 		}
269 		foreach (sc; dec.storageClasses)
270 		{
271 			if (sc.token == tok!"immutable" || sc.token == tok!"const")
272 				return true;
273 		}
274 		if (dec.type !is null)
275 		{
276 			foreach (tk; dec.type.typeConstructors)
277 				if (tk == tok!"immutable" || tk == tok!"const")
278 					return true;
279 			if (dec.type.type2)
280 			{
281 				const tk = dec.type.type2.typeConstructor;
282 				if (tk == tok!"immutable" || tk == tok!"const")
283 					return true;
284 			}
285 		}
286 		return false;
287 	}
288 
289 	static struct VariableInfo
290 	{
291 		string name;
292 		size_t line;
293 		size_t column;
294 		bool isValueType;
295 	}
296 
297 	void popScope()
298 	{
299 		foreach (vi; tree[$ - 1])
300 		{
301 			immutable string errorMessage = "Variable " ~ vi.name
302 				~ " is never modified and could have been declared const or immutable.";
303 			addErrorMessage(vi.line, vi.column, "dscanner.suspicious.unmodified", errorMessage);
304 		}
305 		tree = tree[0 .. $ - 1];
306 	}
307 
308 	void pushScope()
309 	{
310 		tree ~= new RedBlackTree!(VariableInfo*, "a.name < b.name");
311 	}
312 
313 	int blockStatementDepth;
314 
315 	int interest;
316 
317 	int guaranteeUse;
318 
319 	int isImmutable;
320 
321 	bool inAsm;
322 
323 	RedBlackTree!(VariableInfo*, "a.name < b.name")[] tree;
324 }
325 
326 bool isValueTypeSimple(const Type type) pure nothrow @nogc
327 {
328 	if (type.type2 is null)
329 		return false;
330 	return type.type2.builtinType != tok!"" && type.typeSuffixes.length == 0;
331 }
332 
333 @system unittest
334 {
335 	import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
336 	import dscanner.analysis.helpers : assertAnalyzerWarnings;
337 	import std.stdio : stderr;
338 	import std.format : format;
339 
340 	StaticAnalysisConfig sac = disabledConfig();
341 	sac.could_be_immutable_check = Check.enabled;
342 
343 	// fails
344 
345 	assertAnalyzerWarnings(q{
346 		void foo(){int i = 1;} // [warn]: Variable i is never modified and could have been declared const or immutable.
347 	}, sac);
348 
349 	// pass
350 
351 	assertAnalyzerWarnings(q{
352 		void foo(){const(int) i;}
353 	}, sac);
354 
355 	assertAnalyzerWarnings(q{
356 		void foo(){immutable(int)* i;}
357 	}, sac);
358 
359 	assertAnalyzerWarnings(q{
360 		void foo(){enum i = 1;}
361 	}, sac);
362 
363 	assertAnalyzerWarnings(q{
364 		void foo(){E e = new E;}
365 	}, sac);
366 
367 	assertAnalyzerWarnings(q{
368 		void foo(){auto e = new E;}
369 	}, sac);
370 
371 }