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