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 }