diff --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h --- a/clang-tools-extra/clangd/ClangdLSPServer.h +++ b/clang-tools-extra/clangd/ClangdLSPServer.h @@ -277,6 +277,9 @@ bool SupportsOffsetsInSignatureHelp = false; /// Whether the client supports the versioned document changes. bool SupportsDocumentChanges = false; + /// Whether the client supports change annotations on text edits. + bool SupportsChangeAnnotation = false; + std::mutex BackgroundIndexProgressMutex; enum class BackgroundIndexProgress { // Client doesn't support reporting progress. No transitions possible. diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp --- a/clang-tools-extra/clangd/ClangdLSPServer.cpp +++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp @@ -99,19 +99,29 @@ /// Convert from Fix to LSP CodeAction. CodeAction toCodeAction(const Fix &F, const URIForFile &File, const std::optional &Version, - bool SupportsDocumentChanges) { + bool SupportsDocumentChanges, + bool SupportChangeAnnotation) { CodeAction Action; Action.title = F.Message; Action.kind = std::string(CodeAction::QUICKFIX_KIND); Action.edit.emplace(); if (!SupportsDocumentChanges) { Action.edit->changes.emplace(); - (*Action.edit->changes)[File.uri()] = {F.Edits.begin(), F.Edits.end()}; + auto &Changes = (*Action.edit->changes)[File.uri()]; + for (const auto &E : F.Edits) + Changes.push_back({E.range, E.newText, /*annotationId=*/std::nullopt}); } else { Action.edit->documentChanges.emplace(); - TextDocumentEdit& Edit = Action.edit->documentChanges->emplace_back(); + TextDocumentEdit &Edit = Action.edit->documentChanges->emplace_back(); Edit.textDocument = VersionedTextDocumentIdentifier{{File}, Version}; - Edit.edits = {F.Edits.begin(), F.Edits.end()}; + for (const auto &E : F.Edits) + Edit.edits.push_back( + {E.range, E.newText, + SupportChangeAnnotation ? E.annotationId : std::nullopt}); + if (SupportChangeAnnotation) { + for (const auto &[AID, Annotation]: F.Annotations) + Action.edit->changeAnnotations[AID] = Annotation; + } } return Action; } @@ -509,6 +519,7 @@ SupportsReferenceContainer = Params.capabilities.ReferenceContainer; SupportFileStatus = Params.initializationOptions.FileStatus; SupportsDocumentChanges = Params.capabilities.DocumentChanges; + SupportsChangeAnnotation = Params.capabilities.ChangeAnnotation; HoverContentFormat = Params.capabilities.HoverContentFormat; Opts.LineFoldingOnly = Params.capabilities.LineFoldingOnly; SupportsOffsetsInSignatureHelp = Params.capabilities.OffsetsInSignatureHelp; @@ -1742,7 +1753,8 @@ for (const auto &Fix : Fixes) CodeActions.push_back(toCodeAction(Fix, Notification.uri, Notification.version, - SupportsDocumentChanges)); + SupportsDocumentChanges, + SupportsChangeAnnotation)); if (DiagOpts.EmbedFixesInDiagnostics) { Diag.codeActions.emplace(CodeActions); diff --git a/clang-tools-extra/clangd/Diagnostics.h b/clang-tools-extra/clangd/Diagnostics.h --- a/clang-tools-extra/clangd/Diagnostics.h +++ b/clang-tools-extra/clangd/Diagnostics.h @@ -83,6 +83,10 @@ std::string Message; /// TextEdits from clang's fix-its. Must be non-empty. llvm::SmallVector Edits; + + /// Annotations for the Edits. + llvm::SmallVector> + Annotations; }; llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const Fix &F); diff --git a/clang-tools-extra/clangd/IncludeCleaner.cpp b/clang-tools-extra/clangd/IncludeCleaner.cpp --- a/clang-tools-extra/clangd/IncludeCleaner.cpp +++ b/clang-tools-extra/clangd/IncludeCleaner.cpp @@ -44,6 +44,7 @@ #include "llvm/ADT/STLFunctionalExtras.h" #include "llvm/ADT/SmallString.h" #include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringMap.h" #include "llvm/ADT/StringRef.h" #include "llvm/ADT/StringSet.h" #include "llvm/Support/Casting.h" @@ -420,6 +421,118 @@ return {std::move(UnusedIncludes), std::move(MissingIncludes)}; } +Fix removeAllUnusedIncludes(llvm::ArrayRef UnusedIncludes) { + assert(!UnusedIncludes.empty()); + + Fix RemoveAll; + RemoveAll.Message = "remove all unused includes"; + for (const auto &Diag : UnusedIncludes) { + assert(Diag.Fixes.size() == 1 && "Expected exactly one fix."); + RemoveAll.Edits.insert(RemoveAll.Edits.end(), + Diag.Fixes.front().Edits.begin(), + Diag.Fixes.front().Edits.end()); + } + + ChangeAnnotation Annotation = {/*label=*/"", + /*needsConfirmation=*/true, + /*description=*/""}; + static const ChangeAnnotationIdentifier RemoveAllUnusedID = + "RemoveAllUnusedIncludes"; + for (unsigned I = 0; I < RemoveAll.Edits.size(); ++I) { + ChangeAnnotationIdentifier ID = RemoveAllUnusedID + std::to_string(I); + RemoveAll.Edits[I].annotationId = ID; + RemoveAll.Annotations.push_back({ID, Annotation}); + } + return RemoveAll; +} +Fix addAllMissingIncludes(llvm::ArrayRef MissingIncludeDiags) { + assert(!MissingIncludeDiags.empty()); + + Fix AddAllMissing; + AddAllMissing.Message = "add all missing includes"; + // A map to deduplicate the edits with the same new text. + // newText (#include "my_missing_header.h") -> TextEdit. + llvm::StringMap Edits; + for (const auto &Diag : MissingIncludeDiags) { + assert(Diag.Fixes.size() == 1 && "Expected exactly one fix."); + for (const auto& Edit : Diag.Fixes.front().Edits) { + Edits.try_emplace(Edit.newText, Edit); + } + } + // FIXME(hokein): emit used symbol reference in the annotation. + ChangeAnnotation Annotation = {/*label=*/"", + /*needsConfirmation=*/true, + /*description=*/""}; + static const ChangeAnnotationIdentifier AddAllMissingID = + "AddAllMissingIncludes"; + unsigned I = 0; + for (const auto& It : Edits) { + ChangeAnnotationIdentifier ID = AddAllMissingID + std::to_string(I++); + AddAllMissing.Edits.push_back(It.getValue()); + AddAllMissing.Edits.back().annotationId.emplace(ID); + + AddAllMissing.Annotations.push_back({ID, Annotation}); + } + return AddAllMissing; +} +Fix fixAll(const Fix& RemoveAllUnused, const Fix& AddAllMissing) { + Fix FixAll; + FixAll.Message = "fix all includes"; + ChangeAnnotation Annotation = {/*label=*/"", + /*needsConfirmation=*/true, + /*description=*/""}; + for (const auto &F : RemoveAllUnused.Edits) { + FixAll.Edits.push_back(F); + assert(F.annotationId); + FixAll.Annotations.push_back({*F.annotationId, Annotation}); + } + for (const auto &F : AddAllMissing.Edits) { + FixAll.Edits.push_back(F); + assert(F.annotationId); + FixAll.Annotations.push_back({*F.annotationId, Annotation}); + } + + return FixAll; +} + +std::vector generateIncludeCleanerDiagnostic( + ParsedAST &AST, const IncludeCleanerFindings &Findings, + llvm::StringRef Code) { + std::vector UnusedIncludes = generateUnusedIncludeDiagnostics( + AST.tuPath(), Findings.UnusedIncludes, Code); + std::optional RemoveAllUnused;; + if (UnusedIncludes.size() > 1) + RemoveAllUnused = removeAllUnusedIncludes(UnusedIncludes); + + std::vector MissingIncludeDiags = generateMissingIncludeDiagnostics( + AST, Findings.MissingIncludes, Code); + std::optional AddAllMissing; + if (MissingIncludeDiags.size() > 1) + AddAllMissing = addAllMissingIncludes(MissingIncludeDiags); + + std::optional FixAll; + if (RemoveAllUnused && AddAllMissing) + FixAll = fixAll(*RemoveAllUnused, *AddAllMissing); + + auto AddBatchFix = [](const std::optional &F, clang::clangd::Diag *Out) { + if (!F) return; + Out->Fixes.push_back(*F); + }; + for (auto &Diag : MissingIncludeDiags) { + AddBatchFix(AddAllMissing, &Diag); + AddBatchFix(FixAll, &Diag); + } + for (auto &Diag : UnusedIncludes) { + AddBatchFix(RemoveAllUnused, &Diag); + AddBatchFix(FixAll, &Diag); + } + + auto Result = std::move(MissingIncludeDiags); + llvm::move(UnusedIncludes, + std::back_inserter(Result)); + return Result; +} + std::vector issueIncludeCleanerDiagnostics(ParsedAST &AST, llvm::StringRef Code) { // Interaction is only polished for C/CPP. @@ -435,13 +548,7 @@ // will need include-cleaner results, call it once Findings = computeIncludeCleanerFindings(AST); } - - std::vector Result = generateUnusedIncludeDiagnostics( - AST.tuPath(), Findings.UnusedIncludes, Code); - llvm::move( - generateMissingIncludeDiagnostics(AST, Findings.MissingIncludes, Code), - std::back_inserter(Result)); - return Result; + return generateIncludeCleanerDiagnostic(AST, Findings, Code); } std::optional diff --git a/clang-tools-extra/clangd/Protocol.h b/clang-tools-extra/clangd/Protocol.h --- a/clang-tools-extra/clangd/Protocol.h +++ b/clang-tools-extra/clangd/Protocol.h @@ -238,6 +238,8 @@ llvm::json::Value toJSON(const ReferenceLocation &); llvm::raw_ostream &operator<<(llvm::raw_ostream &, const ReferenceLocation &); +using ChangeAnnotationIdentifier = std::string; +// A combination of a LSP standard TextEdit and AnnotatedTextEdit. struct TextEdit { /// The range of the text document to be manipulated. To insert /// text into a document create a range where start === end. @@ -246,14 +248,34 @@ /// The string to be inserted. For delete operations use an /// empty string. std::string newText; + + /// The actual annotation identifier. + std::optional annotationId = std::nullopt; }; inline bool operator==(const TextEdit &L, const TextEdit &R) { - return std::tie(L.newText, L.range) == std::tie(R.newText, R.range); + return std::tie(L.newText, L.range, L.annotationId) == + std::tie(R.newText, R.range, L.annotationId); } bool fromJSON(const llvm::json::Value &, TextEdit &, llvm::json::Path); llvm::json::Value toJSON(const TextEdit &); llvm::raw_ostream &operator<<(llvm::raw_ostream &, const TextEdit &); +struct ChangeAnnotation { + /// A human-readable string describing the actual change. The string + /// is rendered prominent in the user interface. + std::string label; + + /// A flag which indicates that user confirmation is needed + /// before applying the change. + std::optional needsConfirmation; + + /// A human-readable string which is rendered less prominent in + /// the user interface. + std::string description; +}; +bool fromJSON(const llvm::json::Value &, ChangeAnnotation &, llvm::json::Path); +llvm::json::Value toJSON(const ChangeAnnotation &); + struct TextDocumentEdit { /// The text document to change. VersionedTextDocumentIdentifier textDocument; @@ -530,6 +552,9 @@ /// The client supports versioned document changes for WorkspaceEdit. bool DocumentChanges = false; + + /// The client supports change annotations on text edits, + bool ChangeAnnotation = false; /// Whether the client supports the textDocument/inactiveRegions /// notification. This is a clangd extension. @@ -996,6 +1021,10 @@ /// `workspace.workspaceEdit.resourceOperations` then only plain `TextEdit`s /// using the `changes` property are supported. std::optional> documentChanges; + + /// A map of change annotations that can be referenced in + /// AnnotatedTextEdit. + std::map changeAnnotations; }; bool fromJSON(const llvm::json::Value &, WorkspaceEdit &, llvm::json::Path); llvm::json::Value toJSON(const WorkspaceEdit &WE); diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp --- a/clang-tools-extra/clangd/Protocol.cpp +++ b/clang-tools-extra/clangd/Protocol.cpp @@ -187,14 +187,34 @@ bool fromJSON(const llvm::json::Value &Params, TextEdit &R, llvm::json::Path P) { llvm::json::ObjectMapper O(Params, P); - return O && O.map("range", R.range) && O.map("newText", R.newText); + return O && O.map("range", R.range) && O.map("newText", R.newText) && + O.map("annotationId", R.annotationId); } llvm::json::Value toJSON(const TextEdit &P) { - return llvm::json::Object{ + llvm::json::Object Result{ {"range", P.range}, {"newText", P.newText}, }; + if (P.annotationId) + Result["annotationId"] = P.annotationId; + return Result; +} + +bool fromJSON(const llvm::json::Value &Params, ChangeAnnotation &R, + llvm::json::Path P) { + llvm::json::ObjectMapper O(Params, P); + return O && O.map("label", R.label) && + O.map("needsConfirmation", R.needsConfirmation) && + O.mapOptional("description", R.description); +} +llvm::json::Value toJSON(const ChangeAnnotation & CA) { + llvm::json::Object Result{{"label", CA.label}}; + if (CA.needsConfirmation) + Result["needsConfirmation"] = *CA.needsConfirmation; + if (!CA.description.empty()) + Result["description"] = CA.description; + return Result; } bool fromJSON(const llvm::json::Value &Params, TextDocumentEdit &R, @@ -458,6 +478,10 @@ if (auto *WorkspaceEdit = Workspace->getObject("workspaceEdit")) { if (auto DocumentChanges = WorkspaceEdit->getBoolean("documentChanges")) R.DocumentChanges = *DocumentChanges; + if (const auto& ChangeAnnotation = + WorkspaceEdit->getObject("changeAnnotationSupport")) { + R.ChangeAnnotation = true; + } } } if (auto *Window = O->getObject("window")) { @@ -733,7 +757,8 @@ llvm::json::Path P) { llvm::json::ObjectMapper O(Params, P); return O && O.map("changes", R.changes) && - O.map("documentChanges", R.documentChanges); + O.map("documentChanges", R.documentChanges) && + O.mapOptional("changeAnnotations", R.changeAnnotations); } bool fromJSON(const llvm::json::Value &Params, ExecuteCommandParams &R, @@ -888,6 +913,12 @@ } if (WE.documentChanges) Result["documentChanges"] = *WE.documentChanges; + if (!WE.changeAnnotations.empty()) { + llvm::json::Object ChangeAnnotations; + for (auto &Annotation : WE.changeAnnotations) + ChangeAnnotations[Annotation.first] = Annotation.second; + Result["changeAnnotations"] = std::move(ChangeAnnotations); + } return Result; } diff --git a/clang-tools-extra/clangd/test/Inputs/include-cleaner/all1.h b/clang-tools-extra/clangd/test/Inputs/include-cleaner/all1.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/test/Inputs/include-cleaner/all1.h @@ -0,0 +1,4 @@ +#pragma once + +#include "bar.h" +#include "foo.h" diff --git a/clang-tools-extra/clangd/test/Inputs/include-cleaner/all2.h b/clang-tools-extra/clangd/test/Inputs/include-cleaner/all2.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/test/Inputs/include-cleaner/all2.h @@ -0,0 +1,4 @@ +#pragma once + +#include "bar.h" +#include "foo.h" diff --git a/clang-tools-extra/clangd/test/Inputs/include-cleaner/bar.h b/clang-tools-extra/clangd/test/Inputs/include-cleaner/bar.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/test/Inputs/include-cleaner/bar.h @@ -0,0 +1,2 @@ +#pragma once +class Bar {}; diff --git a/clang-tools-extra/clangd/test/Inputs/include-cleaner/foo.h b/clang-tools-extra/clangd/test/Inputs/include-cleaner/foo.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/test/Inputs/include-cleaner/foo.h @@ -0,0 +1,2 @@ +#pragma once +class Foo {}; diff --git a/clang-tools-extra/clangd/test/include-cleaner-batch-fix.test b/clang-tools-extra/clangd/test/include-cleaner-batch-fix.test new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/test/include-cleaner-batch-fix.test @@ -0,0 +1,487 @@ +# We specify a custom path in XDG_CONFIG_HOME, which only works on some systems. +# UNSUPPORTED: system-windows +# UNSUPPORTED: system-darwin + +# RUN: rm -rf %t +# RUN: mkdir -p %t/clangd +# RUN: cp -r %S/Inputs/include-cleaner %t/include +# Create a config file enabling include-cleaner features. +# RUN: echo $'Diagnostics:\n UnusedIncludes: Strict\n MissingIncludes: Strict' >> %t/clangd/config.yaml + +# RUN: env XDG_CONFIG_HOME=%t clangd -lit-test -enable-config --resource-dir=%t < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{"workspace":{"workspaceEdit":{"documentChanges":true, "changeAnnotationSupport":{"groupsOnLabel":true}}}},"trace":"off"}} +--- +{ + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "test:///simple.cpp", + "languageId": "cpp", + "text": "#include \"all1.h\"\n#include \"all2.h\"\n Foo* foo; Bar* bar;" + } + } +} +# First, the diagnostic from the config file. +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [], + +# Then, diagnostic from the main cpp file. +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [ +# CHECK-NEXT: { +# CHECK-NEXT: "code": "missing-includes", +# CHECK-NEXT: "message": "No header providing \"Foo\" is directly included (fixes available)", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 4, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 1, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 2, +# CHECK-NEXT: "source": "clangd" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "code": "missing-includes", +# CHECK-NEXT: "message": "No header providing \"Bar\" is directly included (fixes available)", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 14, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 11, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 2, +# CHECK-NEXT: "source": "clangd" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "code": "unused-includes", +# CHECK-NEXT: "codeDescription": { +# CHECK-NEXT: "href": "https://clangd.llvm.org/guides/include-cleaner" +# CHECK-NEXT: }, +# CHECK-NEXT: "message": "Included header all1.h is not used directly (fixes available)", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 17, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 2, +# CHECK-NEXT: "source": "clangd", +# CHECK-NEXT: "tags": [ +# CHECK-NEXT: 1 +# CHECK-NEXT: ] +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "code": "unused-includes", +# CHECK-NEXT: "codeDescription": { +# CHECK-NEXT: "href": "https://clangd.llvm.org/guides/include-cleaner" +# CHECK-NEXT: }, +# CHECK-NEXT: "message": "Included header all2.h is not used directly (fixes available)", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 17, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 2, +# CHECK-NEXT: "source": "clangd", +# CHECK-NEXT: "tags": [ +# CHECK-NEXT: 1 +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "uri": "file:///clangd-test/simple.cpp", +# CHECK-NEXT: "version": 0 +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":2,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"test:///simple.cpp"},"range":{"start":{"line":2,"character":1},"end":{"line":2,"character":4}},"context":{"diagnostics":[{"range":{"start": {"line": 2, "character": 1}, "end": {"line": 2, "character": 4}},"severity":2,"message":"No header providing \"Foo\" is directly included (fixes available)", "code": "missing-includes", "source": "clangd"}]}}} +# CHECK: "id": 2, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "arguments": [ +# CHECK-NEXT: { +# CHECK-NEXT: "documentChanges": [ +# CHECK-NEXT: { +# CHECK-NEXT: "edits": [ +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "#include \n", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "textDocument": { +# CHECK-NEXT: "uri": "file:///clangd-test/simple.cpp", +# CHECK-NEXT: "version": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "command": "clangd.applyFix", +# CHECK-NEXT: "title": "Apply fix: #include " +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "arguments": [ +# CHECK-NEXT: { +# CHECK-NEXT: "changeAnnotations": { +# CHECK-NEXT: "AddAllMissingIncludes0": { +# CHECK-NEXT: "label": "", +# CHECK-NEXT: "needsConfirmation": true +# CHECK-NEXT: }, +# CHECK-NEXT: "AddAllMissingIncludes1": { +# CHECK-NEXT: "label": "", +# CHECK-NEXT: "needsConfirmation": true +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "documentChanges": [ +# CHECK-NEXT: { +# CHECK-NEXT: "edits": [ +# CHECK-NEXT: { +# CHECK-NEXT: "annotationId": "AddAllMissingIncludes0", +# CHECK-NEXT: "newText": "#include \n", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "annotationId": "AddAllMissingIncludes1", +# CHECK-NEXT: "newText": "#include \n", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "textDocument": { +# CHECK-NEXT: "uri": "file:///clangd-test/simple.cpp", +# CHECK-NEXT: "version": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "command": "clangd.applyFix", +# CHECK-NEXT: "title": "Apply fix: add all missing includes" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "arguments": [ +# CHECK-NEXT: { +# CHECK-NEXT: "changeAnnotations": { +# CHECK-NEXT: "AddAllMissingIncludes0": { +# CHECK-NEXT: "label": "", +# CHECK-NEXT: "needsConfirmation": true +# CHECK-NEXT: }, +# CHECK-NEXT: "AddAllMissingIncludes1": { +# CHECK-NEXT: "label": "", +# CHECK-NEXT: "needsConfirmation": true +# CHECK-NEXT: }, +# CHECK-NEXT: "RemoveAllUnusedIncludes0": { +# CHECK-NEXT: "label": "", +# CHECK-NEXT: "needsConfirmation": true +# CHECK-NEXT: }, +# CHECK-NEXT: "RemoveAllUnusedIncludes1": { +# CHECK-NEXT: "label": "", +# CHECK-NEXT: "needsConfirmation": true +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "documentChanges": [ +# CHECK-NEXT: { +# CHECK-NEXT: "edits": [ +# CHECK-NEXT: { +# CHECK-NEXT: "annotationId": "RemoveAllUnusedIncludes0", +# CHECK-NEXT: "newText": "", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "annotationId": "RemoveAllUnusedIncludes1", +# CHECK-NEXT: "newText": "", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "annotationId": "AddAllMissingIncludes0", +# CHECK-NEXT: "newText": "#include \n", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "annotationId": "AddAllMissingIncludes1", +# CHECK-NEXT: "newText": "#include \n", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "textDocument": { +# CHECK-NEXT: "uri": "file:///clangd-test/simple.cpp", +# CHECK-NEXT: "version": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "command": "clangd.applyFix", +# CHECK-NEXT: "title": "Apply fix: fix all includes" +# CHECK-NEXT: } +# CHECK-NEXT: ] +--- +{"jsonrpc":"2.0","id":3,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"test:///simple.cpp"},"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":17}},"context":{"diagnostics":[{"range":{"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 17}},"severity":2,"message":"Included header all1.h is not used directly (fixes available)", "code": "unused-includes", "source": "clangd"}]}}} +# CHECK: "id": 3, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "arguments": [ +# CHECK-NEXT: { +# CHECK-NEXT: "documentChanges": [ +# CHECK-NEXT: { +# CHECK-NEXT: "edits": [ +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "textDocument": { +# CHECK-NEXT: "uri": "file:///clangd-test/simple.cpp", +# CHECK-NEXT: "version": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "command": "clangd.applyFix", +# CHECK-NEXT: "title": "Apply fix: remove #include directive" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "arguments": [ +# CHECK-NEXT: { +# CHECK-NEXT: "changeAnnotations": { +# CHECK-NEXT: "RemoveAllUnusedIncludes0": { +# CHECK-NEXT: "label": "", +# CHECK-NEXT: "needsConfirmation": true +# CHECK-NEXT: }, +# CHECK-NEXT: "RemoveAllUnusedIncludes1": { +# CHECK-NEXT: "label": "", +# CHECK-NEXT: "needsConfirmation": true +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "documentChanges": [ +# CHECK-NEXT: { +# CHECK-NEXT: "edits": [ +# CHECK-NEXT: { +# CHECK-NEXT: "annotationId": "RemoveAllUnusedIncludes0", +# CHECK-NEXT: "newText": "", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "annotationId": "RemoveAllUnusedIncludes1", +# CHECK-NEXT: "newText": "", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "textDocument": { +# CHECK-NEXT: "uri": "file:///clangd-test/simple.cpp", +# CHECK-NEXT: "version": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "command": "clangd.applyFix", +# CHECK-NEXT: "title": "Apply fix: remove all unused includes" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "arguments": [ +# CHECK-NEXT: { +# CHECK-NEXT: "changeAnnotations": { +# CHECK-NEXT: "AddAllMissingIncludes0": { +# CHECK-NEXT: "label": "", +# CHECK-NEXT: "needsConfirmation": true +# CHECK-NEXT: }, +# CHECK-NEXT: "AddAllMissingIncludes1": { +# CHECK-NEXT: "label": "", +# CHECK-NEXT: "needsConfirmation": true +# CHECK-NEXT: }, +# CHECK-NEXT: "RemoveAllUnusedIncludes0": { +# CHECK-NEXT: "label": "", +# CHECK-NEXT: "needsConfirmation": true +# CHECK-NEXT: }, +# CHECK-NEXT: "RemoveAllUnusedIncludes1": { +# CHECK-NEXT: "label": "", +# CHECK-NEXT: "needsConfirmation": true +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "documentChanges": [ +# CHECK-NEXT: { +# CHECK-NEXT: "edits": [ +# CHECK-NEXT: { +# CHECK-NEXT: "annotationId": "RemoveAllUnusedIncludes0", +# CHECK-NEXT: "newText": "", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "annotationId": "RemoveAllUnusedIncludes1", +# CHECK-NEXT: "newText": "", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "annotationId": "AddAllMissingIncludes0", +# CHECK-NEXT: "newText": "#include \n", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "annotationId": "AddAllMissingIncludes1", +# CHECK-NEXT: "newText": "#include \n", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 2 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "textDocument": { +# CHECK-NEXT: "uri": "file:///clangd-test/simple.cpp", +# CHECK-NEXT: "version": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "command": "clangd.applyFix", +# CHECK-NEXT: "title": "Apply fix: fix all includes" +# CHECK-NEXT: } +# CHECK-NEXT: ] +--- +{"jsonrpc":"2.0","id":4,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} diff --git a/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp b/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp --- a/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp +++ b/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp @@ -1908,11 +1908,11 @@ auto AST = TU.build(); EXPECT_THAT( *AST.getDiagnostics(), - UnorderedElementsAre(AllOf( - Diag(Test.range("diag"), - "included header unused.h is not used directly"), - withTag(DiagnosticTag::Unnecessary), diagSource(Diag::Clangd), - withFix(Fix(Test.range("fix"), "", "remove #include directive"))))); + UnorderedElementsAre( + AllOf(Diag(Test.range("diag"), + "included header unused.h is not used directly"), + withTag(DiagnosticTag::Unnecessary), diagSource(Diag::Clangd), + withFix(Fix(Test.range("fix"), "", "remove #include directive"))))); auto &Diag = AST.getDiagnostics()->front(); EXPECT_EQ(getDiagnosticDocURI(Diag.Source, Diag.ID, Diag.Name), std::string("https://clangd.llvm.org/guides/include-cleaner")); diff --git a/clang-tools-extra/clangd/unittests/IncludeCleanerTests.cpp b/clang-tools-extra/clangd/unittests/IncludeCleanerTests.cpp --- a/clang-tools-extra/clangd/unittests/IncludeCleanerTests.cpp +++ b/clang-tools-extra/clangd/unittests/IncludeCleanerTests.cpp @@ -45,8 +45,8 @@ using ::testing::Pointee; using ::testing::UnorderedElementsAre; -Matcher withFix(::testing::Matcher FixMatcher) { - return Field(&Diag::Fixes, ElementsAre(FixMatcher)); +Matcher withFix(std::vector<::testing::Matcher> FixMatcheres) { + return Field(&Diag::Fixes, testing::UnorderedElementsAreArray(FixMatcheres)); } MATCHER_P2(Diag, Range, Message, @@ -60,6 +60,8 @@ return arg.Message == Message && arg.Edits.size() == 1 && arg.Edits[0].range == Range && arg.Edits[0].newText == Replacement; } +MATCHER_P(FixMessage, Message, "") { return arg.Message == Message; } + std::string guard(llvm::StringRef Code) { return "#pragma once\n" + Code.str(); @@ -255,42 +257,51 @@ UnorderedElementsAre( AllOf(Diag(MainFile.range("b"), "No header providing \"b\" is directly included"), - withFix(Fix(MainFile.range("insert_b"), "#include \"b.h\"\n", - "#include \"b.h\""))), + withFix({Fix(MainFile.range("insert_b"), "#include \"b.h\"\n", + "#include \"b.h\""), + FixMessage("add all missing includes")})), AllOf(Diag(MainFile.range("bar"), "No header providing \"ns::Bar\" is directly included"), - withFix(Fix(MainFile.range("insert_d"), - "#include \"dir/d.h\"\n", "#include \"dir/d.h\""))), + withFix({Fix(MainFile.range("insert_d"), + "#include \"dir/d.h\"\n", "#include \"dir/d.h\""), + FixMessage("add all missing includes")})), AllOf(Diag(MainFile.range("f"), "No header providing \"f\" is directly included"), - withFix(Fix(MainFile.range("insert_f"), "#include \n", - "#include "))), + withFix({Fix(MainFile.range("insert_f"), "#include \n", + "#include "), + FixMessage("add all missing includes")})), AllOf( Diag(MainFile.range("foobar"), "No header providing \"foobar\" is directly included"), - withFix(Fix(MainFile.range("insert_foobar"), - "#include \"public.h\"\n", "#include \"public.h\""))), + withFix({Fix(MainFile.range("insert_foobar"), + "#include \"public.h\"\n", "#include \"public.h\""), + FixMessage("add all missing includes")})), AllOf( Diag(MainFile.range("vector"), "No header providing \"std::vector\" is directly included"), - withFix(Fix(MainFile.range("insert_vector"), - "#include \n", "#include "))), + withFix({Fix(MainFile.range("insert_vector"), + "#include \n", "#include "), + FixMessage("add all missing includes"),})), AllOf(Diag(MainFile.range("FOO"), "No header providing \"FOO\" is directly included"), - withFix(Fix(MainFile.range("insert_foo"), - "#include \"foo.h\"\n", "#include \"foo.h\""))), + withFix({Fix(MainFile.range("insert_foo"), + "#include \"foo.h\"\n", "#include \"foo.h\""), + FixMessage("add all missing includes")})), AllOf(Diag(MainFile.range("DEF"), "No header providing \"Foo\" is directly included"), - withFix(Fix(MainFile.range("insert_foo"), - "#include \"foo.h\"\n", "#include \"foo.h\""))), + withFix({Fix(MainFile.range("insert_foo"), + "#include \"foo.h\"\n", "#include \"foo.h\""), + FixMessage("add all missing includes")})), AllOf(Diag(MainFile.range("BAR"), "No header providing \"BAR\" is directly included"), - withFix(Fix(MainFile.range("insert_foo"), - "#include \"foo.h\"\n", "#include \"foo.h\""))), + withFix({Fix(MainFile.range("insert_foo"), + "#include \"foo.h\"\n", "#include \"foo.h\""), + FixMessage("add all missing includes")})), AllOf(Diag(MainFile.range("Foo"), "No header providing \"Foo\" is directly included"), - withFix(Fix(MainFile.range("insert_foo"), - "#include \"foo.h\"\n", "#include \"foo.h\""))))); + withFix({Fix(MainFile.range("insert_foo"), + "#include \"foo.h\"\n", "#include \"foo.h\""), + FixMessage("add all missing includes")})))); } TEST(IncludeCleaner, IWYUPragmas) {