1 //          Copyright Brian Schott (Hackerpilot) 2014.
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 
6 module analysis.run;
7 
8 import std.stdio;
9 import std.array;
10 import std.conv;
11 import std.algorithm;
12 import std.range;
13 import std.array;
14 import std.functional : toDelegate;
15 import dparse.lexer;
16 import dparse.parser;
17 import dparse.ast;
18 import dparse.rollback_allocator;
19 import std.typecons : scoped;
20 
21 import std.experimental.allocator : CAllocatorImpl;
22 import std.experimental.allocator.mallocator : Mallocator;
23 import std.experimental.allocator.building_blocks.region : Region;
24 import std.experimental.allocator.building_blocks.allocator_list : AllocatorList;
25 
26 import analysis.config;
27 import analysis.base;
28 import analysis.style;
29 import analysis.enumarrayliteral;
30 import analysis.pokemon;
31 import analysis.del;
32 import analysis.fish;
33 import analysis.numbers;
34 import analysis.objectconst;
35 import analysis.range;
36 import analysis.ifelsesame;
37 import analysis.constructors;
38 import analysis.unused;
39 import analysis.unused_label;
40 import analysis.duplicate_attribute;
41 import analysis.opequals_without_tohash;
42 import analysis.length_subtraction;
43 import analysis.builtin_property_names;
44 import analysis.asm_style;
45 import analysis.logic_precedence;
46 import analysis.stats_collector;
47 import analysis.undocumented;
48 import analysis.comma_expression;
49 import analysis.function_attributes;
50 import analysis.local_imports;
51 import analysis.unmodified;
52 import analysis.if_statements;
53 import analysis.redundant_parens;
54 import analysis.mismatched_args;
55 import analysis.label_var_same_name_check;
56 import analysis.line_length;
57 import analysis.auto_ref_assignment;
58 import analysis.incorrect_infinite_range;
59 import analysis.useless_assert;
60 import analysis.alias_syntax_check;
61 import analysis.static_if_else;
62 import analysis.lambda_return_check;
63 import analysis.auto_function;
64 import analysis.imports_sortedness;
65 import analysis.explicitly_annotated_unittests;
66 import analysis.properly_documented_public_functions;
67 import analysis.final_attribute;
68 import analysis.vcall_in_ctor;
69 import analysis.useless_initializer;
70 import analysis.allman;
71 import analysis.redundant_attributes;
72 import analysis.has_public_example;
73 
74 import dsymbol.string_interning : internString;
75 import dsymbol.scope_;
76 import dsymbol.semantic;
77 import dsymbol.conversion;
78 import dsymbol.conversion.first;
79 import dsymbol.conversion.second;
80 import dsymbol.modulecache : ModuleCache;
81 
82 import readers;
83 
84 bool first = true;
85 
86 private alias ASTAllocator = CAllocatorImpl!(
87 		AllocatorList!(n => Region!Mallocator(1024 * 128), Mallocator));
88 
89 void messageFunction(string fileName, size_t line, size_t column, string message, bool isError)
90 {
91 	writefln("%s(%d:%d)[%s]: %s", fileName, line, column, isError ? "error" : "warn", message);
92 }
93 
94 void messageFunctionJSON(string fileName, size_t line, size_t column, string message, bool)
95 {
96 	writeJSON("dscanner.syntax", fileName, line, column, message);
97 }
98 
99 void writeJSON(string key, string fileName, size_t line, size_t column, string message)
100 {
101 	if (!first)
102 		writeln(",");
103 	else
104 		first = false;
105 	writeln("    {");
106 	writeln(`      "key": "`, key, `",`);
107 	writeln(`      "fileName": "`, fileName.replace("\\", "\\\\").replace(`"`, `\"`), `",`);
108 	writeln(`      "line": `, line, `,`);
109 	writeln(`      "column": `, column, `,`);
110 	writeln(`      "message": "`, message.replace("\\", "\\\\").replace(`"`, `\"`), `"`);
111 	write("    }");
112 }
113 
114 bool syntaxCheck(string[] fileNames, ref StringCache stringCache, ref ModuleCache moduleCache)
115 {
116 	StaticAnalysisConfig config = defaultStaticAnalysisConfig();
117 	return analyze(fileNames, config, stringCache, moduleCache, false);
118 }
119 
120 void generateReport(string[] fileNames, const StaticAnalysisConfig config,
121 		ref StringCache cache, ref ModuleCache moduleCache)
122 {
123 	writeln("{");
124 	writeln(`  "issues": [`);
125 	first = true;
126 	StatsCollector stats = new StatsCollector("");
127 	ulong lineOfCodeCount;
128 	foreach (fileName; fileNames)
129 	{
130 		auto code = readFile(fileName);
131 		// Skip files that could not be read and continue with the rest
132 		if (code.length == 0)
133 			continue;
134 		RollbackAllocator r;
135 		const(Token)[] tokens;
136 		const Module m = parseModule(fileName, code, &r, cache, true, tokens, &lineOfCodeCount);
137 		stats.visit(m);
138 		MessageSet results = analyze(fileName, m, config, moduleCache, tokens, true);
139 		foreach (result; results[])
140 		{
141 			writeJSON(result.key, result.fileName, result.line, result.column, result.message);
142 		}
143 	}
144 	writeln();
145 	writeln("  ],");
146 	writefln(`  "interfaceCount": %d,`, stats.interfaceCount);
147 	writefln(`  "classCount": %d,`, stats.classCount);
148 	writefln(`  "functionCount": %d,`, stats.functionCount);
149 	writefln(`  "templateCount": %d,`, stats.templateCount);
150 	writefln(`  "structCount": %d,`, stats.structCount);
151 	writefln(`  "statementCount": %d,`, stats.statementCount);
152 	writefln(`  "lineOfCodeCount": %d,`, lineOfCodeCount);
153 	writefln(`  "undocumentedPublicSymbols": %d`, stats.undocumentedPublicSymbols);
154 	writeln("}");
155 }
156 
157 /**
158  * For multiple files
159  *
160  * Returns: true if there were errors or if there were warnings and `staticAnalyze` was true.
161  */
162 bool analyze(string[] fileNames, const StaticAnalysisConfig config,
163 		ref StringCache cache, ref ModuleCache moduleCache, bool staticAnalyze = true)
164 {
165 	bool hasErrors;
166 	foreach (fileName; fileNames)
167 	{
168 		auto code = readFile(fileName);
169 		// Skip files that could not be read and continue with the rest
170 		if (code.length == 0)
171 			continue;
172 		RollbackAllocator r;
173 		uint errorCount;
174 		uint warningCount;
175 		const(Token)[] tokens;
176 		const Module m = parseModule(fileName, code, &r, cache, false, tokens,
177 				null, &errorCount, &warningCount);
178 		assert(m);
179 		if (errorCount > 0 || (staticAnalyze && warningCount > 0))
180 			hasErrors = true;
181 		MessageSet results = analyze(fileName, m, config, moduleCache, tokens, staticAnalyze);
182 		if (results is null)
183 			continue;
184 		foreach (result; results[])
185 		{
186 			hasErrors = true;
187 			writefln("%s(%d:%d)[warn]: %s", result.fileName, result.line,
188 					result.column, result.message);
189 		}
190 	}
191 	return hasErrors;
192 }
193 
194 const(Module) parseModule(string fileName, ubyte[] code, RollbackAllocator* p,
195 		ref StringCache cache, bool report, ref const(Token)[] tokens,
196 		ulong* linesOfCode = null, uint* errorCount = null, uint* warningCount = null)
197 {
198 	import stats : isLineOfCode;
199 
200 	LexerConfig config;
201 	config.fileName = fileName;
202 	config.stringBehavior = StringBehavior.source;
203 	tokens = getTokensForParser(code, config, &cache);
204 	if (linesOfCode !is null)
205 		(*linesOfCode) += count!(a => isLineOfCode(a.type))(tokens);
206 	return dparse.parser.parseModule(tokens, fileName, p,
207 		report ? toDelegate(&messageFunctionJSON) : toDelegate(&messageFunction),
208 		errorCount, warningCount);
209 }
210 
211 /**
212 Checks whether a module is part of a user-specified include/exclude list.
213 The user can specify a comma-separated list of filters, everyone needs to start with
214 either a '+' (inclusion) or '-' (exclusion).
215 If no includes are specified, all modules are included.
216 */
217 bool shouldRun(string a)(string moduleName, const ref StaticAnalysisConfig config)
218 {
219 	if (mixin("config." ~ a) == Check.disabled)
220 		return false;
221 
222 	// By default, run the check
223 	if (!moduleName.length)
224 		return true;
225 
226 	auto filters = mixin("config.filters." ~ a);
227 
228 	// Check if there are filters are defined
229 	// filters starting with a comma are invalid
230 	if (filters.length == 0 || filters[0].length == 0)
231 		return true;
232 
233 	auto includers = filters.filter!(f => f[0] == '+').map!(f => f[1..$]);
234 	auto excluders = filters.filter!(f => f[0] == '-').map!(f => f[1..$]);
235 
236 	// exclusion has preference over inclusion
237 	if (!excluders.empty && excluders.any!(s => moduleName.canFind(s)))
238 		return false;
239 
240 	if (!includers.empty)
241 		return includers.any!(s => moduleName.canFind(s));
242 
243 	// by default: include all modules
244 	return true;
245 }
246 
247 ///
248 unittest
249 {
250 	bool test(string moduleName, string filters)
251 	{
252 		StaticAnalysisConfig config;
253 		// it doesn't matter which check we test here
254 		config.asm_style_check = Check.enabled;
255 		// this is done automatically by inifiled
256 		config.filters.asm_style_check = filters.split(",");
257 		return shouldRun!"asm_style_check"(moduleName, config);
258 	}
259 
260 	// test inclusion
261 	assert(test("std.foo", "+std."));
262 	// partial matches are ok
263 	assert(test("std.foo", "+bar,+foo"));
264 	// full as well
265 	assert(test("std.foo", "+bar,+std.foo,+foo"));
266 	// mismatch
267 	assert(!test("std.foo", "+bar,+banana"));
268 
269 	// test exclusion
270 	assert(!test("std.foo", "-std."));
271 	assert(!test("std.foo", "-bar,-std.foo"));
272 	assert(!test("std.foo", "-bar,-foo"));
273 	// mismatch
274 	assert(test("std.foo", "-bar,-banana"));
275 
276 	// test combination (exclusion has precedence)
277 	assert(!test("std.foo", "+foo,-foo"));
278 	assert(test("std.foo", "+foo,-bar"));
279 	assert(test("std.bar.foo", "-barr,+bar"));
280 }
281 
282 MessageSet analyze(string fileName, const Module m, const StaticAnalysisConfig analysisConfig,
283 		ref ModuleCache moduleCache, const(Token)[] tokens, bool staticAnalyze = true)
284 {
285 	import dsymbol.symbol : DSymbol;
286 
287 	if (!staticAnalyze)
288 		return null;
289 
290 	auto symbolAllocator = scoped!ASTAllocator();
291 	version (unittest)
292 		enum ut = true;
293 	else
294 		enum ut = false;
295 
296 	string moduleName;
297 	if (m !is null && m.moduleDeclaration !is null &&
298 		  m.moduleDeclaration.moduleName !is null &&
299 		  m.moduleDeclaration.moduleName.identifiers !is null)
300 		moduleName = m.moduleDeclaration.moduleName.identifiers.map!(e => e.text).join(".");
301 
302 	auto first = scoped!FirstPass(m, internString(fileName), symbolAllocator,
303 			symbolAllocator, true, &moduleCache, null);
304 	first.run();
305 
306 	secondPass(first.rootSymbol, first.moduleScope, moduleCache);
307 	auto moduleScope = first.moduleScope;
308 	scope(exit) typeid(DSymbol).destroy(first.rootSymbol.acSymbol);
309 	scope(exit) typeid(SemanticSymbol).destroy(first.rootSymbol);
310 	scope(exit) typeid(Scope).destroy(first.moduleScope);
311 	BaseAnalyzer[] checks;
312 
313 	with(analysisConfig)
314 	if (moduleName.shouldRun!"asm_style_check"(analysisConfig))
315 		checks ~= new AsmStyleCheck(fileName, moduleScope,
316 		asm_style_check == Check.skipTests && !ut);
317 
318 	if (moduleName.shouldRun!"backwards_range_check"(analysisConfig))
319 		checks ~= new BackwardsRangeCheck(fileName, moduleScope,
320 		analysisConfig.backwards_range_check == Check.skipTests && !ut);
321 
322 	if (moduleName.shouldRun!"builtin_property_names_check"(analysisConfig))
323 		checks ~= new BuiltinPropertyNameCheck(fileName, moduleScope,
324 		analysisConfig.builtin_property_names_check == Check.skipTests && !ut);
325 
326 	if (moduleName.shouldRun!"comma_expression_check"(analysisConfig))
327 		checks ~= new CommaExpressionCheck(fileName, moduleScope,
328 		analysisConfig.comma_expression_check == Check.skipTests && !ut);
329 
330 	if (moduleName.shouldRun!"constructor_check"(analysisConfig))
331 		checks ~= new ConstructorCheck(fileName, moduleScope,
332 		analysisConfig.constructor_check == Check.skipTests && !ut);
333 
334 	if (moduleName.shouldRun!"could_be_immutable_check"(analysisConfig))
335 		checks ~= new UnmodifiedFinder(fileName, moduleScope,
336 		analysisConfig.could_be_immutable_check == Check.skipTests && !ut);
337 
338 	if (moduleName.shouldRun!"delete_check"(analysisConfig))
339 		checks ~= new DeleteCheck(fileName, moduleScope,
340 		analysisConfig.delete_check == Check.skipTests && !ut);
341 
342 	if (moduleName.shouldRun!"duplicate_attribute"(analysisConfig))
343 		checks ~= new DuplicateAttributeCheck(fileName, moduleScope,
344 		analysisConfig.duplicate_attribute == Check.skipTests && !ut);
345 
346 	if (moduleName.shouldRun!"enum_array_literal_check"(analysisConfig))
347 		checks ~= new EnumArrayLiteralCheck(fileName, moduleScope,
348 		analysisConfig.enum_array_literal_check == Check.skipTests && !ut);
349 
350 	if (moduleName.shouldRun!"exception_check"(analysisConfig))
351 		checks ~= new PokemonExceptionCheck(fileName, moduleScope,
352 		analysisConfig.exception_check == Check.skipTests && !ut);
353 
354 	if (moduleName.shouldRun!"float_operator_check"(analysisConfig))
355 		checks ~= new FloatOperatorCheck(fileName, moduleScope,
356 		analysisConfig.float_operator_check == Check.skipTests && !ut);
357 
358 	if (moduleName.shouldRun!"function_attribute_check"(analysisConfig))
359 		checks ~= new FunctionAttributeCheck(fileName, moduleScope,
360 		analysisConfig.function_attribute_check == Check.skipTests && !ut);
361 
362 	if (moduleName.shouldRun!"if_else_same_check"(analysisConfig))
363 		checks ~= new IfElseSameCheck(fileName, moduleScope,
364 		analysisConfig.if_else_same_check == Check.skipTests&& !ut);
365 
366 	if (moduleName.shouldRun!"label_var_same_name_check"(analysisConfig))
367 		checks ~= new LabelVarNameCheck(fileName, moduleScope,
368 		analysisConfig.label_var_same_name_check == Check.skipTests && !ut);
369 
370 	if (moduleName.shouldRun!"length_subtraction_check"(analysisConfig))
371 		checks ~= new LengthSubtractionCheck(fileName, moduleScope,
372 		analysisConfig.length_subtraction_check == Check.skipTests && !ut);
373 
374 	if (moduleName.shouldRun!"local_import_check"(analysisConfig))
375 		checks ~= new LocalImportCheck(fileName, moduleScope,
376 		analysisConfig.local_import_check == Check.skipTests && !ut);
377 
378 	if (moduleName.shouldRun!"logical_precedence_check"(analysisConfig))
379 		checks ~= new LogicPrecedenceCheck(fileName, moduleScope,
380 		analysisConfig.logical_precedence_check == Check.skipTests && !ut);
381 
382 	if (moduleName.shouldRun!"mismatched_args_check"(analysisConfig))
383 		checks ~= new MismatchedArgumentCheck(fileName, moduleScope,
384 		analysisConfig.mismatched_args_check == Check.skipTests && !ut);
385 
386 	if (moduleName.shouldRun!"number_style_check"(analysisConfig))
387 		checks ~= new NumberStyleCheck(fileName, moduleScope,
388 		analysisConfig.number_style_check == Check.skipTests && !ut);
389 
390 	if (moduleName.shouldRun!"object_const_check"(analysisConfig))
391 		checks ~= new ObjectConstCheck(fileName, moduleScope,
392 		analysisConfig.object_const_check == Check.skipTests && !ut);
393 
394 	if (moduleName.shouldRun!"opequals_tohash_check"(analysisConfig))
395 		checks ~= new OpEqualsWithoutToHashCheck(fileName, moduleScope,
396 		analysisConfig.opequals_tohash_check == Check.skipTests && !ut);
397 
398 	if (moduleName.shouldRun!"redundant_parens_check"(analysisConfig))
399 		checks ~= new RedundantParenCheck(fileName, moduleScope,
400 		analysisConfig.redundant_parens_check == Check.skipTests && !ut);
401 
402 	if (moduleName.shouldRun!"style_check"(analysisConfig))
403 		checks ~= new StyleChecker(fileName, moduleScope,
404 		analysisConfig.style_check == Check.skipTests && !ut);
405 
406 	if (moduleName.shouldRun!"undocumented_declaration_check"(analysisConfig))
407 		checks ~= new UndocumentedDeclarationCheck(fileName, moduleScope,
408 		analysisConfig.undocumented_declaration_check == Check.skipTests && !ut);
409 
410 	if (moduleName.shouldRun!"unused_label_check"(analysisConfig))
411 		checks ~= new UnusedLabelCheck(fileName, moduleScope,
412 		analysisConfig.unused_label_check == Check.skipTests && !ut);
413 
414 	if (moduleName.shouldRun!"unused_variable_check"(analysisConfig))
415 		checks ~= new UnusedVariableCheck(fileName, moduleScope,
416 		analysisConfig.unused_variable_check == Check.skipTests && !ut);
417 
418 	if (moduleName.shouldRun!"long_line_check"(analysisConfig))
419 		checks ~= new LineLengthCheck(fileName, tokens,
420 		analysisConfig.long_line_check == Check.skipTests && !ut);
421 
422 	if (moduleName.shouldRun!"auto_ref_assignment_check"(analysisConfig))
423 		checks ~= new AutoRefAssignmentCheck(fileName,
424 		analysisConfig.auto_ref_assignment_check == Check.skipTests && !ut);
425 
426 	if (moduleName.shouldRun!"incorrect_infinite_range_check"(analysisConfig))
427 		checks ~= new IncorrectInfiniteRangeCheck(fileName,
428 		analysisConfig.incorrect_infinite_range_check == Check.skipTests && !ut);
429 
430 	if (moduleName.shouldRun!"useless_assert_check"(analysisConfig))
431 		checks ~= new UselessAssertCheck(fileName,
432 		analysisConfig.useless_assert_check == Check.skipTests && !ut);
433 
434 	if (moduleName.shouldRun!"alias_syntax_check"(analysisConfig))
435 		checks ~= new AliasSyntaxCheck(fileName,
436 		analysisConfig.alias_syntax_check == Check.skipTests && !ut);
437 
438 	if (moduleName.shouldRun!"static_if_else_check"(analysisConfig))
439 		checks ~= new StaticIfElse(fileName,
440 		analysisConfig.static_if_else_check == Check.skipTests && !ut);
441 
442 	if (moduleName.shouldRun!"lambda_return_check"(analysisConfig))
443 		checks ~= new LambdaReturnCheck(fileName,
444 		analysisConfig.lambda_return_check == Check.skipTests && !ut);
445 
446 	if (moduleName.shouldRun!"auto_function_check"(analysisConfig))
447 		checks ~= new AutoFunctionChecker(fileName,
448 		analysisConfig.auto_function_check == Check.skipTests && !ut);
449 
450 	if (moduleName.shouldRun!"imports_sortedness"(analysisConfig))
451 		checks ~= new ImportSortednessCheck(fileName,
452 		analysisConfig.imports_sortedness == Check.skipTests && !ut);
453 
454 	if (moduleName.shouldRun!"explicitly_annotated_unittests"(analysisConfig))
455 		checks ~= new ExplicitlyAnnotatedUnittestCheck(fileName,
456 		analysisConfig.explicitly_annotated_unittests == Check.skipTests && !ut);
457 
458 	if (moduleName.shouldRun!"properly_documented_public_functions"(analysisConfig))
459 		checks ~= new ProperlyDocumentedPublicFunctions(fileName,
460 		analysisConfig.properly_documented_public_functions == Check.skipTests && !ut);
461 
462 	if (moduleName.shouldRun!"final_attribute_check"(analysisConfig))
463 		checks ~= new FinalAttributeChecker(fileName,
464 		analysisConfig.final_attribute_check == Check.skipTests && !ut);
465 
466 	if (moduleName.shouldRun!"vcall_in_ctor"(analysisConfig))
467 		checks ~= new VcallCtorChecker(fileName,
468 		analysisConfig.vcall_in_ctor == Check.skipTests && !ut);
469 
470 	if (moduleName.shouldRun!"useless_initializer"(analysisConfig))
471 		checks ~= new UselessInitializerChecker(fileName,
472 		analysisConfig.useless_initializer == Check.skipTests && !ut);
473 
474 	if (moduleName.shouldRun!"allman_braces_check"(analysisConfig))
475 		checks ~= new AllManCheck(fileName, tokens,
476 		analysisConfig.allman_braces_check == Check.skipTests && !ut);
477 
478 	if (moduleName.shouldRun!"redundant_attributes_check"(analysisConfig))
479 		checks ~= new RedundantAttributesCheck(fileName, moduleScope,
480 		analysisConfig.redundant_attributes_check == Check.skipTests && !ut);
481 
482 	if (moduleName.shouldRun!"has_public_example"(analysisConfig))
483 		checks ~= new HasPublicExampleCheck(fileName, moduleScope,
484 		analysisConfig.has_public_example == Check.skipTests && !ut);
485 
486 	version (none)
487 		if (moduleName.shouldRun!"redundant_if_check"(analysisConfig))
488 			checks ~= new IfStatementCheck(fileName, moduleScope,
489 			analysisConfig.redundant_if_check == Check.skipTests && !ut);
490 
491 	foreach (check; checks)
492 	{
493 		check.visit(m);
494 	}
495 
496 	MessageSet set = new MessageSet;
497 	foreach (check; checks)
498 		foreach (message; check.messages)
499 			set.insert(message);
500 	return set;
501 }
502