diff --git a/dub.selections.json b/dub.selections.json index 7547ae1c..455988bd 100644 --- a/dub.selections.json +++ b/dub.selections.json @@ -9,7 +9,7 @@ "dfmt": "0.15.0", "diet-complete": "0.0.3", "diet-ng": "1.8.1", - "dscanner": "0.15.0", + "dscanner": "0.16.0-beta.2", "dub": "1.33.1", "emsi_containers": "0.9.0", "eventcore": "0.9.25", @@ -18,7 +18,7 @@ "isfreedesktop": "0.1.1", "libasync": "0.8.6", "libddoc": "0.8.0", - "libdparse": "0.23.1", + "libdparse": "0.23.2", "memutils": "1.0.9", "mir-algorithm": "3.20.4", "mir-core": "1.5.5", diff --git a/source/served/commands/code_actions.d b/source/served/commands/code_actions.d index 8ca03c1b..9f8a6905 100644 --- a/source/served/commands/code_actions.d +++ b/source/served/commands/code_actions.d @@ -47,6 +47,7 @@ CodeAction[] provideCodeActions(CodeActionParams params) auto instance = activeInstance = backend.getBestInstance(document.uri.uriToFile); if (document.getLanguageId != "d" || !instance) return []; + auto config = workspace(params.textDocument.uri).config; // eagerly load DCD in opened files which request code actions if (instance.has!DCDComponent) @@ -89,7 +90,7 @@ CodeAction[] provideCodeActions(CodeActionParams params) { if (diagnostic.source == DScannerDiagnosticSource) { - addDScannerDiagnostics(ret, instance, document, diagnostic, params); + addDScannerDiagnostics(config, ret, instance, document, diagnostic, params); } else if (diagnostic.source == SyntaxHintDiagnosticSource) { @@ -218,14 +219,30 @@ void addDubDiagnostics(ref CodeAction[] ret, WorkspaceD.Instance instance, } } -void addDScannerDiagnostics(ref CodeAction[] ret, WorkspaceD.Instance instance, +void addDScannerDiagnostics(const ref UserConfiguration config, + ref CodeAction[] ret, WorkspaceD.Instance instance, Document document, Diagnostic diagnostic, CodeActionParams params) { import dscanner.analysis.imports_sortedness : ImportSortednessCheck; + import served.linters.dscanner : diagnosticDataToCodeReplacement, + diagnosticDataToResolveContext, servedDefaultDscannerConfig; + import workspaced.com.dscanner : DScannerAutoFix; - string key = diagnostic.code.orDefault.match!((string s) => s, _ => cast(string)(null)); + Position cachePos; + size_t cacheIndex; + + TextEdit toTextEdit(DScannerAutoFix.CodeReplacement replacement) + { + return TextEdit( + TextRange( + document.nextPositionBytes(cachePos, cacheIndex, replacement.range[0]), + document.nextPositionBytes(cachePos, cacheIndex, replacement.range[1]) + ), + replacement.newText + ); + } - info("Diagnostic: ", diagnostic); + string key = diagnostic.code.orDefault.match!((string s) => s, _ => cast(string)(null)); if (key == ImportSortednessCheck.KEY) { @@ -233,6 +250,73 @@ void addDScannerDiagnostics(ref CodeAction[] ret, WorkspaceD.Instance instance, [JsonValue(document.positionToOffset(params.range[0]))])); } + if (!diagnostic.data.isNone) + { + auto data = diagnostic.data.deref; + if (auto autofixes = "autofixes" in data.object) + { + string checkName = data.object["checkName"].string; + + DScannerAutoFix.ResolveContext[] resolveContexts; + size_t[] resolveContextIndices; + + foreach (autofix; autofixes.array) + { + if (autofix.kind != JsonValue.Kind.object) + { + warning("Unsupported dscanner autofix: ", autofix); + continue; + } + + auto nameJson = "name" in autofix.object; + if (!nameJson) + { + warning("Unsupported dscanner autofix: ", autofix); + continue; + } + string name = (*nameJson).get!string; + + DScannerAutoFix.CodeReplacement[] codeReplacements; + + if (auto replacements = "replacements" in autofix.object) + { + codeReplacements = (*replacements).array.map!( + j => diagnosticDataToCodeReplacement(j) + ).array; + } + else if (auto jcontext = "context" in autofix.object) + { + resolveContexts ~= diagnosticDataToResolveContext(*jcontext); + resolveContextIndices ~= ret.length; + } + else + { + warning("Unsupported dscanner autofix: ", autofix); + continue; + } + + ret ~= CodeAction(name, WorkspaceEdit([ + document.uri: codeReplacements.map!toTextEdit.array + ])); + } + + if (resolveContexts.length) + { + auto codeReplacementsList = instance.get!DscannerComponent.resolveAutoFixes( + checkName, resolveContexts, document.uri.uriToFile, "dscanner.ini", + generateDfmtArgs(config, document.eolAt(0)), document.rawText, false, + servedDefaultDscannerConfig).getYield; + + foreach (i, codeReplacements; codeReplacementsList) + { + ret[resolveContextIndices[i]].edit = WorkspaceEdit([ + document.uri: codeReplacements.map!toTextEdit.array + ]); + } + } + } + } + if (key.length) { JsonValue code = diagnostic.code.match!(() => JsonValue(null), j => j); diff --git a/source/served/linters/dscanner.d b/source/served/linters/dscanner.d index 9b231819..4bc450b2 100644 --- a/source/served/linters/dscanner.d +++ b/source/served/linters/dscanner.d @@ -121,6 +121,72 @@ void convertFromWorkspaced(ref Diagnostic d, Document document, DScannerIssue is /* message: */ suppl.description ) ).array; + if (issue.autofixes.length) + d.data = JsonValue([ + "checkName": JsonValue(issue.checkName), + "autofixes": JsonValue(issue.autofixes.map!toJson.array) + ]); +} + +private JsonValue toJson(DScannerAutoFix f) +{ + import std.array : array; + + JsonValue[string] ret = [ + "name": JsonValue(f.name) + ]; + f.replacements.match!( + (DScannerAutoFix.CodeReplacement[] replacements) { + ret["replacements"] = JsonValue(replacements.map!(a => toJson(a)).array); + }, + (DScannerAutoFix.ResolveContext context) { + ret["context"] = toJson(context); + } + ); + return JsonValue(ret); +} + +private JsonValue toJson(DScannerAutoFix.CodeReplacement replacement) +{ + return JsonValue([ + "r": JsonValue([JsonValue(replacement.range[0]), JsonValue(replacement.range[1])]), + "t": JsonValue(replacement.newText) + ]); +} + +DScannerAutoFix.CodeReplacement diagnosticDataToCodeReplacement(JsonValue json) +{ + return DScannerAutoFix.CodeReplacement( + [ + json.object["r"].array[0].integer, + json.object["r"].array[1].integer + ], + json.object["t"].string + ); +} + +private JsonValue toJson(DScannerAutoFix.ResolveContext context) +{ + auto params = new JsonValue[context.params.length]; + foreach (i, param; context.params) + params[i] = JsonValue(param); + return JsonValue([ + "p": JsonValue(params), + "s": JsonValue(context.extraInfo) + ]); +} + +DScannerAutoFix.ResolveContext diagnosticDataToResolveContext(JsonValue json) +{ + size_t i; + return DScannerAutoFix.ResolveContext( + [ + json.object["p"].array[i++].integer, + json.object["p"].array[i++].integer, + json.object["p"].array[i++].integer, + ], + json.object["s"].string + ); } /// Sets the range for the diagnostic from the issue diff --git a/test/tc_dub/source/app.d b/test/tc_dub/source/app.d index 206f7cc2..c8181ac9 100644 --- a/test/tc_dub/source/app.d +++ b/test/tc_dub/source/app.d @@ -41,7 +41,8 @@ void main() try { auto result = dub.build.getBlocking; - assert(result.count!(a => a.type == ErrorType.Warning || a.type == ErrorType.Error) == 0); + assert(result.count!(a => a.type == ErrorType.Warning || a.type == ErrorType.Error) == 0, + "got unexpected warnings/errors: " ~ result.to!string); } catch (Exception e) { diff --git a/workspace-d/dub.sdl b/workspace-d/dub.sdl index 74016af3..26f226b7 100644 --- a/workspace-d/dub.sdl +++ b/workspace-d/dub.sdl @@ -9,7 +9,7 @@ dependency "inifiled" version="1.3.3" dependency "serve-d:dcd" path=".." dependency "dub" version="1.33.1" dependency "emsi_containers" version="0.9.0" -dependency "dscanner" version="0.15.0" +dependency "dscanner" version="~>0.16.0-beta.1" dependency "libdparse" version="~>0.23.0" dependency "standardpaths" version="0.8.2" dependency "mir-algorithm" version="~>3.20" diff --git a/workspace-d/dub.selections.json b/workspace-d/dub.selections.json index bfa43fe3..83ef9735 100644 --- a/workspace-d/dub.selections.json +++ b/workspace-d/dub.selections.json @@ -3,13 +3,13 @@ "versions": { "dcd": "0.16.0-beta.2", "dfmt": "0.15.0", - "dscanner": "0.15.0", + "dscanner": "0.16.0-beta.2", "dub": "1.33.1", "emsi_containers": "0.9.0", "inifiled": "1.3.3", "isfreedesktop": "0.1.1", "libddoc": "0.8.0", - "libdparse": "0.23.1", + "libdparse": "0.23.2", "mir-algorithm": "3.20.4", "mir-core": "1.5.5", "msgpack-d": "1.0.4", diff --git a/workspace-d/source/workspaced/com/dfmt.d b/workspace-d/source/workspaced/com/dfmt.d index 47b11ec4..7552d1d9 100644 --- a/workspace-d/source/workspaced/com/dfmt.d +++ b/workspace-d/source/workspaced/com/dfmt.d @@ -39,6 +39,17 @@ class DfmtComponent : ComponentWrapper if (!code.strip.length) return null; + Config config = parseConfig(arguments); + auto output = appender!string; + fmt("stdin", cast(ubyte[]) code, output, &config); + if (output.data.length) + return output.data; + else + return code.idup; + } + + static Config parseConfig(string[] arguments) + { Config config; config.initializeWithDefaults(); string configPath; @@ -143,12 +154,7 @@ class DfmtComponent : ComponentWrapper ); //dfmt on } - auto output = appender!string; - fmt("stdin", cast(ubyte[]) code, output, &config); - if (output.data.length) - return output.data; - else - return code.idup; + return config; } /// Finds dfmt instruction comments (dfmt off, dfmt on) diff --git a/workspace-d/source/workspaced/com/dscanner.d b/workspace-d/source/workspaced/com/dscanner.d index 7ba16d54..30661b5a 100644 --- a/workspace-d/source/workspaced/com/dscanner.d +++ b/workspace-d/source/workspaced/com/dscanner.d @@ -3,6 +3,7 @@ module workspaced.com.dscanner; version (unittest) debug = ResolveRange; +import mir.algebraic; import std.algorithm; import std.array; import std.conv; @@ -10,8 +11,8 @@ import std.experimental.logger; import std.file; import std.json; import std.meta : AliasSeq; +import std.range : repeat; import std.stdio; -import std.sumtype; import std.traits; import std.typecons; @@ -113,6 +114,8 @@ class DscannerComponent : ComponentWrapper issue.type = typeForWarning(msg.key); issue.description = msg.message; issue.key = msg.key; + issue.checkName = msg.checkName; + issue.autofixes = msg.autofixes.map!(f => DScannerAutoFix(f)).array; if (resolveRanges) { if (!this.resolveRange(tokens, issue)) @@ -151,6 +154,108 @@ class DscannerComponent : ComponentWrapper return ret; } + Future!(DScannerAutoFix.CodeReplacement[][]) resolveAutoFixes( + string messageCheckName, + DScannerAutoFix.ResolveContext[] contexts, + string file = "", string ini = "dscanner.ini", + string[] dfmtArgs = null, + scope const(char)[] code = "", bool skipWorkspacedPaths = false, + const StaticAnalysisConfig defaultConfig = StaticAnalysisConfig.init) + { + import dscanner.analysis.run : resolveAutoFix; + + auto ret = new typeof(return); + gthreads.create({ + mixin(traceTask); + try + { + if (code.length && !file.length) + file = "stdin"; + auto config = getConfig(ini, skipWorkspacedPaths, defaultConfig); + if (!code.length) + code = readText(file); + if (!code.length) + { + ret.finish(null); + return; + } + RollbackAllocator r; + const(Token)[] tokens; + StringCache cache = StringCache(StringCache.defaultBucketCount); + DScannerIssue[] parseIssues; + const Module m = parseModule(file, cast(ubyte[]) code, &r, cache, tokens, parseIssues); + if (!m) + throw new Exception(text("parseModule returned null?! - file: '", + file, "', code: '", code, "'")); + + ModuleCache moduleCache; + AutoFixFormatting formatting = parseDfmtArgs(dfmtArgs); + DScannerAutoFix.CodeReplacement[][] replacementsList; + foreach (context; contexts) + { + // ensured by static asserts in DScannerAutoFix that these casts work + auto dscannerContext = *cast(AutoFix.ResolveContext*)&context; + auto resolved = resolveAutoFix(messageCheckName, dscannerContext, file, moduleCache, tokens, m, config, formatting); + replacementsList ~= *cast(DScannerAutoFix.CodeReplacement[]*)&resolved; + } + assert(replacementsList.length == contexts.length); + ret.finish(replacementsList); + } + catch (Throwable e) + { + ret.error(e); + } + }); + return ret; + } + + private static AutoFixFormatting parseDfmtArgs(string[] dfmtArgs) + { + import std.getopt; + import workspaced.com.dfmt; + import dfmt.editorconfig : IndentStyle, EOL; + import dfmt.config : BraceStyle; + + auto config = DfmtComponent.parseConfig(dfmtArgs); + + AutoFixFormatting.BraceStyle braceStyle; + switch (config.dfmt_brace_style) with (AutoFixFormatting.BraceStyle) + { + case BraceStyle.otbs: + braceStyle = otbs; + break; + case BraceStyle.stroustrup: + braceStyle = stroustrup; + break; + case BraceStyle.knr: + braceStyle = knr; + break; + default: + case BraceStyle.allman: + braceStyle = allman; + break; + } + + string indentString = "\t"; + if (config.indent_style == IndentStyle.tab) + indentString = "\t"; + else if (config.indent_style == IndentStyle.space) + indentString = (cast(immutable)' ').repeat(config.indent_size).array; + + string eol = "\n"; + if (config.end_of_line == EOL.crlf) + eol = "\r\n"; + else if (config.end_of_line == EOL.cr) + eol = "\r"; + + return AutoFixFormatting( + braceStyle, + indentString, + config.indent_size, + eol + ); + } + /// Takes line & column from the D-Scanner issue array and resolves the /// start & end locations for the issues by changing the values in-place. /// In the JSON RPC this returns the modified array, in workspace-d as a @@ -390,7 +495,7 @@ class DscannerComponent : ComponentWrapper void addIssue(string fileName, size_t line, size_t column, string message, bool isError) { - issues ~= DScannerIssue(file, isError ? "error" : "warn", message, null, + issues ~= DScannerIssue(file, isError ? "error" : "warn", message, null, null, [ResolvedLocation(0, cast(uint) line, cast(uint) column), ResolvedLocation(0, cast(uint) line, cast(uint) column)]); } @@ -542,10 +647,91 @@ struct DScannerIssue string description; /// string key; + /// + string checkName; /// Issue range ResolvedLocation[2] range; /// Supplemental information Supplemental[] supplemental; + /// + DScannerAutoFix[] autofixes; +} + +struct DScannerAutoFix +{ + /// + struct CodeReplacement + { + /// Byte index `[start, end)` within the file what text to replace. + /// `start == end` if text is only getting inserted. + size_t[2] range; + /// The new text to put inside the range. (empty to delete text) + string newText; + + this(size_t[2] range, string newText) + { + this.range = range; + this.newText = newText; + } + + this(AutoFix.CodeReplacement dscannerCodeReplacement) + { + // see static assert below + this.tupleof = dscannerCodeReplacement.tupleof; + } + } + + /// Context that the analyzer resolve method can use to generate the + /// resolved `CodeReplacement` with. + struct ResolveContext + { + /// Arbitrary analyzer-defined parameters. May grow in the future with + /// more items. + ulong[3] params; + /// For dynamically sized data, may contain binary data. + string extraInfo; + + this(ulong[3] params, string extraInfo) + { + this.params = params; + this.extraInfo = extraInfo; + } + + this(AutoFix.ResolveContext dscannerResolveContext) + { + // see static assert below + this.tupleof = dscannerResolveContext.tupleof; + } + } + + /// Display name for the UI. + string name; + /// Either code replacements, sorted by range start, never overlapping, or a + /// context that can be passed to `BaseAnalyzer.resolveAutoFix` along with + /// the message key from the parent `Message` object. + /// + /// `CodeReplacement[]` should be applied to the code in reverse, otherwise + /// an offset to the following start indices must be calculated and be kept + /// track of. + Variant!(CodeReplacement[], ResolveContext) replacements; + + this(AutoFix f) + { + import std.sumtype : match; + + static assert(AutoFix.CodeReplacement.sizeof == CodeReplacement.sizeof); + static assert(typeof(AutoFix.CodeReplacement.tupleof).stringof == typeof(CodeReplacement.tupleof).stringof); + static assert(AutoFix.CodeReplacement.tupleof.stringof == CodeReplacement.tupleof.stringof); + static assert(AutoFix.ResolveContext.sizeof == ResolveContext.sizeof); + static assert(typeof(AutoFix.ResolveContext.tupleof).stringof == typeof(ResolveContext.tupleof).stringof); + static assert(AutoFix.ResolveContext.tupleof.stringof == ResolveContext.tupleof.stringof); + + name = f.name; + replacements = f.replacements.match!( + (AutoFix.CodeReplacement[] r) => typeof(replacements)(*cast(CodeReplacement[]*)&r), + (AutoFix.ResolveContext r) => typeof(replacements)(*cast(ResolveContext*)&r), + ); + } } /// Describes a code location in exact byte offset, line number and column for a @@ -634,6 +820,8 @@ public import workspaced.index_format : DefinitionElement, ModuleDefinition; bool isVisibleOutside(DefinitionElement.Visibility v) { + import std.sumtype : match; + return v.match!( (typeof(null) _) => true, (DefinitionElement.BasicVisibility v) => v != DefinitionElement.BasicVisibility.protected_ @@ -644,6 +832,8 @@ bool isVisibleOutside(DefinitionElement.Visibility v) bool isPublicImportVisibility(DefinitionElement.Visibility v) { + import std.sumtype : match; + return v.match!( (typeof(null) _) => false, (DefinitionElement.BasicVisibility v) => v != DefinitionElement.BasicVisibility.protected_ @@ -1285,6 +1475,8 @@ struct ContextType string toLegacyAccess(DefinitionElement.Visibility v) { + import std.sumtype : match; + return v.match!( (typeof(null) _) => null, (DefinitionElement.PackageVisibility v) => "protected",