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