1 module workspaced.com.dscanner; 2 3 import std.json; 4 import std.conv; 5 import std.path; 6 import std.stdio; 7 import std.regex; 8 import std.string; 9 import std.process; 10 import std.algorithm; 11 import core.thread; 12 13 import painlessjson; 14 15 import workspaced.api; 16 17 @component("dscanner") : 18 19 /// Load function for dscanner. Call with `{"cmd": "load", "components": ["dscanner"]}` 20 /// This will store the working directory and executable name for future use. 21 /// It also checks for the version. All dub methods are used with `"cmd": "dscanner"` 22 @load void start(string dir, string dscannerPath = "dscanner") 23 { 24 cwd = dir; 25 execPath = dscannerPath; 26 if (!checkVersion(execPath.getVersionAndFixPath, [0, 4, 0])) 27 broadcast(JSONValue([ 28 "type": JSONValue("outdated"), 29 "component": JSONValue("dscanner") 30 ])); 31 else 32 canStdin = true; 33 } 34 35 /// Unloads dscanner. Has no purpose right now. 36 @unload void stop() 37 { 38 } 39 40 /// Asynchronously lints the file passed. 41 /// If you provide code and DScanner supports reading from stdin (stable 0.4.0 and above) then code will be used. 42 /// Returns: `[{file: string, line: int, column: int, type: string, description: string}]` 43 /// Call_With: `{"subcmd": "lint"}` 44 @arguments("subcmd", "lint") 45 @async void lint(AsyncCallback cb, string file = "", string ini = "dscanner.ini", string code = "") 46 { 47 new Thread({ 48 try 49 { 50 if (canStdin && code.length) 51 file = "stdin"; 52 auto args = [execPath, "-S", file]; 53 if (getConfigPath("dscanner.ini", ini)) 54 stderr.writeln("Overriding Dscanner ini with workspace-d dscanner.ini config file"); 55 else if (ini && ini.length) 56 { 57 if (ini.isAbsolute) 58 args ~= ["--config", ini]; 59 else 60 args ~= ["--config", buildPath(cwd, ini)]; 61 } 62 ProcessPipes pipes = raw(args); 63 if (canStdin && code.length) 64 { 65 pipes.stdin.write(code); 66 pipes.stdin.flush(); 67 pipes.stdin.close(); 68 } 69 scope (exit) 70 pipes.pid.wait(); 71 string[] res; 72 while (pipes.stdout.isOpen && !pipes.stdout.eof) 73 res ~= pipes.stdout.readln(); 74 DScannerIssue[] issues; 75 foreach (line; res) 76 { 77 if (!line.length) 78 continue; 79 auto match = line.chomp.matchFirst(dscannerIssueRegex); 80 if (!match) 81 continue; 82 DScannerIssue issue; 83 issue.file = match[1]; 84 if (issue.file == "stdin" && canStdin && code.length) 85 issue.file = file; 86 issue.line = match[2].to!int; 87 issue.column = match[3].to!int; 88 issue.type = match[4]; 89 issue.description = match[5]; 90 issues ~= issue; 91 } 92 cb(null, issues.toJSON); 93 } 94 catch (Throwable e) 95 { 96 cb(e, JSONValue(null)); 97 } 98 }).start(); 99 } 100 101 /// Asynchronously lists all definitions in the specified file. 102 /// If you provide code and DScanner supports reading from stdin (stable 0.4.0 and above) then code will be used. 103 /// Returns: `[{name: string, line: int, type: string, attributes: string[string]}]` 104 /// Call_With: `{"subcmd": "list-definitions"}` 105 @arguments("subcmd", "list-definitions") 106 @async void listDefinitions(AsyncCallback cb, string file, string code = "") 107 { 108 new Thread({ 109 try 110 { 111 if (canStdin && code.length) 112 file = "stdin"; 113 ProcessPipes pipes = raw([execPath, "-c", file]); 114 scope (exit) 115 pipes.pid.wait(); 116 if (canStdin && code.length) 117 { 118 pipes.stdin.write(code); 119 pipes.stdin.flush(); 120 pipes.stdin.close(); 121 } 122 string[] res; 123 while (pipes.stdout.isOpen && !pipes.stdout.eof) 124 res ~= pipes.stdout.readln(); 125 DefinitionElement[] definitions; 126 foreach (line; res) 127 { 128 if (!line.length || line[0] == '!') 129 continue; 130 line = line.chomp; 131 string[] splits = line.split('\t'); 132 DefinitionElement definition; 133 definition.name = splits[0]; 134 definition.type = splits[3]; 135 definition.line = splits[4][5 .. $].to!int; 136 if (splits.length > 5) 137 foreach (attribute; splits[5 .. $]) 138 { 139 string[] sides = attribute.split(':'); 140 definition.attributes[sides[0]] = sides[1 .. $].join(':'); 141 } 142 definitions ~= definition; 143 } 144 cb(null, definitions.toJSON); 145 } 146 catch (Throwable e) 147 { 148 cb(e, JSONValue(null)); 149 } 150 }).start(); 151 } 152 153 /// Asynchronously finds all definitions of a symbol in the import paths. 154 /// Returns: `[{name: string, line: int, column: int}]` 155 /// Call_With: `{"subcmd": "find-symbol"}` 156 @arguments("subcmd", "find-symbol") 157 @async void findSymbol(AsyncCallback cb, string symbol) 158 { 159 new Thread({ 160 try 161 { 162 ProcessPipes pipes = raw([execPath, "-d", symbol] ~ importPathProvider()); 163 scope (exit) 164 pipes.pid.wait(); 165 string[] res; 166 while (pipes.stdout.isOpen && !pipes.stdout.eof) 167 res ~= pipes.stdout.readln(); 168 FileLocation[] files; 169 foreach (line; res) 170 { 171 auto match = line.chomp.matchFirst(dscannerFileRegex); 172 if (!match) 173 continue; 174 FileLocation file; 175 file.file = match[1]; 176 file.line = match[2].to!int; 177 file.column = match[3].to!int; 178 files ~= file; 179 } 180 cb(null, files.toJSON); 181 } 182 catch (Throwable e) 183 { 184 cb(e, JSONValue(null)); 185 } 186 }).start(); 187 } 188 189 private: 190 191 __gshared 192 { 193 string cwd, execPath; 194 bool canStdin; 195 } 196 197 auto raw(string[] args, Redirect redirect = Redirect.all) 198 { 199 auto pipes = pipeProcess(args, redirect, null, Config.none, cwd); 200 return pipes; 201 } 202 203 auto dscannerIssueRegex = ctRegex!`^(.+?)\((\d+)\:(\d+)\)\[(.*?)\]: (.*)`; 204 auto dscannerFileRegex = ctRegex!`^(.*?)\((\d+):(\d+)\)`; 205 struct DScannerIssue 206 { 207 string file; 208 int line, column; 209 string type; 210 string description; 211 } 212 213 struct FileLocation 214 { 215 string file; 216 int line, column; 217 } 218 219 struct OutlineTreeNode 220 { 221 string definition; 222 int line; 223 OutlineTreeNode[] children; 224 } 225 226 struct DefinitionElement 227 { 228 string name; 229 int line; 230 string type; 231 string[string] attributes; 232 }