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 }