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