[clang-tools-extra] 4b1cec0 - [clangd] Add batch fixes for include-cleaner diagnostics

Haojian Wu via cfe-commits cfe-commits at lists.llvm.org
Thu Apr 27 03:10:33 PDT 2023


Author: Haojian Wu
Date: 2023-04-27T12:08:59+02:00
New Revision: 4b1cec065227db64f870a179a2d45cffa314641a

URL: https://github.com/llvm/llvm-project/commit/4b1cec065227db64f870a179a2d45cffa314641a
DIFF: https://github.com/llvm/llvm-project/commit/4b1cec065227db64f870a179a2d45cffa314641a.diff

LOG: [clangd] Add batch fixes for include-cleaner diagnostics

For each unused-include/missing-include diagnostic, we provide fix-all
alternative to them.

This patch also adds LSP ChangeAnnotation support.

Differential Revision: https://reviews.llvm.org/D147684

Added: 
    clang-tools-extra/clangd/test/Inputs/include-cleaner/all1.h
    clang-tools-extra/clangd/test/Inputs/include-cleaner/all2.h
    clang-tools-extra/clangd/test/Inputs/include-cleaner/bar.h
    clang-tools-extra/clangd/test/Inputs/include-cleaner/foo.h
    clang-tools-extra/clangd/test/include-cleaner-batch-fix.test

Modified: 
    clang-tools-extra/clangd/ClangdLSPServer.cpp
    clang-tools-extra/clangd/ClangdLSPServer.h
    clang-tools-extra/clangd/CodeComplete.cpp
    clang-tools-extra/clangd/Diagnostics.cpp
    clang-tools-extra/clangd/Diagnostics.h
    clang-tools-extra/clangd/IncludeCleaner.cpp
    clang-tools-extra/clangd/Protocol.cpp
    clang-tools-extra/clangd/Protocol.h
    clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp
    clang-tools-extra/clangd/unittests/IncludeCleanerTests.cpp

Removed: 
    


################################################################################
diff  --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp
index bef6c4de131ed..26e239fa7bed6 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.cpp
+++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp
@@ -99,19 +99,29 @@ CodeAction toCodeAction(const ClangdServer::TweakRef &T, const URIForFile &File,
 /// Convert from Fix to LSP CodeAction.
 CodeAction toCodeAction(const Fix &F, const URIForFile &File,
                         const std::optional<int64_t> &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=*/""});
   } 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 : ""});
+    if (SupportChangeAnnotation) {
+      for (const auto &[AID, Annotation]: F.Annotations)
+        Action.edit->changeAnnotations[AID] = Annotation;
+    }
   }
   return Action;
 }
@@ -509,6 +519,7 @@ void ClangdLSPServer::onInitialize(const InitializeParams &Params,
   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 @@ void ClangdLSPServer::onDiagnosticsReady(PathRef File, llvm::StringRef Version,
                  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/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h
index e5e2104dcec2f..33c8d221f9bd4 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.h
+++ b/clang-tools-extra/clangd/ClangdLSPServer.h
@@ -277,6 +277,9 @@ class ClangdLSPServer : private ClangdServer::Callbacks,
   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/CodeComplete.cpp b/clang-tools-extra/clangd/CodeComplete.cpp
index 260c44b2064b5..bb01fa421b498 100644
--- a/clang-tools-extra/clangd/CodeComplete.cpp
+++ b/clang-tools-extra/clangd/CodeComplete.cpp
@@ -2242,7 +2242,7 @@ CompletionItem CodeCompletion::render(const CodeCompleteOptions &Opts) const {
   }
   LSP.sortText = sortText(Score.Total, FilterText);
   LSP.filterText = FilterText;
-  LSP.textEdit = {CompletionTokenRange, RequiredQualifier + Name};
+  LSP.textEdit = {CompletionTokenRange, RequiredQualifier + Name, ""};
   // Merge continuous additionalTextEdits into main edit. The main motivation
   // behind this is to help LSP clients, it seems most of them are confused when
   // they are provided with additionalTextEdits that are consecutive to main

diff  --git a/clang-tools-extra/clangd/Diagnostics.cpp b/clang-tools-extra/clangd/Diagnostics.cpp
index eb6744f965267..d1726142c5ded 100644
--- a/clang-tools-extra/clangd/Diagnostics.cpp
+++ b/clang-tools-extra/clangd/Diagnostics.cpp
@@ -770,7 +770,7 @@ void StoreDiags::HandleDiagnostic(DiagnosticsEngine::Level DiagLevel,
     if (Message.empty()) // either !SyntheticMessage, or we failed to make one.
       Info.FormatDiagnostic(Message);
     LastDiag->Fixes.push_back(
-        Fix{std::string(Message.str()), std::move(Edits)});
+        Fix{std::string(Message.str()), std::move(Edits), {}});
     return true;
   };
 

diff  --git a/clang-tools-extra/clangd/Diagnostics.h b/clang-tools-extra/clangd/Diagnostics.h
index 592bfd68bef4f..d4c0478c63a5c 100644
--- a/clang-tools-extra/clangd/Diagnostics.h
+++ b/clang-tools-extra/clangd/Diagnostics.h
@@ -83,6 +83,10 @@ struct Fix {
   std::string Message;
   /// TextEdits from clang's fix-its. Must be non-empty.
   llvm::SmallVector<TextEdit, 1> Edits;
+
+  /// Annotations for the Edits.
+  llvm::SmallVector<std::pair<ChangeAnnotationIdentifier, ChangeAnnotation>>
+      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
index b88ad0ff5c5b5..d3754de689eb0 100644
--- a/clang-tools-extra/clangd/IncludeCleaner.cpp
+++ b/clang-tools-extra/clangd/IncludeCleaner.cpp
@@ -45,6 +45,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"
@@ -435,6 +436,115 @@ IncludeCleanerFindings computeIncludeCleanerFindings(ParsedAST &AST) {
   return {std::move(UnusedIncludes), std::move(MissingIncludes)};
 }
 
+Fix removeAllUnusedIncludes(llvm::ArrayRef<Diag> 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());
+  }
+
+  // TODO(hokein): emit a suitable text for the label.
+  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<Diag> 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<TextEdit> 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 (auto &It : Edits) {
+    ChangeAnnotationIdentifier ID = AddAllMissingID + std::to_string(I++);
+    AddAllMissing.Edits.push_back(std::move(It.getValue()));
+    AddAllMissing.Edits.back().annotationId = ID;
+
+    AddAllMissing.Annotations.push_back({ID, Annotation});
+  }
+  return AddAllMissing;
+}
+Fix fixAll(const Fix& RemoveAllUnused, const Fix& AddAllMissing) {
+  Fix FixAll;
+  FixAll.Message = "fix all includes";
+
+  for (const auto &F : RemoveAllUnused.Edits)
+    FixAll.Edits.push_back(F);
+  for (const auto &F : AddAllMissing.Edits)
+    FixAll.Edits.push_back(F);
+
+  for (const auto& A : RemoveAllUnused.Annotations)
+    FixAll.Annotations.push_back(A);
+  for (const auto& A : AddAllMissing.Annotations)
+    FixAll.Annotations.push_back(A);
+  return FixAll;
+}
+
+std::vector<Diag> generateIncludeCleanerDiagnostic(
+    ParsedAST &AST, const IncludeCleanerFindings &Findings,
+    llvm::StringRef Code) {
+  std::vector<Diag> UnusedIncludes = generateUnusedIncludeDiagnostics(
+      AST.tuPath(), Findings.UnusedIncludes, Code);
+  std::optional<Fix> RemoveAllUnused;;
+  if (UnusedIncludes.size() > 1)
+    RemoveAllUnused = removeAllUnusedIncludes(UnusedIncludes);
+
+  std::vector<Diag> MissingIncludeDiags = generateMissingIncludeDiagnostics(
+      AST, Findings.MissingIncludes, Code);
+  std::optional<Fix> AddAllMissing;
+  if (MissingIncludeDiags.size() > 1)
+    AddAllMissing = addAllMissingIncludes(MissingIncludeDiags);
+
+  std::optional<Fix> FixAll;
+  if (RemoveAllUnused && AddAllMissing)
+    FixAll = fixAll(*RemoveAllUnused, *AddAllMissing);
+
+  auto AddBatchFix = [](const std::optional<Fix> &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<Diag> issueIncludeCleanerDiagnostics(ParsedAST &AST,
                                                  llvm::StringRef Code) {
   // Interaction is only polished for C/CPP.
@@ -450,13 +560,7 @@ std::vector<Diag> issueIncludeCleanerDiagnostics(ParsedAST &AST,
     // will need include-cleaner results, call it once
     Findings = computeIncludeCleanerFindings(AST);
   }
-
-  std::vector<Diag> 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<include_cleaner::Header>

diff  --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp
index ad851ca119178..465b5fc3d69cf 100644
--- 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, TextDocumentItem &R,
 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.mapOptional("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.empty())
+    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 @@ bool fromJSON(const llvm::json::Value &Params, ClientCapabilities &R,
     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 @@ bool fromJSON(const llvm::json::Value &Params, WorkspaceEdit &R,
               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 @@ llvm::json::Value toJSON(const WorkspaceEdit &WE) {
   }
   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/Protocol.h b/clang-tools-extra/clangd/Protocol.h
index 508a863a32233..1d6fccff6ea4d 100644
--- a/clang-tools-extra/clangd/Protocol.h
+++ b/clang-tools-extra/clangd/Protocol.h
@@ -238,6 +238,8 @@ struct ReferenceLocation : Location {
 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,35 @@ struct TextEdit {
   /// The string to be inserted. For delete operations use an
   /// empty string.
   std::string newText;
+
+  /// The actual annotation identifier (optional)
+  /// If empty, then this field is nullopt.
+  ChangeAnnotationIdentifier annotationId = "";
 };
 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<bool> 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 +553,9 @@ struct ClientCapabilities {
 
   /// 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 +1022,10 @@ struct WorkspaceEdit {
 	/// `workspace.workspaceEdit.resourceOperations` then only plain `TextEdit`s
 	/// using the `changes` property are supported.
   std::optional<std::vector<TextDocumentEdit>> documentChanges;
+  
+  /// A map of change annotations that can be referenced in
+	/// AnnotatedTextEdit.
+  std::map<std::string, ChangeAnnotation> 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/test/Inputs/include-cleaner/all1.h b/clang-tools-extra/clangd/test/Inputs/include-cleaner/all1.h
new file mode 100644
index 0000000000000..c1ec51b8f24b9
--- /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
index 0000000000000..c1ec51b8f24b9
--- /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
index 0000000000000..f70dd9565fd48
--- /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
index 0000000000000..f35930c9fe80d
--- /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
index 0000000000000..b07da2a9b4df8
--- /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 \"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": "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": "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://{{.*}}/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 <foo.h>\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://{{.*}}/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 <foo.h>"
+# 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 <bar.h>\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 <foo.h>\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://{{.*}}/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 <bar.h>\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 <foo.h>\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://{{.*}}/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://{{.*}}/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://{{.*}}/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 <bar.h>\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 <foo.h>\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://{{.*}}/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
index 289767b0dec0f..552f4f4ecf886 100644
--- a/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp
+++ b/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp
@@ -1908,11 +1908,11 @@ TEST(DiagnosticsTest, IncludeCleaner) {
   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
index 32b7c5444e06f..a38c01b43270f 100644
--- a/clang-tools-extra/clangd/unittests/IncludeCleanerTests.cpp
+++ b/clang-tools-extra/clangd/unittests/IncludeCleanerTests.cpp
@@ -45,8 +45,8 @@ using ::testing::Matcher;
 using ::testing::Pointee;
 using ::testing::UnorderedElementsAre;
 
-Matcher<const Diag &> withFix(::testing::Matcher<Fix> FixMatcher) {
-  return Field(&Diag::Fixes, ElementsAre(FixMatcher));
+Matcher<const Diag &> withFix(std::vector<::testing::Matcher<Fix>> FixMatcheres) {
+  return Field(&Diag::Fixes, testing::UnorderedElementsAreArray(FixMatcheres));
 }
 
 MATCHER_P2(Diag, Range, Message,
@@ -60,6 +60,8 @@ MATCHER_P3(Fix, Range, Replacement, Message,
   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 @@ TEST(IncludeCleaner, GenerateMissingHeaderDiags) {
       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 <f.h>\n",
-                            "#include <f.h>"))),
+                withFix({Fix(MainFile.range("insert_f"), "#include <f.h>\n",
+                             "#include <f.h>"),
+                         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 <vector>\n", "#include <vector>"))),
+              withFix({Fix(MainFile.range("insert_vector"),
+                           "#include <vector>\n", "#include <vector>"),
+                       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) {


        


More information about the cfe-commits mailing list