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 } 32 33 /// Unloads dscanner. Has no purpose right now. 34 @unload void stop() 35 { 36 } 37 38 /// Asynchronously lints the file passed. 39 /// Returns: `[{file: string, line: int, column: int, type: string, description: string}]` 40 /// Call_With: `{"subcmd": "lint"}` 41 @arguments("subcmd", "lint") 42 @async void lint(AsyncCallback cb, string file, string ini = "dscanner.ini") 43 { 44 new Thread({ 45 try 46 { 47 auto args = [execPath, "-S", file]; 48 if (getConfigPath("dscanner.ini", ini)) 49 stderr.writeln("Overriding Dscanner ini with workspace-d dscanner.ini config file"); 50 else if (ini && ini.length) 51 { 52 if (ini.isAbsolute) 53 args ~= ["--config", ini]; 54 else 55 args ~= ["--config", buildPath(cwd, ini)]; 56 } 57 ProcessPipes pipes = raw(args); 58 scope (exit) 59 pipes.pid.wait(); 60 string[] res; 61 while (pipes.stdout.isOpen && !pipes.stdout.eof) 62 res ~= pipes.stdout.readln(); 63 DScannerIssue[] issues; 64 foreach (line; res) 65 { 66 if (!line.length) 67 continue; 68 auto match = line.chomp.matchFirst(dscannerIssueRegex); 69 if (!match) 70 continue; 71 DScannerIssue issue; 72 issue.file = match[1]; 73 issue.line = match[2].to!int; 74 issue.column = match[3].to!int; 75 issue.type = match[4]; 76 issue.description = match[5]; 77 issues ~= issue; 78 } 79 cb(null, issues.toJSON); 80 } 81 catch (Throwable e) 82 { 83 cb(e, JSONValue(null)); 84 } 85 }).start(); 86 } 87 88 /// Asynchronously lists all definitions in the specified file. 89 /// Returns: `[{name: string, line: int, type: string, attributes: string[string]}]` 90 /// Call_With: `{"subcmd": "list-definitions"}` 91 @arguments("subcmd", "list-definitions") 92 @async void listDefinitions(AsyncCallback cb, string file) 93 { 94 new Thread({ 95 try 96 { 97 ProcessPipes pipes = raw([execPath, "-c", file]); 98 scope (exit) 99 pipes.pid.wait(); 100 string[] res; 101 while (pipes.stdout.isOpen && !pipes.stdout.eof) 102 res ~= pipes.stdout.readln(); 103 DefinitionElement[] definitions; 104 foreach (line; res) 105 { 106 if (!line.length || line[0] == '!') 107 continue; 108 line = line.chomp; 109 string[] splits = line.split('\t'); 110 DefinitionElement definition; 111 definition.name = splits[0]; 112 definition.type = splits[3]; 113 definition.line = splits[4][5 .. $].to!int; 114 if (splits.length > 5) 115 foreach (attribute; splits[5 .. $]) 116 { 117 string[] sides = attribute.split(':'); 118 definition.attributes[sides[0]] = sides[1 .. $].join(':'); 119 } 120 definitions ~= definition; 121 } 122 cb(null, definitions.toJSON); 123 } 124 catch (Throwable e) 125 { 126 cb(e, JSONValue(null)); 127 } 128 }).start(); 129 } 130 131 /// Asynchronously finds all definitions of a symbol in the import paths. 132 /// Returns: `[{name: string, line: int, column: int}]` 133 /// Call_With: `{"subcmd": "find-symbol"}` 134 @arguments("subcmd", "find-symbol") 135 @async void findSymbol(AsyncCallback cb, string symbol) 136 { 137 new Thread({ 138 try 139 { 140 ProcessPipes pipes = raw([execPath, "-d", symbol] ~ importPathProvider()); 141 scope (exit) 142 pipes.pid.wait(); 143 string[] res; 144 while (pipes.stdout.isOpen && !pipes.stdout.eof) 145 res ~= pipes.stdout.readln(); 146 FileLocation[] files; 147 foreach (line; res) 148 { 149 auto match = line.chomp.matchFirst(dscannerFileRegex); 150 if (!match) 151 continue; 152 FileLocation file; 153 file.file = match[1]; 154 file.line = match[2].to!int; 155 file.column = match[3].to!int; 156 files ~= file; 157 } 158 cb(null, files.toJSON); 159 } 160 catch (Throwable e) 161 { 162 cb(e, JSONValue(null)); 163 } 164 }).start(); 165 } 166 167 private: 168 169 __gshared 170 { 171 string cwd, execPath; 172 } 173 174 auto raw(string[] args, Redirect redirect = Redirect.all) 175 { 176 auto pipes = pipeProcess(args, redirect, null, Config.none, cwd); 177 return pipes; 178 } 179 180 auto dscannerIssueRegex = ctRegex!`^(.+?)\((\d+)\:(\d+)\)\[(.*?)\]: (.*)`; 181 auto dscannerFileRegex = ctRegex!`^(.*?)\((\d+):(\d+)\)`; 182 struct DScannerIssue 183 { 184 string file; 185 int line, column; 186 string type; 187 string description; 188 } 189 190 struct FileLocation 191 { 192 string file; 193 int line, column; 194 } 195 196 struct OutlineTreeNode 197 { 198 string definition; 199 int line; 200 OutlineTreeNode[] children; 201 } 202 203 struct DefinitionElement 204 { 205 string name; 206 int line; 207 string type; 208 string[string] attributes; 209 }