1 module workspaced.com.dscanner; 2 3 version (unittest) 4 debug = ResolveRange; 5 6 import std.algorithm; 7 import std.array; 8 import std.conv; 9 import std.experimental.logger; 10 import std.file; 11 import std.json; 12 import std.stdio; 13 import std.typecons; 14 import std.meta : AliasSeq; 15 16 import core.sync.mutex; 17 import core.thread; 18 19 import dscanner.analysis.base; 20 import dscanner.analysis.config; 21 import dscanner.analysis.run; 22 import dscanner.symbol_finder; 23 24 import inifiled : INI, readINIFile; 25 26 import dparse.ast; 27 import dparse.lexer; 28 import dparse.parser; 29 import dparse.rollback_allocator; 30 import dsymbol.builtin.names; 31 import dsymbol.modulecache : ModuleCache; 32 33 import workspaced.api; 34 import workspaced.dparseext; 35 import workspaced.helpers; 36 37 static immutable LocalImportCheckKEY = "dscanner.suspicious.local_imports"; 38 static immutable LongLineCheckKEY = "dscanner.style.long_line"; 39 40 @component("dscanner") 41 class DscannerComponent : ComponentWrapper 42 { 43 mixin DefaultComponentWrapper; 44 45 /// Asynchronously lints the file passed. 46 /// If you provide code then the code will be used and file will be ignored. 47 /// See_Also: $(LREF getConfig) 48 Future!(DScannerIssue[]) lint(string file = "", string ini = "dscanner.ini", 49 scope const(char)[] code = "", bool skipWorkspacedPaths = false, 50 const StaticAnalysisConfig defaultConfig = StaticAnalysisConfig.init, 51 bool resolveRanges = false) 52 { 53 auto ret = new typeof(return); 54 gthreads.create({ 55 mixin(traceTask); 56 try 57 { 58 if (code.length && !file.length) 59 file = "stdin"; 60 auto config = getConfig(ini, skipWorkspacedPaths, defaultConfig); 61 if (!code.length) 62 code = readText(file); 63 DScannerIssue[] issues; 64 if (!code.length) 65 { 66 ret.finish(issues); 67 return; 68 } 69 RollbackAllocator r; 70 const(Token)[] tokens; 71 StringCache cache = StringCache(StringCache.defaultBucketCount); 72 const Module m = parseModule(file, cast(ubyte[]) code, &r, cache, tokens, issues); 73 if (!m) 74 throw new Exception(text("parseModule returned null?! - file: '", 75 file, "', code: '", code, "'")); 76 77 // resolve syntax errors (immediately set by parseModule) 78 if (resolveRanges) 79 { 80 foreach_reverse (i, ref issue; issues) 81 { 82 if (!resolveRange(tokens, issue)) 83 issues = issues.remove(i); 84 } 85 } 86 87 MessageSet results; 88 ModuleCache moduleCache; 89 results = analyze(file, m, config, moduleCache, tokens, true); 90 if (results is null) 91 { 92 ret.finish(issues); 93 return; 94 } 95 foreach (msg; results) 96 { 97 DScannerIssue issue; 98 issue.file = msg.fileName; 99 issue.line = cast(int) msg.line; 100 issue.column = cast(int) msg.column; 101 issue.type = typeForWarning(msg.key); 102 issue.description = msg.message; 103 issue.key = msg.key; 104 if (resolveRanges) 105 { 106 if (!this.resolveRange(tokens, issue)) 107 continue; 108 } 109 issues ~= issue; 110 } 111 ret.finish(issues); 112 } 113 catch (Throwable e) 114 { 115 ret.error(e); 116 } 117 }); 118 return ret; 119 } 120 121 /// Takes line & column from the D-Scanner issue array and resolves the 122 /// start & end locations for the issues by changing the values in-place. 123 /// In the JSON RPC this returns the modified array, in workspace-d as a 124 /// library this changes the parameter values in place. 125 void resolveRanges(scope const(char)[] code, scope ref DScannerIssue[] issues) 126 { 127 LexerConfig config; 128 auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 129 if (!tokens.length) 130 return; 131 132 foreach_reverse (i, ref issue; issues) 133 { 134 if (!resolveRange(tokens, issue)) 135 issues = issues.remove(i); 136 } 137 } 138 139 /// Adjusts a D-Scanner line:column location to a start & end range, potentially 140 /// improving the error message through tokens nearby. 141 /// Returns: `false` if this issue should be discarded (handled by other issues) 142 private bool resolveRange(scope const(Token)[] tokens, ref DScannerIssue issue) 143 out 144 { 145 debug (ResolveRange) if (issue.range != typeof(issue.range).init) 146 { 147 assert(issue.range[0].line > 0); 148 assert(issue.range[0].column > 0); 149 assert(issue.range[1].line > 0); 150 assert(issue.range[1].column > 0); 151 } 152 } 153 do 154 { 155 auto tokenIndex = tokens.tokenIndexAtPosition(issue.line, issue.column); 156 if (tokenIndex >= tokens.length) 157 { 158 if (tokens.length) 159 issue.range = makeTokenRange(tokens[$ - 1]); 160 else 161 issue.range = typeof(issue.range).init; 162 return true; 163 } 164 165 switch (issue.key) 166 { 167 case null: 168 // syntax errors 169 if (!adjustRangeForSyntaxError(tokens, tokenIndex, issue)) 170 return false; 171 improveErrorMessage(issue); 172 return true; 173 case LocalImportCheckKEY: 174 if (adjustRangeForLocalImportsError(tokens, tokenIndex, issue)) 175 return true; 176 goto default; 177 case LongLineCheckKEY: 178 issue.range = makeTokenRange(tokens[tokenIndex], tokens[min($ - 1, tokens.tokenIndexAtPosition(issue.line, 1000))]); 179 return true; 180 default: 181 issue.range = makeTokenRange(tokens[tokenIndex]); 182 return true; 183 } 184 } 185 186 private void improveErrorMessage(ref DScannerIssue issue) 187 { 188 // identifier is not literally expected 189 issue.description = issue.description.replace("`identifier`", "identifier"); 190 191 static immutable expectedIdentifierStart = "Expected identifier instead of `"; 192 static immutable keywordReplacement = "Expected identifier instead of reserved keyword `"; 193 if (issue.description.startsWith(expectedIdentifierStart)) 194 { 195 if (issue.description.length > expectedIdentifierStart.length + 1 196 && issue.description[expectedIdentifierStart.length].isIdentifierChar) 197 { 198 // expected identifier instead of keyword (probably) here because 199 // first character of "instead of `..." is an identifier character. 200 issue.description = keywordReplacement ~ issue.description[expectedIdentifierStart.length .. $]; 201 } 202 } 203 } 204 205 private bool adjustRangeForSyntaxError(scope const(Token)[] tokens, size_t currentToken, ref DScannerIssue issue) 206 { 207 auto s = issue.description; 208 209 if (s.startsWith("Expected `")) 210 { 211 s = s["Expected ".length .. $]; 212 if (s.startsWith("`;`")) 213 { 214 // span after last word 215 size_t issueStartExclusive = currentToken; 216 foreach_reverse (i, token; tokens[0 .. currentToken]) 217 { 218 if (token.type == tok!";") 219 { 220 // this ain't right, expected semicolon issue but 221 // semicolon is the first thing before this token 222 // happens when syntax before is broken, let's discard! 223 // for example in `foo.foreach(a;b)` 224 return false; 225 } 226 issueStartExclusive = i; 227 if (token.isLikeIdentifier) 228 break; 229 } 230 231 size_t issueEnd = issueStartExclusive; 232 auto line = tokens[issueEnd].line; 233 234 // span until newline or next word character 235 foreach (i, token; tokens[issueStartExclusive + 1 .. $]) 236 { 237 if (token.line != line || token.isLikeIdentifier) 238 break; 239 issueEnd = issueStartExclusive + 1 + i; 240 } 241 242 issue.range = [makeTokenEnd(tokens[issueStartExclusive]), makeTokenEnd(tokens[issueEnd])]; 243 return true; 244 } 245 else if (s.startsWith("`identifier` instead of `")) 246 { 247 auto wanted = s["`identifier` instead of `".length .. $]; 248 if (wanted.length && wanted[0].isIdentifierChar) 249 { 250 // wants identifier instead of some keyword (probably) 251 // happens e.g. after a . and then nothing written and next line contains a keyword 252 // want to remove the "instead of" in case it's not in the same line 253 if (currentToken > 0 && tokens[currentToken - 1].line != tokens[currentToken].line) 254 { 255 issue.description = "Expected identifier"; 256 issue.range = [makeTokenEnd(tokens[currentToken - 1]), makeTokenStart(tokens[currentToken])]; 257 return true; 258 } 259 } 260 } 261 262 // span from start of last word 263 size_t issueStart = min(max(0, cast(ptrdiff_t)tokens.length - 1), currentToken + 1); 264 // if a non-identifier was expected, include word before 265 if (issueStart > 0 && s.length > 2 && s[1].isIdentifierSeparatingChar) 266 issueStart--; 267 foreach_reverse (i, token; tokens[0 .. issueStart]) 268 { 269 issueStart = i; 270 if (token.isLikeIdentifier) 271 break; 272 } 273 274 // span to end of next word 275 size_t searchStart = issueStart; 276 if (tokens[searchStart].column + tokens[searchStart].tokenText.length <= issue.column) 277 searchStart++; 278 size_t issueEnd = min(max(0, cast(ptrdiff_t)tokens.length - 1), searchStart); 279 foreach (i, token; tokens[searchStart .. $]) 280 { 281 if (token.isLikeIdentifier) 282 break; 283 issueEnd = searchStart + i; 284 } 285 286 issue.range = makeTokenRange(tokens[issueStart], tokens[issueEnd]); 287 } 288 else 289 { 290 if (tokens[currentToken].type == tok!"auto") 291 { 292 // syntax error on the word "auto" 293 // check for foreach (auto key; value) 294 295 if (currentToken >= 2 296 && tokens[currentToken - 1].type == tok!"(" 297 && (tokens[currentToken - 2].type == tok!"foreach" || tokens[currentToken - 2].type == tok!"foreach_reverse")) 298 { 299 // this is foreach (auto 300 issue.key = "workspaced.foreach-auto"; 301 issue.description = "foreach (auto key; value) is not valid D " 302 ~ "syntax. Use foreach (key; value) instead."; 303 // range is used in code_actions to remove auto 304 issue.range = makeTokenRange(tokens[currentToken]); 305 return true; 306 } 307 } 308 309 issue.range = makeTokenRange(tokens[currentToken]); 310 } 311 return true; 312 } 313 314 // adjusts error location of 315 // import |std.stdio; 316 // to 317 // ~import std.stdio;~ 318 private bool adjustRangeForLocalImportsError(scope const(Token)[] tokens, size_t currentToken, ref DScannerIssue issue) 319 { 320 size_t startIndex = currentToken; 321 size_t endIndex = currentToken; 322 323 while (startIndex > 0 && tokens[startIndex].type != tok!"import") 324 startIndex--; 325 while (endIndex < tokens.length && tokens[endIndex].type != tok!";") 326 endIndex++; 327 328 issue.range = makeTokenRange(tokens[startIndex], tokens[endIndex]); 329 return true; 330 } 331 332 /// Gets the used D-Scanner config, optionally reading from a given 333 /// dscanner.ini file. 334 /// Params: 335 /// ini = an ini to load. Only reading from it if it exists. If this is 336 /// relative, this function will try both in getcwd and in the 337 /// instance.cwd, if an instance is set. 338 /// skipWorkspacedPaths = if true, don't attempt to override the given ini 339 /// with workspace-d user configs. 340 /// defaultConfig = default D-Scanner configuration to use if no user 341 /// config exists (workspace-d specific or ini argument) 342 StaticAnalysisConfig getConfig(string ini = "dscanner.ini", 343 bool skipWorkspacedPaths = false, 344 const StaticAnalysisConfig defaultConfig = StaticAnalysisConfig.init) 345 { 346 import std.path : buildPath; 347 348 StaticAnalysisConfig config = defaultConfig is StaticAnalysisConfig.init 349 ? defaultStaticAnalysisConfig() 350 : cast()defaultConfig; 351 if (!skipWorkspacedPaths && getConfigPath("dscanner.ini", ini)) 352 { 353 static bool didWarn = false; 354 if (!didWarn) 355 { 356 warning("Overriding Dscanner ini with workspace-d dscanner.ini config file"); 357 didWarn = true; 358 } 359 } 360 string cwd = getcwd; 361 if (refInstance !is null) 362 cwd = refInstance.cwd; 363 364 if (ini.exists) 365 { 366 readINIFile(config, ini); 367 } 368 else 369 { 370 auto p = buildPath(cwd, ini); 371 if (p != ini && p.exists) 372 readINIFile(config, p); 373 } 374 return config; 375 } 376 377 private const(Module) parseModule(string file, ubyte[] code, RollbackAllocator* p, 378 ref StringCache cache, ref const(Token)[] tokens, ref DScannerIssue[] issues) 379 { 380 LexerConfig config; 381 config.fileName = file; 382 config.stringBehavior = StringBehavior.source; 383 tokens = getTokensForParser(code, config, &cache); 384 385 void addIssue(string fileName, size_t line, size_t column, string message, bool isError) 386 { 387 issues ~= DScannerIssue(file, cast(int) line, cast(int) column, isError 388 ? "error" : "warn", message); 389 } 390 391 uint err, warn; 392 return dparse.parser.parseModule(tokens, file, p, &addIssue, &err, &warn); 393 } 394 395 /// Asynchronously lists all definitions in the specified file. 396 /// 397 /// If you provide code the file wont be manually read. 398 /// 399 /// Set verbose to true if you want to receive more temporary symbols and 400 /// things that could be considered clutter as well. 401 Future!(DefinitionElement[]) listDefinitions(string file, 402 scope const(char)[] code = "", bool verbose = false) 403 { 404 auto ret = new typeof(return); 405 gthreads.create({ 406 mixin(traceTask); 407 try 408 { 409 if (code.length && !file.length) 410 file = "stdin"; 411 if (!code.length) 412 code = readText(file); 413 if (!code.length) 414 { 415 DefinitionElement[] arr; 416 ret.finish(arr); 417 return; 418 } 419 420 RollbackAllocator r; 421 LexerConfig config; 422 auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 423 424 auto m = dparse.parser.parseModule(tokens.array, file, &r); 425 426 auto defFinder = new DefinitionFinder(); 427 defFinder.verbose = verbose; 428 defFinder.visit(m); 429 430 ret.finish(defFinder.definitions); 431 } 432 catch (Throwable e) 433 { 434 ret.error(e); 435 } 436 }); 437 return ret; 438 } 439 440 /// Asynchronously finds all definitions of a symbol in the import paths. 441 Future!(FileLocation[]) findSymbol(string symbol) 442 { 443 auto ret = new typeof(return); 444 gthreads.create({ 445 mixin(traceTask); 446 try 447 { 448 import dscanner.utils : expandArgs; 449 450 string[] paths = expandArgs([""] ~ importPaths); 451 foreach_reverse (i, path; paths) 452 if (path == "stdin") 453 paths = paths.remove(i); 454 FileLocation[] files; 455 findDeclarationOf((fileName, line, column) { 456 FileLocation file; 457 file.file = fileName; 458 file.line = cast(int) line; 459 file.column = cast(int) column; 460 files ~= file; 461 }, symbol, paths); 462 ret.finish(files); 463 } 464 catch (Throwable e) 465 { 466 ret.error(e); 467 } 468 }); 469 return ret; 470 } 471 472 /// Returns: all keys & documentation that can be used in a dscanner.ini 473 INIEntry[] listAllIniFields() 474 { 475 import std.traits : getUDAs; 476 477 INIEntry[] ret; 478 foreach (mem; __traits(allMembers, StaticAnalysisConfig)) 479 static if (is(typeof(__traits(getMember, StaticAnalysisConfig, mem)) == string)) 480 { 481 alias docs = getUDAs!(__traits(getMember, StaticAnalysisConfig, mem), INI); 482 ret ~= INIEntry(mem, docs.length ? docs[0].msg : ""); 483 } 484 return ret; 485 } 486 } 487 488 /// dscanner.ini setting type 489 struct INIEntry 490 { 491 /// 492 string name, documentation; 493 } 494 495 /// Issue type returned by lint 496 struct DScannerIssue 497 { 498 /// 499 string file; 500 /// one-based line & column (in bytes) of this diagnostic location 501 int line, column; 502 /// 503 string type; 504 /// 505 string description; 506 /// 507 string key; 508 /// Resolved range for content that can be filled with a call to resolveRanges 509 ResolvedLocation[2] range; 510 } 511 512 /// Describes a code location in exact byte offset, line number and column for a 513 /// given source code this was resolved against. 514 struct ResolvedLocation 515 { 516 /// byte offset of the character in question - may be 0 if line and column are set 517 ulong index; 518 /// one-based line 519 uint line; 520 /// one-based character offset inside the line in bytes 521 uint column; 522 } 523 524 ResolvedLocation[2] makeTokenRange(const Token token) 525 { 526 return makeTokenRange(token, token); 527 } 528 529 ResolvedLocation[2] makeTokenRange(const Token start, const Token end) 530 { 531 return [makeTokenStart(start), makeTokenEnd(end)]; 532 } 533 534 ResolvedLocation makeTokenStart(const Token token) 535 { 536 ResolvedLocation ret; 537 ret.index = cast(uint) token.index; 538 ret.line = cast(uint) token.line; 539 ret.column = cast(uint) token.column; 540 return ret; 541 } 542 543 ResolvedLocation makeTokenEnd(const Token token) 544 { 545 import std.string : lineSplitter; 546 547 ResolvedLocation ret; 548 auto text = tokenText(token); 549 ret.index = token.index + text.length; 550 int numLines; 551 size_t lastLength; 552 foreach (line; lineSplitter(text)) 553 { 554 numLines++; 555 lastLength = line.length; 556 } 557 if (numLines > 1) 558 { 559 ret.line = cast(uint)(token.line + numLines - 1); 560 ret.column = cast(uint)(lastLength + 1); 561 } 562 else 563 { 564 ret.line = cast(uint)(token.line); 565 ret.column = cast(uint)(token.column + text.length); 566 } 567 return ret; 568 } 569 570 /// Returned by find-symbol 571 struct FileLocation 572 { 573 /// 574 string file; 575 /// 1-based line number and column byte offset 576 int line, column; 577 } 578 579 /// Returned by list-definitions 580 struct DefinitionElement 581 { 582 /// 583 string name; 584 /// 1-based line number 585 int line; 586 /// One of 587 /// * `c` = class 588 /// * `s` = struct 589 /// * `i` = interface 590 /// * `T` = template 591 /// * `f` = function/ctor/dtor 592 /// * `g` = enum {} 593 /// * `u` = union 594 /// * `e` = enum member/definition 595 /// * `v` = variable/invariant 596 /// * `a` = alias 597 /// * `U` = unittest (only in verbose mode) 598 /// * `D` = debug specification (only in verbose mode) 599 /// * `V` = version specification (only in verbose mode) 600 /// * `C` = static module ctor (only in verbose mode) 601 /// * `S` = shared static module ctor (only in verbose mode) 602 /// * `Q` = static module dtor (only in verbose mode) 603 /// * `W` = shared static module dtor (only in verbose mode) 604 /// * `P` = postblit/copy ctor (only in verbose mode) 605 string type; 606 /// 607 string[string] attributes; 608 /// 609 int[2] range; 610 611 bool isVerboseType() const 612 { 613 import std.ascii : isUpper; 614 615 return type.length == 1 && type[0] != 'T' && isUpper(type[0]); 616 } 617 } 618 619 private: 620 621 string typeForWarning(string key) 622 { 623 switch (key) 624 { 625 case "dscanner.bugs.backwards_slices": 626 case "dscanner.bugs.if_else_same": 627 case "dscanner.bugs.logic_operator_operands": 628 case "dscanner.bugs.self_assignment": 629 case "dscanner.confusing.argument_parameter_mismatch": 630 case "dscanner.confusing.brexp": 631 case "dscanner.confusing.builtin_property_names": 632 case "dscanner.confusing.constructor_args": 633 case "dscanner.confusing.function_attributes": 634 case "dscanner.confusing.lambda_returns_lambda": 635 case "dscanner.confusing.logical_precedence": 636 case "dscanner.confusing.struct_constructor_default_args": 637 case "dscanner.deprecated.delete_keyword": 638 case "dscanner.deprecated.floating_point_operators": 639 case "dscanner.if_statement": 640 case "dscanner.performance.enum_array_literal": 641 case "dscanner.style.allman": 642 case "dscanner.style.alias_syntax": 643 case "dscanner.style.doc_missing_params": 644 case "dscanner.style.doc_missing_returns": 645 case "dscanner.style.doc_non_existing_params": 646 case "dscanner.style.explicitly_annotated_unittest": 647 case "dscanner.style.has_public_example": 648 case "dscanner.style.imports_sortedness": 649 case "dscanner.style.long_line": 650 case "dscanner.style.number_literals": 651 case "dscanner.style.phobos_naming_convention": 652 case "dscanner.style.undocumented_declaration": 653 case "dscanner.suspicious.auto_ref_assignment": 654 case "dscanner.suspicious.catch_em_all": 655 case "dscanner.suspicious.comma_expression": 656 case "dscanner.suspicious.incomplete_operator_overloading": 657 case "dscanner.suspicious.incorrect_infinite_range": 658 case "dscanner.suspicious.label_var_same_name": 659 case "dscanner.suspicious.length_subtraction": 660 case "dscanner.suspicious.local_imports": 661 case "dscanner.suspicious.missing_return": 662 case "dscanner.suspicious.object_const": 663 case "dscanner.suspicious.redundant_attributes": 664 case "dscanner.suspicious.redundant_parens": 665 case "dscanner.suspicious.static_if_else": 666 case "dscanner.suspicious.unmodified": 667 case "dscanner.suspicious.unused_label": 668 case "dscanner.suspicious.unused_parameter": 669 case "dscanner.suspicious.unused_variable": 670 case "dscanner.suspicious.useless_assert": 671 case "dscanner.unnecessary.duplicate_attribute": 672 case "dscanner.useless.final": 673 case "dscanner.useless-initializer": 674 case "dscanner.vcall_ctor": 675 return "warn"; 676 case "dscanner.syntax": 677 return "error"; 678 default: 679 stderr.writeln("Warning: unimplemented DScanner reason, assuming warning: ", key); 680 return "warn"; 681 } 682 } 683 684 final class DefinitionFinder : ASTVisitor 685 { 686 override void visit(const ClassDeclaration dec) 687 { 688 if (!dec.structBody) 689 return; 690 definitions ~= makeDefinition(dec.name.text, dec.name.line, "c", context, 691 [ 692 cast(int) dec.structBody.safeStartLocation, 693 cast(int) dec.structBody.safeEndLocation 694 ]); 695 auto c = context; 696 context = ContextType(["class": dec.name.text], null, "public"); 697 dec.accept(this); 698 context = c; 699 } 700 701 override void visit(const StructDeclaration dec) 702 { 703 if (!dec.structBody) 704 return; 705 if (dec.name == tok!"") 706 { 707 dec.accept(this); 708 return; 709 } 710 definitions ~= makeDefinition(dec.name.text, dec.name.line, "s", context, 711 [ 712 cast(int) dec.structBody.safeStartLocation, 713 cast(int) dec.structBody.safeEndLocation 714 ]); 715 auto c = context; 716 context = ContextType(["struct": dec.name.text], null, "public"); 717 dec.accept(this); 718 context = c; 719 } 720 721 override void visit(const InterfaceDeclaration dec) 722 { 723 if (!dec.structBody) 724 return; 725 definitions ~= makeDefinition(dec.name.text, dec.name.line, "i", context, 726 [ 727 cast(int) dec.structBody.safeStartLocation, 728 cast(int) dec.structBody.safeEndLocation 729 ]); 730 auto c = context; 731 context = ContextType(["interface:": dec.name.text], null, context.access); 732 dec.accept(this); 733 context = c; 734 } 735 736 override void visit(const TemplateDeclaration dec) 737 { 738 auto def = makeDefinition(dec.name.text, dec.name.line, "T", context, 739 [cast(int) dec.safeStartLocation, cast(int) dec.safeEndLocation]); 740 def.attributes["signature"] = paramsToString(dec); 741 definitions ~= def; 742 auto c = context; 743 context = ContextType(["template": dec.name.text], null, context.access); 744 dec.accept(this); 745 context = c; 746 } 747 748 override void visit(const FunctionDeclaration dec) 749 { 750 auto def = makeDefinition(dec.name.text, dec.name.line, "f", context, 751 [ 752 cast(int) dec.functionBody.safeStartLocation, 753 cast(int) dec.functionBody.safeEndLocation 754 ]); 755 def.attributes["signature"] = paramsToString(dec); 756 if (dec.returnType !is null) 757 def.attributes["return"] = astToString(dec.returnType); 758 definitions ~= def; 759 } 760 761 override void visit(const Constructor dec) 762 { 763 auto def = makeDefinition("this", dec.line, "f", context, 764 [ 765 cast(int) dec.functionBody.safeStartLocation, 766 cast(int) dec.functionBody.safeEndLocation 767 ]); 768 def.attributes["signature"] = paramsToString(dec); 769 definitions ~= def; 770 } 771 772 override void visit(const Destructor dec) 773 { 774 definitions ~= makeDefinition("~this", dec.line, "f", context, 775 [ 776 cast(int) dec.functionBody.safeStartLocation, 777 cast(int) dec.functionBody.safeEndLocation 778 ]); 779 } 780 781 override void visit(const Postblit dec) 782 { 783 if (!verbose) 784 return; 785 786 definitions ~= makeDefinition("this(this)", dec.line, "f", context, 787 [ 788 cast(int) dec.functionBody.safeStartLocation, 789 cast(int) dec.functionBody.safeEndLocation 790 ]); 791 } 792 793 override void visit(const EnumDeclaration dec) 794 { 795 if (!dec.enumBody) 796 return; 797 definitions ~= makeDefinition(dec.name.text, dec.name.line, "g", context, 798 [cast(int) dec.enumBody.safeStartLocation, cast(int) dec.enumBody.safeEndLocation]); 799 auto c = context; 800 context = ContextType(["enum": dec.name.text], null, context.access); 801 dec.accept(this); 802 context = c; 803 } 804 805 override void visit(const UnionDeclaration dec) 806 { 807 if (!dec.structBody) 808 return; 809 if (dec.name == tok!"") 810 { 811 dec.accept(this); 812 return; 813 } 814 definitions ~= makeDefinition(dec.name.text, dec.name.line, "u", context, 815 [ 816 cast(int) dec.structBody.safeStartLocation, 817 cast(int) dec.structBody.safeEndLocation 818 ]); 819 auto c = context; 820 context = ContextType(["union": dec.name.text], null, context.access); 821 dec.accept(this); 822 context = c; 823 } 824 825 override void visit(const AnonymousEnumMember mem) 826 { 827 definitions ~= makeDefinition(mem.name.text, mem.name.line, "e", context, 828 [ 829 cast(int) mem.name.index, 830 cast(int) mem.name.index + cast(int) mem.name.text.length 831 ]); 832 } 833 834 override void visit(const EnumMember mem) 835 { 836 definitions ~= makeDefinition(mem.name.text, mem.name.line, "e", context, 837 [ 838 cast(int) mem.name.index, 839 cast(int) mem.name.index + cast(int) mem.name.text.length 840 ]); 841 } 842 843 override void visit(const VariableDeclaration dec) 844 { 845 foreach (d; dec.declarators) 846 definitions ~= makeDefinition(d.name.text, d.name.line, "v", context, 847 [ 848 cast(int) d.name.index, 849 cast(int) d.name.index + cast(int) d.name.text.length 850 ]); 851 dec.accept(this); 852 } 853 854 override void visit(const AutoDeclaration dec) 855 { 856 foreach (i; dec.parts.map!(a => a.identifier)) 857 definitions ~= makeDefinition(i.text, i.line, "v", context, 858 [cast(int) i.index, cast(int) i.index + cast(int) i.text.length]); 859 dec.accept(this); 860 } 861 862 override void visit(const Invariant dec) 863 { 864 if (!dec.blockStatement) 865 return; 866 definitions ~= makeDefinition("invariant", dec.line, "v", context, 867 [cast(int) dec.index, cast(int) dec.blockStatement.safeEndLocation]); 868 } 869 870 override void visit(const ModuleDeclaration dec) 871 { 872 context = ContextType(null, null, "public"); 873 dec.accept(this); 874 } 875 876 override void visit(const Attribute attribute) 877 { 878 if (attribute.attribute != tok!"") 879 { 880 switch (attribute.attribute.type) 881 { 882 case tok!"export": 883 context.access = "public"; 884 break; 885 case tok!"public": 886 context.access = "public"; 887 break; 888 case tok!"package": 889 context.access = "protected"; 890 break; 891 case tok!"protected": 892 context.access = "protected"; 893 break; 894 case tok!"private": 895 context.access = "private"; 896 break; 897 default: 898 } 899 } 900 else if (attribute.deprecated_ !is null) 901 { 902 string reason; 903 if (attribute.deprecated_.assignExpression) 904 reason = evaluateExpressionString(attribute.deprecated_.assignExpression); 905 context.attr["deprecation"] = reason.length ? reason : ""; 906 } 907 908 attribute.accept(this); 909 } 910 911 override void visit(const AtAttribute atAttribute) 912 { 913 if (atAttribute.argumentList) 914 { 915 foreach (item; atAttribute.argumentList.items) 916 { 917 auto str = evaluateExpressionString(item); 918 919 if (str !is null) 920 context.privateAttr["utName"] = str; 921 } 922 } 923 atAttribute.accept(this); 924 } 925 926 override void visit(const AttributeDeclaration dec) 927 { 928 accessSt = AccessState.Keep; 929 dec.accept(this); 930 } 931 932 override void visit(const Declaration dec) 933 { 934 auto c = context; 935 dec.accept(this); 936 937 final switch (accessSt) with (AccessState) 938 { 939 case Reset: 940 context = c; 941 break; 942 case Keep: 943 break; 944 } 945 accessSt = AccessState.Reset; 946 } 947 948 override void visit(const DebugSpecification dec) 949 { 950 if (!verbose) 951 return; 952 953 auto tok = dec.identifierOrInteger; 954 auto def = makeDefinition(tok.tokenText, tok.line, "D", context, 955 [ 956 cast(int) tok.index, 957 cast(int) tok.index + cast(int) tok.text.length 958 ]); 959 960 definitions ~= def; 961 dec.accept(this); 962 } 963 964 override void visit(const VersionSpecification dec) 965 { 966 if (!verbose) 967 return; 968 969 auto tok = dec.token; 970 auto def = makeDefinition(tok.tokenText, tok.line, "V", context, 971 [ 972 cast(int) tok.index, 973 cast(int) tok.index + cast(int) tok.text.length 974 ]); 975 976 definitions ~= def; 977 dec.accept(this); 978 } 979 980 override void visit(const Unittest dec) 981 { 982 if (!verbose) 983 return; 984 985 if (!dec.blockStatement) 986 return; 987 string testName = text("__unittest_L", dec.line, "_C", dec.column); 988 definitions ~= makeDefinition(testName, dec.line, "U", context, 989 [ 990 cast(int) dec.tokens[0].index, 991 cast(int) dec.blockStatement.safeEndLocation 992 ], "U"); 993 994 // TODO: decide if we want to include types nested in unittests 995 // dec.accept(this); 996 } 997 998 private static immutable CtorTypes = ["C", "S", "Q", "W"]; 999 private static immutable CtorNames = [ 1000 "static this()", "shared static this()", 1001 "static ~this()", "shared static ~this()" 1002 ]; 1003 static foreach (i, T; AliasSeq!(StaticConstructor, SharedStaticConstructor, 1004 StaticDestructor, SharedStaticDestructor)) 1005 { 1006 override void visit(const T dec) 1007 { 1008 if (!verbose) 1009 return; 1010 1011 definitions ~= makeDefinition(CtorNames[i], dec.line, CtorTypes[i], context, 1012 [ 1013 cast(int) dec.functionBody.safeStartLocation, 1014 cast(int) dec.functionBody.safeEndLocation 1015 ]); 1016 } 1017 } 1018 1019 override void visit(const AliasDeclaration dec) 1020 { 1021 // Old style alias 1022 if (dec.declaratorIdentifierList) 1023 foreach (i; dec.declaratorIdentifierList.identifiers) 1024 definitions ~= makeDefinition(i.text, i.line, "a", context, 1025 [cast(int) i.index, cast(int) i.index + cast(int) i.text.length]); 1026 dec.accept(this); 1027 } 1028 1029 override void visit(const AliasInitializer dec) 1030 { 1031 definitions ~= makeDefinition(dec.name.text, dec.name.line, "a", context, 1032 [ 1033 cast(int) dec.name.index, 1034 cast(int) dec.name.index + cast(int) dec.name.text.length 1035 ]); 1036 1037 dec.accept(this); 1038 } 1039 1040 override void visit(const AliasThisDeclaration dec) 1041 { 1042 auto name = dec.identifier; 1043 definitions ~= makeDefinition(name.text, name.line, "a", context, 1044 [cast(int) name.index, cast(int) name.index + cast(int) name.text.length]); 1045 1046 dec.accept(this); 1047 } 1048 1049 alias visit = ASTVisitor.visit; 1050 1051 ContextType context; 1052 AccessState accessSt; 1053 DefinitionElement[] definitions; 1054 bool verbose; 1055 } 1056 1057 DefinitionElement makeDefinition(string name, size_t line, string type, 1058 ContextType context, int[2] range, string forType = null) 1059 { 1060 string[string] attr = context.attr.dup; 1061 if (context.access.length) 1062 attr["access"] = context.access; 1063 1064 if (forType == "U") 1065 { 1066 if (auto utName = "utName" in context.privateAttr) 1067 attr["name"] = *utName; 1068 } 1069 return DefinitionElement(name, cast(int) line, type, attr, range); 1070 } 1071 1072 enum AccessState 1073 { 1074 Reset, /// when ascending the AST reset back to the previous access. 1075 Keep /// when ascending the AST keep the new access. 1076 } 1077 1078 struct ContextType 1079 { 1080 string[string] attr; 1081 string[string] privateAttr; 1082 string access; 1083 } 1084 1085 unittest 1086 { 1087 StaticAnalysisConfig check = StaticAnalysisConfig.init; 1088 assert(check is StaticAnalysisConfig.init); 1089 } 1090 1091 unittest 1092 { 1093 scope backend = new WorkspaceD(); 1094 auto workspace = makeTemporaryTestingWorkspace; 1095 auto instance = backend.addInstance(workspace.directory); 1096 backend.register!DscannerComponent; 1097 DscannerComponent dscanner = instance.get!DscannerComponent; 1098 1099 bool verbose; 1100 DefinitionElement[] expectedDefinitions; 1101 runTestDataFileTests("test/data/list_definition", 1102 () { 1103 verbose = false; 1104 expectedDefinitions = null; 1105 }, 1106 (code, variable, value) { 1107 switch (variable) 1108 { 1109 case "verbose": 1110 verbose = value.boolean; 1111 break; 1112 default: 1113 assert(false, "Unknown test variable " ~ variable); 1114 } 1115 }, 1116 (code, parts, line) { 1117 assert(parts.length == 6, "malformed definition test line: " ~ line); 1118 1119 string[string] dict; 1120 foreach (k, v; parseJSON(parts[3]).object) 1121 dict[k] = v.str; 1122 1123 expectedDefinitions ~= DefinitionElement( 1124 parts[0], 1125 parts[1].to!int, 1126 parts[2], 1127 dict, 1128 [parts[4].to!int, parts[5].to!int] 1129 ); 1130 }, 1131 (code) { 1132 auto defs = dscanner.listDefinitions("stdin", code, verbose).getBlocking(); 1133 assert(defs == expectedDefinitions, highlightDiff(defs, expectedDefinitions)); 1134 }); 1135 } 1136 1137 version (unittest) private string highlightDiff(T)(T[] a, T[] b) 1138 { 1139 string ret; 1140 if (a.length != b.length) 1141 ret ~= text("length mismatch: ", a.length, " != ", b.length, "\n"); 1142 foreach (i; 0 .. min(a.length, b.length)) 1143 { 1144 ret ~= text(a[i] == b[i] ? "\x1B[0m " : "\x1B[33m ! ", a[i], a[i] == b[i] ? " == " : " != ", b[i], "\x1B[0m\n"); 1145 } 1146 if (a.length < b.length) 1147 { 1148 foreach (i; a.length .. b.length) 1149 ret ~= text("\x1B[31m + ", b[i], "\x1B[0m\n"); 1150 } 1151 else 1152 { 1153 foreach (i; b.length .. a.length) 1154 ret ~= text("\x1B[31m - ", a[i], "\x1B[0m\n"); 1155 } 1156 return ret; 1157 } 1158 1159 size_t safeStartLocation(T)(const T b) 1160 { 1161 return (b !is null && b.tokens.length > 0) ? b.tokens[0].index : 0; 1162 } 1163 1164 size_t safeEndLocation(T)(const T b) 1165 { 1166 return (b !is null && b.tokens.length > 0) ? (b.tokens[$ - 1].index + b.tokens[$ - 1].tokenText.length) : 0; 1167 }