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 }