1 /* 2 *Copyright (C) 2018 Laurent Tréguier 3 * 4 *This file is part of DLS. 5 * 6 *DLS is free software: you can redistribute it and/or modify 7 *it under the terms of the GNU General Public License as published by 8 *the Free Software Foundation, either version 3 of the License, or 9 *(at your option) any later version. 10 * 11 *DLS is distributed in the hope that it will be useful, 12 *but WITHOUT ANY WARRANTY; without even the implied warranty of 13 *MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 *GNU General Public License for more details. 15 * 16 *You should have received a copy of the GNU General Public License 17 *along with DLS. If not, see <http://www.gnu.org/licenses/>. 18 * 19 */ 20 21 module dls.tools.analysis_tool; 22 23 import dls.tools.tool : Tool; 24 25 private immutable diagnosticSource = "D-Scanner"; 26 27 //dfmt off 28 private enum DScannerWarnings : string 29 { 30 bugs_backwardsSlices = "dscanner.bugs.backwards_slices", 31 bugs_ifElseSame = "dscanner.bugs.if_else_same", 32 bugs_logicOperatorOperands = "dscanner.bugs.logic_operator_operands", 33 bugs_selfAssignment = "dscanner.bugs.self_assignment", 34 confusing_argumentParameter_Mismatch = "dscanner.confusing.argument_parameter_mismatch", 35 confusing_brexp = "dscanner.confusing.brexp", 36 confusing_builtinPropertyNames = "dscanner.confusing.builtin_property_names", 37 confusing_constructor_args = "dscanner.confusing.constructor_args", 38 confusing_functionAttributes = "dscanner.confusing.function_attributes", 39 confusing_lambdaReturnsLambda = "dscanner.confusing.lambda_returns_lambda", 40 confusing_logicalPrecedence = "dscanner.confusing.logical_precedence", 41 confusing_structConstructorDefaultArgs = "dscanner.confusing.struct_constructor_default_args", 42 deprecated_deleteKeyword = "dscanner.deprecated.delete_keyword", 43 deprecated_floatingPointOperators = "dscanner.deprecated.floating_point_operators", 44 ifStatement = "dscanner.if_statement", 45 performance_enumArrayLiteral = "dscanner.performance.enum_array_literal", 46 style_aliasSyntax = "dscanner.style.alias_syntax", 47 style_allman = "dscanner.style.allman", 48 style_assertWithoutMsg = "dscanner.style.assert_without_msg", 49 style_docMissingParams = "dscanner.style.doc_missing_params", 50 style_docMissingReturns = "dscanner.style.doc_missing_returns", 51 style_docMissingThrow = "dscanner.style.doc_missing_throw", 52 style_docNonExistingParams = "dscanner.style.doc_non_existing_params", 53 style_explicitlyAnnotatedUnittest = "dscanner.style.explicitly_annotated_unittest", 54 style_hasPublicExample = "dscanner.style.has_public_example", 55 style_ifConstraintsIndent = "dscanner.style.if_constraints_indent", 56 style_importsSortedness = "dscanner.style.imports_sortedness", 57 style_longLine = "dscanner.style.long_line", 58 style_numberLiterals = "dscanner.style.number_literals", 59 style_phobosNamingConvention = "dscanner.style.phobos_naming_convention", 60 style_undocumentedDeclaration = "dscanner.style.undocumented_declaration", 61 suspicious_autoRefAssignment = "dscanner.suspicious.auto_ref_assignment", 62 suspicious_catchEmAll = "dscanner.suspicious.catch_em_all", 63 suspicious_commaExpression = "dscanner.suspicious.comma_expression", 64 suspicious_incompleteOperatorOverloading = "dscanner.suspicious.incomplete_operator_overloading", 65 suspicious_incorrectInfiniteRange = "dscanner.suspicious.incorrect_infinite_range", 66 suspicious_labelVarSameName = "dscanner.suspicious.label_var_same_name", 67 suspicious_lengthSubtraction = "dscanner.suspicious.length_subtraction", 68 suspicious_localImports = "dscanner.suspicious.local_imports", 69 suspicious_missingReturn = "dscanner.suspicious.missing_return", 70 suspicious_objectConst = "dscanner.suspicious.object_const", 71 suspicious_redundantAttributes = "dscanner.suspicious.redundant_attributes", 72 suspicious_redundantParens = "dscanner.suspicious.redundant_parens", 73 suspicious_staticIfElse = "dscanner.suspicious.static_if_else", 74 suspicious_unmodified = "dscanner.suspicious.unmodified", 75 suspicious_unusedLabel = "dscanner.suspicious.unused_label", 76 suspicious_unusedParameter = "dscanner.suspicious.unused_parameter", 77 suspicious_unusedVariable = "dscanner.suspicious.unused_variable", 78 suspicious_uselessAssert = "dscanner.suspicious.useless_assert", 79 suspicious_uselessInitializer = "dscanner.suspicious.useless-initializer", 80 trustTooMuch = "dscanner.trust_too_much", 81 unnecessary_duplicateAttribute = "dscanner.unnecessary.duplicate_attribute", 82 useless_final = "dscanner.useless.final", 83 vcallCtor = "dscanner.vcall_ctor" 84 } 85 //dfmt on 86 87 class AnalysisTool : Tool 88 { 89 import dls.protocol.definitions : Command, Diagnostic, Range, TextEdit, WorkspaceEdit; 90 import dls.protocol.interfaces : CodeAction, CodeActionKind; 91 import dls.util.uri : Uri; 92 import dscanner.analysis.config : StaticAnalysisConfig; 93 94 private static AnalysisTool _instance; 95 96 static void initialize(AnalysisTool tool) 97 { 98 _instance = tool; 99 _instance.addConfigHook("configFile", (const Uri uri) { 100 import std.path : baseName; 101 102 const currentConfigFile = _instance._analysisConfigPaths.get(uri.path, "dscanner.ini").baseName; 103 104 if (getConfig(uri).analysis.configFile != currentConfigFile) 105 { 106 _instance.updateAnalysisConfig(uri); 107 } 108 }); 109 _instance.addConfigHook("filePatterns", (const Uri uri) { 110 auto newPatterns = getConfig(uri).analysis.filePatterns; 111 112 if (newPatterns != _instance._currentPatterns.get(uri, [])) 113 { 114 _instance._currentPatterns[uri] = newPatterns; 115 _instance.scanAllWorkspaces(); 116 } 117 }); 118 } 119 120 static void shutdown() 121 { 122 destroy(_instance); 123 } 124 125 @property static AnalysisTool instance() 126 { 127 return _instance; 128 } 129 130 private string[string] _analysisConfigPaths; 131 private StaticAnalysisConfig[string] _analysisConfigs; 132 private string[][string] _currentPatterns; 133 134 auto getScannableFilesUris(out Uri[] discardedFiles) 135 { 136 import dls.tools.symbol_tool : SymbolTool; 137 import dls.util.uri : sameFile; 138 import std.algorithm : canFind, filter; 139 import std.file : SpanMode, dirEntries; 140 import std.path : buildPath, globMatch; 141 import std.range : chain; 142 143 Uri[] globMatches; 144 auto workspacesFilesUris = SymbolTool.instance.workspacesFilesUris; 145 146 foreach (wUri; workspacesUris) 147 { 148 auto filePatterns = getConfig(wUri).analysis.filePatterns; 149 _currentPatterns[wUri] = filePatterns; 150 151 foreach (entry; dirEntries(wUri.path, SpanMode.depth).filter!q{a.isFile}) 152 { 153 auto entryUri = Uri.fromPath(entry.name); 154 155 foreach (pattern; filePatterns) 156 { 157 if (globMatch(entry.name, buildPath(wUri.path, pattern))) 158 { 159 globMatches ~= entryUri; 160 break; 161 } 162 } 163 164 if (!(globMatches.length > 0 && globMatches[$ - 1] is entryUri) 165 && !workspacesFilesUris.canFind!sameFile(entryUri) 166 && globMatch(entry.name, "*.{d,di}")) 167 { 168 discardedFiles ~= entryUri; 169 } 170 } 171 } 172 173 return chain(workspacesFilesUris, globMatches); 174 } 175 176 void scanAllWorkspaces() 177 { 178 import dls.protocol.jsonrpc : send; 179 import dls.protocol.interfaces : PublishDiagnosticsParams; 180 import dls.protocol.messages.methods : TextDocument; 181 import std.algorithm : each; 182 183 Uri[] discardedFiles; 184 185 getScannableFilesUris(discardedFiles).each!((uri) { 186 import dls.util.disposable_fiber : DisposableFiber; 187 188 DisposableFiber.yield(); 189 send(TextDocument.publishDiagnostics, new PublishDiagnosticsParams(uri, 190 _instance.diagnostics(uri))); 191 }); 192 193 foreach (file; discardedFiles) 194 { 195 send(TextDocument.publishDiagnostics, new PublishDiagnosticsParams(file, [])); 196 } 197 } 198 199 void addAnalysisConfig(const Uri uri) 200 { 201 import dscanner.analysis.config : defaultStaticAnalysisConfig; 202 203 _analysisConfigs[uri.path] = defaultStaticAnalysisConfig(); 204 updateAnalysisConfig(uri); 205 } 206 207 void removeAnalysisConfig(const Uri workspaceUri) 208 { 209 _analysisConfigPaths.remove(workspaceUri.path); 210 _analysisConfigs.remove(workspaceUri.path); 211 } 212 213 void updateAnalysisConfig(const Uri workspaceUri) 214 { 215 import dls.protocol.logger : logger; 216 import dls.server : Server; 217 import dscanner.analysis.config : defaultStaticAnalysisConfig; 218 import inifiled : readINIFile; 219 import std.file : exists; 220 import std.path : buildNormalizedPath; 221 222 auto configPath = getAnalysisConfigUri(workspaceUri).path; 223 auto conf = defaultStaticAnalysisConfig(); 224 225 if (exists(configPath)) 226 { 227 logger.info("Updating config from file %s", configPath); 228 readINIFile(conf, configPath); 229 } 230 231 _analysisConfigPaths[workspaceUri.path] = configPath; 232 _analysisConfigs[workspaceUri.path] = conf; 233 234 if (Server.initialized) 235 { 236 scanAllWorkspaces(); 237 } 238 } 239 240 Diagnostic[] diagnostics(const Uri uri) 241 { 242 import dls.protocol.definitions : DiagnosticSeverity; 243 import dls.protocol.logger : logger; 244 import dls.tools.symbol_tool : SymbolTool; 245 import dls.util.document : Document, minusOne; 246 import dparse.lexer : LexerConfig, StringBehavior, StringCache, getTokensForParser; 247 import dparse.parser : parseModule; 248 import dparse.rollback_allocator : RollbackAllocator; 249 import dscanner.analysis.run : analyze; 250 import std.array : appender; 251 import std.json : JSONValue; 252 import std.regex : matchFirst, regex; 253 import std.typecons : Nullable, nullable; 254 import std.utf : toUTF16; 255 256 logger.info("Fetching diagnostics for %s", uri.path); 257 258 auto stringCache = StringCache(StringCache.defaultBucketCount); 259 auto tokens = getTokensForParser(Document.get(uri).toString(), 260 LexerConfig(uri.path, StringBehavior.source), &stringCache); 261 RollbackAllocator ra; 262 auto document = Document.get(uri); 263 auto diagnostics = appender!(Diagnostic[]); 264 265 immutable syntaxProblemhandler = (string path, size_t line, size_t column, 266 string msg, bool isError) { 267 auto severity = (isError ? DiagnosticSeverity.error : DiagnosticSeverity.warning); 268 diagnostics ~= new Diagnostic(document.wordRangeAtLineAndByte(minusOne(line), minusOne(column)), msg, 269 severity.nullable, Nullable!JSONValue(), diagnosticSource.nullable); 270 }; 271 272 const mod = parseModule(tokens, uri.path, &ra, syntaxProblemhandler); 273 const analysisResults = analyze(uri.path, mod, getAnalysisConfig(uri), 274 SymbolTool.instance.cache, tokens, true); 275 276 foreach (result; analysisResults) 277 { 278 if (!document.lines[minusOne(result.line)].matchFirst( 279 regex(`//.*@suppress\s*\(\s*`w ~ result.key.toUTF16() ~ `\s*\)`w))) 280 { 281 diagnostics ~= new Diagnostic(document.wordRangeAtLineAndByte(minusOne(result.line), 282 minusOne(result.column)), 283 result.message, DiagnosticSeverity.warning.nullable, 284 JSONValue(result.key).nullable, diagnosticSource.nullable); 285 } 286 } 287 288 return diagnostics.data; 289 } 290 291 Command[] codeAction(const Uri uri, const Range range, 292 Diagnostic[] diagnostics, bool commandCompat) 293 { 294 import dls.protocol.definitions : Position; 295 import dls.protocol.logger : logger; 296 import dls.tools.command_tool : Commands; 297 import dls.util.document : Document; 298 import dls.util.i18n : Tr, tr; 299 import dls.util.json : convertToJSON; 300 import std.algorithm : filter; 301 import std.array : appender; 302 import std.json : JSONValue; 303 import std..string : stripRight; 304 import std.typecons : nullable; 305 306 if (commandCompat) 307 { 308 logger.info("Fetching commands for %s at range %s,%s to %s,%s", uri.path, 309 range.start.line, range.start.character, range.end.line, range.end.character); 310 } 311 312 auto result = appender!(Command[]); 313 314 foreach (diagnostic; diagnostics.filter!q{!a.code.isNull}) 315 { 316 StaticAnalysisConfig config; 317 auto code = diagnostic.code.get().str; 318 319 if (getDiagnosticParameter(config, code) !is null) 320 { 321 { 322 auto title = tr(Tr.app_command_diagnostic_disableCheck_local, [code]); 323 auto document = Document.get(uri); 324 auto line = document.lines[diagnostic.range.end.line].stripRight(); 325 auto pos = new Position(diagnostic.range.end.line, line.length); 326 auto textEdit = new TextEdit(new Range(pos, pos), " // @suppress(" ~ code ~ ")"); 327 auto edit = makeFileWorkspaceEdit(uri, [textEdit]); 328 result ~= new Command(title, Commands.workspaceEdit, 329 [convertToJSON(edit).get()].nullable); 330 } 331 332 { 333 auto title = tr(Tr.app_command_diagnostic_disableCheck_global, [code]); 334 auto args = [JSONValue(uri.toString()), JSONValue(code)]; 335 result ~= new Command(title, 336 Commands.codeAction_analysis_disableCheck, args.nullable); 337 } 338 } 339 } 340 341 return result.data; 342 } 343 344 CodeAction[] codeAction(const Uri uri, const Range range, 345 Diagnostic[] diagnostics, const CodeActionKind[] kinds) 346 { 347 import dls.protocol.definitions : Command, Position; 348 import dls.protocol.logger : logger; 349 import dls.tools.command_tool : Commands; 350 import dls.util.document : Document; 351 import dls.util.i18n : Tr, tr; 352 import dls.util.json : convertFromJSON; 353 import std.algorithm : canFind, filter; 354 import std.array : appender; 355 import std.typecons : Nullable, nullable; 356 357 logger.info("Fetching code actions for %s at range %s,%s to %s,%s", uri.path, 358 range.start.line, range.start.character, range.end.line, range.end.character); 359 360 if (kinds.length > 0 && !kinds.canFind(CodeActionKind.quickfix)) 361 { 362 return []; 363 } 364 365 auto result = appender!(CodeAction[]); 366 367 foreach (diagnostic; diagnostics.filter!q{!a.code.isNull}) 368 { 369 foreach (command; codeAction(uri, range, [diagnostic], false)) 370 { 371 auto action = new CodeAction(command.title, 372 CodeActionKind.quickfix.nullable, [diagnostic].nullable); 373 374 if (command.command == Commands.workspaceEdit) 375 { 376 action.edit = convertFromJSON!WorkspaceEdit(command.arguments[0]).nullable; 377 } 378 else 379 { 380 action.command = command.nullable; 381 } 382 383 result ~= action; 384 } 385 } 386 387 return result.data; 388 } 389 390 package void disableCheck(const Uri uri, const string code) 391 { 392 import dls.tools.symbol_tool : SymbolTool; 393 import dscanner.analysis.config : Check; 394 import inifiled : INI, writeINIFile; 395 import std.path : buildNormalizedPath; 396 397 auto config = getAnalysisConfig(uri); 398 *getDiagnosticParameter(config, code) = Check.disabled; 399 writeINIFile(config, _analysisConfigPaths[SymbolTool.instance.getWorkspace(uri).path]); 400 } 401 402 private Uri getAnalysisConfigUri(const Uri workspaceUri) 403 { 404 import std.algorithm : filter, map; 405 import std.array : array; 406 import std.file : exists; 407 import std.path : buildNormalizedPath; 408 409 auto possibleFiles = [getConfig(workspaceUri).analysis.configFile, 410 "dscanner.ini", ".dscanner.ini"].map!( 411 file => buildNormalizedPath(workspaceUri.path, file)); 412 return Uri.fromPath((possibleFiles.filter!exists.array ~ buildNormalizedPath(workspaceUri.path, 413 "dscanner.ini"))[0]); 414 } 415 416 private StaticAnalysisConfig getAnalysisConfig(const Uri uri) 417 { 418 import dls.tools.symbol_tool : SymbolTool; 419 import dscanner.analysis.config : defaultStaticAnalysisConfig; 420 421 const workspaceUri = SymbolTool.instance.getWorkspace(uri); 422 immutable workspacePath = workspaceUri is null ? "" : workspaceUri.path; 423 return _analysisConfigs.get(workspacePath, defaultStaticAnalysisConfig()); 424 } 425 426 private string* getDiagnosticParameter(return ref StaticAnalysisConfig config, const string code) 427 { 428 //dfmt off 429 switch (code) 430 { 431 case DScannerWarnings.bugs_backwardsSlices : return &config.backwards_range_check; 432 case DScannerWarnings.bugs_ifElseSame : return &config.if_else_same_check; 433 case DScannerWarnings.bugs_logicOperatorOperands : return &config.if_else_same_check; 434 case DScannerWarnings.bugs_selfAssignment : return &config.if_else_same_check; 435 case DScannerWarnings.confusing_argumentParameter_Mismatch : return &config.mismatched_args_check; 436 case DScannerWarnings.confusing_brexp : return &config.asm_style_check; 437 case DScannerWarnings.confusing_builtinPropertyNames : return &config.builtin_property_names_check; 438 case DScannerWarnings.confusing_constructor_args : return &config.constructor_check; 439 case DScannerWarnings.confusing_functionAttributes : return &config.function_attribute_check; 440 case DScannerWarnings.confusing_lambdaReturnsLambda : return &config.lambda_return_check; 441 case DScannerWarnings.confusing_logicalPrecedence : return &config.logical_precedence_check; 442 case DScannerWarnings.confusing_structConstructorDefaultArgs : return &config.constructor_check; 443 case DScannerWarnings.deprecated_deleteKeyword : return &config.delete_check; 444 case DScannerWarnings.deprecated_floatingPointOperators : return &config.float_operator_check; 445 case DScannerWarnings.ifStatement : return &config.redundant_if_check; 446 case DScannerWarnings.performance_enumArrayLiteral : return &config.enum_array_literal_check; 447 case DScannerWarnings.style_aliasSyntax : return &config.alias_syntax_check; 448 case DScannerWarnings.style_allman : return &config.allman_braces_check; 449 case DScannerWarnings.style_assertWithoutMsg : return &config.assert_without_msg; 450 case DScannerWarnings.style_docMissingParams : return &config.properly_documented_public_functions; 451 case DScannerWarnings.style_docMissingReturns : return &config.properly_documented_public_functions; 452 case DScannerWarnings.style_docMissingThrow : return &config.properly_documented_public_functions; 453 case DScannerWarnings.style_docNonExistingParams : return &config.properly_documented_public_functions; 454 case DScannerWarnings.style_explicitlyAnnotatedUnittest : return &config.explicitly_annotated_unittests; 455 case DScannerWarnings.style_hasPublicExample : return &config.has_public_example; 456 case DScannerWarnings.style_ifConstraintsIndent : return &config.if_constraints_indent; 457 case DScannerWarnings.style_importsSortedness : return &config.imports_sortedness; 458 case DScannerWarnings.style_longLine : return &config.long_line_check; 459 case DScannerWarnings.style_numberLiterals : return &config.number_style_check; 460 case DScannerWarnings.style_phobosNamingConvention : return &config.style_check; 461 case DScannerWarnings.style_undocumentedDeclaration : return &config.undocumented_declaration_check; 462 case DScannerWarnings.suspicious_autoRefAssignment : return &config.auto_ref_assignment_check; 463 case DScannerWarnings.suspicious_catchEmAll : return &config.exception_check; 464 case DScannerWarnings.suspicious_commaExpression : return &config.comma_expression_check; 465 case DScannerWarnings.suspicious_incompleteOperatorOverloading : return &config.opequals_tohash_check; 466 case DScannerWarnings.suspicious_incorrectInfiniteRange : return &config.incorrect_infinite_range_check; 467 case DScannerWarnings.suspicious_labelVarSameName : return &config.label_var_same_name_check; 468 case DScannerWarnings.suspicious_lengthSubtraction : return &config.length_subtraction_check; 469 case DScannerWarnings.suspicious_localImports : return &config.local_import_check; 470 case DScannerWarnings.suspicious_missingReturn : return &config.auto_function_check; 471 case DScannerWarnings.suspicious_objectConst : return &config.object_const_check; 472 case DScannerWarnings.suspicious_redundantAttributes : return &config.redundant_attributes_check; 473 case DScannerWarnings.suspicious_redundantParens : return &config.redundant_parens_check; 474 case DScannerWarnings.suspicious_staticIfElse : return &config.static_if_else_check; 475 case DScannerWarnings.suspicious_unmodified : return &config.could_be_immutable_check; 476 case DScannerWarnings.suspicious_unusedLabel : return &config.unused_label_check; 477 case DScannerWarnings.suspicious_unusedParameter : return &config.unused_variable_check; 478 case DScannerWarnings.suspicious_unusedVariable : return &config.unused_variable_check; 479 case DScannerWarnings.suspicious_uselessAssert : return &config.useless_assert_check; 480 case DScannerWarnings.suspicious_uselessInitializer : return &config.useless_initializer; 481 case DScannerWarnings.trustTooMuch : return &config.trust_too_much; 482 case DScannerWarnings.unnecessary_duplicateAttribute : return &config.duplicate_attribute; 483 case DScannerWarnings.useless_final : return &config.final_attribute_check; 484 case DScannerWarnings.vcallCtor : return &config.vcall_in_ctor; 485 default : return null; 486 } 487 //dfmt on 488 } 489 490 private WorkspaceEdit makeFileWorkspaceEdit(const Uri uri, TextEdit[] edits) 491 { 492 import dls.protocol.definitions : TextDocumentEdit, VersionedTextDocumentIdentifier; 493 import dls.util.document : Document; 494 import std.typecons : nullable; 495 496 auto document = Document.get(uri); 497 auto changes = [uri.toString() : edits]; 498 auto identifier = new VersionedTextDocumentIdentifier(uri, document.version_); 499 auto documentChanges = [new TextDocumentEdit(identifier, changes[uri])]; 500 return new WorkspaceEdit(changes.nullable, documentChanges.nullable); 501 } 502 }