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