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