1 module served.linters.dscanner; 2 3 import std.algorithm; 4 import std.conv; 5 import std.file; 6 import std.path; 7 import std.string; 8 9 import served.extension; 10 import served.linters.diagnosticmanager; 11 import served.types; 12 13 import workspaced.api; 14 import workspaced.coms; 15 16 import workspaced.com.dscanner; 17 18 import dscanner.analysis.config : StaticAnalysisConfig, Check; 19 20 import dscanner.analysis.local_imports : LocalImportCheck; 21 22 static immutable string DScannerDiagnosticSource = "DScanner"; 23 static immutable string SyntaxHintDiagnosticSource = "serve-d"; 24 25 //dfmt off 26 static immutable StaticAnalysisConfig servedDefaultDscannerConfig = { 27 could_be_immutable_check: Check.disabled, 28 undocumented_declaration_check: Check.disabled 29 }; 30 //dfmt on 31 32 enum DiagnosticSlot = 0; 33 34 void lint(Document document) 35 { 36 if (document.getLanguageId != "d") 37 return; 38 39 auto instance = activeInstance = backend.getBestInstance!DscannerComponent( 40 document.uri.uriToFile); 41 if (!instance) 42 return; 43 44 auto fileConfig = config(document.uri); 45 if (!fileConfig.d.enableLinting || !fileConfig.d.enableStaticLinting) 46 return; 47 48 auto ignoredKeys = fileConfig.dscanner.ignoredKeys; 49 50 auto ini = getDscannerIniForDocument(document.uri, instance); 51 auto issues = instance.get!DscannerComponent.lint(document.uri.uriToFile, 52 ini, document.rawText, false, servedDefaultDscannerConfig, true).getYield; 53 Diagnostic[] result; 54 55 foreach (issue; issues) 56 { 57 if (ignoredKeys.canFind(issue.key)) 58 continue; 59 Diagnostic d; 60 scope text = document.lineAtScope(cast(uint) issue.line - 1).stripRight; 61 string keyNormalized = issue.key.startsWith("dscanner.") 62 ? issue.key["dscanner.".length .. $] : issue.key; 63 if (text.canFind("@suppress(all)", "@suppress:all", 64 "@suppress(" ~ issue.key ~ ")", "@suppress:" ~ issue.key, 65 "@suppress(" ~ keyNormalized ~ ")", "@suppress:" ~ keyNormalized) || text 66 .endsWith("stfu")) 67 continue; 68 69 if (!d.adjustRangeForType(document, issue)) 70 continue; 71 d.adjustSeverityForType(document, issue); 72 73 if (d.source.orDefault.length) 74 { 75 // handled by previous functions 76 result ~= d; 77 continue; 78 } 79 80 if (issue.key.startsWith("workspaced")) 81 d.source = SyntaxHintDiagnosticSource; 82 else 83 d.source = DScannerDiagnosticSource; 84 85 d.message = issue.description; 86 d.code = JsonValue(issue.key); 87 result ~= d; 88 } 89 90 createDiagnosticsFor!DiagnosticSlot(document.uri) = result; 91 updateDiagnostics(document.uri); 92 } 93 94 void clear() 95 { 96 diagnostics[DiagnosticSlot] = null; 97 updateDiagnostics(); 98 } 99 100 string getDscannerIniForDocument(DocumentUri document, WorkspaceD.Instance instance = null) 101 { 102 if (!instance) 103 instance = backend.getBestInstance!DscannerComponent(document.uriToFile); 104 105 if (!instance) 106 return "dscanner.ini"; 107 108 auto ini = buildPath(instance.cwd, "dscanner.ini"); 109 if (!exists(ini)) 110 ini = "dscanner.ini"; 111 return ini; 112 } 113 114 /// Sets the range for the diagnostic from the issue 115 /// Returns: `false` if this issue should be discarded (handled by other issues) 116 bool adjustRangeForType(ref Diagnostic d, Document document, DScannerIssue issue) 117 { 118 d.range = TextRange( 119 document.lineColumnBytesToPosition(issue.range[0].line - 1, issue.range[0].column - 1), 120 document.lineColumnBytesToPosition(issue.range[1].line - 1, issue.range[1].column - 1) 121 ); 122 123 auto s = issue.description; 124 if (s.startsWith("Line is longer than ") && s.endsWith(" characters")) 125 { 126 d.range.start.character = s["Line is longer than ".length .. $ - " characters".length].to!uint; 127 d.range.end.line = d.range.start.line + 1; 128 d.range.end.character = 0; 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.deref.match!( 216 (string s) => s == key, 217 _ => false 218 )) 219 .array; 220 } 221 222 Diagnostic[] syntaxErrorsAt(Position location) 223 { 224 return diagnosticsAt(location).filter!(a => !issues[a.index].key.length) 225 .map!"a.value" 226 .array; 227 } 228 229 void build(Document document) 230 { 231 issues = lint(document.rawText); 232 233 diagnostics = null; 234 foreach (issue; issues) 235 { 236 Diagnostic d; 237 if (!d.adjustRangeForType(document, issue)) 238 continue; 239 d.adjustSeverityForType(document, issue); 240 241 if (d.source.orDefault.length) 242 { 243 // handled by previous functions 244 diagnostics ~= d; 245 continue; 246 } 247 248 d.code = JsonValue(issue.key); 249 d.message = issue.description; 250 diagnostics ~= d; 251 } 252 } 253 } 254 } 255 256 unittest 257 { 258 DiagnosticTester test = new DiagnosticTester("test-syntax-errors"); 259 scope (exit) test.shutdown(false); 260 261 Document document = Document.nullDocument(q{ 262 void main() 263 { 264 if x == 4 { 265 } 266 } 267 }); 268 269 test.build(document); 270 assert(test.syntaxErrorsAt(Position(0, 0)).length == 0); 271 assert(test.syntaxErrorsAt(Position(3, 4)).length == 1); 272 assert(test.syntaxErrorsAt(Position(3, 4))[0].message == "Expected `(` instead of `x`"); 273 assert(test.syntaxErrorsAt(Position(3, 4))[0].range == TextRange(3, 1, 3, 5)); 274 275 document = Document.nullDocument(q{ 276 void main() 277 { 278 foo() 279 } 280 }); 281 282 test.build(document); 283 assert(test.syntaxErrorsAt(Position(0, 0)).length == 0); 284 assert(test.syntaxErrorsAt(Position(3, 3)).length == 0); 285 assert(test.syntaxErrorsAt(Position(3, 4)).length == 1); 286 assert(test.syntaxErrorsAt(Position(3, 4))[0].message == "Expected `;` instead of `}`"); 287 assert(test.syntaxErrorsAt(Position(3, 4))[0].range == TextRange(3, 4, 3, 6)); 288 289 document = Document.nullDocument(q{ 290 void main() 291 { 292 foo(hello) {} 293 } 294 }); 295 296 test.build(document); 297 assert(test.syntaxErrorsAt(Position(3, 3)).length == 0); 298 assert(test.syntaxErrorsAt(Position(3, 3)).length == 0); 299 assert(test.syntaxErrorsAt(Position(3, 4)).length == 0); 300 assert(test.syntaxErrorsAt(Position(3, 9)).length == 0); 301 assert(test.syntaxErrorsAt(Position(3, 10)).length == 1); 302 assert(test.syntaxErrorsAt(Position(3, 10))[0].message == "Expected `;` instead of `{`"); 303 assert(test.syntaxErrorsAt(Position(3, 10))[0].range == TextRange(3, 10, 3, 15)); 304 305 document = Document.nullDocument(q{ 306 void main() 307 { 308 foo..foreach(a; b); 309 } 310 }); 311 312 test.build(document); 313 assert(test.syntaxErrorsAt(Position(0, 0)).length == 0); 314 assert(test.syntaxErrorsAt(Position(3, 4)).length == 0); 315 assert(test.syntaxErrorsAt(Position(3, 5)).length == 1); 316 assert(test.syntaxErrorsAt(Position(3, 5))[0].message == "Expected identifier instead of reserved keyword `foreach`"); 317 assert(test.syntaxErrorsAt(Position(3, 5))[0].range == TextRange(3, 5, 3, 12)); 318 319 document = Document.nullDocument(q{ 320 void main() 321 { 322 foo. 323 foreach(a; b); 324 } 325 }); 326 327 test.build(document); 328 // import std.stdio; stderr.writeln("diagnostics:\n", test.diagnostics); 329 assert(test.syntaxErrorsAt(Position(0, 0)).length == 0); 330 assert(test.syntaxErrorsAt(Position(3, 4)).length == 0); 331 assert(test.syntaxErrorsAt(Position(3, 5)).length == 1); 332 assert(test.syntaxErrorsAt(Position(3, 5))[0].message == "Expected identifier"); 333 assert(test.syntaxErrorsAt(Position(3, 5))[0].range == TextRange(3, 5, 4, 1)); 334 } 335 336 unittest 337 { 338 DiagnosticTester test = new DiagnosticTester("test-syntax-issues"); 339 scope (exit) test.shutdown(false); 340 341 Document document = Document.nullDocument(q{ 342 void main() 343 { 344 foreach (auto key; value) 345 { 346 } 347 } 348 }); 349 350 test.build(document); 351 assert(test.diagnosticsAt(Position(0, 0), "workspaced.foreach-auto").length == 0); 352 auto diag = test.diagnosticsAt(Position(3, 11), "workspaced.foreach-auto"); 353 assert(diag.length == 1); 354 assert(diag[0].range == TextRange(3, 10, 3, 14)); 355 } 356 357 unittest 358 { 359 DiagnosticTester test = new DiagnosticTester("test-syntax-issues"); 360 scope (exit) test.shutdown(false); 361 362 Document document = Document.nullDocument(q{ 363 void main() 364 { 365 foreach (/* cool */ auto key; value) 366 { 367 } 368 } 369 }); 370 371 test.build(document); 372 assert(test.diagnosticsAt(Position(0, 0), "workspaced.foreach-auto").length == 0); 373 auto diag = test.diagnosticsAt(Position(3, 22), "workspaced.foreach-auto"); 374 assert(diag.length == 1); 375 assert(diag[0].range == TextRange(3, 21, 3, 25)); 376 } 377 378 unittest 379 { 380 DiagnosticTester test = new DiagnosticTester("test-suspicious-local-imports"); 381 scope (exit) test.shutdown(false); 382 383 Document document = Document.nullDocument(q{ 384 void main() 385 { 386 import imports.stdio; 387 388 writeln("hello"); 389 } 390 }); 391 392 test.build(document); 393 assert(test.diagnosticsAt(Position(0, 0), LocalImportCheckKEY).length == 0); 394 auto diag = test.diagnosticsAt(Position(3, 11), LocalImportCheckKEY); 395 assert(diag.length == 1); 396 assert(diag[0].range == TextRange(3, 1, 3, 24)); 397 }