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