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 if (uri is null) 103 { 104 return; 105 } 106 107 const currentConfigFile = _instance._analysisConfigPaths.get(uri.path, "dscanner.ini").baseName; 108 109 if (getConfig(uri).analysis.configFile != currentConfigFile) 110 { 111 _instance.updateAnalysisConfig(uri); 112 } 113 }); 114 _instance.addConfigHook("filePatterns", (const Uri uri) { 115 if (uri is null) 116 { 117 return; 118 } 119 120 auto newPatterns = getConfig(uri).analysis.filePatterns; 121 122 if (newPatterns != _instance._currentPatterns.get(uri, [])) 123 { 124 _instance._currentPatterns[uri] = newPatterns; 125 _instance.scanAllWorkspaces(); 126 } 127 }); 128 } 129 130 static void shutdown() 131 { 132 destroy(_instance); 133 } 134 135 @property static AnalysisTool instance() 136 { 137 return _instance; 138 } 139 140 private string[string] _analysisConfigPaths; 141 private StaticAnalysisConfig[string] _analysisConfigs; 142 private string[][string] _currentPatterns; 143 144 auto getScannableFilesUris(out Uri[] discardedFiles) 145 { 146 import dls.tools.symbol_tool : SymbolTool; 147 import dls.util.uri : filenameCmp; 148 import std.algorithm : filter, sort; 149 import std.file : SpanMode, dirEntries; 150 import std.path : buildPath, globMatch; 151 import std.range : chain; 152 153 Uri[] globMatches; 154 auto workspacesFilesUris = SymbolTool.instance.workspacesFilesUris.sort!((a, b) => filenameCmp(a, b) < 0); 155 156 foreach (wUri; workspacesUris) 157 { 158 auto filePatterns = getConfig(wUri).analysis.filePatterns; 159 _currentPatterns[wUri] = filePatterns; 160 161 LNextFile: foreach (entry; dirEntries(wUri.path, SpanMode.depth).filter!q{a.isFile}) 162 { 163 auto entryUri = Uri.fromPath(entry.name); 164 165 foreach (pattern; filePatterns) 166 { 167 if (globMatch(entry.name, buildPath(wUri.path, pattern))) 168 { 169 globMatches ~= entryUri; 170 continue LNextFile; 171 } 172 } 173 174 if (globMatch(entry.name, "*.{d,di}") && !workspacesFilesUris.contains(entryUri)) 175 { 176 discardedFiles ~= entryUri; 177 } 178 } 179 } 180 181 return chain(workspacesFilesUris, globMatches); 182 } 183 184 void scanAllWorkspaces() 185 { 186 import dls.protocol.jsonrpc : send; 187 import dls.protocol.interfaces : PublishDiagnosticsParams; 188 import dls.protocol.messages.methods : TextDocument; 189 import std.algorithm : each; 190 191 Uri[] discardedFiles; 192 193 getScannableFilesUris(discardedFiles).each!((uri) { 194 send(TextDocument.publishDiagnostics, new PublishDiagnosticsParams(uri, 195 _instance.diagnostics(uri))); 196 }); 197 198 foreach (file; discardedFiles) 199 { 200 send(TextDocument.publishDiagnostics, new PublishDiagnosticsParams(file, [])); 201 } 202 } 203 204 void addAnalysisConfig(const Uri uri) 205 { 206 import dscanner.analysis.config : defaultStaticAnalysisConfig; 207 208 _analysisConfigs[uri.path] = defaultStaticAnalysisConfig(); 209 updateAnalysisConfig(uri); 210 } 211 212 void removeAnalysisConfig(const Uri workspaceUri) 213 { 214 _analysisConfigPaths.remove(workspaceUri.path); 215 _analysisConfigs.remove(workspaceUri.path); 216 } 217 218 void updateAnalysisConfig(const Uri workspaceUri) 219 { 220 import dls.protocol.logger : logger; 221 import dls.server : Server; 222 import dscanner.analysis.config : defaultStaticAnalysisConfig; 223 import inifiled : readINIFile; 224 import std.file : exists; 225 import std.path : buildNormalizedPath; 226 227 auto configPath = getAnalysisConfigUri(workspaceUri).path; 228 auto conf = defaultStaticAnalysisConfig(); 229 230 if (exists(configPath)) 231 { 232 logger.info("Updating config from file %s", configPath); 233 readINIFile(conf, configPath); 234 } 235 236 _analysisConfigPaths[workspaceUri.path] = configPath; 237 _analysisConfigs[workspaceUri.path] = conf; 238 239 if (Server.initialized) 240 { 241 scanAllWorkspaces(); 242 } 243 } 244 245 Diagnostic[] diagnostics(const Uri uri) 246 { 247 import dls.protocol.definitions : DiagnosticSeverity; 248 import dls.protocol.logger : logger; 249 import dls.tools.symbol_tool : SymbolTool; 250 import dls.util.document : Document, minusOne; 251 import dparse.lexer : LexerConfig, StringBehavior, StringCache, getTokensForParser; 252 import dparse.parser : parseModule; 253 import dparse.rollback_allocator : RollbackAllocator; 254 import dscanner.analysis.run : analyze; 255 import std.array : appender; 256 import std.json : JSONValue; 257 import std.regex : matchFirst, regex; 258 import dls.util.nullable: Nullable, nullable; 259 import std.utf : toUTF16; 260 261 logger.info("Fetching diagnostics for %s", uri.path); 262 263 auto stringCache = StringCache(StringCache.defaultBucketCount); 264 auto tokens = getTokensForParser(Document.get(uri).toString(), 265 LexerConfig(uri.path, StringBehavior.source), &stringCache); 266 RollbackAllocator ra; 267 auto document = Document.get(uri); 268 auto diagnostics = appender!(Diagnostic[]); 269 270 immutable syntaxProblemhandler = (string path, size_t line, size_t column, 271 string msg, bool isError) { 272 auto severity = (isError ? DiagnosticSeverity.error : DiagnosticSeverity.warning); 273 diagnostics ~= new Diagnostic(document.wordRangeAtLineAndByte(minusOne(line), minusOne(column)), msg, 274 severity.nullable, Nullable!JSONValue(), diagnosticSource.nullable); 275 }; 276 277 const mod = parseModule(tokens, uri.path, &ra, syntaxProblemhandler); 278 279 if (mod.declarations.length == 0) 280 { 281 return diagnostics.data; 282 } 283 284 const analysisResults = analyze(uri.path, mod, getAnalysisConfig(uri), 285 SymbolTool.instance.cache, tokens, true); 286 287 foreach (result; analysisResults) 288 { 289 if (!document.lines[minusOne(result.line)].matchFirst( 290 regex(`//.*@suppress\s*\(\s*`w ~ result.key.toUTF16() ~ `\s*\)`w))) 291 { 292 diagnostics ~= new Diagnostic(document.wordRangeAtLineAndByte(minusOne(result.line), 293 minusOne(result.column)), 294 result.message, DiagnosticSeverity.warning.nullable, 295 JSONValue(result.key).nullable, diagnosticSource.nullable); 296 } 297 } 298 299 return diagnostics.data; 300 } 301 302 Command[] codeAction(const Uri uri, const Range range, 303 Diagnostic[] diagnostics, bool commandCompat) 304 { 305 import dls.protocol.definitions : Position; 306 import dls.protocol.logger : logger; 307 import dls.tools.command_tool : Commands; 308 import dls.util.document : Document; 309 import dls.util.i18n : Tr, tr; 310 import dls.util.json : convertToJSON; 311 import std.algorithm : filter; 312 import std.array : appender; 313 import std.json : JSONValue; 314 import std..string : stripRight; 315 import dls.util.nullable: nullable; 316 317 if (commandCompat) 318 { 319 logger.info("Fetching commands for %s at range %s,%s to %s,%s", uri.path, 320 range.start.line, range.start.character, range.end.line, range.end.character); 321 } 322 323 auto result = appender!(Command[]); 324 325 foreach (diagnostic; diagnostics.filter!q{!a.code.isNull}) 326 { 327 StaticAnalysisConfig config; 328 auto code = diagnostic.code.get().str; 329 330 if (getDiagnosticParameter(config, code) !is null) 331 { 332 { 333 auto title = tr(Tr.app_command_diagnostic_disableCheck_local, [code]); 334 auto document = Document.get(uri); 335 auto line = document.lines[diagnostic.range.end.line].stripRight(); 336 auto pos = new Position(diagnostic.range.end.line, line.length); 337 auto textEdit = new TextEdit(new Range(pos, pos), " // @suppress(" ~ code ~ ")"); 338 auto edit = makeFileWorkspaceEdit(uri, [textEdit]); 339 result ~= new Command(title, Commands.workspaceEdit, 340 [convertToJSON(edit).get()].nullable); 341 } 342 343 { 344 auto title = tr(Tr.app_command_diagnostic_disableCheck_global, [code]); 345 auto args = [JSONValue(uri.toString()), JSONValue(code)]; 346 result ~= new Command(title, 347 Commands.codeAction_analysis_disableCheck, args.nullable); 348 } 349 } 350 } 351 352 return result.data; 353 } 354 355 CodeAction[] codeAction(const Uri uri, const Range range, 356 Diagnostic[] diagnostics, const CodeActionKind[] kinds) 357 { 358 import dls.protocol.definitions : Command, Position; 359 import dls.protocol.logger : logger; 360 import dls.tools.command_tool : Commands; 361 import dls.util.document : Document; 362 import dls.util.i18n : Tr, tr; 363 import dls.util.json : convertFromJSON; 364 import std.algorithm : canFind, filter; 365 import std.array : appender; 366 import dls.util.nullable: Nullable, nullable; 367 368 logger.info("Fetching code actions for %s at range %s,%s to %s,%s", uri.path, 369 range.start.line, range.start.character, range.end.line, range.end.character); 370 371 if (kinds.length > 0 && !kinds.canFind(CodeActionKind.quickfix)) 372 { 373 return []; 374 } 375 376 auto result = appender!(CodeAction[]); 377 378 foreach (diagnostic; diagnostics.filter!q{!a.code.isNull}) 379 { 380 foreach (command; codeAction(uri, range, [diagnostic], false)) 381 { 382 auto action = new CodeAction(command.title, 383 CodeActionKind.quickfix.nullable, [diagnostic].nullable); 384 385 if (command.command == Commands.workspaceEdit) 386 { 387 action.edit = convertFromJSON!WorkspaceEdit(command.arguments[0]).nullable; 388 } 389 else 390 { 391 action.command = command.nullable; 392 } 393 394 result ~= action; 395 } 396 } 397 398 return result.data; 399 } 400 401 package void disableCheck(const Uri uri, const string code) 402 { 403 import dls.tools.symbol_tool : SymbolTool; 404 import dscanner.analysis.config : Check; 405 import inifiled : INI, writeINIFile; 406 import std.path : buildNormalizedPath; 407 408 auto config = getAnalysisConfig(uri); 409 *getDiagnosticParameter(config, code) = Check.disabled; 410 writeINIFile(config, _analysisConfigPaths[SymbolTool.instance.getWorkspace(uri).path]); 411 } 412 413 private Uri getAnalysisConfigUri(const Uri workspaceUri) 414 { 415 import std.algorithm : filter, map; 416 import std.array : array; 417 import std.file : exists; 418 import std.path : buildNormalizedPath; 419 420 auto possibleFiles = [getConfig(workspaceUri).analysis.configFile, 421 "dscanner.ini", ".dscanner.ini"].map!( 422 file => buildNormalizedPath(workspaceUri.path, file)); 423 return Uri.fromPath((possibleFiles.filter!exists.array ~ buildNormalizedPath(workspaceUri.path, 424 "dscanner.ini"))[0]); 425 } 426 427 private StaticAnalysisConfig getAnalysisConfig(const Uri uri) 428 { 429 import dls.tools.symbol_tool : SymbolTool; 430 import dscanner.analysis.config : defaultStaticAnalysisConfig; 431 432 const workspaceUri = SymbolTool.instance.getWorkspace(uri); 433 immutable workspacePath = workspaceUri is null ? "" : workspaceUri.path; 434 return _analysisConfigs.get(workspacePath, defaultStaticAnalysisConfig()); 435 } 436 437 private string* getDiagnosticParameter(return ref StaticAnalysisConfig config, const string code) 438 { 439 //dfmt off 440 switch (code) 441 { 442 case DScannerWarnings.bugs_backwardsSlices : return &config.backwards_range_check; 443 case DScannerWarnings.bugs_ifElseSame : return &config.if_else_same_check; 444 case DScannerWarnings.bugs_logicOperatorOperands : return &config.if_else_same_check; 445 case DScannerWarnings.bugs_selfAssignment : return &config.if_else_same_check; 446 case DScannerWarnings.confusing_argumentParameter_Mismatch : return &config.mismatched_args_check; 447 case DScannerWarnings.confusing_brexp : return &config.asm_style_check; 448 case DScannerWarnings.confusing_builtinPropertyNames : return &config.builtin_property_names_check; 449 case DScannerWarnings.confusing_constructor_args : return &config.constructor_check; 450 case DScannerWarnings.confusing_functionAttributes : return &config.function_attribute_check; 451 case DScannerWarnings.confusing_lambdaReturnsLambda : return &config.lambda_return_check; 452 case DScannerWarnings.confusing_logicalPrecedence : return &config.logical_precedence_check; 453 case DScannerWarnings.confusing_structConstructorDefaultArgs : return &config.constructor_check; 454 case DScannerWarnings.deprecated_deleteKeyword : return &config.delete_check; 455 case DScannerWarnings.deprecated_floatingPointOperators : return &config.float_operator_check; 456 case DScannerWarnings.ifStatement : return &config.redundant_if_check; 457 case DScannerWarnings.performance_enumArrayLiteral : return &config.enum_array_literal_check; 458 case DScannerWarnings.style_aliasSyntax : return &config.alias_syntax_check; 459 case DScannerWarnings.style_allman : return &config.allman_braces_check; 460 case DScannerWarnings.style_assertWithoutMsg : return &config.assert_without_msg; 461 case DScannerWarnings.style_docMissingParams : return &config.properly_documented_public_functions; 462 case DScannerWarnings.style_docMissingReturns : return &config.properly_documented_public_functions; 463 case DScannerWarnings.style_docMissingThrow : return &config.properly_documented_public_functions; 464 case DScannerWarnings.style_docNonExistingParams : return &config.properly_documented_public_functions; 465 case DScannerWarnings.style_explicitlyAnnotatedUnittest : return &config.explicitly_annotated_unittests; 466 case DScannerWarnings.style_hasPublicExample : return &config.has_public_example; 467 case DScannerWarnings.style_ifConstraintsIndent : return &config.if_constraints_indent; 468 case DScannerWarnings.style_importsSortedness : return &config.imports_sortedness; 469 case DScannerWarnings.style_longLine : return &config.long_line_check; 470 case DScannerWarnings.style_numberLiterals : return &config.number_style_check; 471 case DScannerWarnings.style_phobosNamingConvention : return &config.style_check; 472 case DScannerWarnings.style_undocumentedDeclaration : return &config.undocumented_declaration_check; 473 case DScannerWarnings.suspicious_autoRefAssignment : return &config.auto_ref_assignment_check; 474 case DScannerWarnings.suspicious_catchEmAll : return &config.exception_check; 475 case DScannerWarnings.suspicious_commaExpression : return &config.comma_expression_check; 476 case DScannerWarnings.suspicious_incompleteOperatorOverloading : return &config.opequals_tohash_check; 477 case DScannerWarnings.suspicious_incorrectInfiniteRange : return &config.incorrect_infinite_range_check; 478 case DScannerWarnings.suspicious_labelVarSameName : return &config.label_var_same_name_check; 479 case DScannerWarnings.suspicious_lengthSubtraction : return &config.length_subtraction_check; 480 case DScannerWarnings.suspicious_localImports : return &config.local_import_check; 481 case DScannerWarnings.suspicious_missingReturn : return &config.auto_function_check; 482 case DScannerWarnings.suspicious_objectConst : return &config.object_const_check; 483 case DScannerWarnings.suspicious_redundantAttributes : return &config.redundant_attributes_check; 484 case DScannerWarnings.suspicious_redundantParens : return &config.redundant_parens_check; 485 case DScannerWarnings.suspicious_staticIfElse : return &config.static_if_else_check; 486 case DScannerWarnings.suspicious_unmodified : return &config.could_be_immutable_check; 487 case DScannerWarnings.suspicious_unusedLabel : return &config.unused_label_check; 488 case DScannerWarnings.suspicious_unusedParameter : return &config.unused_variable_check; 489 case DScannerWarnings.suspicious_unusedVariable : return &config.unused_variable_check; 490 case DScannerWarnings.suspicious_uselessAssert : return &config.useless_assert_check; 491 case DScannerWarnings.suspicious_uselessInitializer : return &config.useless_initializer; 492 case DScannerWarnings.trustTooMuch : return &config.trust_too_much; 493 case DScannerWarnings.unnecessary_duplicateAttribute : return &config.duplicate_attribute; 494 case DScannerWarnings.useless_final : return &config.final_attribute_check; 495 case DScannerWarnings.vcallCtor : return &config.vcall_in_ctor; 496 default : return null; 497 } 498 //dfmt on 499 } 500 501 private WorkspaceEdit makeFileWorkspaceEdit(const Uri uri, TextEdit[] edits) 502 { 503 import dls.protocol.definitions : TextDocumentEdit, VersionedTextDocumentIdentifier; 504 import dls.util.document : Document; 505 import dls.util.nullable: nullable; 506 507 auto document = Document.get(uri); 508 auto changes = [uri.toString() : edits]; 509 auto identifier = new VersionedTextDocumentIdentifier(uri, document.version_); 510 auto documentChanges = [new TextDocumentEdit(identifier, changes[uri])]; 511 return new WorkspaceEdit(changes.nullable, documentChanges.nullable); 512 } 513 }