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