1 module workspaced.com.dscanner; 2 3 import std.algorithm; 4 import std.array; 5 import std.conv; 6 import std.file; 7 import std.json; 8 import std.stdio; 9 import std.typecons; 10 11 import core.sync.mutex; 12 import core.thread; 13 14 import dscanner.analysis.base; 15 import dscanner.analysis.config; 16 import dscanner.analysis.run; 17 import dscanner.symbol_finder; 18 19 import inifiled : INI, readINIFile; 20 21 import dparse.ast; 22 import dparse.lexer; 23 import dparse.parser; 24 import dparse.rollback_allocator; 25 import dsymbol.builtin.names; 26 import dsymbol.modulecache : ASTAllocator, ModuleCache; 27 28 import painlessjson; 29 30 import workspaced.api; 31 import workspaced.dparseext; 32 33 @component("dscanner") 34 class DscannerComponent : ComponentWrapper 35 { 36 mixin DefaultComponentWrapper; 37 38 /// Asynchronously lints the file passed. 39 /// If you provide code then the code will be used and file will be ignored. 40 Future!(DScannerIssue[]) lint(string file = "", string ini = "dscanner.ini", 41 scope const(char)[] code = "") 42 { 43 auto ret = new Future!(DScannerIssue[]); 44 gthreads.create({ 45 mixin(traceTask); 46 try 47 { 48 if (code.length && !file.length) 49 file = "stdin"; 50 auto config = defaultStaticAnalysisConfig(); 51 if (getConfigPath("dscanner.ini", ini)) 52 stderr.writeln("Overriding Dscanner ini with workspace-d dscanner.ini config file"); 53 if (ini.exists) 54 readINIFile(config, ini); 55 if (!code.length) 56 code = readText(file); 57 DScannerIssue[] issues; 58 if (!code.length) 59 { 60 ret.finish(issues); 61 return; 62 } 63 RollbackAllocator r; 64 const(Token)[] tokens; 65 StringCache cache = StringCache(StringCache.defaultBucketCount); 66 const Module m = parseModule(file, cast(ubyte[]) code, &r, cache, tokens, issues); 67 if (!m) 68 throw new Exception(text("parseModule returned null?! - file: '", 69 file, "', code: '", code, "'")); 70 MessageSet results; 71 auto alloc = scoped!ASTAllocator(); 72 auto moduleCache = ModuleCache(alloc); 73 results = analyze(file, m, config, moduleCache, tokens, true); 74 if (results is null) 75 { 76 ret.finish(issues); 77 return; 78 } 79 foreach (msg; results) 80 { 81 DScannerIssue issue; 82 issue.file = msg.fileName; 83 issue.line = cast(int) msg.line; 84 issue.column = cast(int) msg.column; 85 issue.type = typeForWarning(msg.key); 86 issue.description = msg.message; 87 issue.key = msg.key; 88 issues ~= issue; 89 } 90 ret.finish(issues); 91 } 92 catch (Throwable e) 93 { 94 ret.error(e); 95 } 96 }); 97 return ret; 98 } 99 100 private const(Module) parseModule(string file, ubyte[] code, RollbackAllocator* p, 101 ref StringCache cache, ref const(Token)[] tokens, ref DScannerIssue[] issues) 102 { 103 LexerConfig config; 104 config.fileName = file; 105 config.stringBehavior = StringBehavior.source; 106 tokens = getTokensForParser(code, config, &cache); 107 108 void addIssue(string fileName, size_t line, size_t column, string message, bool isError) 109 { 110 issues ~= DScannerIssue(file, cast(int) line, cast(int) column, isError 111 ? "error" : "warn", message); 112 } 113 114 uint err, warn; 115 return dparse.parser.parseModule(tokens, file, p, &addIssue, &err, &warn); 116 } 117 118 /// Asynchronously lists all definitions in the specified file. 119 /// If you provide code the file wont be manually read. 120 Future!(DefinitionElement[]) listDefinitions(string file, scope const(char)[] code = "") 121 { 122 auto ret = new Future!(DefinitionElement[]); 123 gthreads.create({ 124 mixin(traceTask); 125 try 126 { 127 if (code.length && !file.length) 128 file = "stdin"; 129 if (!code.length) 130 code = readText(file); 131 if (!code.length) 132 { 133 DefinitionElement[] arr; 134 ret.finish(arr); 135 return; 136 } 137 138 RollbackAllocator r; 139 LexerConfig config; 140 auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 141 142 auto m = dparse.parser.parseModule(tokens.array, file, &r); 143 144 auto defFinder = new DefinitionFinder(); 145 defFinder.visit(m); 146 147 ret.finish(defFinder.definitions); 148 } 149 catch (Throwable e) 150 { 151 ret.error(e); 152 } 153 }); 154 return ret; 155 } 156 157 /// Asynchronously finds all definitions of a symbol in the import paths. 158 Future!(FileLocation[]) findSymbol(string symbol) 159 { 160 auto ret = new Future!(FileLocation[]); 161 gthreads.create({ 162 mixin(traceTask); 163 try 164 { 165 import dscanner.utils : expandArgs; 166 167 string[] paths = expandArgs([""] ~ importPaths); 168 foreach_reverse (i, path; paths) 169 if (path == "stdin") 170 paths = paths.remove(i); 171 FileLocation[] files; 172 findDeclarationOf((fileName, line, column) { 173 FileLocation file; 174 file.file = fileName; 175 file.line = cast(int) line; 176 file.column = cast(int) column; 177 files ~= file; 178 }, symbol, paths); 179 ret.finish(files); 180 } 181 catch (Throwable e) 182 { 183 ret.error(e); 184 } 185 }); 186 return ret; 187 } 188 189 /// Returns: all keys & documentation that can be used in a dscanner.ini 190 INIEntry[] listAllIniFields() 191 { 192 import std.traits : getUDAs; 193 194 INIEntry[] ret; 195 foreach (mem; __traits(allMembers, StaticAnalysisConfig)) 196 static if (is(typeof(__traits(getMember, StaticAnalysisConfig, mem)) == string)) 197 { 198 alias docs = getUDAs!(__traits(getMember, StaticAnalysisConfig, mem), INI); 199 ret ~= INIEntry(mem, docs.length ? docs[0].msg : ""); 200 } 201 return ret; 202 } 203 } 204 205 /// dscanner.ini setting type 206 struct INIEntry 207 { 208 /// 209 string name, documentation; 210 } 211 212 /// Issue type returned by lint 213 struct DScannerIssue 214 { 215 /// 216 string file; 217 /// 218 int line, column; 219 /// 220 string type; 221 /// 222 string description; 223 /// 224 string key; 225 } 226 227 /// Returned by find-symbol 228 struct FileLocation 229 { 230 /// 231 string file; 232 /// 233 int line, column; 234 } 235 236 /// Returned by list-definitions 237 struct DefinitionElement 238 { 239 /// 240 string name; 241 /// 242 int line; 243 /// One of "c" (class), "s" (struct), "i" (interface), "T" (template), "f" (function/ctor/dtor), "g" (enum {}), "u" (union), "e" (enum member/definition), "v" (variable/invariant) 244 string type; 245 /// 246 string[string] attributes; 247 /// 248 int[2] range; 249 } 250 251 private: 252 253 string typeForWarning(string key) 254 { 255 switch (key) 256 { 257 case "dscanner.bugs.backwards_slices": 258 case "dscanner.bugs.if_else_same": 259 case "dscanner.bugs.logic_operator_operands": 260 case "dscanner.bugs.self_assignment": 261 case "dscanner.confusing.argument_parameter_mismatch": 262 case "dscanner.confusing.brexp": 263 case "dscanner.confusing.builtin_property_names": 264 case "dscanner.confusing.constructor_args": 265 case "dscanner.confusing.function_attributes": 266 case "dscanner.confusing.lambda_returns_lambda": 267 case "dscanner.confusing.logical_precedence": 268 case "dscanner.confusing.struct_constructor_default_args": 269 case "dscanner.deprecated.delete_keyword": 270 case "dscanner.deprecated.floating_point_operators": 271 case "dscanner.if_statement": 272 case "dscanner.performance.enum_array_literal": 273 case "dscanner.style.allman": 274 case "dscanner.style.alias_syntax": 275 case "dscanner.style.doc_missing_params": 276 case "dscanner.style.doc_missing_returns": 277 case "dscanner.style.doc_non_existing_params": 278 case "dscanner.style.explicitly_annotated_unittest": 279 case "dscanner.style.has_public_example": 280 case "dscanner.style.imports_sortedness": 281 case "dscanner.style.long_line": 282 case "dscanner.style.number_literals": 283 case "dscanner.style.phobos_naming_convention": 284 case "dscanner.style.undocumented_declaration": 285 case "dscanner.suspicious.auto_ref_assignment": 286 case "dscanner.suspicious.catch_em_all": 287 case "dscanner.suspicious.comma_expression": 288 case "dscanner.suspicious.incomplete_operator_overloading": 289 case "dscanner.suspicious.incorrect_infinite_range": 290 case "dscanner.suspicious.label_var_same_name": 291 case "dscanner.suspicious.length_subtraction": 292 case "dscanner.suspicious.local_imports": 293 case "dscanner.suspicious.missing_return": 294 case "dscanner.suspicious.object_const": 295 case "dscanner.suspicious.redundant_attributes": 296 case "dscanner.suspicious.redundant_parens": 297 case "dscanner.suspicious.static_if_else": 298 case "dscanner.suspicious.unmodified": 299 case "dscanner.suspicious.unused_label": 300 case "dscanner.suspicious.unused_parameter": 301 case "dscanner.suspicious.unused_variable": 302 case "dscanner.suspicious.useless_assert": 303 case "dscanner.unnecessary.duplicate_attribute": 304 case "dscanner.useless.final": 305 case "dscanner.useless-initializer": 306 case "dscanner.vcall_ctor": 307 return "warn"; 308 case "dscanner.syntax": 309 return "error"; 310 default: 311 stderr.writeln("Warning: unimplemented DScanner reason, assuming warning: ", key); 312 return "warn"; 313 } 314 } 315 316 final class DefinitionFinder : ASTVisitor 317 { 318 override void visit(const ClassDeclaration dec) 319 { 320 if (!dec.structBody) 321 return; 322 definitions ~= makeDefinition(dec.name.text, dec.name.line, "c", context, 323 [ 324 cast(int) dec.structBody.startLocation, 325 cast(int) dec.structBody.endLocation 326 ]); 327 auto c = context; 328 context = ContextType(["class": dec.name.text], "public"); 329 dec.accept(this); 330 context = c; 331 } 332 333 override void visit(const StructDeclaration dec) 334 { 335 if (!dec.structBody) 336 return; 337 if (dec.name == tok!"") 338 { 339 dec.accept(this); 340 return; 341 } 342 definitions ~= makeDefinition(dec.name.text, dec.name.line, "s", context, 343 [ 344 cast(int) dec.structBody.startLocation, 345 cast(int) dec.structBody.endLocation 346 ]); 347 auto c = context; 348 context = ContextType(["struct": dec.name.text], "public"); 349 dec.accept(this); 350 context = c; 351 } 352 353 override void visit(const InterfaceDeclaration dec) 354 { 355 if (!dec.structBody) 356 return; 357 definitions ~= makeDefinition(dec.name.text, dec.name.line, "i", context, 358 [ 359 cast(int) dec.structBody.startLocation, 360 cast(int) dec.structBody.endLocation 361 ]); 362 auto c = context; 363 context = ContextType(["interface:": dec.name.text], context.access); 364 dec.accept(this); 365 context = c; 366 } 367 368 override void visit(const TemplateDeclaration dec) 369 { 370 auto def = makeDefinition(dec.name.text, dec.name.line, "T", context, 371 [cast(int) dec.startLocation, cast(int) dec.endLocation]); 372 def.attributes["signature"] = paramsToString(dec); 373 definitions ~= def; 374 auto c = context; 375 context = ContextType(["template": dec.name.text], context.access); 376 dec.accept(this); 377 context = c; 378 } 379 380 override void visit(const FunctionDeclaration dec) 381 { 382 if (!dec.functionBody || !dec.functionBody.specifiedFunctionBody 383 || !dec.functionBody.specifiedFunctionBody.blockStatement) 384 return; 385 auto def = makeDefinition(dec.name.text, dec.name.line, "f", context, 386 [ 387 cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.startLocation, 388 cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.endLocation 389 ]); 390 def.attributes["signature"] = paramsToString(dec); 391 if (dec.returnType !is null) 392 def.attributes["return"] = astToString(dec.returnType); 393 definitions ~= def; 394 } 395 396 override void visit(const Constructor dec) 397 { 398 if (!dec.functionBody || !dec.functionBody.specifiedFunctionBody 399 || !dec.functionBody.specifiedFunctionBody.blockStatement) 400 return; 401 auto def = makeDefinition("this", dec.line, "f", context, 402 [ 403 cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.startLocation, 404 cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.endLocation 405 ]); 406 def.attributes["signature"] = paramsToString(dec); 407 definitions ~= def; 408 } 409 410 override void visit(const Destructor dec) 411 { 412 if (!dec.functionBody || !dec.functionBody.specifiedFunctionBody 413 || !dec.functionBody.specifiedFunctionBody.blockStatement) 414 return; 415 definitions ~= makeDefinition("~this", dec.line, "f", context, 416 [ 417 cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.startLocation, 418 cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.endLocation 419 ]); 420 } 421 422 override void visit(const EnumDeclaration dec) 423 { 424 if (!dec.enumBody) 425 return; 426 definitions ~= makeDefinition(dec.name.text, dec.name.line, "g", context, 427 [cast(int) dec.enumBody.startLocation, cast(int) dec.enumBody.endLocation]); 428 auto c = context; 429 context = ContextType(["enum": dec.name.text], context.access); 430 dec.accept(this); 431 context = c; 432 } 433 434 override void visit(const UnionDeclaration dec) 435 { 436 if (!dec.structBody) 437 return; 438 if (dec.name == tok!"") 439 { 440 dec.accept(this); 441 return; 442 } 443 definitions ~= makeDefinition(dec.name.text, dec.name.line, "u", context, 444 [ 445 cast(int) dec.structBody.startLocation, 446 cast(int) dec.structBody.endLocation 447 ]); 448 auto c = context; 449 context = ContextType(["union": dec.name.text], context.access); 450 dec.accept(this); 451 context = c; 452 } 453 454 override void visit(const AnonymousEnumMember mem) 455 { 456 definitions ~= makeDefinition(mem.name.text, mem.name.line, "e", context, 457 [ 458 cast(int) mem.name.index, 459 cast(int) mem.name.index + cast(int) mem.name.text.length 460 ]); 461 } 462 463 override void visit(const EnumMember mem) 464 { 465 definitions ~= makeDefinition(mem.name.text, mem.name.line, "e", context, 466 [ 467 cast(int) mem.name.index, 468 cast(int) mem.name.index + cast(int) mem.name.text.length 469 ]); 470 } 471 472 override void visit(const VariableDeclaration dec) 473 { 474 foreach (d; dec.declarators) 475 definitions ~= makeDefinition(d.name.text, d.name.line, "v", context, 476 [ 477 cast(int) d.name.index, 478 cast(int) d.name.index + cast(int) d.name.text.length 479 ]); 480 dec.accept(this); 481 } 482 483 override void visit(const AutoDeclaration dec) 484 { 485 foreach (i; dec.parts.map!(a => a.identifier)) 486 definitions ~= makeDefinition(i.text, i.line, "v", context, 487 [cast(int) i.index, cast(int) i.index + cast(int) i.text.length]); 488 dec.accept(this); 489 } 490 491 override void visit(const Invariant dec) 492 { 493 if (!dec.blockStatement) 494 return; 495 definitions ~= makeDefinition("invariant", dec.line, "v", context, 496 [cast(int) dec.index, cast(int) dec.blockStatement.endLocation]); 497 } 498 499 override void visit(const ModuleDeclaration dec) 500 { 501 context = ContextType(null, "public"); 502 dec.accept(this); 503 } 504 505 override void visit(const Attribute attribute) 506 { 507 if (attribute.attribute != tok!"") 508 { 509 switch (attribute.attribute.type) 510 { 511 case tok!"export": 512 context.access = "public"; 513 break; 514 case tok!"public": 515 context.access = "public"; 516 break; 517 case tok!"package": 518 context.access = "protected"; 519 break; 520 case tok!"protected": 521 context.access = "protected"; 522 break; 523 case tok!"private": 524 context.access = "private"; 525 break; 526 default: 527 } 528 } 529 else if (attribute.deprecated_ !is null) 530 { 531 // TODO: find out how to get deprecation message 532 context.attr["deprecation"] = ""; 533 } 534 535 attribute.accept(this); 536 } 537 538 override void visit(const AttributeDeclaration dec) 539 { 540 accessSt = AccessState.Keep; 541 dec.accept(this); 542 } 543 544 override void visit(const Declaration dec) 545 { 546 auto c = context; 547 dec.accept(this); 548 549 final switch (accessSt) with (AccessState) 550 { 551 case Reset: 552 context = c; 553 break; 554 case Keep: 555 break; 556 } 557 accessSt = AccessState.Reset; 558 } 559 560 override void visit(const Unittest dec) 561 { 562 // skipping symbols inside a unit test to not clutter the ctags file 563 // with "temporary" symbols. 564 // TODO when phobos have a unittest library investigate how that could 565 // be used to describe the tests. 566 // Maybe with UDA's to give the unittest a "name". 567 } 568 569 override void visit(const AliasDeclaration dec) 570 { 571 // Old style alias 572 if (dec.declaratorIdentifierList) 573 foreach (i; dec.declaratorIdentifierList.identifiers) 574 definitions ~= makeDefinition(i.text, i.line, "a", context, 575 [cast(int) i.index, cast(int) i.index + cast(int) i.text.length]); 576 dec.accept(this); 577 } 578 579 override void visit(const AliasInitializer dec) 580 { 581 definitions ~= makeDefinition(dec.name.text, dec.name.line, "a", context, 582 [ 583 cast(int) dec.name.index, 584 cast(int) dec.name.index + cast(int) dec.name.text.length 585 ]); 586 587 dec.accept(this); 588 } 589 590 override void visit(const AliasThisDeclaration dec) 591 { 592 auto name = dec.identifier; 593 definitions ~= makeDefinition(name.text, name.line, "a", context, 594 [cast(int) name.index, cast(int) name.index + cast(int) name.text.length]); 595 596 dec.accept(this); 597 } 598 599 alias visit = ASTVisitor.visit; 600 601 ContextType context; 602 AccessState accessSt; 603 DefinitionElement[] definitions; 604 } 605 606 DefinitionElement makeDefinition(string name, size_t line, string type, 607 ContextType context, int[2] range) 608 { 609 string[string] attr = context.attr; 610 if (context.access.length) 611 attr["access"] = context.access; 612 return DefinitionElement(name, cast(int) line, type, attr, range); 613 } 614 615 enum AccessState 616 { 617 Reset, /// when ascending the AST reset back to the previous access. 618 Keep /// when ascending the AST keep the new access. 619 } 620 621 struct ContextType 622 { 623 string[string] attr; 624 string access; 625 }