From 245e24d2112821f8a06ed014872b1b21ce75a4b5 Mon Sep 17 00:00:00 2001 From: Remi Thebault Date: Mon, 14 Nov 2022 00:55:26 +0100 Subject: [PATCH] implement CCDB based DCD imports and linting two new settings are added: - d.ccdbPath: user specified path to `compile_commands.json` to consider as input - d.enableCcdbLinting: whether to activate linting of CCDB files Linting works by actually compiling the file using command specified in CCDB and scanning for compiler error. --- source/served/commands/ccdb.d | 84 +++++++ source/served/commands/format.d | 1 + source/served/extension.d | 212 ++++++++++++------ source/served/linters/ccdb.d | 178 +++++++++++++++ source/served/linters/diagnosticmanager.d | 2 +- source/served/types.d | 2 + views/de.txt | 1 + views/en.txt | 1 + views/fr.txt | 3 +- workspace-d/source/workspaced/com/ccdb.d | 253 +++++++++++++++------- workspace-d/source/workspaced/com/dub.d | 73 ++++--- 11 files changed, 630 insertions(+), 180 deletions(-) create mode 100644 source/served/commands/ccdb.d create mode 100644 source/served/linters/ccdb.d diff --git a/source/served/commands/ccdb.d b/source/served/commands/ccdb.d new file mode 100644 index 00000000..e9df629b --- /dev/null +++ b/source/served/commands/ccdb.d @@ -0,0 +1,84 @@ +/// Clang compilation database (aka. compile_commands.json) related functions +module served.commands.ccdb; + +import served.io.nothrow_fs; +import served.lsp.protocol; +import served.lsp.uri; +import served.types; +import served.utils.events; + +import std.experimental.logger; +import fs = std.file; +import std.path; + +import workspaced.api; +import workspaced.coms; + +string discoverCcdb(string root) +{ + import std.algorithm : count, map, sort; + import std.array : array; + + trace("discovering CCDB in ", root); + + if (fs.exists(chainPath(root, "compile_commands.json"))) + return buildNormalizedPath(root, "compile_commands.json"); + + string[] dbs = tryDirEntries(root, "compile_commands.json", fs.SpanMode.breadth) + .map!(e => buildNormalizedPath(e.name)) + .array; + + // using in priority: + // - those which have fewer directory depth + // - lexical order + dbs.sort!((a, b) { + const depthA = count(a, dirSeparator); + const depthB = count(b, dirSeparator); + if (depthA != depthB) + return depthA < depthB; + return a < b; + }); + + tracef("discovered following CCDB:%-(\n - %s%)", dbs); + + return dbs.length ? dbs[0] : null; +} + +@protocolNotification("workspace/didChangeWatchedFiles") +void onCcdbFileChange(DidChangeWatchedFilesParams params) +{ + import std.algorithm : endsWith, map; + + foreach (c; params.changes) + { + trace("watched file did change: ", c); + + if (!c.uri.endsWith("compile_commands.json")) + continue; + + string filename = c.uri.uriToFile; + + auto inst = backend.getBestInstance!ClangCompilationDatabaseComponent(filename); + if (!inst) + continue; + + string ccdbPath = inst.get!ClangCompilationDatabaseComponent.getDbPath(); + if (!ccdbPath) + continue; + + filename = filename.buildNormalizedPath(); + + if (filename == ccdbPath) + { + if (c.type == FileChangeType.deleted) + { + filename = discoverCcdb(inst.cwd); + tracef("CCDB file deleted. Switching from %s to %s", ccdbPath, filename ? filename + : "(null)"); + } + + tracef("will (re)load %s", filename); + inst.get!ClangCompilationDatabaseComponent.setDbPath(filename); + } + } +} diff --git a/source/served/commands/format.d b/source/served/commands/format.d index f7dbf35a..e31c8520 100644 --- a/source/served/commands/format.d +++ b/source/served/commands/format.d @@ -8,6 +8,7 @@ import workspaced.com.snippets : SnippetLevel; import workspaced.coms; import std.conv : to; +import std.experimental.logger; import std.json; import std.string; diff --git a/source/served/extension.d b/source/served/extension.d index d36c536a..64051f19 100644 --- a/source/served/extension.d +++ b/source/served/extension.d @@ -21,7 +21,7 @@ import std.format : format; import std.functional : toDelegate; import std.meta : AliasSeq; import std.path : baseName, buildNormalizedPath, buildPath, chainPath, dirName, - globMatch, relativePath; + dirSeparator, globMatch, relativePath; import std.string : join; import io = std.stdio; @@ -32,6 +32,7 @@ import workspaced.coms; // list of all commands for auto dispatch public import served.commands.calltips; +public import served.commands.ccdb; public import served.commands.code_actions; public import served.commands.code_lens; public import served.commands.color; @@ -111,6 +112,14 @@ void changedConfig(ConfigWorkspace target, string[] paths, served.types.Configur backend.get!DCDComponent(workspaceFs) .addImports(config.d.projectImportPaths.map!(a => a.userPath).array); break; + case "d.ccdbPath": + if (config.d.ccdbPath.length && + backend.has!ClangCompilationDatabaseComponent(workspaceFs)) + { + const ccdbPath = config.d.ccdbPath.userPath.buildNormalizedPath(); + backend.get!ClangCompilationDatabaseComponent(workspaceFs).setDbPath(ccdbPath); + } + break; case "d.dubConfiguration": if (backend.has!DubComponent(workspaceFs)) { @@ -124,7 +133,7 @@ void changedConfig(ConfigWorkspace target, string[] paths, served.types.Configur { if (!configs.canFind(defaultConfig)) rpc.window.showErrorMessage( - translate!"d.ext.config.invalid.configuration"(defaultConfig)); + translate!"d.ext.config.invalid.configuration"(defaultConfig)); else backend.get!DubComponent(workspaceFs).setConfiguration(defaultConfig); } @@ -137,19 +146,19 @@ void changedConfig(ConfigWorkspace target, string[] paths, served.types.Configur if (backend.has!DubComponent(workspaceFs) && config.d.dubArchType.length && !backend.get!DubComponent(workspaceFs).setArchType(config.d.dubArchType)) rpc.window.showErrorMessage( - translate!"d.ext.config.invalid.archType"(config.d.dubArchType)); + translate!"d.ext.config.invalid.archType"(config.d.dubArchType)); break; case "d.dubBuildType": if (backend.has!DubComponent(workspaceFs) && config.d.dubBuildType.length && !backend.get!DubComponent(workspaceFs).setBuildType(config.d.dubBuildType)) rpc.window.showErrorMessage( - translate!"d.ext.config.invalid.buildType"(config.d.dubBuildType)); + translate!"d.ext.config.invalid.buildType"(config.d.dubBuildType)); break; case "d.dubCompiler": if (backend.has!DubComponent(workspaceFs) && config.d.dubCompiler.length && !backend.get!DubComponent(workspaceFs).setCompiler(config.d.dubCompiler)) rpc.window.showErrorMessage( - translate!"d.ext.config.invalid.compiler"(config.d.dubCompiler)); + translate!"d.ext.config.invalid.compiler"(config.d.dubCompiler)); break; case "d.enableAutoComplete": if (config.d.enableAutoComplete) @@ -170,9 +179,11 @@ void changedConfig(ConfigWorkspace target, string[] paths, served.types.Configur { import served.linters.dscanner : clear1 = clear; import served.linters.dub : clear2 = clear; + import served.linters.ccdb : clear3 = clear; clear1(); clear2(); + clear3(); } break; case "d.enableStaticLinting": @@ -191,13 +202,21 @@ void changedConfig(ConfigWorkspace target, string[] paths, served.types.Configur clear(); } break; + case "d.enableCcdbLinting": + if (!config.d.enableCcdbLinting) + { + import served.linters.ccdb : clear; + + clear(); + } + break; default: break; } } trace("Finished config change of ", target.uri, " with ", paths.length, - " changes in ", sw.peek, "."); + " changes in ", sw.peek, "."); } @onConfigFinished @@ -214,7 +233,7 @@ string[] getPossibleSourceRoots(string workspaceFolder) import std.file; auto confPaths = config(workspaceFolder.uriFromFile, false).d.projectImportPaths.map!( - a => a.isAbsolute ? a : buildNormalizedPath(workspaceRoot, a)); + a => a.isAbsolute ? a : buildNormalizedPath(workspaceRoot, a)); if (!confPaths.empty) return confPaths.array; auto a = buildNormalizedPath(workspaceFolder, "source"); @@ -277,9 +296,7 @@ InitializeResult initialize(InitializeParams params) SignatureHelpOptions signatureHelpProvider = { triggerCharacters: ["(", "[", ","] }; - CodeLensOptions codeLensProvider = { - resolveProvider: true - }; + CodeLensOptions codeLensProvider = {resolveProvider: true}; WorkspaceFoldersServerCapabilities workspaceFolderCapabilities = { supported: true, changeNotifications: true @@ -313,7 +330,9 @@ InitializeResult initialize(InitializeParams params) }; result.capabilities = serverCapabilities; - version (unittest) {} + version (unittest) + { + } else { // only included in non-test builds, because served.info is excluded from the unittest configurations @@ -379,6 +398,8 @@ void doGlobalStartup(UserConfiguration config) backend.register!DubComponent(false); trace("Registering fsworkspace"); backend.register!FSWorkspaceComponent(false); + trace("Registering ccdb"); + backend.register!ClangCompilationDatabaseComponent; trace("Registering dcd"); backend.register!DCDComponent; trace("Registering dcdext"); @@ -416,11 +437,11 @@ void doGlobalStartup(UserConfiguration config) ? backend.get!DCDComponent.serverInstalledVersion : "none"; string outdatedMessage = translate!"d.served.outdatedDCD"( - DCDComponent.latestKnownVersion.to!(string[]).join("."), installed); + DCDComponent.latestKnownVersion.to!(string[]).join("."), installed); dcdUpdating = true; dcdUpdateReason = format!"DCD is outdated. Expected: %(%s.%), got %s"( - DCDComponent.latestKnownVersion, installed); + DCDComponent.latestKnownVersion, installed); if (config.d.aggressiveUpdate) spawnFiber((&updateDCD).toDelegate); else @@ -433,8 +454,8 @@ void doGlobalStartup(UserConfiguration config) action = translate!"d.ext.downloadProgram"("DCD"); auto res = rpc.window.requestMessage(MessageType.error, outdatedMessage, [ - action - ]); + action + ]); if (res == action) spawnFiber((&updateDCD).toDelegate); @@ -442,7 +463,7 @@ void doGlobalStartup(UserConfiguration config) } } - cast(void)emitExtensionEvent!onRegisteredComponents; + cast(void) emitExtensionEvent!onRegisteredComponents; } catch (Exception e) { @@ -462,8 +483,7 @@ struct RootSuggestion bool useDub; } -RootSuggestion[] rootsForProject(string root, bool recursive, string[] blocked, - string[] extra) +RootSuggestion[] rootsForProject(string root, bool recursive, string[] blocked, string[] extra) { RootSuggestion[] ret; void addSuggestion(string dir, bool useDub) @@ -494,7 +514,8 @@ RootSuggestion[] rootsForProject(string root, bool recursive, string[] blocked, if (recursive) { - PackageDescriptorLoop: foreach (pkg; tryDirEntries(root, "dub.{json,sdl}", fs.SpanMode.breadth)) + PackageDescriptorLoop: foreach (pkg; tryDirEntries(root, "dub.{json,sdl}", fs + .SpanMode.breadth)) { auto dir = dirName(pkg); if (dir.canFind(".dub")) @@ -539,11 +560,12 @@ void doStartup(string workspaceUri, UserConfiguration userConfig) scope roots = appender!(Root[]); auto rootSuggestions = rootsForProject(workspaceUri.uriToFile, proj.config.d.scanAllFolders, - proj.config.d.disabledRootGlobs, proj.config.d.extraRoots); + proj.config.d.disabledRootGlobs, proj.config.d.extraRoots); foreach (i, root; rootSuggestions) { - reportProgress(ProgressType.workspaceStartup, i, rootSuggestions.length, root.dir.uriFromFile); + reportProgress(ProgressType.workspaceStartup, i, rootSuggestions.length, root + .dir.uriFromFile); info("registering instance for root ", root); auto workspaceRoot = root.dir; @@ -554,9 +576,10 @@ void doStartup(string workspaceUri, UserConfiguration userConfig) "serverPath": WConfiguration.ValueT(proj.config.dcdServerPath.userPath), "port": WConfiguration.ValueT(9166) ]), - "dmd": WConfiguration.Section([ - "path": WConfiguration.ValueT(proj.config.d.dmdPath.userPath) - ]) + "dmd": WConfiguration.Section( + [ + "path": WConfiguration.ValueT(proj.config.d.dmdPath.userPath) + ]) ]; auto instance = backend.addInstance(workspaceRoot, config); if (!activeInstance) @@ -565,7 +588,7 @@ void doStartup(string workspaceUri, UserConfiguration userConfig) roots ~= Root(root, workspaceUri, instance); emitExtensionEvent!onProjectAvailable(instance, workspaceRoot, workspaceUri); - if (auto lazyInstance = cast(LazyWorkspaceD.LazyInstance)instance) + if (auto lazyInstance = cast(LazyWorkspaceD.LazyInstance) instance) { auto lazyLoadCallback(WorkspaceD.Instance instance, string workspaceRoot, string workspaceUri, RootSuggestion root) { @@ -586,7 +609,7 @@ void doStartup(string workspaceUri, UserConfiguration userConfig) foreach (i, root; roots.data) { reportProgress(ProgressType.completionStartup, i, roots.data.length, - root.instance.cwd.uriFromFile); + root.instance.cwd.uriFromFile); lazyStartDCDServer(root.instance, root.uri); } @@ -618,8 +641,8 @@ void delayedProjectActivation(WorkspaceD.Instance instance, string workspaceRoot auto loadButton = translate!"d.served.tooManySubprojects.load"; auto skipButton = translate!"d.served.tooManySubprojects.skip"; auto res = rpc.window.requestMessage(MessageType.warning, - translate!"d.served.tooManySubprojects.path"(root.dir), - [loadButton, skipButton]); + translate!"d.served.tooManySubprojects.path"(root.dir), + [loadButton, skipButton]); if (res != loadButton) goto case ManyProjectsAction.skip; break; @@ -640,6 +663,19 @@ void delayedProjectActivation(WorkspaceD.Instance instance, string workspaceRoot emitExtensionEvent!onAddingProject(instance, workspaceRoot, workspaceUri); + scope (success) + { + trace("Started files provider for root ", root); + + trace("Loaded Components for ", instance.cwd, ": ", + instance.instanceComponents.map!"a.info.name"); + + emitExtensionEvent!onAddedProject(instance, workspaceRoot, workspaceUri); + + rootTimer.stop(); + info("Root ", root, " initialized in ", rootTimer.peek); + } + bool disableDub = proj.config.d.neverUseDub || !root.useDub; bool loadedDub; Exception err; @@ -668,44 +704,69 @@ void delayedProjectActivation(WorkspaceD.Instance instance, string workspaceRoot } if (!loadedDub) + { error("Exception starting dub: ", err); + proj.startupError(workspaceRoot, translate!"d.ext.dubFail"(instance.cwd, err ? err.msg + : "")); + } else - trace("Started dub with root dependencies ", instance.get!DubComponent.rootDependencies); + trace("Started dub with root dependencies ", instance + .get!DubComponent.rootDependencies); } - if (!loadedDub) + + if (loadedDub) + didLoadDubProject(); + + string ccdbPath = proj.config.d.ccdbPath; + if (!ccdbPath.length && !loadedDub) + ccdbPath = discoverCcdb(workspaceRoot); + bool loadedCcdb; + if (ccdbPath.length) { - if (!disableDub) + trace("starting CCDB with ", ccdbPath); + + try { - error("Failed starting dub in ", root, " - falling back to fsworkspace"); - proj.startupError(workspaceRoot, translate!"d.ext.dubFail"(instance.cwd, err ? err.msg : "")); + if (backend.attachEager(instance, "ccdb", err)) + { + instance.get!ClangCompilationDatabaseComponent.setDbPath(ccdbPath); + loadedCcdb = true; + } } - try + catch (Exception ex) { - trace("Starting fsworkspace..."); - - instance.config.set("fsworkspace", "additionalPaths", - getPossibleSourceRoots(workspaceRoot)); - if (!backend.attachEager(instance, "fsworkspace", err)) - throw new Exception("Attach returned failure: " ~ err.msg); + err = ex; } - catch (Exception e) + if (!loadedCcdb) { - error(e); - proj.startupError(workspaceRoot, translate!"d.ext.fsworkspaceFail"(instance.cwd)); + + error("Exception loading CCDB: ", err); + proj.startupError(workspaceRoot, translate!"d.ext.ccdbFail"(instance.cwd, err ? err.msg : "")); } + else + trace("Initialized CCDB with import paths ", instance + .get!ClangCompilationDatabaseComponent.importPaths); } - else - didLoadDubProject(); - trace("Started files provider for root ", root); + if (loadedDub || loadedCcdb) + return; - trace("Loaded Components for ", instance.cwd, ": ", - instance.instanceComponents.map!"a.info.name"); + error("Failed starting dub or CCDB in ", root, " - falling back to fsworkspace"); - emitExtensionEvent!onAddedProject(instance, workspaceRoot, workspaceUri); + try + { + trace("Starting fsworkspace..."); - rootTimer.stop(); - info("Root ", root, " initialized in ", rootTimer.peek); + instance.config.set("fsworkspace", "additionalPaths", + getPossibleSourceRoots(workspaceRoot)); + if (!backend.attachEager(instance, "fsworkspace", err)) + throw new Exception("Attach returned failure: " ~ err.msg); + } + catch (Exception e) + { + error(e); + proj.startupError(workspaceRoot, translate!"d.ext.fsworkspaceFail"(instance.cwd)); + } } void didLoadDubProject() @@ -732,6 +793,7 @@ class MessageHandler : IMessageHandler void warn(WorkspaceD.Instance instance, string component, int id, string message, string details = null) { + warningf("[%s] com=%s: %s: %s %s", instance ? instance.cwd : "global", component, id, message, details); @@ -740,6 +802,7 @@ class MessageHandler : IMessageHandler void error(WorkspaceD.Instance instance, string component, int id, string message, string details = null) { + errorf("[%s] com=%s: %s: %s %s", instance ? instance.cwd : "global", component, id, message, details); @@ -750,9 +813,7 @@ class MessageHandler : IMessageHandler { if (component == "dcd") { - spawnFiber(() { - startDCDServer(instance, instance.cwd.uriFromFile); - }); + spawnFiber(() { startDCDServer(instance, instance.cwd.uriFromFile); }); } } } @@ -806,10 +867,10 @@ void startDCDServer(WorkspaceD.Instance instance, string workspaceUri) void lazyStartDCDServer(WorkspaceD.Instance instance, string workspaceUri) { - auto lazyInstance = cast(LazyWorkspaceD.LazyInstance)instance; + auto lazyInstance = cast(LazyWorkspaceD.LazyInstance) instance; if (lazyInstance) { - lazyInstance.onLazyLoad("dcd", delegate() nothrow { + lazyInstance.onLazyLoad("dcd", delegate() nothrow{ try { reportProgress(ProgressType.importReload, 0, 1, workspaceUri); @@ -837,6 +898,7 @@ string determineOutputFolder() { import std.process : environment; + // dfmt off version (linux) { if (fs.exists(buildPath(environment["HOME"], ".local", "share"))) @@ -852,6 +914,7 @@ string determineOutputFolder() { return buildPath(environment["HOME"], ".code-d", "bin"); } + // dfmt on } @protocolMethod("shutdown") @@ -893,7 +956,10 @@ void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) @protocolNotification("textDocument/didOpen") void onDidOpenDocument(DidOpenTextDocumentParams params) { - string lintSetting = config(params.textDocument.uri).d.lintOnFileOpen; + auto config = workspace(params.textDocument.uri).config; + auto document = documents[params.textDocument.uri]; + + string lintSetting = config.d.lintOnFileOpen; bool shouldLint; if (lintSetting == "always") shouldLint = true; @@ -901,8 +967,18 @@ void onDidOpenDocument(DidOpenTextDocumentParams params) shouldLint = workspaceIndex(params.textDocument.uri) != size_t.max; if (shouldLint) + { onDidChangeDocument(DidChangeTextDocumentParams( - VersionedTextDocumentIdentifier(params.textDocument.uri, params.textDocument.version_))); + VersionedTextDocumentIdentifier( + params.textDocument.uri, params.textDocument.version_))); + + if (config.d.enableCcdbLinting && document.languageId == "d") + { + import served.linters.ccdb; + + lint(document); + } + } } @protocolNotification("textDocument/didClose") @@ -971,7 +1047,7 @@ DScannerIniSection[] getDscannerConfig(SimpleTextDocumentIdentifierParams params import served.linters.dscanner : getDscannerIniForDocument; auto instance = backend.getBestInstance!DscannerComponent( - params.textDocument.uri.uriToFile); + params.textDocument.uri.uriToFile); if (!instance) return null; @@ -1057,10 +1133,8 @@ ServedInfoResponse getServedInfo(ServedInfoParams params) @protocolNotification("textDocument/didSave") void onDidSaveDocument(DidSaveTextDocumentParams params) { - auto workspaceRoot = workspaceRootFor(params.textDocument.uri); auto config = workspace(params.textDocument.uri).config; auto document = documents[params.textDocument.uri]; - auto fileName = params.textDocument.uri.uriToFile.baseName; if (document.getLanguageId == "d" || document.getLanguageId == "diet") { @@ -1079,6 +1153,13 @@ void onDidSaveDocument(DidSaveTextDocumentParams params) { import served.linters.dub; + lint(document); + } + }, { + if (config.d.enableCcdbLinting && document.languageId == "d") + { + import served.linters.ccdb; + lint(document); } }); @@ -1088,6 +1169,7 @@ void onDidSaveDocument(DidSaveTextDocumentParams params) shared static this() { import core.time : MonoTime; + startupTime = MonoTime.currTime(); } @@ -1102,7 +1184,8 @@ shared static this() if (factory.info.name == "dcd") { - error("Failed to attach DCD component to ", instance ? instance.cwd : null, ": ", err.msg); + error("Failed to attach DCD component to ", instance ? instance.cwd + : null, ": ", err.msg); if (instance && !dcdUpdating) instance.config.set("dcd", "errorlog", instance.config.get("dcd", "errorlog", "") ~ "\n" ~ err.msg); @@ -1110,11 +1193,11 @@ shared static this() } tracef("bind fail:\n\tinstance %s\n\tfactory %s\n\tstacktrace:\n%s\n------", - instance, factory.info.name, err); + instance, factory.info.name, err); if (instance) { rpc.window.showErrorMessage( - "Failed to load component " ~ factory.info.name ~ " for workspace " + "Failed to load component " ~ factory.info.name ~ " for workspace " ~ instance.cwd ~ "\n\nError: " ~ err.msg); } }; @@ -1131,6 +1214,7 @@ shared static ~this() //dfmt off alias memberModules = AliasSeq!( served.commands.calltips, + served.commands.ccdb, served.commands.code_actions, served.commands.code_lens, served.commands.color, diff --git a/source/served/linters/ccdb.d b/source/served/linters/ccdb.d new file mode 100644 index 00000000..684143a6 --- /dev/null +++ b/source/served/linters/ccdb.d @@ -0,0 +1,178 @@ +/// Linting module for ClangCompilationDatabase +/// +/// This linter will simply run the command in the `compile_commands.json` file +/// and scan the output for errors. +module served.linters.ccdb; + +import served.linters.diagnosticmanager; +import served.types; + +import workspaced.api; +import workspaced.coms; + +import std.algorithm; +import std.experimental.logger; +import std.file; +import std.process; +import std.range; + +enum DiagnosticSlot = 3; +enum CcdbDiagnosticSource = "compile_commands.json"; + +private struct DocLinterStatus +{ + bool running; + bool retryAtEnd; +} + +DocLinterStatus[DocumentUri] linterStatus; + +void lint(Document document) +{ + void removeForDoc() + { + auto diag = diagnostics[DiagnosticSlot]; + diag = diag.remove!(d => d.uri == document.uri); + diagnostics[DiagnosticSlot] = diag; + } + + void noErrors() + { + removeForDoc(); + updateDiagnostics(); + } + + auto instance = activeInstance = backend.getBestInstance!ClangCompilationDatabaseComponent( + document.uri.uriToFile); + if (!instance) + return noErrors(); + + auto fileConfig = config(document.uri); + if (!fileConfig.d.enableLinting || !fileConfig.d.enableCcdbLinting) + return noErrors(); + + auto command = instance.get!ClangCompilationDatabaseComponent.getCompileCommand( + uriToFile(document.uri)); + if (!command) + { + auto dbPath = instance.get!ClangCompilationDatabaseComponent.getDbPath(); + warningf("No command entry for %s in CCDB %s", uriToFile(document.uri), dbPath); + return noErrors(); + } + + auto statusp = document.uri in linterStatus; + if (!statusp) + { + linterStatus[document.uri] = DocLinterStatus(); + statusp = document.uri in linterStatus; + } + assert(statusp); + + if (statusp.running) + { + statusp.retryAtEnd = true; + return; + } + + statusp.running = true; + scope (exit) + statusp.running = false; + + do + { + statusp.retryAtEnd = false; + + tracef("running CCDB command for ", document.uri); + auto issues = command.run().getYield(); + auto result = appender!(PublishDiagnosticsParams[]); + + void pushError(Diagnostic error, string uri) + { + bool found; + foreach (ref elem; result.data) + if (elem.uri == uri) + { + found = true; + elem.diagnostics ~= error; + } + if (!found) + result ~= PublishDiagnosticsParams(uri, [error]); + } + + while (!issues.empty) + { + import served.linters.dub : applyDubLintType; + + auto issue = issues.front; + issues.popFront(); + tracef("issue %s", issue); + int numSupplemental = cast(int) issues.length; + foreach (i, other; issues) + if (!other.cont) + { + numSupplemental = cast(int) i; + break; + } + auto supplemental = issues[0 .. numSupplemental]; + if (numSupplemental > 0) + issues = issues[numSupplemental .. $]; + + auto uri = uriFromFile(command.getPath(issue.file)); + + Diagnostic error; + error.range = TextRange(issue.line - 1, issue.column - 1, issue.line - 1, uint.max); + applyDubLintType(error, issue.type); + error.source = CcdbDiagnosticSource; + error.message = issue.text; + if (supplemental.length) + error.relatedInformation = opt(supplemental.map!((other) { + DiagnosticRelatedInformation related; + string otherUri = other.file != issue.file ? command.getPath( + other.file) : uri; + related.location = Location(otherUri, TextRange(other.line - 1, + other.column - 1, other.line - 1, uint.max)); + related.message = other.text; + return related; + }).array); + + //extendErrorRange(error.range, instance, uri, error); + pushError(error, uri); + + foreach (i, suppl; supplemental) + { + if (suppl.text.startsWith("instantiated from here:")) + { + // add all "instantiated from here" errors in the project as diagnostics + + auto supplUri = issue.file != suppl.file ? uriFromFile( + command.getPath(suppl.file)) : uri; + + if (workspaceIndex(supplUri) == size_t.max) + continue; + + Diagnostic supplError; + supplError.range = TextRange(suppl.line - 1, suppl.column - 1, suppl.line - 1, uint + .max); + applyDubLintType(supplError, issue.type); + supplError.source = CcdbDiagnosticSource; + supplError.message = issue.text ~ "\n" ~ suppl.text; + if (i + 1 < supplemental.length) + supplError.relatedInformation = opt( + error.relatedInformation.deref[i + 1 .. $]); + pushError(supplError, supplUri); + } + } + } + + removeForDoc(); + diagnostics[DiagnosticSlot] ~= result.data; + updateDiagnostics(); + } + while (statusp.retryAtEnd); +} + +void clear() +{ + diagnostics[DiagnosticSlot] = null; + updateDiagnostics(); +} diff --git a/source/served/linters/diagnosticmanager.d b/source/served/linters/diagnosticmanager.d index 8f2dd317..9e4edb7f 100644 --- a/source/served/linters/diagnosticmanager.d +++ b/source/served/linters/diagnosticmanager.d @@ -6,7 +6,7 @@ import std.algorithm : map, sort; import served.utils.memory; import served.types; -enum NumDiagnosticProviders = 3; +enum NumDiagnosticProviders = 4; alias DiagnosticCollection = PublishDiagnosticsParams[]; DiagnosticCollection[NumDiagnosticProviders] diagnostics; diff --git a/source/served/types.d b/source/served/types.d index a6d0edef..55968d2a 100644 --- a/source/served/types.d +++ b/source/served/types.d @@ -80,6 +80,7 @@ struct Configuration bool enableSDLLinting = true; bool enableStaticLinting = true; bool enableDubLinting = true; + bool enableCcdbLinting = true; bool enableAutoComplete = true; bool enableAutoImportCompletions = true; bool enableFormatting = true; @@ -89,6 +90,7 @@ struct Configuration bool enableGCProfilerDecorations = true; bool neverUseDub = false; string[] projectImportPaths; + string ccdbPath; string dubConfiguration; string dubArchType; string dubBuildType; diff --git a/views/de.txt b/views/de.txt index ab71b50e..e7fe0c07 100644 --- a/views/de.txt +++ b/views/de.txt @@ -105,6 +105,7 @@ d.ext.dubInvalidRecipeSyntax: Im dub.json/dub.sdl Rezept wurde ein Fehler gefund d.ext.dubRecipeMaybeBroken: Konnte dub nicht starten. Das dub.json/dub.sdl Rezept könnte möglicherweise Fehlerhaft sein! Bitte Fehler beheben und neu speichern. d.ext.dubUpgradeFail: Konnte nicht dub upgrade durchführen d.ext.dubImportFail: Konnte keine Importpfäde finden. Bitte Einstellungen in der Statusleiste überprüfen. +d.ext.ccdbFail: Konnte die Clang Kompilierung Datenbank (compile_commands.json) für {0} nicht starten ({1}). Funktionalität begrenzt! d.ext.configSwitchFail: Konfiguration konnte nicht gewechselt werden. In die Entwicklertools umschalten für Details. d.ext.archSwitchFail: Architektur konnte nicht gewechselt werden. In die Entwicklertools umschalten für Details. d.ext.buildTypeSwitchFail: Build Type konnte nicht gewechselt werden. In die Entwicklertools umschalten für Details. diff --git a/views/en.txt b/views/en.txt index a6a92805..061feb8e 100644 --- a/views/en.txt +++ b/views/en.txt @@ -105,6 +105,7 @@ d.ext.dubInvalidRecipeSyntax: There is an issue in your dub.json/dub.sdl package d.ext.dubRecipeMaybeBroken: Dub could not be started. Your dub.json/dub.sdl package recipe might be faulty! Fix it and save the file again. d.ext.dubUpgradeFail: Could not upgrade dub project d.ext.dubImportFail: Could not update import paths. Please check your build settings in the status bar. +d.ext.ccdbFail: Could not initialize Clang Compilation Database (compile_commands.json) for {0} ({1}). Falling back to limited functionality! d.ext.configSwitchFail: Failed to switch configuration. See console for details. d.ext.archSwitchFail: Failed to switch arch type. See console for details. d.ext.buildTypeSwitchFail: Failed to switch build type. See console for details. diff --git a/views/fr.txt b/views/fr.txt index d207d8ab..c35b19a3 100644 --- a/views/fr.txt +++ b/views/fr.txt @@ -90,7 +90,8 @@ d.ext.config.invalid.archType: L'architecture '{0}' spécifiée dans la configur d.ext.config.invalid.buildType: Le type de construction '{0}' spécifié dans la configuration n'est pas disponible ! d.ext.config.invalid.compiler: Le compilateur '{0}' spécifié dans la configuration n'est pas disponible ! d.ext.dubFail : Dub n'a pas pu être initialisé pour {0}. Vous n'aurez pas accès à toutes les fonctionnalités.\n\n{1} -d.ext.fsworkspaceFail: Dub n'a pas pu être initialisé fsworkspace pour {0}. Regardez votre console pour plus de détails +d.ext.ccdbFail : La base de données de compilation Clang (compile_commands.json) n'a pas pu être initialisée pour {0} ({1}). Vous n'aurez pas accès à toutes les fonctionnalités. +d.ext.fsworkspaceFail: fsworkspace n'a pas pu être initialisé pour {0}. Regardez votre console pour plus de détails. d.ext.dcdFail: DCD n'a pas pu être initialisé pour l'espace de travail {0}.{1} d.ext.gcLens: {0} octets alloués / {1} allocations d.ext.stdlibNoPhobosNoDRuntime: Le d.stdlibPath configuré ne contient pas de chemin vers phobos ou druntime. L'autocomplétion pourra manquer certains symboles ! diff --git a/workspace-d/source/workspaced/com/ccdb.d b/workspace-d/source/workspaced/com/ccdb.d index feb30982..e7214033 100644 --- a/workspace-d/source/workspaced/com/ccdb.d +++ b/workspace-d/source/workspaced/com/ccdb.d @@ -4,14 +4,15 @@ module workspaced.com.ccdb; import std.exception; -import std.experimental.logger : trace; -import std.file; import std.json; -import std.stdio; +import std.path; +import fs = std.file; import workspaced.api; +import workspaced.com.dcd; import containers.hashset; +import workspaced.com.dub; @component("ccdb") class ClangCompilationDatabaseComponent : ComponentWrapper @@ -22,6 +23,9 @@ class ClangCompilationDatabaseComponent : ComponentWrapper { trace("loading ccdb component"); + if (!refInstance) + throw new Exception("ccdb requires to be instanced"); + if (config.get!bool("ccdb", "registerImportProvider", true)) importPathProvider = &imports; if (config.get!bool("ccdb", "registerStringImportProvider", true)) @@ -33,35 +37,36 @@ class ClangCompilationDatabaseComponent : ComponentWrapper if (config.get!bool("ccdb", "registerDebugSpecificationsProvider", true)) debugSpecificationsProvider = &debugVersions; - try - { - if (config.get!string("ccdb", null)) - { - const dbPath = config.get!string("ccdb", "dbPath"); - if (!dbPath) - { - throw new Exception("ccdb.dbPath is not provided"); - } - loadDb(dbPath); - } - } - catch (Exception e) - { - stderr.writeln("Clang-DB Error (ignored): ", e); - } + if (auto dbPath = config.get!string("ccdb", "dbPath", null)) + loadDb(dbPath); + } + + void setDbPath(string dbPath) + { + import std.path : buildNormalizedPath; + + if (dbPath.length) + loadDb(dbPath); + else + unloadDb(); + + config.set("ccdb", "dbPath", dbPath.buildNormalizedPath()); + + if (refInstance.has!DCDComponent) + refInstance.get!DCDComponent.refreshImports(); + } + + string getDbPath() const + { + return config.get!string("ccdb", "dbPath", null); } - private void loadDb(string filename) + private void loadDb(string dbPath) { import std.algorithm : each, filter, map; import std.array : array; - string jsonString = cast(string) assumeUnique(read(filename)); - auto json = parseJSON(jsonString); - // clang db can be quite large (e.g. 100 k lines of JSON data on large projects) - // we release memory when possible to avoid having at the same time more than - // two represention of the same data - jsonString = null; + trace("parsing CCDB from ", dbPath); HashSet!string imports; HashSet!string stringImports; @@ -69,12 +74,26 @@ class ClangCompilationDatabaseComponent : ComponentWrapper HashSet!string versions; HashSet!string debugVersions; - json.array - .map!(jv => CompileCommand.fromJson(jv)) - .filter!(cc => cc.isValid) - .each!(cc => - cc.feedOptions(imports, stringImports, fileImports, versions, debugVersions) - ); + _compileCommands.clear(); + + { + string jsonString = cast(string) assumeUnique(fs.read(dbPath)); + auto json = parseJSON(jsonString); + // clang db can be quite large (e.g. 100 k lines of JSON data on large projects) + // we release memory when possible to avoid having at the same time more than + // two represention of the same data + jsonString = null; + + auto ccRng = json.array + .map!(jv => CompileCommand.fromJson(jv)) + .filter!(cc => cc.isValid); + + foreach (cc; ccRng) + { + cc.feedOptions(imports, stringImports, fileImports, versions, debugVersions); + _compileCommands[cc.getNormalizedFilePath()] = cc; + } + } _importPaths = imports[].array; _stringImportPaths = stringImports[].array; @@ -83,6 +102,16 @@ class ClangCompilationDatabaseComponent : ComponentWrapper _debugVersions = debugVersions[].array; } + private void unloadDb() + { + _importPaths = null; + _stringImportPaths = null; + _importFiles = null; + _versions = null; + _debugVersions = null; + _compileCommands.clear(); + } + /// Lists all import paths string[] imports() @property nothrow { @@ -113,27 +142,41 @@ class ClangCompilationDatabaseComponent : ComponentWrapper return _debugVersions; } + /// Return the compile command for the given D source file, or null if this file is not + /// in the database. + CompileCommand getCompileCommand(string filename) @property + { + auto normalized = buildNormalizedPath(filename); + auto ccp = normalized in _compileCommands; + if (ccp) + return ccp.dup; + return CompileCommand.init; + } + private: string[] _importPaths, _stringImportPaths, _importFiles, _versions, _debugVersions; + CompileCommand[string] _compileCommands; } -private struct CompileCommand +public struct CompileCommand { string directory; string file; string[] args; string output; - static CompileCommand fromJson(JSONValue json) + private static CompileCommand fromJson(JSONValue json) { import std.algorithm : map; import std.array : array; CompileCommand cc; - cc.directory = json["directory"].str; - cc.file = json["file"].str; + cc.directory = enforce("directory" in json, "'directory' missing from Clang compilation database entry") + .str; + cc.file = enforce("file" in json, "'file' missing from Clang compilation database entry") + .str; if (auto args = "arguments" in json) { @@ -146,7 +189,7 @@ private struct CompileCommand else { throw new Exception( - "Either 'arguments' or 'command' missing from Clang compilation database"); + "Either 'arguments' or 'command' missing from Clang compilation database entry"); } if (auto o = "output" in json) @@ -168,63 +211,105 @@ private struct CompileCommand return true; } - void feedOptions( - ref HashSet!string imports, - ref HashSet!string stringImports, - ref HashSet!string fileImports, - ref HashSet!string versions, - ref HashSet!string debugVersions) + bool opCast(T : bool)() const { - import std.algorithm : startsWith; - - enum importMark = "-I"; // optional = - enum stringImportMark = "-J"; // optional = - enum fileImportMark = "-i="; - enum versionMark = "-version="; - enum debugMark = "-debug="; + return isValid; + } - foreach (arg; args) - { - const mark = arg.startsWith( - importMark, stringImportMark, fileImportMark, versionMark, debugMark - ); + @property CompileCommand dup() const + { + return CompileCommand(directory, file, args.dup, output); + } - switch (mark) - { - case 0: - break; - case 1: - case 2: - if (arg.length == 2) - break; // ill-formed flag, we don't need to care here - const st = arg[2] == '=' ? 3 : 2; - const path = getPath(arg[st .. $]); - if (mark == 1) - imports.put(path); - else - stringImports.put(path); - break; - case 3: - fileImports.put(getPath(arg[fileImportMark.length .. $])); - break; - case 4: - versions.put(getPath(arg[versionMark.length .. $])); - break; - case 5: - debugVersions.put(getPath(arg[debugMark.length .. $])); - break; - default: - break; - } - } + string getNormalizedFilePath() const + { + return getPath(file).buildNormalizedPath(); } - string getPath(string filename) + string getPath(string filename) const { import std.path : absolutePath; return absolutePath(filename, directory); } + + Future!(BuildIssue[]) run() const + { + import std.algorithm : canFind, remove; + import std.process : Config, execute; + + return Future!(BuildIssue[]).async({ + trace("stripping color from ", args); + string[] program = args.dup.remove!(a => a.canFind("-color=on") || a.canFind( + "-enable-color")); + trace("running ", program); + auto res = execute(program, null, Config.none, size_t.max, directory); + trace(res.status, " ", res.output); + auto issues = parseBuildIssues(res.output); + trace("found ", issues.length, " issue(s)!"); + return issues; + }); + } +} + +void feedOptions( + in CompileCommand cc, + ref HashSet!string imports, + ref HashSet!string stringImports, + ref HashSet!string fileImports, + ref HashSet!string versions, + ref HashSet!string debugVersions) +{ + import std.algorithm : startsWith; + + enum importMark = "-I"; // optional = + enum stringImportMark = "-J"; // optional = + enum fileImportMark = "-i="; + enum dmdVersionMark = "-version="; + enum ldcVersionMark = "--d-version="; + enum dmdDebugMark = "-debug="; + enum ldcDebugMark = "--d-debug="; + + foreach (arg; cc.args) + { + const mark = arg.startsWith( + importMark, stringImportMark, fileImportMark, dmdVersionMark, ldcVersionMark, dmdDebugMark, ldcDebugMark, + ); + + switch (mark) + { + case 0: + break; + case 1: + case 2: + if (arg.length == 2) + break; // ill-formed flag, we don't need to care here + const st = arg[2] == '=' ? 3 : 2; + const path = cc.getPath(arg[st .. $]); + if (mark == 1) + imports.put(path); + else + stringImports.put(path); + break; + case 3: + fileImports.put(cc.getPath(arg[fileImportMark.length .. $])); + break; + case 4: + versions.put(arg[dmdVersionMark.length .. $]); + break; + case 5: + versions.put(arg[ldcVersionMark.length .. $]); + break; + case 6: + debugVersions.put(arg[dmdDebugMark.length .. $]); + break; + case 7: + debugVersions.put(arg[ldcDebugMark.length .. $]); + break; + default: + break; + } + } } private string[] unescapeCommand(string cmd) diff --git a/workspace-d/source/workspaced/com/dub.d b/workspace-d/source/workspaced/com/dub.d index f941b99b..907a673c 100644 --- a/workspace-d/source/workspaced/com/dub.d +++ b/workspace-d/source/workspaced/com/dub.d @@ -702,36 +702,8 @@ class DubComponent : ComponentWrapper settings.compileCallback = (status, output) { trace(status, " ", output); - string[] lines = output.splitLines; - foreach (line; lines) - { - auto match = line.matchFirst(errorFormat); - if (match) - { - issues ~= BuildIssue(match[2].to!int, match[3].toOr!int(0), - match[1], match[4].to!ErrorType, match[5]); - } - else - { - auto contMatch = line.matchFirst(errorFormatCont); - if (issues.data.length && contMatch) - { - issues ~= BuildIssue(contMatch[2].to!int, - contMatch[3].toOr!int(1), contMatch[1], - issues.data[$ - 1].type, contMatch[4], true); - } - else if (line.canFind("is deprecated")) - { - auto deprMatch = line.matchFirst(deprecationFormat); - if (deprMatch) - { - issues ~= BuildIssue(deprMatch[2].to!int, deprMatch[3].toOr!int(1), - deprMatch[1], ErrorType.Deprecation, - deprMatch[4] ~ " is deprecated" ~ deprMatch[5]); - } - } - } - } + + appendBuildIssues(output, issues); }; try { @@ -802,6 +774,47 @@ private: string[] _failedPackages; } +BuildIssue[] parseBuildIssues(string buildOutput) +{ + auto issues = appender!(BuildIssue[]); + appendBuildIssues(buildOutput, issues); + return issues.data; +} + +void appendBuildIssues(string buildOutput, ref Appender!(BuildIssue[]) issues) +{ + string[] lines = buildOutput.splitLines; + foreach (line; lines) + { + auto match = line.matchFirst(errorFormat); + if (match) + { + issues ~= BuildIssue(match[2].to!int, match[3].toOr!int(0), + match[1], match[4].to!ErrorType, match[5]); + } + else + { + auto contMatch = line.matchFirst(errorFormatCont); + if (issues.data.length && contMatch) + { + issues ~= BuildIssue(contMatch[2].to!int, + contMatch[3].toOr!int(1), contMatch[1], + issues.data[$ - 1].type, contMatch[4], true); + } + else if (line.canFind("is deprecated")) + { + auto deprMatch = line.matchFirst(deprecationFormat); + if (deprMatch) + { + issues ~= BuildIssue(deprMatch[2].to!int, deprMatch[3].toOr!int(1), + deprMatch[1], ErrorType.Deprecation, + deprMatch[4] ~ " is deprecated" ~ deprMatch[5]); + } + } + } + } +} + /// enum ErrorType : ubyte {