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 }