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