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