diff --git a/protocol/source/served/lsp/uri.d b/protocol/source/served/lsp/uri.d index 06003dd..5d46e83 100644 --- a/protocol/source/served/lsp/uri.d +++ b/protocol/source/served/lsp/uri.d @@ -14,17 +14,21 @@ private void assertEquals(T)(T a, T b) DocumentUri uriFromFile(scope const(char)[] file) { + import std.ascii : isAlpha, toLower; import std.uri : encodeComponent; if ((!isAbsolute(file) && !file.startsWith("/")) || !file.length) throw new Exception(text("Tried to pass relative path '", file, "' to uriFromFile")); - file = file.buildNormalizedPath.replace("\\", "/"); + file = file.buildNormalizedPath().replace("\\", "/"); assert(file.length); - if (file.ptr[0] != '/') - file = '/' ~ file; // always triple slash at start but never quad slash - if (file.length >= 2 && file[0 .. 2] == "//") // Shares (\\share\bob) are different somehow + if (file.length >= 2 && file[0].isAlpha && file[1] == ':') + file = file[0].toLower ~ file[1 .. $]; + else if (file.length >= 2 && file[0 .. 2] == "//") // Shares (\\share\bob) are different somehow file = file[2 .. $]; + else if (file.ptr[0] != '/') + file = '/' ~ file; // always triple slash at start but never quad slash + return text("file://", file.encodeComponent.replace("%2F", "/")); } @@ -34,7 +38,7 @@ unittest version (Windows) { - + assertEquals(uriFromFile(`C:\Home\foo\bar.d`), `file://c%3A/Home/foo/bar.d`); } else { @@ -283,7 +287,7 @@ unittest { assertEquals(uriNormalize(`b/../a.d`), `a.d`); assertEquals(uriNormalize(`b/../../a.d`), `../a.d`); - + foreach (prefix; ["file:///", "file://", "", "/", "//"]) { assertEquals(uriNormalize(prefix ~ `foo/bar/./a.d`), prefix ~ `foo/bar/a.d`); diff --git a/source/served/commands/ccdb.d b/source/served/commands/ccdb.d new file mode 100644 index 0000000..e9df629 --- /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/extension.d b/source/served/extension.d index 715128e..77d3c44 100644 --- a/source/served/extension.d +++ b/source/served/extension.d @@ -33,6 +33,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; @@ -112,6 +113,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)) { @@ -171,9 +180,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": @@ -192,6 +203,14 @@ 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; } @@ -215,7 +234,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"); @@ -390,6 +409,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"); @@ -762,6 +783,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; @@ -852,44 +886,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 && !proj.config.d.neverUseCcdb) + 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 notifySkippedRoots() @@ -1086,7 +1145,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; @@ -1094,8 +1156,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") @@ -1288,10 +1360,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") { @@ -1310,6 +1380,13 @@ void onDidSaveDocument(DidSaveTextDocumentParams params) { import served.linters.dub; + lint(document); + } + }, { + if (config.d.enableCcdbLinting && document.languageId == "d") + { + import served.linters.ccdb; + lint(document); } }); @@ -1362,6 +1439,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 0000000..2801027 --- /dev/null +++ b/source/served/linters/ccdb.d @@ -0,0 +1,186 @@ +/// 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; + + removeForDoc(); + + do + { + statusp.retryAtEnd = false; + + tracef("running CCDB command for %s", 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(); + 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, issueColumn(issue.column), 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, issueColumn(other.column), 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, issueColumn(suppl.column), 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); +} + +int issueColumn(const int column) pure +{ + return column > 0 ? column - 1 : 0; +} + +void clear() +{ + diagnostics[DiagnosticSlot] = null; + updateDiagnostics(); +} diff --git a/source/served/linters/diagnosticmanager.d b/source/served/linters/diagnosticmanager.d index 8f2dd31..9e4edb7 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 4d2a04a..a9e8b06 100644 --- a/source/served/types.d +++ b/source/served/types.d @@ -87,6 +87,7 @@ struct Configuration bool enableSDLLinting = true; bool enableStaticLinting = true; bool enableDubLinting = true; + bool enableCcdbLinting = true; bool enableAutoComplete = true; bool enableAutoImportCompletions = true; bool enableFormatting = true; @@ -97,7 +98,9 @@ struct Configuration bool enableDCDHighlight = true; bool enableFallbackHighlight = true; bool neverUseDub = false; + bool neverUseCcdb = false; string[] projectImportPaths; + string ccdbPath; string dubConfiguration; string dubArchType; string dubBuildType; diff --git a/test/tc_dub/source/app.d b/test/tc_dub/source/app.d index ac6766b..6bea764 100644 --- a/test/tc_dub/source/app.d +++ b/test/tc_dub/source/app.d @@ -24,10 +24,10 @@ void main() // if no dependencies are fetched // or with all dependencies there a lot more assert(dub.imports.length >= 2, dub.imports.to!string); - assert(dub.stringImports[$ - 1].endsWith("views") - || dub.stringImports[$ - 1].endsWith("views/") - || dub.stringImports[$ - 1].endsWith("views\\"), - dub.stringImports.to!string ~ " doesn't end with `views`!"); + assert(dub.stringImports[0].endsWith("views") + || dub.stringImports[0].endsWith("views/") + || dub.stringImports[0].endsWith("views\\"), + dub.stringImports.to!string ~ "[0] doesn't end with `views`!"); assert(dub.fileImports.length > 10); assert(dub.configurations.length == 2); assert(dub.buildTypes.length); diff --git a/views/de.txt b/views/de.txt index ab71b50..e7fe0c0 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 f60a036..ce9cd23 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 d207d8a..c35b19a 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 feb3098..5807118 100644 --- a/workspace-d/source/workspaced/com/ccdb.d +++ b/workspace-d/source/workspaced/com/ccdb.d @@ -4,14 +4,16 @@ 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 std.string; +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 +24,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 +38,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(); } - private void loadDb(string filename) + string getDbPath() const + { + return config.get!string("ccdb", "dbPath", null); + } + + 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 +75,24 @@ 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; + + json.array + .map!(jv => new CompileCommand(jv)) + .filter!(cc => cc.isValid) + .each!((cc) { + cc.feedOptions(imports, stringImports, fileImports, versions, debugVersions); + _compileCommands[cc.getNormalizedFilePath()] = cc; + }); + } _importPaths = imports[].array; _stringImportPaths = stringImports[].array; @@ -83,6 +101,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,48 +141,62 @@ 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 + { + const normalized = normalizedCcdbPath(filename); + auto ccp = normalized in _compileCommands; + if (ccp) + return *ccp; + return null; + } + private: string[] _importPaths, _stringImportPaths, _importFiles, _versions, _debugVersions; + CompileCommand[string] _compileCommands; } -private struct CompileCommand +public class CompileCommand { string directory; string file; string[] args; string output; - static CompileCommand fromJson(JSONValue json) + private bool argsAdjusted; + + private this(JSONValue json) { import std.algorithm : map; import std.array : array; - CompileCommand cc; - - cc.directory = json["directory"].str; - cc.file = json["file"].str; + this.directory = normalizedCcdbPath( + enforce("directory" in json, "'directory' missing from Clang compilation database entry") + .str + ); + this.file = enforce("file" in json, "'file' missing from Clang compilation database entry") + .str; if (auto args = "arguments" in json) { - cc.args = args.array.map!(jv => jv.str).array; + this.args = args.array.map!(jv => jv.str).array; } else if (auto cmd = "command" in json) { - cc.args = unescapeCommand(cmd.str); + this.args = unescapeCommand(cmd.str); } 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) { - cc.output = o.str; + this.output = o.str; } - - return cc; } @property bool isValid() const @@ -168,62 +210,170 @@ 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) + string getNormalizedFilePath() const + { + return normalizedCcdbPath(getPath(file)); + } + + string getPath(string filename) const + { + import std.path : absolutePath; + + return absolutePath(filename, directory); + } + + Future!(BuildIssue[]) run() + { + import std.process : Config, execute; + + if (!argsAdjusted) + { + argsAdjusted = true; + adjustArgs(); + } + + return Future!(BuildIssue[]).async({ + trace("running ", args); + auto res = execute(args, 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; + }); + } + + private void adjustArgs() nothrow { - import std.algorithm : startsWith; + import std.algorithm : canFind, remove, startsWith; + + // determine compiler + const exe = args.length ? args[0].baseName : null; + if (!exe) + return; + + const isDmd = exe.canFind("dmd"); + const isLdc = !isDmd && exe.canFind("ldc"); + const isGdc = !isDmd && !isLdc && exe.canFind("gdc"); - enum importMark = "-I"; // optional = - enum stringImportMark = "-J"; // optional = - enum fileImportMark = "-i="; - enum versionMark = "-version="; - enum debugMark = "-debug="; + if (!isDmd && !isLdc && !isGdc) + return; - foreach (arg; args) + version (Posix) + enum nulFile = "/dev/null"; + else version (Windows) + enum nulFile = "NUL"; + + if (isDmd || isLdc) { - const mark = arg.startsWith( - importMark, stringImportMark, fileImportMark, versionMark, debugMark - ); + const ofArg = isDmd ? "-of" : "--of="; + const columnsArg = isDmd ? "-vcolumns" : "--vcolumns"; + bool foundColumns; + + for (size_t i = 1; i < args.length; ++i) + { + if (args[i].startsWith(ofArg)) + { + args[i] = ofArg ~ nulFile; + } + else if (args[i] == columnsArg) + { + foundColumns = true; + } + else if (isDmd && args[i].startsWith("-color")) + { + args[i] = "-color=off"; + } + else if (isLdc && args[i].startsWith("--enable-color")) + { + args[i] = "--disable-color"; + } + } - switch (mark) + if (!foundColumns) + args ~= columnsArg; + } + else if (isGdc) + { + for (size_t i = 1; i < args.length; ++i) { - 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; + if (args[i] == "-o" && (i + 1) < args.length) + { + args[i + 1] = nulFile; + break; + } } } } +} + +string normalizedCcdbPath(string filePath) +{ + const normalized = filePath.buildNormalizedPath(); + // Let the drive be lower case on Windows (not handled by buildNormalizedPath). + // This is needed because commands are resolved by simple string comparison. + version (Windows) + return driveName(normalized).toLower() ~ stripDrive(normalized); + else + return normalized; +} + +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; - string getPath(string filename) + 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) { - import std.path : absolutePath; + const mark = arg.startsWith( + importMark, stringImportMark, fileImportMark, dmdVersionMark, ldcVersionMark, dmdDebugMark, ldcDebugMark, + ); - return absolutePath(filename, directory); + 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; + } } } diff --git a/workspace-d/source/workspaced/com/dub.d b/workspace-d/source/workspaced/com/dub.d index c008eb7..4503a88 100644 --- a/workspace-d/source/workspaced/com/dub.d +++ b/workspace-d/source/workspaced/com/dub.d @@ -746,36 +746,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 { @@ -846,6 +818,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 {