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 }