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 threads.create({ 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 }); 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 threads.create({ 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 auto m = dparse.parser.parseModule(tokens.array, file, &r); 139 140 auto defFinder = new DefinitionFinder(); 141 defFinder.visit(m); 142 143 ret.finish(defFinder.definitions); 144 } 145 catch (Throwable e) 146 { 147 ret.error(e); 148 } 149 }); 150 return ret; 151 } 152 153 /// Asynchronously finds all definitions of a symbol in the import paths. 154 Future!(FileLocation[]) findSymbol(string symbol) 155 { 156 auto ret = new Future!(FileLocation[]); 157 threads.create({ 158 try 159 { 160 import dscanner.utils : expandArgs; 161 162 string[] paths = expandArgs([""] ~ importPaths); 163 foreach_reverse (i, path; paths) 164 if (path == "stdin") 165 paths = paths.remove(i); 166 FileLocation[] files; 167 findDeclarationOf((fileName, line, column) { 168 FileLocation file; 169 file.file = fileName; 170 file.line = cast(int) line; 171 file.column = cast(int) column; 172 files ~= file; 173 }, symbol, paths); 174 ret.finish(files); 175 } 176 catch (Throwable e) 177 { 178 ret.error(e); 179 } 180 }); 181 return ret; 182 } 183 184 /// Returns: all keys & documentation that can be used in a dscanner.ini 185 INIEntry[] listAllIniFields() 186 { 187 import std.traits : getUDAs; 188 189 INIEntry[] ret; 190 foreach (mem; __traits(allMembers, StaticAnalysisConfig)) 191 static if (is(typeof(__traits(getMember, StaticAnalysisConfig, mem)) == string)) 192 { 193 alias docs = getUDAs!(__traits(getMember, StaticAnalysisConfig, mem), INI); 194 ret ~= INIEntry(mem, docs.length ? docs[0].msg : ""); 195 } 196 return ret; 197 } 198 } 199 200 /// dscanner.ini setting type 201 struct INIEntry 202 { 203 /// 204 string name, documentation; 205 } 206 207 /// Issue type returned by lint 208 struct DScannerIssue 209 { 210 /// 211 string file; 212 /// 213 int line, column; 214 /// 215 string type; 216 /// 217 string description; 218 /// 219 string key; 220 } 221 222 /// Returned by find-symbol 223 struct FileLocation 224 { 225 /// 226 string file; 227 /// 228 int line, column; 229 } 230 231 /// Returned by list-definitions 232 struct DefinitionElement 233 { 234 /// 235 string name; 236 /// 237 int line; 238 /// 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) 239 string type; 240 /// 241 string[string] attributes; 242 /// 243 int[2] range; 244 } 245 246 private: 247 248 string typeForWarning(string key) 249 { 250 switch (key) 251 { 252 case "dscanner.bugs.backwards_slices": 253 case "dscanner.bugs.if_else_same": 254 case "dscanner.bugs.logic_operator_operands": 255 case "dscanner.bugs.self_assignment": 256 case "dscanner.confusing.argument_parameter_mismatch": 257 case "dscanner.confusing.brexp": 258 case "dscanner.confusing.builtin_property_names": 259 case "dscanner.confusing.constructor_args": 260 case "dscanner.confusing.function_attributes": 261 case "dscanner.confusing.lambda_returns_lambda": 262 case "dscanner.confusing.logical_precedence": 263 case "dscanner.confusing.struct_constructor_default_args": 264 case "dscanner.deprecated.delete_keyword": 265 case "dscanner.deprecated.floating_point_operators": 266 case "dscanner.if_statement": 267 case "dscanner.performance.enum_array_literal": 268 case "dscanner.style.allman": 269 case "dscanner.style.alias_syntax": 270 case "dscanner.style.doc_missing_params": 271 case "dscanner.style.doc_missing_returns": 272 case "dscanner.style.doc_non_existing_params": 273 case "dscanner.style.explicitly_annotated_unittest": 274 case "dscanner.style.has_public_example": 275 case "dscanner.style.imports_sortedness": 276 case "dscanner.style.long_line": 277 case "dscanner.style.number_literals": 278 case "dscanner.style.phobos_naming_convention": 279 case "dscanner.style.undocumented_declaration": 280 case "dscanner.suspicious.auto_ref_assignment": 281 case "dscanner.suspicious.catch_em_all": 282 case "dscanner.suspicious.comma_expression": 283 case "dscanner.suspicious.incomplete_operator_overloading": 284 case "dscanner.suspicious.incorrect_infinite_range": 285 case "dscanner.suspicious.label_var_same_name": 286 case "dscanner.suspicious.length_subtraction": 287 case "dscanner.suspicious.local_imports": 288 case "dscanner.suspicious.missing_return": 289 case "dscanner.suspicious.object_const": 290 case "dscanner.suspicious.redundant_attributes": 291 case "dscanner.suspicious.redundant_parens": 292 case "dscanner.suspicious.static_if_else": 293 case "dscanner.suspicious.unmodified": 294 case "dscanner.suspicious.unused_label": 295 case "dscanner.suspicious.unused_parameter": 296 case "dscanner.suspicious.unused_variable": 297 case "dscanner.suspicious.useless_assert": 298 case "dscanner.unnecessary.duplicate_attribute": 299 case "dscanner.useless.final": 300 case "dscanner.useless-initializer": 301 case "dscanner.vcall_ctor": 302 return "warn"; 303 case "dscanner.syntax": 304 return "error"; 305 default: 306 stderr.writeln("Warning: unimplemented DScanner reason, assuming warning: ", key); 307 return "warn"; 308 } 309 } 310 311 final class DefinitionFinder : ASTVisitor 312 { 313 override void visit(const ClassDeclaration dec) 314 { 315 if (!dec.structBody) 316 return; 317 definitions ~= makeDefinition(dec.name.text, dec.name.line, "c", context, 318 [cast(int) dec.structBody.startLocation, cast(int) dec.structBody.endLocation]); 319 auto c = context; 320 context = ContextType(["class" : dec.name.text], "public"); 321 dec.accept(this); 322 context = c; 323 } 324 325 override void visit(const StructDeclaration dec) 326 { 327 if (!dec.structBody) 328 return; 329 if (dec.name == tok!"") 330 { 331 dec.accept(this); 332 return; 333 } 334 definitions ~= makeDefinition(dec.name.text, dec.name.line, "s", context, 335 [cast(int) dec.structBody.startLocation, cast(int) dec.structBody.endLocation]); 336 auto c = context; 337 context = ContextType(["struct" : dec.name.text], "public"); 338 dec.accept(this); 339 context = c; 340 } 341 342 override void visit(const InterfaceDeclaration dec) 343 { 344 if (!dec.structBody) 345 return; 346 definitions ~= makeDefinition(dec.name.text, dec.name.line, "i", context, 347 [cast(int) dec.structBody.startLocation, cast(int) dec.structBody.endLocation]); 348 auto c = context; 349 context = ContextType(["interface:" : dec.name.text], context.access); 350 dec.accept(this); 351 context = c; 352 } 353 354 override void visit(const TemplateDeclaration dec) 355 { 356 auto def = makeDefinition(dec.name.text, dec.name.line, "T", context, 357 [cast(int) dec.startLocation, cast(int) dec.endLocation]); 358 def.attributes["signature"] = paramsToString(dec); 359 definitions ~= def; 360 auto c = context; 361 context = ContextType(["template" : dec.name.text], context.access); 362 dec.accept(this); 363 context = c; 364 } 365 366 override void visit(const FunctionDeclaration dec) 367 { 368 if (!dec.functionBody || !dec.functionBody.specifiedFunctionBody || !dec.functionBody.specifiedFunctionBody.blockStatement) 369 return; 370 auto def = makeDefinition(dec.name.text, dec.name.line, "f", context, 371 [cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.startLocation, 372 cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.endLocation]); 373 def.attributes["signature"] = paramsToString(dec); 374 if (dec.returnType !is null) 375 def.attributes["return"] = astToString(dec.returnType); 376 definitions ~= def; 377 } 378 379 override void visit(const Constructor dec) 380 { 381 if (!dec.functionBody || !dec.functionBody.specifiedFunctionBody || !dec.functionBody.specifiedFunctionBody.blockStatement) 382 return; 383 auto def = makeDefinition("this", dec.line, "f", context, 384 [cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.startLocation, 385 cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.endLocation]); 386 def.attributes["signature"] = paramsToString(dec); 387 definitions ~= def; 388 } 389 390 override void visit(const Destructor dec) 391 { 392 if (!dec.functionBody || !dec.functionBody.specifiedFunctionBody || !dec.functionBody.specifiedFunctionBody.blockStatement) 393 return; 394 definitions ~= makeDefinition("~this", dec.line, "f", context, 395 [cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.startLocation, 396 cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.endLocation]); 397 } 398 399 override void visit(const EnumDeclaration dec) 400 { 401 if (!dec.enumBody) 402 return; 403 definitions ~= makeDefinition(dec.name.text, dec.name.line, "g", context, 404 [cast(int) dec.enumBody.startLocation, cast(int) dec.enumBody.endLocation]); 405 auto c = context; 406 context = ContextType(["enum" : dec.name.text], context.access); 407 dec.accept(this); 408 context = c; 409 } 410 411 override void visit(const UnionDeclaration dec) 412 { 413 if (!dec.structBody) 414 return; 415 if (dec.name == tok!"") 416 { 417 dec.accept(this); 418 return; 419 } 420 definitions ~= makeDefinition(dec.name.text, dec.name.line, "u", context, 421 [cast(int) dec.structBody.startLocation, cast(int) dec.structBody.endLocation]); 422 auto c = context; 423 context = ContextType(["union" : dec.name.text], context.access); 424 dec.accept(this); 425 context = c; 426 } 427 428 override void visit(const AnonymousEnumMember mem) 429 { 430 definitions ~= makeDefinition(mem.name.text, mem.name.line, "e", context, 431 [cast(int) mem.name.index, cast(int) mem.name.index + cast(int) mem.name.text.length]); 432 } 433 434 override void visit(const EnumMember mem) 435 { 436 definitions ~= makeDefinition(mem.name.text, mem.name.line, "e", context, 437 [cast(int) mem.name.index, cast(int) mem.name.index + cast(int) mem.name.text.length]); 438 } 439 440 override void visit(const VariableDeclaration dec) 441 { 442 foreach (d; dec.declarators) 443 definitions ~= makeDefinition(d.name.text, d.name.line, "v", context, 444 [cast(int) d.name.index, cast(int) d.name.index + cast(int) d.name.text.length]); 445 dec.accept(this); 446 } 447 448 override void visit(const AutoDeclaration dec) 449 { 450 foreach (i; dec.parts.map!(a => a.identifier)) 451 definitions ~= makeDefinition(i.text, i.line, "v", context, 452 [cast(int) i.index, cast(int) i.index + cast(int) i.text.length]); 453 dec.accept(this); 454 } 455 456 override void visit(const Invariant dec) 457 { 458 if (!dec.blockStatement) 459 return; 460 definitions ~= makeDefinition("invariant", dec.line, "v", context, 461 [cast(int) dec.index, cast(int) dec.blockStatement.endLocation]); 462 } 463 464 override void visit(const ModuleDeclaration dec) 465 { 466 context = ContextType(null, "public"); 467 dec.accept(this); 468 } 469 470 override void visit(const Attribute attribute) 471 { 472 if (attribute.attribute != tok!"") 473 { 474 switch (attribute.attribute.type) 475 { 476 case tok!"export": 477 context.access = "public"; 478 break; 479 case tok!"public": 480 context.access = "public"; 481 break; 482 case tok!"package": 483 context.access = "protected"; 484 break; 485 case tok!"protected": 486 context.access = "protected"; 487 break; 488 case tok!"private": 489 context.access = "private"; 490 break; 491 default: 492 } 493 } 494 else if (attribute.deprecated_ !is null) 495 { 496 // TODO: find out how to get deprecation message 497 context.attr["deprecation"] = ""; 498 } 499 500 attribute.accept(this); 501 } 502 503 override void visit(const AttributeDeclaration dec) 504 { 505 accessSt = AccessState.Keep; 506 dec.accept(this); 507 } 508 509 override void visit(const Declaration dec) 510 { 511 auto c = context; 512 dec.accept(this); 513 514 final switch (accessSt) with (AccessState) 515 { 516 case Reset: 517 context = c; 518 break; 519 case Keep: 520 break; 521 } 522 accessSt = AccessState.Reset; 523 } 524 525 override void visit(const Unittest dec) 526 { 527 // skipping symbols inside a unit test to not clutter the ctags file 528 // with "temporary" symbols. 529 // TODO when phobos have a unittest library investigate how that could 530 // be used to describe the tests. 531 // Maybe with UDA's to give the unittest a "name". 532 } 533 534 override void visit(const AliasDeclaration dec) 535 { 536 // Old style alias 537 if (dec.declaratorIdentifierList) 538 foreach (i; dec.declaratorIdentifierList.identifiers) 539 definitions ~= makeDefinition(i.text, i.line, "a", context, 540 [cast(int) i.index, cast(int) i.index + cast(int) i.text.length]); 541 dec.accept(this); 542 } 543 544 override void visit(const AliasInitializer dec) 545 { 546 definitions ~= makeDefinition(dec.name.text, dec.name.line, "a", context, 547 [cast(int) dec.name.index, cast(int) dec.name.index + cast(int) dec.name.text.length]); 548 549 dec.accept(this); 550 } 551 552 override void visit(const AliasThisDeclaration dec) 553 { 554 auto name = dec.identifier; 555 definitions ~= makeDefinition(name.text, name.line, "a", context, 556 [cast(int) name.index, cast(int) name.index + cast(int) name.text.length]); 557 558 dec.accept(this); 559 } 560 561 alias visit = ASTVisitor.visit; 562 563 ContextType context; 564 AccessState accessSt; 565 DefinitionElement[] definitions; 566 } 567 568 DefinitionElement makeDefinition(string name, size_t line, string type, 569 ContextType context, int[2] range) 570 { 571 string[string] attr = context.attr; 572 if (context.access.length) 573 attr["access"] = context.access; 574 return DefinitionElement(name, cast(int) line, type, attr, range); 575 } 576 577 enum AccessState 578 { 579 Reset, /// when ascending the AST reset back to the previous access. 580 Keep /// when ascending the AST keep the new access. 581 } 582 583 struct ContextType 584 { 585 string[string] attr; 586 string access; 587 }