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 }