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 }