1 module served.linters.dub;
2 
3 import core.thread;
4 
5 import painlessjson;
6 
7 import served.extension;
8 import served.linters.diagnosticmanager;
9 import served.types;
10 
11 import std.algorithm;
12 import std.array;
13 import std.datetime.stopwatch : StopWatch;
14 import std.experimental.logger;
15 import std.file;
16 import std.json;
17 import std.path;
18 import std.process;
19 import std.range;
20 import std.stdio;
21 import std.string;
22 
23 import workspaced.api;
24 import workspaced.coms;
25 
26 import workspaced.com.dub : BuildIssue, ErrorType;
27 
28 enum DiagnosticSlot = 1;
29 
30 enum DubDiagnosticSource = "DUB";
31 
32 string fixPath(string cwd, string workspaceRoot, string path, string[] stringImportPaths)
33 {
34 	auto mixinIndex = path.indexOf("-mixin-");
35 	if (mixinIndex != -1)
36 		path = path[0 .. mixinIndex];
37 
38 	// the dub API uses getcwd by default for the build folder
39 	auto absPath = isAbsolute(path) ? path : buildNormalizedPath(cwd, path);
40 
41 	// but just in case it changes, let's add a fallback to the workspaceRoot (which is the dub path)...
42 	if (!exists(absPath))
43 		absPath = buildNormalizedPath(workspaceRoot, path);
44 
45 	// .d files are always emitted by the compiler, just use them
46 	if (path.endsWith(".d"))
47 		path = absPath;
48 	else if (!isAbsolute(path))
49 	{
50 		// this is the fallback code for .dt files or other custom error locations thrown by libraries
51 		// with pragma messages
52 		bool found;
53 		foreach (imp; chain([cwd, workspaceRoot], stringImportPaths))
54 		{
55 			if (!isAbsolute(imp))
56 				imp = buildNormalizedPath(workspaceRoot, imp);
57 			auto modPath = buildNormalizedPath(imp, path);
58 			if (exists(modPath))
59 			{
60 				path = modPath;
61 				found = true;
62 				break;
63 			}
64 		}
65 		if (!found)
66 			path = absPath;
67 	}
68 	else
69 		path = absPath;
70 	return path;
71 }
72 
73 DiagnosticSeverity mapDubLintType(ErrorType type)
74 {
75 	final switch (type)
76 	{
77 	case ErrorType.Deprecation:
78 		return DiagnosticSeverity.information;
79 	case ErrorType.Warning:
80 		return DiagnosticSeverity.warning;
81 	case ErrorType.Error:
82 		return DiagnosticSeverity.error;
83 	}
84 }
85 
86 void applyDubLintType(ref Diagnostic error, ErrorType type)
87 {
88 	error.severity = mapDubLintType(type);
89 	if (type == ErrorType.Deprecation)
90 		error.tags = opt([DiagnosticTag.deprecated_]);
91 }
92 
93 bool dubLintRunning, retryDubAtEnd;
94 Duration lastDubDuration;
95 int currentBuildToken;
96 
97 void lint(Document document)
98 {
99 	auto instance = activeInstance = backend.getBestInstance!DubComponent(document.uri.uriToFile);
100 	if (!instance)
101 		return;
102 
103 	auto fileConfig = config(document.uri);
104 	if (!fileConfig.d.enableLinting || !fileConfig.d.enableDubLinting)
105 		return;
106 
107 	if (dubLintRunning)
108 	{
109 		retryDubAtEnd = true;
110 		return;
111 	}
112 
113 	dubLintRunning = true;
114 	scope (exit)
115 		dubLintRunning = false;
116 
117 	while (true)
118 	{
119 		stderr.writeln("Running dub build");
120 		currentBuildToken++;
121 		int startToken = currentBuildToken;
122 		setTimeout({
123 			// dub build taking much longer now, we probably succeeded compiling where we failed last time so erase diagnostics
124 			if (dubLintRunning && startToken == currentBuildToken)
125 			{
126 				diagnostics[DiagnosticSlot] = null;
127 				updateDiagnostics();
128 			}
129 		}, lastDubDuration + 100.msecs);
130 		StopWatch sw;
131 		sw.start();
132 		auto imports = instance.get!DubComponent.stringImports;
133 		auto issues = instance.get!DubComponent.build.getYield;
134 		sw.stop();
135 		lastDubDuration = sw.peek;
136 		trace("dub build finished in ", sw.peek, " with ", issues.length, " issues");
137 		trace(issues);
138 		auto result = appender!(PublishDiagnosticsParams[]);
139 
140 		void pushError(Diagnostic error, string uri)
141 		{
142 			bool found;
143 			foreach (ref elem; result.data)
144 				if (elem.uri == uri)
145 				{
146 					found = true;
147 					elem.diagnostics ~= error;
148 				}
149 			if (!found)
150 				result ~= PublishDiagnosticsParams(uri, [error]);
151 		}
152 
153 		while (!issues.empty)
154 		{
155 			auto issue = issues.front;
156 			issues.popFront();
157 			int numSupplemental = cast(int) issues.length;
158 			foreach (i, other; issues)
159 				if (!other.cont)
160 				{
161 					numSupplemental = cast(int) i;
162 					break;
163 				}
164 			auto supplemental = issues[0 .. numSupplemental];
165 			if (numSupplemental > 0)
166 				issues = issues[numSupplemental .. $];
167 
168 			auto uri = uriFromFile(fixPath(getcwd(),
169 					instance.get!DubComponent.path.toString, issue.file, imports));
170 
171 			Diagnostic error;
172 			error.range = TextRange(issue.line - 1, issue.column - 1, issue.line - 1, issue.column);
173 			applyDubLintType(error, issue.type);
174 			error.source = DubDiagnosticSource;
175 			error.message = issue.text;
176 			if (supplemental.length)
177 				error.relatedInformation = opt(supplemental.map!((other) {
178 						DiagnosticRelatedInformation related;
179 						string otherUri = other.file != issue.file ? uriFromFile(fixPath(getcwd(),
180 						instance.get!DubComponent.path.toString, other.file, imports)) : uri;
181 						related.location = Location(otherUri, TextRange(other.line - 1,
182 						other.column - 1, other.line - 1, other.column));
183 						extendErrorRange(related.location.range, instance, otherUri);
184 						related.message = other.text;
185 						return related;
186 					}).array);
187 
188 			extendErrorRange(error.range, instance, uri, error);
189 			pushError(error, uri);
190 
191 			foreach (i, suppl; supplemental)
192 			{
193 				if (suppl.text.startsWith("instantiated from here:"))
194 				{
195 					// add all "instantiated from here" errors in the project as diagnostics
196 
197 					auto supplUri = issue.file != suppl.file ? uriFromFile(fixPath(getcwd(),
198 							instance.get!DubComponent.path.toString, suppl.file, imports)) : uri;
199 
200 					if (workspaceIndex(supplUri) == size_t.max)
201 						continue;
202 
203 					Diagnostic supplError;
204 					supplError.range = TextRange(Position(suppl.line - 1, suppl.column - 1));
205 					applyDubLintType(supplError, issue.type);
206 					supplError.source = DubDiagnosticSource;
207 					supplError.message = issue.text ~ "\n" ~ suppl.text;
208 					if (i + 1 < supplemental.length)
209 						supplError.relatedInformation = opt(error.relatedInformation.get[i + 1 .. $]);
210 					extendErrorRange(supplError.range, instance, supplUri, supplError);
211 					pushError(supplError, supplUri);
212 				}
213 			}
214 		}
215 
216 		diagnostics[DiagnosticSlot] = result.data;
217 		updateDiagnostics();
218 
219 		if (!retryDubAtEnd)
220 			break;
221 		else
222 			retryDubAtEnd = false;
223 	}
224 }
225 
226 void extendErrorRange(ref TextRange range, WorkspaceD.Instance instance,
227 	string uri, Diagnostic info = Diagnostic.init)
228 {
229 	auto doc = documents.tryGet(uri);
230 	if (!doc.rawText.length)
231 		return;
232 
233 	if (info.message.length)
234 	{
235 		auto loc = doc.positionToBytes(range.start);
236 		auto result = instance.get!DubComponent.resolveDiagnosticRange(
237 				doc.rawText, cast(int)loc, info.message);
238 		if (result[0] != result[1])
239 		{
240 			auto start = doc.movePositionBytes(range.start, loc, result[0]);
241 			auto end = doc.movePositionBytes(start, result[0], result[1]);
242 			range = TextRange(start, end);
243 			return;
244 		}
245 	}
246 
247 	range = doc.wordRangeAt(range.start);
248 }
249 
250 void clear()
251 {
252 	diagnostics[DiagnosticSlot] = null;
253 	updateDiagnostics();
254 }