1 module workspaced.com.dscanner; 2 3 import std.algorithm; 4 import std.array; 5 import std.conv; 6 import std.file; 7 import std.json; 8 import std.stdio; 9 import std.typecons; 10 11 import core.sync.mutex; 12 import core.thread; 13 14 import dscanner.analysis.base; 15 import dscanner.analysis.config; 16 import dscanner.analysis.run; 17 import dscanner.symbol_finder; 18 19 import inifiled : INI, readINIFile; 20 21 import dparse.ast; 22 import dparse.lexer; 23 import dparse.parser; 24 import dparse.rollback_allocator; 25 import dsymbol.builtin.names; 26 import dsymbol.modulecache : ASTAllocator, ModuleCache; 27 28 import painlessjson; 29 30 import workspaced.api; 31 import workspaced.dparseext; 32 33 @component("dscanner") 34 class DscannerComponent : ComponentWrapper 35 { 36 mixin DefaultComponentWrapper; 37 38 /// Asynchronously lints the file passed. 39 /// If you provide code then the code will be used and file will be ignored. 40 Future!(DScannerIssue[]) lint(string file = "", string ini = "dscanner.ini", scope const(char)[] code = "") 41 { 42 auto ret = new Future!(DScannerIssue[]); 43 gthreads.create({ 44 mixin(traceTask); 45 try 46 { 47 if (code.length && !file.length) 48 file = "stdin"; 49 auto config = defaultStaticAnalysisConfig(); 50 if (getConfigPath("dscanner.ini", ini)) 51 stderr.writeln("Overriding Dscanner ini with workspace-d dscanner.ini config file"); 52 if (ini.exists) 53 readINIFile(config, ini); 54 if (!code.length) 55 code = readText(file); 56 DScannerIssue[] issues; 57 if (!code.length) 58 { 59 ret.finish(issues); 60 return; 61 } 62 RollbackAllocator r; 63 const(Token)[] tokens; 64 StringCache cache = StringCache(StringCache.defaultBucketCount); 65 const Module m = parseModule(file, cast(ubyte[]) code, &r, cache, tokens, issues); 66 if (!m) 67 throw new Exception(text("parseModule returned null?! - file: '", 68 file, "', code: '", code, "'")); 69 MessageSet results; 70 auto alloc = scoped!ASTAllocator(); 71 auto moduleCache = ModuleCache(alloc); 72 results = analyze(file, m, config, moduleCache, tokens, true); 73 if (results is null) 74 { 75 ret.finish(issues); 76 return; 77 } 78 foreach (msg; results) 79 { 80 DScannerIssue issue; 81 issue.file = msg.fileName; 82 issue.line = cast(int) msg.line; 83 issue.column = cast(int) msg.column; 84 issue.type = typeForWarning(msg.key); 85 issue.description = msg.message; 86 issue.key = msg.key; 87 issues ~= issue; 88 } 89 ret.finish(issues); 90 } 91 catch (Throwable e) 92 { 93 ret.error(e); 94 } 95 }); 96 return ret; 97 } 98 99 private const(Module) parseModule(string file, ubyte[] code, RollbackAllocator* p, 100 ref StringCache cache, ref const(Token)[] tokens, ref DScannerIssue[] issues) 101 { 102 LexerConfig config; 103 config.fileName = file; 104 config.stringBehavior = StringBehavior.source; 105 tokens = getTokensForParser(code, config, &cache); 106 107 void addIssue(string fileName, size_t line, size_t column, string message, bool isError) 108 { 109 issues ~= DScannerIssue(file, cast(int) line, cast(int) column, isError 110 ? "error" : "warn", message); 111 } 112 113 uint err, warn; 114 return dparse.parser.parseModule(tokens, file, p, &addIssue, &err, &warn); 115 } 116 117 /// Asynchronously lists all definitions in the specified file. 118 /// If you provide code the file wont be manually read. 119 Future!(DefinitionElement[]) listDefinitions(string file, scope const(char)[] code = "") 120 { 121 auto ret = new Future!(DefinitionElement[]); 122 gthreads.create({ 123 mixin(traceTask); 124 try 125 { 126 if (code.length && !file.length) 127 file = "stdin"; 128 if (!code.length) 129 code = readText(file); 130 if (!code.length) 131 { 132 DefinitionElement[] arr; 133 ret.finish(arr); 134 return; 135 } 136 137 RollbackAllocator r; 138 LexerConfig config; 139 auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 140 141 auto m = dparse.parser.parseModule(tokens.array, file, &r); 142 143 auto defFinder = new DefinitionFinder(); 144 defFinder.visit(m); 145 146 ret.finish(defFinder.definitions); 147 } 148 catch (Throwable e) 149 { 150 ret.error(e); 151 } 152 }); 153 return ret; 154 } 155 156 /// Asynchronously finds all definitions of a symbol in the import paths. 157 Future!(FileLocation[]) findSymbol(string symbol) 158 { 159 auto ret = new Future!(FileLocation[]); 160 gthreads.create({ 161 mixin(traceTask); 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 }); 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.specifiedFunctionBody || !dec.functionBody.specifiedFunctionBody.blockStatement) 373 return; 374 auto def = makeDefinition(dec.name.text, dec.name.line, "f", context, 375 [cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.startLocation, 376 cast(int) dec.functionBody.specifiedFunctionBody.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.specifiedFunctionBody || !dec.functionBody.specifiedFunctionBody.blockStatement) 386 return; 387 auto def = makeDefinition("this", dec.line, "f", context, 388 [cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.startLocation, 389 cast(int) dec.functionBody.specifiedFunctionBody.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.specifiedFunctionBody || !dec.functionBody.specifiedFunctionBody.blockStatement) 397 return; 398 definitions ~= makeDefinition("~this", dec.line, "f", context, 399 [cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.startLocation, 400 cast(int) dec.functionBody.specifiedFunctionBody.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 }