1 module served.linters.dscanner; 2 3 import std.algorithm; 4 import std.conv; 5 import std.file; 6 import std.json; 7 import std.path; 8 import std.string; 9 10 import served.extension; 11 import served.linters.diagnosticmanager; 12 import served.types; 13 14 import workspaced.api; 15 import workspaced.coms; 16 17 import workspaced.com.dscanner; 18 19 import dscanner.analysis.config : StaticAnalysisConfig, Check; 20 21 import dscanner.analysis.local_imports : LocalImportCheck; 22 23 static immutable string DScannerDiagnosticSource = "DScanner"; 24 static immutable string SyntaxHintDiagnosticSource = "serve-d"; 25 26 //dfmt off 27 static immutable StaticAnalysisConfig servedDefaultDscannerConfig = { 28 could_be_immutable_check: Check.disabled, 29 undocumented_declaration_check: Check.disabled 30 }; 31 //dfmt on 32 33 enum DiagnosticSlot = 0; 34 35 void lint(Document document) 36 { 37 if (document.getLanguageId != "d") 38 return; 39 40 auto instance = activeInstance = backend.getBestInstance!DscannerComponent( 41 document.uri.uriToFile); 42 if (!instance) 43 return; 44 45 auto fileConfig = config(document.uri); 46 if (!fileConfig.d.enableLinting || !fileConfig.d.enableStaticLinting) 47 return; 48 49 auto ignoredKeys = fileConfig.dscanner.ignoredKeys; 50 51 auto ini = getDscannerIniForDocument(document.uri, instance); 52 auto issues = instance.get!DscannerComponent.lint(document.uri.uriToFile, 53 ini, document.rawText, false, servedDefaultDscannerConfig, true).getYield; 54 Diagnostic[] result; 55 56 foreach (issue; issues) 57 { 58 if (ignoredKeys.canFind(issue.key)) 59 continue; 60 Diagnostic d; 61 scope text = document.lineAtScope(cast(uint) issue.line - 1).stripRight; 62 string keyNormalized = issue.key.startsWith("dscanner.") 63 ? issue.key["dscanner.".length .. $] : issue.key; 64 if (text.canFind("@suppress(all)", "@suppress:all", 65 "@suppress(" ~ issue.key ~ ")", "@suppress:" ~ issue.key, 66 "@suppress(" ~ keyNormalized ~ ")", "@suppress:" ~ keyNormalized) || text 67 .endsWith("stfu")) 68 continue; 69 70 if (!d.adjustRangeForType(document, issue)) 71 continue; 72 d.adjustSeverityForType(document, issue); 73 74 if (!d.source.isNull && d.source.get.length) 75 { 76 // handled by previous functions 77 result ~= d; 78 continue; 79 } 80 81 if (issue.key.startsWith("workspaced")) 82 d.source = SyntaxHintDiagnosticSource; 83 else 84 d.source = DScannerDiagnosticSource; 85 86 d.message = issue.description; 87 d.code = JSONValue(issue.key); 88 result ~= d; 89 } 90 91 createDiagnosticsFor!DiagnosticSlot(document.uri) = result; 92 updateDiagnostics(document.uri); 93 } 94 95 void clear() 96 { 97 diagnostics[DiagnosticSlot] = null; 98 updateDiagnostics(); 99 } 100 101 string getDscannerIniForDocument(DocumentUri document, WorkspaceD.Instance instance = null) 102 { 103 if (!instance) 104 instance = backend.getBestInstance!DscannerComponent(document.uriToFile); 105 106 if (!instance) 107 return "dscanner.ini"; 108 109 auto ini = buildPath(instance.cwd, "dscanner.ini"); 110 if (!exists(ini)) 111 ini = "dscanner.ini"; 112 return ini; 113 } 114 115 /// Sets the range for the diagnostic from the issue 116 /// Returns: `false` if this issue should be discarded (handled by other issues) 117 bool adjustRangeForType(ref Diagnostic d, Document document, DScannerIssue issue) 118 { 119 d.range = TextRange( 120 document.lineColumnBytesToPosition(issue.range[0].line - 1, issue.range[0].column - 1), 121 document.lineColumnBytesToPosition(issue.range[1].line - 1, issue.range[1].column - 1) 122 ); 123 124 auto s = issue.description; 125 if (s.startsWith("Line is longer than ") && s.endsWith(" characters")) 126 { 127 d.range.start.character = s["Line is longer than ".length .. $ - " characters".length].to!uint; 128 d.range.end.character = 1000; 129 } 130 131 return true; 132 } 133 134 void adjustSeverityForType(ref Diagnostic d, Document, DScannerIssue issue) 135 { 136 if (issue.key == "dscanner.suspicious.unused_parameter" 137 || issue.key == "dscanner.suspicious.unused_variable") 138 { 139 d.severity = DiagnosticSeverity.hint; 140 d.tags = opt([DiagnosticTag.unnecessary]); 141 } 142 else 143 { 144 d.severity = issue.type == "error" ? DiagnosticSeverity.error : DiagnosticSeverity 145 .warning; 146 } 147 } 148 149 version (unittest) 150 { 151 import dscanner.analysis.config : defaultStaticAnalysisConfig; 152 import inifiled : writeINIFile; 153 import std.array : array; 154 import std.file : tempDir, write; 155 import std.path : buildPath; 156 import std.range : enumerate; 157 158 private class DiagnosticTester 159 { 160 WorkspaceD backend; 161 DscannerComponent dscanner; 162 string dscannerIni; 163 164 DScannerIssue[] issues; 165 Diagnostic[] diagnostics; 166 167 this(string id) 168 { 169 backend = new WorkspaceD(); 170 // use instance-less 171 dscanner = new DscannerComponent(); 172 dscanner.workspaced = backend; 173 174 auto config = defaultStaticAnalysisConfig; 175 foreach (ref value; config.tupleof) 176 static if (is(typeof(value) == string)) 177 value = "enabled"; 178 179 dscannerIni = buildPath(tempDir(), id ~ "-dscanner.ini"); 180 writeINIFile(config, dscannerIni); 181 } 182 183 ~this() 184 { 185 shutdown(true); 186 } 187 188 void shutdown(bool dtor) 189 { 190 if (dscanner) 191 dscanner.shutdown(dtor); 192 dscanner = null; 193 if (backend) 194 backend.shutdown(dtor); 195 backend = null; 196 } 197 198 DScannerIssue[] lint(scope const(char)[] code) 199 { 200 return dscanner.lint("", dscannerIni, code, false, 201 servedDefaultDscannerConfig, true).getBlocking(); 202 } 203 204 auto diagnosticsAt(Position location) 205 { 206 return diagnostics.enumerate.filter!(a 207 => a.value.range.contains(location)); 208 } 209 210 Diagnostic[] diagnosticsAt(Position location, string key) 211 { 212 return diagnostics 213 .filter!(a 214 => a.range.contains(location) 215 && a.code.get.type == JSONType..string 216 && a.code.get.str == key) 217 .array; 218 } 219 220 Diagnostic[] syntaxErrorsAt(Position location) 221 { 222 return diagnosticsAt(location).filter!(a => !issues[a.index].key.length) 223 .map!"a.value" 224 .array; 225 } 226 227 void build(Document document) 228 { 229 issues = lint(document.rawText); 230 231 diagnostics = null; 232 foreach (issue; issues) 233 { 234 Diagnostic d; 235 if (!d.adjustRangeForType(document, issue)) 236 continue; 237 d.adjustSeverityForType(document, issue); 238 239 if (!d.source.isNull && d.source.get.length) 240 { 241 // handled by previous functions 242 diagnostics ~= d; 243 continue; 244 } 245 246 d.code = JSONValue(issue.key).opt; 247 d.message = issue.description; 248 diagnostics ~= d; 249 } 250 } 251 } 252 } 253 254 unittest 255 { 256 DiagnosticTester test = new DiagnosticTester("test-syntax-errors"); 257 scope (exit) test.shutdown(false); 258 259 Document document = Document.nullDocument(q{ 260 void main() 261 { 262 if x == 4 { 263 } 264 } 265 }); 266 267 test.build(document); 268 assert(test.syntaxErrorsAt(Position(0, 0)).length == 0); 269 assert(test.syntaxErrorsAt(Position(3, 4)).length == 1); 270 assert(test.syntaxErrorsAt(Position(3, 4))[0].message == "Expected `(` instead of `x`"); 271 assert(test.syntaxErrorsAt(Position(3, 4))[0].range == TextRange(3, 1, 3, 5)); 272 273 document = Document.nullDocument(q{ 274 void main() 275 { 276 foo() 277 } 278 }); 279 280 test.build(document); 281 assert(test.syntaxErrorsAt(Position(0, 0)).length == 0); 282 assert(test.syntaxErrorsAt(Position(3, 3)).length == 0); 283 assert(test.syntaxErrorsAt(Position(3, 4)).length == 1); 284 assert(test.syntaxErrorsAt(Position(3, 4))[0].message == "Expected `;` instead of `}`"); 285 assert(test.syntaxErrorsAt(Position(3, 4))[0].range == TextRange(3, 4, 3, 6)); 286 287 document = Document.nullDocument(q{ 288 void main() 289 { 290 foo(hello) {} 291 } 292 }); 293 294 test.build(document); 295 assert(test.syntaxErrorsAt(Position(3, 3)).length == 0); 296 assert(test.syntaxErrorsAt(Position(3, 3)).length == 0); 297 assert(test.syntaxErrorsAt(Position(3, 4)).length == 0); 298 assert(test.syntaxErrorsAt(Position(3, 9)).length == 0); 299 assert(test.syntaxErrorsAt(Position(3, 10)).length == 1); 300 assert(test.syntaxErrorsAt(Position(3, 10))[0].message == "Expected `;` instead of `{`"); 301 assert(test.syntaxErrorsAt(Position(3, 10))[0].range == TextRange(3, 10, 3, 15)); 302 303 document = Document.nullDocument(q{ 304 void main() 305 { 306 foo..foreach(a; b); 307 } 308 }); 309 310 test.build(document); 311 assert(test.syntaxErrorsAt(Position(0, 0)).length == 0); 312 assert(test.syntaxErrorsAt(Position(3, 4)).length == 0); 313 assert(test.syntaxErrorsAt(Position(3, 5)).length == 1); 314 assert(test.syntaxErrorsAt(Position(3, 5))[0].message == "Expected identifier instead of reserved keyword `foreach`"); 315 assert(test.syntaxErrorsAt(Position(3, 5))[0].range == TextRange(3, 5, 3, 12)); 316 317 document = Document.nullDocument(q{ 318 void main() 319 { 320 foo. 321 foreach(a; b); 322 } 323 }); 324 325 test.build(document); 326 // import std.stdio; stderr.writeln("diagnostics:\n", test.diagnostics); 327 assert(test.syntaxErrorsAt(Position(0, 0)).length == 0); 328 assert(test.syntaxErrorsAt(Position(3, 4)).length == 0); 329 assert(test.syntaxErrorsAt(Position(3, 5)).length == 1); 330 assert(test.syntaxErrorsAt(Position(3, 5))[0].message == "Expected identifier"); 331 assert(test.syntaxErrorsAt(Position(3, 5))[0].range == TextRange(3, 5, 4, 1)); 332 } 333 334 unittest 335 { 336 DiagnosticTester test = new DiagnosticTester("test-syntax-issues"); 337 scope (exit) test.shutdown(false); 338 339 Document document = Document.nullDocument(q{ 340 void main() 341 { 342 foreach (auto key; value) 343 { 344 } 345 } 346 }); 347 348 test.build(document); 349 assert(test.diagnosticsAt(Position(0, 0), "workspaced.foreach-auto").length == 0); 350 auto diag = test.diagnosticsAt(Position(3, 11), "workspaced.foreach-auto"); 351 assert(diag.length == 1); 352 assert(diag[0].range == TextRange(3, 10, 3, 14)); 353 } 354 355 unittest 356 { 357 DiagnosticTester test = new DiagnosticTester("test-syntax-issues"); 358 scope (exit) test.shutdown(false); 359 360 Document document = Document.nullDocument(q{ 361 void main() 362 { 363 foreach (/* cool */ auto key; value) 364 { 365 } 366 } 367 }); 368 369 test.build(document); 370 assert(test.diagnosticsAt(Position(0, 0), "workspaced.foreach-auto").length == 0); 371 auto diag = test.diagnosticsAt(Position(3, 22), "workspaced.foreach-auto"); 372 assert(diag.length == 1); 373 assert(diag[0].range == TextRange(3, 21, 3, 25)); 374 } 375 376 unittest 377 { 378 DiagnosticTester test = new DiagnosticTester("test-suspicious-local-imports"); 379 scope (exit) test.shutdown(false); 380 381 Document document = Document.nullDocument(q{ 382 void main() 383 { 384 import imports.stdio; 385 386 writeln("hello"); 387 } 388 }); 389 390 test.build(document); 391 assert(test.diagnosticsAt(Position(0, 0), LocalImportCheckKEY).length == 0); 392 auto diag = test.diagnosticsAt(Position(3, 11), LocalImportCheckKEY); 393 assert(diag.length == 1); 394 assert(diag[0].range == TextRange(3, 1, 3, 24)); 395 }