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