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