Skip to content

[clangd] Add code action to suppress clang-tidy diagnostics#188796

Open
Lancern wants to merge 1 commit intollvm:mainfrom
Lancern:clangd/code-action-suppress-clang-tidy-diag
Open

[clangd] Add code action to suppress clang-tidy diagnostics#188796
Lancern wants to merge 1 commit intollvm:mainfrom
Lancern:clangd/code-action-suppress-clang-tidy-diag

Conversation

@Lancern
Copy link
Copy Markdown
Member

@Lancern Lancern commented Mar 26, 2026

This patch adds code actions to suppress diagnostics generated by clang-tidy. There are two variants of the code action. The first one suppresses such diagnostics by appending // NOLINT(check-name) to the end of the source line. The second one suppresses such diagnostics by inserting a source line containing // NOLINTNEXTLINE(check-name) before the source line of the diagnostics.

A previous PR #114661 attempted to land similar feature, but it ends up being closed by its author.

Assisted-by: Github Copilot / Claude Opus 4.6

This patch adds code actions to suppress diagnostics generated by clang-tidy.
There are two variants of the code action. The first one suppresses such
diagnostics by appending `// NOLINT(check-name)` to the end of the source line.
The second one suppresses such diagnostics by inserting a source line containing
`// NOLINTNEXTLINE(check-name)` before the source line of the diagnostics.

A previous PR llvm#114661 attempted to land similar feature, but it ends up being
closed by its author.

Assisted-by: Github Copilot / Claude Opus 4.6
@llvmbot
Copy link
Copy Markdown
Member

llvmbot commented Mar 26, 2026

@llvm/pr-subscribers-clangd

Author: Sirui Mu (Lancern)

Changes

This patch adds code actions to suppress diagnostics generated by clang-tidy. There are two variants of the code action. The first one suppresses such diagnostics by appending // NOLINT(check-name) to the end of the source line. The second one suppresses such diagnostics by inserting a source line containing // NOLINTNEXTLINE(check-name) before the source line of the diagnostics.

A previous PR #114661 attempted to land similar feature, but it ends up being closed by its author.

Assisted-by: Github Copilot / Claude Opus 4.6


Patch is 20.99 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/188796.diff

10 Files Affected:

  • (modified) clang-tools-extra/clangd/Diagnostics.cpp (+71-1)
  • (modified) clang-tools-extra/clangd/Diagnostics.h (+2-1)
  • (modified) clang-tools-extra/clangd/ParsedAST.cpp (+2-2)
  • (modified) clang-tools-extra/clangd/Preamble.cpp (+2-2)
  • (modified) clang-tools-extra/clangd/TUScheduler.cpp (+5-4)
  • (modified) clang-tools-extra/clangd/test/diagnostics-tidy.test (+77-1)
  • (modified) clang-tools-extra/clangd/tool/Check.cpp (+1-1)
  • (modified) clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp (+147-14)
  • (modified) clang-tools-extra/clangd/unittests/CompilerTests.cpp (+1-1)
  • (modified) clang-tools-extra/clangd/unittests/TestTU.cpp (+1-1)
diff --git a/clang-tools-extra/clangd/Diagnostics.cpp b/clang-tools-extra/clangd/Diagnostics.cpp
index f68092b9a0886..bf638c52d00aa 100644
--- a/clang-tools-extra/clangd/Diagnostics.cpp
+++ b/clang-tools-extra/clangd/Diagnostics.cpp
@@ -571,7 +571,73 @@ int getSeverity(DiagnosticsEngine::Level L) {
   llvm_unreachable("Unknown diagnostic level!");
 }
 
-std::vector<Diag> StoreDiags::take(const clang::tidy::ClangTidyContext *Tidy) {
+// For clang-tidy diagnostics, generates a fix that suppresses the diagnostic
+// on the current line by appending a NOLINT comment.
+static std::optional<Fix> makeNolintFix(llvm::StringRef Code, const Diag &D) {
+  if (D.Source != Diag::ClangTidy || D.Name.empty() || !D.InsideMainFile)
+    return std::nullopt;
+
+  llvm::Expected<size_t> StartOffset = positionToOffset(Code, D.Range.start);
+  if (!StartOffset) {
+    llvm::consumeError(StartOffset.takeError());
+    return std::nullopt;
+  }
+  size_t LineEnd = Code.find('\n', *StartOffset);
+  if (LineEnd == llvm::StringRef::npos)
+    LineEnd = Code.size();
+  Position InsertPos = offsetToPosition(Code, LineEnd);
+
+  TextEdit Edit;
+  Edit.range = {InsertPos, InsertPos};
+  Edit.newText = llvm::formatv(" // NOLINT({0})", D.Name);
+
+  Fix F;
+  F.Message = llvm::formatv("suppress this warning with NOLINT");
+  F.Edits.push_back(std::move(Edit));
+
+  return F;
+}
+
+// For clang-tidy diagnostics, generates a fix that suppresses the diagnostic on
+// the current line by inserting a NOLINTNEXTLINE comment before the current
+// line.
+static std::optional<Fix> makeNolintNextLineFix(llvm::StringRef Code,
+                                                const Diag &D) {
+  if (D.Source != Diag::ClangTidy || D.Name.empty() || !D.InsideMainFile)
+    return std::nullopt;
+
+  llvm::Expected<size_t> StartOffset = positionToOffset(Code, D.Range.start);
+  if (!StartOffset) {
+    llvm::consumeError(StartOffset.takeError());
+    return std::nullopt;
+  }
+  size_t LineStart = Code.rfind('\n', *StartOffset);
+  if (LineStart == llvm::StringRef::npos)
+    LineStart = 0;
+  else
+    ++LineStart;
+
+  size_t LineTextStart = Code.find_first_not_of(" \t", LineStart);
+  if (LineTextStart == llvm::StringRef::npos || LineTextStart > *StartOffset)
+    LineTextStart = *StartOffset;
+
+  size_t Indentation = LineTextStart - LineStart;
+  Position InsertPos = offsetToPosition(Code, LineStart);
+
+  TextEdit Edit;
+  Edit.range = {InsertPos, InsertPos};
+  Edit.newText = std::string(Indentation, ' ');
+  Edit.newText.append(llvm::formatv("// NOLINTNEXTLINE({0})\n", D.Name));
+
+  Fix F;
+  F.Message = llvm::formatv("suppress this warning with NOLINTNEXTLINE");
+  F.Edits.push_back(std::move(Edit));
+
+  return F;
+}
+
+std::vector<Diag> StoreDiags::take(llvm::StringRef Code,
+                                   const clang::tidy::ClangTidyContext *Tidy) {
   // Do not forget to emit a pending diagnostic if there is one.
   flushLastDiag();
 
@@ -605,6 +671,10 @@ std::vector<Diag> StoreDiags::take(const clang::tidy::ClangTidyContext *Tidy) {
       if (!TidyDiag.empty()) {
         Diag.Name = std::move(TidyDiag);
         Diag.Source = Diag::ClangTidy;
+        if (auto NolintFix = makeNolintFix(Code, Diag))
+          Diag.Fixes.push_back(std::move(*NolintFix));
+        if (auto NolintNextLineFix = makeNolintNextLineFix(Code, Diag))
+          Diag.Fixes.push_back(std::move(*NolintNextLineFix));
         // clang-tidy bakes the name into diagnostic messages. Strip it out.
         // It would be much nicer to make clang-tidy not do this.
         auto CleanMessage = [&](std::string &Msg) {
diff --git a/clang-tools-extra/clangd/Diagnostics.h b/clang-tools-extra/clangd/Diagnostics.h
index d433abb530151..641468c2a8974 100644
--- a/clang-tools-extra/clangd/Diagnostics.h
+++ b/clang-tools-extra/clangd/Diagnostics.h
@@ -138,7 +138,8 @@ std::optional<std::string> getDiagnosticDocURI(Diag::DiagSource, unsigned ID,
 class StoreDiags : public DiagnosticConsumer {
 public:
   // The ClangTidyContext populates Source and Name for clang-tidy diagnostics.
-  std::vector<Diag> take(const clang::tidy::ClangTidyContext *Tidy = nullptr);
+  std::vector<Diag> take(llvm::StringRef Code,
+                         const clang::tidy::ClangTidyContext *Tidy = nullptr);
 
   void BeginSourceFile(const LangOptions &Opts,
                        const Preprocessor *PP) override;
diff --git a/clang-tools-extra/clangd/ParsedAST.cpp b/clang-tools-extra/clangd/ParsedAST.cpp
index 4e873f1257a17..41b6977b71930 100644
--- a/clang-tools-extra/clangd/ParsedAST.cpp
+++ b/clang-tools-extra/clangd/ParsedAST.cpp
@@ -471,7 +471,7 @@ ParsedAST::build(llvm::StringRef Filename, const ParseInputs &Inputs,
   if (!Clang) {
     // The last diagnostic contains information about the reason of this
     // failure.
-    std::vector<Diag> Diags(ASTDiags.take());
+    std::vector<Diag> Diags(ASTDiags.take(Inputs.Contents));
     elog("Failed to prepare a compiler instance: {0}",
          !Diags.empty() ? static_cast<DiagBase &>(Diags.back()).Message
                         : "unknown error");
@@ -748,7 +748,7 @@ ParsedAST::build(llvm::StringRef Filename, const ParseInputs &Inputs,
     llvm::append_range(Diags, Patch->patchedDiags());
   // Finally, add diagnostics coming from the AST.
   {
-    std::vector<Diag> D = ASTDiags.take(&*CTContext);
+    std::vector<Diag> D = ASTDiags.take(Inputs.Contents, &*CTContext);
     Diags.insert(Diags.end(), D.begin(), D.end());
   }
   ParsedAST Result(Filename, Inputs.Version, std::move(Preamble),
diff --git a/clang-tools-extra/clangd/Preamble.cpp b/clang-tools-extra/clangd/Preamble.cpp
index f5e512793e98e..984c9369fe27b 100644
--- a/clang-tools-extra/clangd/Preamble.cpp
+++ b/clang-tools-extra/clangd/Preamble.cpp
@@ -661,7 +661,7 @@ buildPreamble(PathRef FileName, CompilerInvocation CI,
     log("Built preamble of size {0} for file {1} version {2} in {3} seconds",
         BuiltPreamble->getSize(), FileName, Inputs.Version,
         PreambleTimer.getTime());
-    std::vector<Diag> Diags = PreambleDiagnostics.take();
+    std::vector<Diag> Diags = PreambleDiagnostics.take(Inputs.Contents);
     auto Result = std::make_shared<PreambleData>(std::move(*BuiltPreamble));
     Result->Version = Inputs.Version;
     Result->CompileCommand = Inputs.CompileCommand;
@@ -708,7 +708,7 @@ buildPreamble(PathRef FileName, CompilerInvocation CI,
 
   elog("Could not build a preamble for file {0} version {1}: {2}", FileName,
        Inputs.Version, BuiltPreamble.getError().message());
-  for (const Diag &D : PreambleDiagnostics.take()) {
+  for (const Diag &D : PreambleDiagnostics.take(Inputs.Contents)) {
     if (D.Severity < DiagnosticsEngine::Error)
       continue;
     // Not an ideal way to show errors, but better than nothing!
diff --git a/clang-tools-extra/clangd/TUScheduler.cpp b/clang-tools-extra/clangd/TUScheduler.cpp
index 0661ecb58008e..ee4f786503960 100644
--- a/clang-tools-extra/clangd/TUScheduler.cpp
+++ b/clang-tools-extra/clangd/TUScheduler.cpp
@@ -918,7 +918,7 @@ void ASTWorker::update(ParseInputs Inputs, WantDiagnostics WantDiags,
     if (!CC1Args.empty())
       vlog("Driver produced command: cc1 {0}", printArgv(CC1Args));
     std::vector<Diag> CompilerInvocationDiags =
-        CompilerInvocationDiagConsumer.take();
+        CompilerInvocationDiagConsumer.take(Inputs.Contents);
     if (!Invocation) {
       elog("Could not build CompilerInvocation for file {0}", FileName);
       // Remove the old AST if it's still in cache.
@@ -995,9 +995,10 @@ void ASTWorker::runWithAST(
       // return a compatible preamble as ASTWorker::update blocks.
       std::optional<ParsedAST> NewAST;
       if (Invocation) {
-        NewAST = ParsedAST::build(FileName, FileInputs, std::move(Invocation),
-                                  CompilerInvocationDiagConsumer.take(),
-                                  getPossiblyStalePreamble());
+        NewAST = ParsedAST::build(
+            FileName, FileInputs, std::move(Invocation),
+            CompilerInvocationDiagConsumer.take(FileInputs.Contents),
+            getPossiblyStalePreamble());
         ++ASTBuildCount;
       }
       AST = NewAST ? std::make_unique<ParsedAST>(std::move(*NewAST)) : nullptr;
diff --git a/clang-tools-extra/clangd/test/diagnostics-tidy.test b/clang-tools-extra/clangd/test/diagnostics-tidy.test
index e592c9a0be7c3..864d028d8ad4d 100644
--- a/clang-tools-extra/clangd/test/diagnostics-tidy.test
+++ b/clang-tools-extra/clangd/test/diagnostics-tidy.test
@@ -11,7 +11,7 @@
 # CHECK-NEXT:        "codeDescription": {
 # CHECK-NEXT:          "href": "https://clang.llvm.org/extra/clang-tidy/checks/bugprone/sizeof-expression.html"
 # CHECK-NEXT:        },
-# CHECK-NEXT:        "message": "Suspicious usage of 'sizeof(K)'; did you mean 'K'?",
+# CHECK-NEXT:        "message": "Suspicious usage of 'sizeof(K)'; did you mean 'K'? (fixes available)",
 # CHECK-NEXT:        "range": {
 # CHECK-NEXT:          "end": {
 # CHECK-NEXT:            "character": 16,
@@ -30,6 +30,82 @@
 # CHECK-NEXT:    "version": 0
 # CHECK-NEXT:  }
 ---
+{"jsonrpc":"2.0","id":2,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"test:///foo.c"},"range":{"start":{"line":1,"character":6},"end":{"line":1,"character":16}},"context":{"diagnostics":[{"range":{"start":{"line":1,"character":6},"end":{"line":1,"character":16}},"severity":2,"message":"Suspicious usage of 'sizeof(K)'; did you mean 'K'? (fixes available)","code":"bugprone-sizeof-expression","source":"clang-tidy"}]}}}
+#      CHECK: {
+#      CHECK:   "result": [
+# CHECK-NEXT:     {
+# CHECK-NEXT:       "arguments": [
+# CHECK-NEXT:         {
+# CHECK-NEXT:           "changes": {
+# CHECK-NEXT:             "file:///{{.*}}/foo.c": [
+# CHECK-NEXT:               {
+# CHECK-NEXT:                 "newText": " // NOLINT(bugprone-sizeof-expression)",
+# CHECK-NEXT:                 "range": {
+# CHECK-NEXT:                   "end": {
+# CHECK-NEXT:                     "character": 17,
+# CHECK-NEXT:                     "line": 1
+# CHECK-NEXT:                   },
+# CHECK-NEXT:                   "start": {
+# CHECK-NEXT:                     "character": 17,
+# CHECK-NEXT:                     "line": 1
+# CHECK-NEXT:                   }
+# CHECK-NEXT:                 }
+# CHECK-NEXT:               }
+# CHECK-NEXT:             ]
+# CHECK-NEXT:           }
+# CHECK-NEXT:         }
+# CHECK-NEXT:       ],
+# CHECK-NEXT:       "command": "clangd.applyFix",
+# CHECK-NEXT:       "title": "Apply fix: suppress this warning with NOLINT"
+# CHECK-NEXT:     },
+# CHECK-NEXT:     {
+# CHECK-NEXT:       "arguments": [
+# CHECK-NEXT:         {
+# CHECK-NEXT:           "changes": {
+# CHECK-NEXT:             "file:///{{.*}}/foo.c": [
+# CHECK-NEXT:               {
+# CHECK-NEXT:                 "newText": "// NOLINTNEXTLINE(bugprone-sizeof-expression)\n",
+# 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": 1
+# CHECK-NEXT:                   }
+# CHECK-NEXT:                 }
+# CHECK-NEXT:               }
+# CHECK-NEXT:             ]
+# CHECK-NEXT:           }
+# CHECK-NEXT:         }
+# CHECK-NEXT:       ],
+# CHECK-NEXT:       "command": "clangd.applyFix",
+# CHECK-NEXT:       "title": "Apply fix: suppress this warning with NOLINTNEXTLINE"
+# CHECK-NEXT:     },
+# CHECK-NEXT:     {
+# CHECK-NEXT:       "arguments": [
+# CHECK-NEXT:         {
+# CHECK-NEXT:           "file": "file:///{{.*}}/foo.c",
+# CHECK-NEXT:           "selection": {
+# CHECK-NEXT:             "end": {
+# CHECK-NEXT:               "character": 16,
+# CHECK-NEXT:               "line": 1
+# CHECK-NEXT:             },
+# CHECK-NEXT:             "start": {
+# CHECK-NEXT:               "character": 6,
+# CHECK-NEXT:               "line": 1
+# CHECK-NEXT:             }
+# CHECK-NEXT:           },
+# CHECK-NEXT:           "tweakID": "ExtractVariable"
+# CHECK-NEXT:         }
+# CHECK-NEXT:       ],
+# CHECK-NEXT:       "command": "clangd.applyTweak",
+# CHECK-NEXT:       "title": "Extract subexpression to variable"
+# CHECK-NEXT:     }
+# CHECK-NEXT:   ]
+# CHECK-NEXT: }
+---
 {"jsonrpc":"2.0","id":2,"method":"sync","params":null}
 ---
 {"jsonrpc":"2.0","method":"textDocument/didClose","params":{"textDocument":{"uri":"test:///foo.c"}}}
diff --git a/clang-tools-extra/clangd/tool/Check.cpp b/clang-tools-extra/clangd/tool/Check.cpp
index 03c4f58a49c9c..35695e9b4e995 100644
--- a/clang-tools-extra/clangd/tool/Check.cpp
+++ b/clang-tools-extra/clangd/tool/Check.cpp
@@ -227,7 +227,7 @@ class Checker {
     log("Parsing command...");
     Invocation =
         buildCompilerInvocation(Inputs, CaptureInvocationDiags, &CC1Args);
-    auto InvocationDiags = CaptureInvocationDiags.take();
+    auto InvocationDiags = CaptureInvocationDiags.take(Inputs.Contents);
     ErrCount += showErrors(InvocationDiags);
     log("internal (cc1) args are: {0}", printArgv(CC1Args));
     if (!Invocation) {
diff --git a/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp b/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp
index 95bf5e54fc792..b743f3d312496 100644
--- a/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp
+++ b/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp
@@ -223,23 +223,37 @@ TEST_F(LSPTest, ClangTidyRename) {
   ASSERT_TRUE(Diags && !Diags->empty());
   auto RenameDiag = Diags->front();
 
-  auto RenameCommand =
-      (*Client
-            .call("textDocument/codeAction",
-                  llvm::json::Object{
-                      {"textDocument", Client.documentID("foo.cpp")},
-                      {"context",
-                       llvm::json::Object{
-                           {"diagnostics", llvm::json::Array{RenameDiag}}}},
-                      {"range", Source.range()}})
-            .takeValue()
-            .getAsArray())[0];
-
-  ASSERT_EQ((*RenameCommand.getAsObject())["title"],
+  auto CodeActions =
+      *Client
+           .call("textDocument/codeAction",
+                 llvm::json::Object{
+                     {"textDocument", Client.documentID("foo.cpp")},
+                     {"context",
+                      llvm::json::Object{
+                          {"diagnostics", llvm::json::Array{RenameDiag}}}},
+                     {"range", Source.range()}})
+           .takeValue()
+           .getAsArray();
+
+  // Find the rename code action by title.
+  const llvm::json::Value *RenameCommand = nullptr;
+  for (const auto &CA : CodeActions) {
+    if (const auto *Obj = CA.getAsObject()) {
+      if (auto Title = Obj->getString("title")) {
+        if (Title->starts_with("Apply fix: change")) {
+          RenameCommand = &CA;
+          break;
+        }
+      }
+    }
+  }
+  ASSERT_NE(RenameCommand, nullptr);
+
+  ASSERT_EQ(*RenameCommand->getAsObject()->getString("title"),
             "Apply fix: change 'foo' to 'Foo'");
 
   Client.expectServerCall("workspace/applyEdit");
-  Client.call("workspace/executeCommand", RenameCommand);
+  Client.call("workspace/executeCommand", *RenameCommand);
   Client.sync();
 
   auto Params = Client.takeCallParams("workspace/applyEdit");
@@ -281,6 +295,125 @@ TEST_F(LSPTest, ClangTidyCrash_Issue109367) {
   Client.sync();
 }
 
+TEST_F(LSPTest, ClangTidyNolintCodeAction) {
+  // This test requires clang-tidy checks to be linked in.
+  if (!CLANGD_TIDY_CHECKS)
+    return;
+  Annotations Source(R"cpp(
+    int *$diag[[p]] = 0;$comment[[]]
+  )cpp");
+  constexpr auto ClangTidyProvider = [](tidy::ClangTidyOptions &ClangTidyOpts,
+                                        llvm::StringRef) {
+    ClangTidyOpts.Checks = {"-*,modernize-use-nullptr"};
+  };
+  Opts.ClangTidyProvider = ClangTidyProvider;
+  auto &Client = start();
+  Client.didOpen("foo.cpp", Source.code());
+
+  auto Diags = Client.diagnostics("foo.cpp");
+  ASSERT_TRUE(Diags && !Diags->empty());
+  auto UnusedDiag = Diags->front();
+
+  auto CodeActions =
+      Client
+          .call("textDocument/codeAction",
+                llvm::json::Object{
+                    {"textDocument", Client.documentID("foo.cpp")},
+                    {"context",
+                     llvm::json::Object{
+                         {"diagnostics", llvm::json::Array{UnusedDiag}}}},
+                    {"range", Source.range("diag")}})
+          .takeValue();
+
+  // Find the NOLINT code action.
+  const llvm::json::Object *NolintAction = nullptr;
+  for (const auto &CA : *CodeActions.getAsArray()) {
+    if (const auto *Obj = CA.getAsObject()) {
+      if (auto Title = Obj->getString("title")) {
+        if (Title->contains("NOLINT") && !Title->contains("NOLINTNEXTLINE")) {
+          NolintAction = Obj;
+          break;
+        }
+      }
+    }
+  }
+  ASSERT_NE(NolintAction, nullptr) << "Expected a NOLINT code action";
+  EXPECT_EQ(NolintAction->getString("title"),
+            "Apply fix: suppress this warning with NOLINT");
+
+  auto Uri = [&](llvm::StringRef Path) {
+    return Client.uri(Path).getAsString().value().str();
+  };
+  llvm::json::Array ExpectedArguments = llvm::json::Array{llvm::json::Object{
+      {"changes",
+       llvm::json::Object{{Uri("foo.cpp"),
+                           llvm::json::Array{llvm::json::Object{
+                               {"range", Source.range("comment")},
+                               {"newText", " // NOLINT(modernize-use-nullptr)"},
+                           }}}}}}};
+  EXPECT_EQ(*NolintAction->getArray("arguments"), ExpectedArguments);
+}
+
+TEST_F(LSPTest, ClangTidyNolintNextLineCodeAction) {
+  // This test requires clang-tidy checks to be linked in.
+  if (!CLANGD_TIDY_CHECKS)
+    return;
+  Annotations Source(R"cpp(
+$comment[[]]    int *$diag[[p]] = 0;
+  )cpp");
+  constexpr auto ClangTidyProvider = [](tidy::ClangTidyOptions &ClangTidyOpts,
+                                        llvm::StringRef) {
+    ClangTidyOpts.Checks = {"-*,modernize-use-nullptr"};
+  };
+  Opts.ClangTidyProvider = ClangTidyProvider;
+  auto &Client = start();
+  Client.didOpen("foo.cpp", Source.code());
+
+  auto Diags = Client.diagnostics("foo.cpp");
+  ASSERT_TRUE(Diags && !Diags->empty());
+  auto UnusedDiag = Diags->front();
+
+  auto CodeActions =
+      Client
+          .call("textDocument/codeAction",
+                llvm::json::Object{
+                    {"textDocument", Client.documentID("foo.cpp")},
+                    {"context",
+                     llvm::json::Object{
+                         {"diagnostics", llvm::json::Array{UnusedDiag}}}},
+                    {"range", Source.range("diag")}})
+          .takeValue();
+
+  // Find the NOLINT code action.
+  const llvm::json::Object *NolintAction = nullptr;
+  for (const auto &CA : *CodeActions.getAsArray()) {
+    if (const auto *Obj = CA.getAsObject()) {
+      if (auto Title = Obj->getString("title")) {
+        if (Title->contains("NOLINTNEXTLINE")) {
+          NolintAction = Obj;
+          break;
+        }
+      }
+    }
+  }
+  ASSERT_NE(NolintAction, nullptr) << "Expected a NOLINTNEXTLINE code action";
+  EXPECT_EQ(NolintAction->getString("title"),
+            "Apply fix: suppress this warning with NOLINTNEXTLINE");
+
+  auto Uri = [&](llvm::StringRef Path) {
+    return Client.uri(Path).getAsString().value().str();
+  };
+  llvm::json::Array ExpectedArguments = llvm::json::Array{llvm::json::Object{
+      {"changes",
+       llvm::json::Object{
+           {Uri("foo.cpp"),
+            llvm::json::Array{llvm::json::Object{
+                {"range", Source.range("comment")},
+                {"newText", "    // NOLINTNEXTLINE(modernize-use-nullptr)\n"},
+            }}}}}}};
+  EXPECT_EQ(*NolintAction->getArray("arguments"), ExpectedArguments);
+}
+
 TEST_F(LSPTest, IncomingCalls) {
   Annotations Code(R"cpp(
     void calle^e(int);
diff --git a/clang-tools-extra/clangd/unittests/CompilerTests.cpp b/clang-tools-extra/clangd/unittests/CompilerTests.cpp
index 9c8ad8d70b47b..0534a9f711c80 100644
--- a/clang-tools-extra/clangd/unittests/CompilerTests.cpp
+++ b/clang-tools-extra/clangd/unittests/CompilerTests.cpp
@@ -126,7 +126,7 @@ TEST(BuildCompilerInvocation, SuppressDiags) {
   Cfg.Diagnostics.Suppress = {"drv_unknown_argument"};
   Wit...
[truncated]

@llvmbot
Copy link
Copy Markdown
Member

llvmbot commented Mar 26, 2026

@llvm/pr-subscribers-clang-tools-extra

Author: Sirui Mu (Lancern)

Changes

This patch adds code actions to suppress diagnostics generated by clang-tidy. There are two variants of the code action. The first one suppresses such diagnostics by appending // NOLINT(check-name) to the end of the source line. The second one suppresses such diagnostics by inserting a source line containing // NOLINTNEXTLINE(check-name) before the source line of the diagnostics.

A previous PR #114661 attempted to land similar feature, but it ends up being closed by its author.

Assisted-by: Github Copilot / Claude Opus 4.6


Patch is 20.99 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/188796.diff

10 Files Affected:

  • (modified) clang-tools-extra/clangd/Diagnostics.cpp (+71-1)
  • (modified) clang-tools-extra/clangd/Diagnostics.h (+2-1)
  • (modified) clang-tools-extra/clangd/ParsedAST.cpp (+2-2)
  • (modified) clang-tools-extra/clangd/Preamble.cpp (+2-2)
  • (modified) clang-tools-extra/clangd/TUScheduler.cpp (+5-4)
  • (modified) clang-tools-extra/clangd/test/diagnostics-tidy.test (+77-1)
  • (modified) clang-tools-extra/clangd/tool/Check.cpp (+1-1)
  • (modified) clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp (+147-14)
  • (modified) clang-tools-extra/clangd/unittests/CompilerTests.cpp (+1-1)
  • (modified) clang-tools-extra/clangd/unittests/TestTU.cpp (+1-1)
diff --git a/clang-tools-extra/clangd/Diagnostics.cpp b/clang-tools-extra/clangd/Diagnostics.cpp
index f68092b9a0886..bf638c52d00aa 100644
--- a/clang-tools-extra/clangd/Diagnostics.cpp
+++ b/clang-tools-extra/clangd/Diagnostics.cpp
@@ -571,7 +571,73 @@ int getSeverity(DiagnosticsEngine::Level L) {
   llvm_unreachable("Unknown diagnostic level!");
 }
 
-std::vector<Diag> StoreDiags::take(const clang::tidy::ClangTidyContext *Tidy) {
+// For clang-tidy diagnostics, generates a fix that suppresses the diagnostic
+// on the current line by appending a NOLINT comment.
+static std::optional<Fix> makeNolintFix(llvm::StringRef Code, const Diag &D) {
+  if (D.Source != Diag::ClangTidy || D.Name.empty() || !D.InsideMainFile)
+    return std::nullopt;
+
+  llvm::Expected<size_t> StartOffset = positionToOffset(Code, D.Range.start);
+  if (!StartOffset) {
+    llvm::consumeError(StartOffset.takeError());
+    return std::nullopt;
+  }
+  size_t LineEnd = Code.find('\n', *StartOffset);
+  if (LineEnd == llvm::StringRef::npos)
+    LineEnd = Code.size();
+  Position InsertPos = offsetToPosition(Code, LineEnd);
+
+  TextEdit Edit;
+  Edit.range = {InsertPos, InsertPos};
+  Edit.newText = llvm::formatv(" // NOLINT({0})", D.Name);
+
+  Fix F;
+  F.Message = llvm::formatv("suppress this warning with NOLINT");
+  F.Edits.push_back(std::move(Edit));
+
+  return F;
+}
+
+// For clang-tidy diagnostics, generates a fix that suppresses the diagnostic on
+// the current line by inserting a NOLINTNEXTLINE comment before the current
+// line.
+static std::optional<Fix> makeNolintNextLineFix(llvm::StringRef Code,
+                                                const Diag &D) {
+  if (D.Source != Diag::ClangTidy || D.Name.empty() || !D.InsideMainFile)
+    return std::nullopt;
+
+  llvm::Expected<size_t> StartOffset = positionToOffset(Code, D.Range.start);
+  if (!StartOffset) {
+    llvm::consumeError(StartOffset.takeError());
+    return std::nullopt;
+  }
+  size_t LineStart = Code.rfind('\n', *StartOffset);
+  if (LineStart == llvm::StringRef::npos)
+    LineStart = 0;
+  else
+    ++LineStart;
+
+  size_t LineTextStart = Code.find_first_not_of(" \t", LineStart);
+  if (LineTextStart == llvm::StringRef::npos || LineTextStart > *StartOffset)
+    LineTextStart = *StartOffset;
+
+  size_t Indentation = LineTextStart - LineStart;
+  Position InsertPos = offsetToPosition(Code, LineStart);
+
+  TextEdit Edit;
+  Edit.range = {InsertPos, InsertPos};
+  Edit.newText = std::string(Indentation, ' ');
+  Edit.newText.append(llvm::formatv("// NOLINTNEXTLINE({0})\n", D.Name));
+
+  Fix F;
+  F.Message = llvm::formatv("suppress this warning with NOLINTNEXTLINE");
+  F.Edits.push_back(std::move(Edit));
+
+  return F;
+}
+
+std::vector<Diag> StoreDiags::take(llvm::StringRef Code,
+                                   const clang::tidy::ClangTidyContext *Tidy) {
   // Do not forget to emit a pending diagnostic if there is one.
   flushLastDiag();
 
@@ -605,6 +671,10 @@ std::vector<Diag> StoreDiags::take(const clang::tidy::ClangTidyContext *Tidy) {
       if (!TidyDiag.empty()) {
         Diag.Name = std::move(TidyDiag);
         Diag.Source = Diag::ClangTidy;
+        if (auto NolintFix = makeNolintFix(Code, Diag))
+          Diag.Fixes.push_back(std::move(*NolintFix));
+        if (auto NolintNextLineFix = makeNolintNextLineFix(Code, Diag))
+          Diag.Fixes.push_back(std::move(*NolintNextLineFix));
         // clang-tidy bakes the name into diagnostic messages. Strip it out.
         // It would be much nicer to make clang-tidy not do this.
         auto CleanMessage = [&](std::string &Msg) {
diff --git a/clang-tools-extra/clangd/Diagnostics.h b/clang-tools-extra/clangd/Diagnostics.h
index d433abb530151..641468c2a8974 100644
--- a/clang-tools-extra/clangd/Diagnostics.h
+++ b/clang-tools-extra/clangd/Diagnostics.h
@@ -138,7 +138,8 @@ std::optional<std::string> getDiagnosticDocURI(Diag::DiagSource, unsigned ID,
 class StoreDiags : public DiagnosticConsumer {
 public:
   // The ClangTidyContext populates Source and Name for clang-tidy diagnostics.
-  std::vector<Diag> take(const clang::tidy::ClangTidyContext *Tidy = nullptr);
+  std::vector<Diag> take(llvm::StringRef Code,
+                         const clang::tidy::ClangTidyContext *Tidy = nullptr);
 
   void BeginSourceFile(const LangOptions &Opts,
                        const Preprocessor *PP) override;
diff --git a/clang-tools-extra/clangd/ParsedAST.cpp b/clang-tools-extra/clangd/ParsedAST.cpp
index 4e873f1257a17..41b6977b71930 100644
--- a/clang-tools-extra/clangd/ParsedAST.cpp
+++ b/clang-tools-extra/clangd/ParsedAST.cpp
@@ -471,7 +471,7 @@ ParsedAST::build(llvm::StringRef Filename, const ParseInputs &Inputs,
   if (!Clang) {
     // The last diagnostic contains information about the reason of this
     // failure.
-    std::vector<Diag> Diags(ASTDiags.take());
+    std::vector<Diag> Diags(ASTDiags.take(Inputs.Contents));
     elog("Failed to prepare a compiler instance: {0}",
          !Diags.empty() ? static_cast<DiagBase &>(Diags.back()).Message
                         : "unknown error");
@@ -748,7 +748,7 @@ ParsedAST::build(llvm::StringRef Filename, const ParseInputs &Inputs,
     llvm::append_range(Diags, Patch->patchedDiags());
   // Finally, add diagnostics coming from the AST.
   {
-    std::vector<Diag> D = ASTDiags.take(&*CTContext);
+    std::vector<Diag> D = ASTDiags.take(Inputs.Contents, &*CTContext);
     Diags.insert(Diags.end(), D.begin(), D.end());
   }
   ParsedAST Result(Filename, Inputs.Version, std::move(Preamble),
diff --git a/clang-tools-extra/clangd/Preamble.cpp b/clang-tools-extra/clangd/Preamble.cpp
index f5e512793e98e..984c9369fe27b 100644
--- a/clang-tools-extra/clangd/Preamble.cpp
+++ b/clang-tools-extra/clangd/Preamble.cpp
@@ -661,7 +661,7 @@ buildPreamble(PathRef FileName, CompilerInvocation CI,
     log("Built preamble of size {0} for file {1} version {2} in {3} seconds",
         BuiltPreamble->getSize(), FileName, Inputs.Version,
         PreambleTimer.getTime());
-    std::vector<Diag> Diags = PreambleDiagnostics.take();
+    std::vector<Diag> Diags = PreambleDiagnostics.take(Inputs.Contents);
     auto Result = std::make_shared<PreambleData>(std::move(*BuiltPreamble));
     Result->Version = Inputs.Version;
     Result->CompileCommand = Inputs.CompileCommand;
@@ -708,7 +708,7 @@ buildPreamble(PathRef FileName, CompilerInvocation CI,
 
   elog("Could not build a preamble for file {0} version {1}: {2}", FileName,
        Inputs.Version, BuiltPreamble.getError().message());
-  for (const Diag &D : PreambleDiagnostics.take()) {
+  for (const Diag &D : PreambleDiagnostics.take(Inputs.Contents)) {
     if (D.Severity < DiagnosticsEngine::Error)
       continue;
     // Not an ideal way to show errors, but better than nothing!
diff --git a/clang-tools-extra/clangd/TUScheduler.cpp b/clang-tools-extra/clangd/TUScheduler.cpp
index 0661ecb58008e..ee4f786503960 100644
--- a/clang-tools-extra/clangd/TUScheduler.cpp
+++ b/clang-tools-extra/clangd/TUScheduler.cpp
@@ -918,7 +918,7 @@ void ASTWorker::update(ParseInputs Inputs, WantDiagnostics WantDiags,
     if (!CC1Args.empty())
       vlog("Driver produced command: cc1 {0}", printArgv(CC1Args));
     std::vector<Diag> CompilerInvocationDiags =
-        CompilerInvocationDiagConsumer.take();
+        CompilerInvocationDiagConsumer.take(Inputs.Contents);
     if (!Invocation) {
       elog("Could not build CompilerInvocation for file {0}", FileName);
       // Remove the old AST if it's still in cache.
@@ -995,9 +995,10 @@ void ASTWorker::runWithAST(
       // return a compatible preamble as ASTWorker::update blocks.
       std::optional<ParsedAST> NewAST;
       if (Invocation) {
-        NewAST = ParsedAST::build(FileName, FileInputs, std::move(Invocation),
-                                  CompilerInvocationDiagConsumer.take(),
-                                  getPossiblyStalePreamble());
+        NewAST = ParsedAST::build(
+            FileName, FileInputs, std::move(Invocation),
+            CompilerInvocationDiagConsumer.take(FileInputs.Contents),
+            getPossiblyStalePreamble());
         ++ASTBuildCount;
       }
       AST = NewAST ? std::make_unique<ParsedAST>(std::move(*NewAST)) : nullptr;
diff --git a/clang-tools-extra/clangd/test/diagnostics-tidy.test b/clang-tools-extra/clangd/test/diagnostics-tidy.test
index e592c9a0be7c3..864d028d8ad4d 100644
--- a/clang-tools-extra/clangd/test/diagnostics-tidy.test
+++ b/clang-tools-extra/clangd/test/diagnostics-tidy.test
@@ -11,7 +11,7 @@
 # CHECK-NEXT:        "codeDescription": {
 # CHECK-NEXT:          "href": "https://clang.llvm.org/extra/clang-tidy/checks/bugprone/sizeof-expression.html"
 # CHECK-NEXT:        },
-# CHECK-NEXT:        "message": "Suspicious usage of 'sizeof(K)'; did you mean 'K'?",
+# CHECK-NEXT:        "message": "Suspicious usage of 'sizeof(K)'; did you mean 'K'? (fixes available)",
 # CHECK-NEXT:        "range": {
 # CHECK-NEXT:          "end": {
 # CHECK-NEXT:            "character": 16,
@@ -30,6 +30,82 @@
 # CHECK-NEXT:    "version": 0
 # CHECK-NEXT:  }
 ---
+{"jsonrpc":"2.0","id":2,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"test:///foo.c"},"range":{"start":{"line":1,"character":6},"end":{"line":1,"character":16}},"context":{"diagnostics":[{"range":{"start":{"line":1,"character":6},"end":{"line":1,"character":16}},"severity":2,"message":"Suspicious usage of 'sizeof(K)'; did you mean 'K'? (fixes available)","code":"bugprone-sizeof-expression","source":"clang-tidy"}]}}}
+#      CHECK: {
+#      CHECK:   "result": [
+# CHECK-NEXT:     {
+# CHECK-NEXT:       "arguments": [
+# CHECK-NEXT:         {
+# CHECK-NEXT:           "changes": {
+# CHECK-NEXT:             "file:///{{.*}}/foo.c": [
+# CHECK-NEXT:               {
+# CHECK-NEXT:                 "newText": " // NOLINT(bugprone-sizeof-expression)",
+# CHECK-NEXT:                 "range": {
+# CHECK-NEXT:                   "end": {
+# CHECK-NEXT:                     "character": 17,
+# CHECK-NEXT:                     "line": 1
+# CHECK-NEXT:                   },
+# CHECK-NEXT:                   "start": {
+# CHECK-NEXT:                     "character": 17,
+# CHECK-NEXT:                     "line": 1
+# CHECK-NEXT:                   }
+# CHECK-NEXT:                 }
+# CHECK-NEXT:               }
+# CHECK-NEXT:             ]
+# CHECK-NEXT:           }
+# CHECK-NEXT:         }
+# CHECK-NEXT:       ],
+# CHECK-NEXT:       "command": "clangd.applyFix",
+# CHECK-NEXT:       "title": "Apply fix: suppress this warning with NOLINT"
+# CHECK-NEXT:     },
+# CHECK-NEXT:     {
+# CHECK-NEXT:       "arguments": [
+# CHECK-NEXT:         {
+# CHECK-NEXT:           "changes": {
+# CHECK-NEXT:             "file:///{{.*}}/foo.c": [
+# CHECK-NEXT:               {
+# CHECK-NEXT:                 "newText": "// NOLINTNEXTLINE(bugprone-sizeof-expression)\n",
+# 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": 1
+# CHECK-NEXT:                   }
+# CHECK-NEXT:                 }
+# CHECK-NEXT:               }
+# CHECK-NEXT:             ]
+# CHECK-NEXT:           }
+# CHECK-NEXT:         }
+# CHECK-NEXT:       ],
+# CHECK-NEXT:       "command": "clangd.applyFix",
+# CHECK-NEXT:       "title": "Apply fix: suppress this warning with NOLINTNEXTLINE"
+# CHECK-NEXT:     },
+# CHECK-NEXT:     {
+# CHECK-NEXT:       "arguments": [
+# CHECK-NEXT:         {
+# CHECK-NEXT:           "file": "file:///{{.*}}/foo.c",
+# CHECK-NEXT:           "selection": {
+# CHECK-NEXT:             "end": {
+# CHECK-NEXT:               "character": 16,
+# CHECK-NEXT:               "line": 1
+# CHECK-NEXT:             },
+# CHECK-NEXT:             "start": {
+# CHECK-NEXT:               "character": 6,
+# CHECK-NEXT:               "line": 1
+# CHECK-NEXT:             }
+# CHECK-NEXT:           },
+# CHECK-NEXT:           "tweakID": "ExtractVariable"
+# CHECK-NEXT:         }
+# CHECK-NEXT:       ],
+# CHECK-NEXT:       "command": "clangd.applyTweak",
+# CHECK-NEXT:       "title": "Extract subexpression to variable"
+# CHECK-NEXT:     }
+# CHECK-NEXT:   ]
+# CHECK-NEXT: }
+---
 {"jsonrpc":"2.0","id":2,"method":"sync","params":null}
 ---
 {"jsonrpc":"2.0","method":"textDocument/didClose","params":{"textDocument":{"uri":"test:///foo.c"}}}
diff --git a/clang-tools-extra/clangd/tool/Check.cpp b/clang-tools-extra/clangd/tool/Check.cpp
index 03c4f58a49c9c..35695e9b4e995 100644
--- a/clang-tools-extra/clangd/tool/Check.cpp
+++ b/clang-tools-extra/clangd/tool/Check.cpp
@@ -227,7 +227,7 @@ class Checker {
     log("Parsing command...");
     Invocation =
         buildCompilerInvocation(Inputs, CaptureInvocationDiags, &CC1Args);
-    auto InvocationDiags = CaptureInvocationDiags.take();
+    auto InvocationDiags = CaptureInvocationDiags.take(Inputs.Contents);
     ErrCount += showErrors(InvocationDiags);
     log("internal (cc1) args are: {0}", printArgv(CC1Args));
     if (!Invocation) {
diff --git a/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp b/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp
index 95bf5e54fc792..b743f3d312496 100644
--- a/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp
+++ b/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp
@@ -223,23 +223,37 @@ TEST_F(LSPTest, ClangTidyRename) {
   ASSERT_TRUE(Diags && !Diags->empty());
   auto RenameDiag = Diags->front();
 
-  auto RenameCommand =
-      (*Client
-            .call("textDocument/codeAction",
-                  llvm::json::Object{
-                      {"textDocument", Client.documentID("foo.cpp")},
-                      {"context",
-                       llvm::json::Object{
-                           {"diagnostics", llvm::json::Array{RenameDiag}}}},
-                      {"range", Source.range()}})
-            .takeValue()
-            .getAsArray())[0];
-
-  ASSERT_EQ((*RenameCommand.getAsObject())["title"],
+  auto CodeActions =
+      *Client
+           .call("textDocument/codeAction",
+                 llvm::json::Object{
+                     {"textDocument", Client.documentID("foo.cpp")},
+                     {"context",
+                      llvm::json::Object{
+                          {"diagnostics", llvm::json::Array{RenameDiag}}}},
+                     {"range", Source.range()}})
+           .takeValue()
+           .getAsArray();
+
+  // Find the rename code action by title.
+  const llvm::json::Value *RenameCommand = nullptr;
+  for (const auto &CA : CodeActions) {
+    if (const auto *Obj = CA.getAsObject()) {
+      if (auto Title = Obj->getString("title")) {
+        if (Title->starts_with("Apply fix: change")) {
+          RenameCommand = &CA;
+          break;
+        }
+      }
+    }
+  }
+  ASSERT_NE(RenameCommand, nullptr);
+
+  ASSERT_EQ(*RenameCommand->getAsObject()->getString("title"),
             "Apply fix: change 'foo' to 'Foo'");
 
   Client.expectServerCall("workspace/applyEdit");
-  Client.call("workspace/executeCommand", RenameCommand);
+  Client.call("workspace/executeCommand", *RenameCommand);
   Client.sync();
 
   auto Params = Client.takeCallParams("workspace/applyEdit");
@@ -281,6 +295,125 @@ TEST_F(LSPTest, ClangTidyCrash_Issue109367) {
   Client.sync();
 }
 
+TEST_F(LSPTest, ClangTidyNolintCodeAction) {
+  // This test requires clang-tidy checks to be linked in.
+  if (!CLANGD_TIDY_CHECKS)
+    return;
+  Annotations Source(R"cpp(
+    int *$diag[[p]] = 0;$comment[[]]
+  )cpp");
+  constexpr auto ClangTidyProvider = [](tidy::ClangTidyOptions &ClangTidyOpts,
+                                        llvm::StringRef) {
+    ClangTidyOpts.Checks = {"-*,modernize-use-nullptr"};
+  };
+  Opts.ClangTidyProvider = ClangTidyProvider;
+  auto &Client = start();
+  Client.didOpen("foo.cpp", Source.code());
+
+  auto Diags = Client.diagnostics("foo.cpp");
+  ASSERT_TRUE(Diags && !Diags->empty());
+  auto UnusedDiag = Diags->front();
+
+  auto CodeActions =
+      Client
+          .call("textDocument/codeAction",
+                llvm::json::Object{
+                    {"textDocument", Client.documentID("foo.cpp")},
+                    {"context",
+                     llvm::json::Object{
+                         {"diagnostics", llvm::json::Array{UnusedDiag}}}},
+                    {"range", Source.range("diag")}})
+          .takeValue();
+
+  // Find the NOLINT code action.
+  const llvm::json::Object *NolintAction = nullptr;
+  for (const auto &CA : *CodeActions.getAsArray()) {
+    if (const auto *Obj = CA.getAsObject()) {
+      if (auto Title = Obj->getString("title")) {
+        if (Title->contains("NOLINT") && !Title->contains("NOLINTNEXTLINE")) {
+          NolintAction = Obj;
+          break;
+        }
+      }
+    }
+  }
+  ASSERT_NE(NolintAction, nullptr) << "Expected a NOLINT code action";
+  EXPECT_EQ(NolintAction->getString("title"),
+            "Apply fix: suppress this warning with NOLINT");
+
+  auto Uri = [&](llvm::StringRef Path) {
+    return Client.uri(Path).getAsString().value().str();
+  };
+  llvm::json::Array ExpectedArguments = llvm::json::Array{llvm::json::Object{
+      {"changes",
+       llvm::json::Object{{Uri("foo.cpp"),
+                           llvm::json::Array{llvm::json::Object{
+                               {"range", Source.range("comment")},
+                               {"newText", " // NOLINT(modernize-use-nullptr)"},
+                           }}}}}}};
+  EXPECT_EQ(*NolintAction->getArray("arguments"), ExpectedArguments);
+}
+
+TEST_F(LSPTest, ClangTidyNolintNextLineCodeAction) {
+  // This test requires clang-tidy checks to be linked in.
+  if (!CLANGD_TIDY_CHECKS)
+    return;
+  Annotations Source(R"cpp(
+$comment[[]]    int *$diag[[p]] = 0;
+  )cpp");
+  constexpr auto ClangTidyProvider = [](tidy::ClangTidyOptions &ClangTidyOpts,
+                                        llvm::StringRef) {
+    ClangTidyOpts.Checks = {"-*,modernize-use-nullptr"};
+  };
+  Opts.ClangTidyProvider = ClangTidyProvider;
+  auto &Client = start();
+  Client.didOpen("foo.cpp", Source.code());
+
+  auto Diags = Client.diagnostics("foo.cpp");
+  ASSERT_TRUE(Diags && !Diags->empty());
+  auto UnusedDiag = Diags->front();
+
+  auto CodeActions =
+      Client
+          .call("textDocument/codeAction",
+                llvm::json::Object{
+                    {"textDocument", Client.documentID("foo.cpp")},
+                    {"context",
+                     llvm::json::Object{
+                         {"diagnostics", llvm::json::Array{UnusedDiag}}}},
+                    {"range", Source.range("diag")}})
+          .takeValue();
+
+  // Find the NOLINT code action.
+  const llvm::json::Object *NolintAction = nullptr;
+  for (const auto &CA : *CodeActions.getAsArray()) {
+    if (const auto *Obj = CA.getAsObject()) {
+      if (auto Title = Obj->getString("title")) {
+        if (Title->contains("NOLINTNEXTLINE")) {
+          NolintAction = Obj;
+          break;
+        }
+      }
+    }
+  }
+  ASSERT_NE(NolintAction, nullptr) << "Expected a NOLINTNEXTLINE code action";
+  EXPECT_EQ(NolintAction->getString("title"),
+            "Apply fix: suppress this warning with NOLINTNEXTLINE");
+
+  auto Uri = [&](llvm::StringRef Path) {
+    return Client.uri(Path).getAsString().value().str();
+  };
+  llvm::json::Array ExpectedArguments = llvm::json::Array{llvm::json::Object{
+      {"changes",
+       llvm::json::Object{
+           {Uri("foo.cpp"),
+            llvm::json::Array{llvm::json::Object{
+                {"range", Source.range("comment")},
+                {"newText", "    // NOLINTNEXTLINE(modernize-use-nullptr)\n"},
+            }}}}}}};
+  EXPECT_EQ(*NolintAction->getArray("arguments"), ExpectedArguments);
+}
+
 TEST_F(LSPTest, IncomingCalls) {
   Annotations Code(R"cpp(
     void calle^e(int);
diff --git a/clang-tools-extra/clangd/unittests/CompilerTests.cpp b/clang-tools-extra/clangd/unittests/CompilerTests.cpp
index 9c8ad8d70b47b..0534a9f711c80 100644
--- a/clang-tools-extra/clangd/unittests/CompilerTests.cpp
+++ b/clang-tools-extra/clangd/unittests/CompilerTests.cpp
@@ -126,7 +126,7 @@ TEST(BuildCompilerInvocation, SuppressDiags) {
   Cfg.Diagnostics.Suppress = {"drv_unknown_argument"};
   Wit...
[truncated]

@github-actions
Copy link
Copy Markdown

🪟 Windows x64 Test Results

  • 3110 tests passed
  • 30 tests skipped
  • 3 tests failed

Failed Tests

(click on a test name to see its output)

Clangd Unit Tests

Clangd Unit Tests._/ClangdTests_exe/DiagnosticTest/ClangTidySelfContainedDiags
Script:
--
C:\_work\llvm-project\llvm-project\build\tools\clang\tools\extra\clangd\unittests\.\ClangdTests.exe --gtest_filter=DiagnosticTest.ClangTidySelfContainedDiags
--
C:\_work\llvm-project\llvm-project\clang-tools-extra\clangd\unittests\DiagnosticsTests.cpp:936
Value of: TU.build().getDiagnostics()
Expected: has 4 elements and there exists some permutation of elements such that:
 - element #0 (Diag at 4:8-4:14 = ['A' should be initialized in a member initializer of the constructor]) and (is an object whose given field has 1 element that LSP fix 'A' should be initialized in a member initializer of the constructor {3:11-3:11 => " : A(1)", 4:8-4:14 => ""}), and
 - element #1 (Diag at 5:8-5:14 = ['B' should be initialized in a member initializer of the constructor]) and (is an object whose given field has 1 element that LSP fix 'B' should be initialized in a member initializer of the constructor {3:11-3:11 => " : B(1)", 5:8-5:14 => ""}), and
 - element #2 (Diag at 9:12-9:13 = [variable 'C' is not initialized]) and (is an object whose given field has 1 element that LSP fix variable 'C' is not initialized {9:13-9:13 => " = NAN", 0:0-0:0 => "#include <math.h>\0A\0A"}), and
 - element #3 (Diag at 10:13-10:14 = [variable 'D' is not initialized]) and (is an object whose given field has 1 element that LSP fix variable 'D' is not initialized {10:14-10:14 => " = NAN", 0:0-0:0 => "#include <math.h>\0A\0A"})
  Actual: { [4:8-4:14] 'A' should be initialized in a member initializer of the constructor, fixes: {'A' should be initialized in a member initializer of the constructor {3:11-3:11 => " : A(1)", 4:8-4:14 => ""}, suppress this warning with NOLINT {4:14-4:14 => " // NOLINT(cppcoreguidelines-prefer-member-initializer)"}, suppress this warning with NOLINTNEXTLINE {4:0-4:0 => "        // NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer)\0A"}}, [5:8-5:14] 'B' should be initialized in a member initializer of the constructor, fixes: {'B' should be initialized in a member initializer of the constructor {3:11-3:11 => " : B(1)", 5:8-5:14 => ""}, suppress this warning with NOLINT {5:14-5:14 => " // NOLINT(cppcoreguidelines-prefer-member-initializer)"}, suppress this warning with NOLINTNEXTLINE {5:0-5:0 => "        // NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer)\0A"}}, [9:12-9:13] variable 'C' is not initialized, fixes: {variable 'C' is not initialized {9:13-9:13 => " = NAN", 0:0-0:0 => "#include <math.h>\0A\0A"}, suppress this warning with NOLINT {9:14-9:14 => " // NOLINT(cppcoreguidelines-init-variables)"}, suppress this warning with NOLINTNEXTLINE {9:0-9:0 => "      // NOLINTNEXTLINE(cppcoreguidelines-init-variables)\0A"}}, [10:13-10:14] variable 'D' is not initialized, fixes: {variable 'D' is not initialized {10:14-10:14 => " = NAN", 0:0-0:0 => "#include <math.h>\0A\0A"}, suppress this warning with NOLINT {10:15-10:15 => " // NOLINT(cppcoreguidelines-init-variables)"}, suppress this warning with NOLINTNEXTLINE {10:0-10:0 => "      // NOLINTNEXTLINE(cppcoreguidelines-init-variables)\0A"}} }, where the following matchers don't match any elements:
matcher #0: (Diag at 4:8-4:14 = ['A' should be initialized in a member initializer of the constructor]) and (is an object whose given field has 1 element that LSP fix 'A' should be initialized in a member initializer of the constructor {3:11-3:11 => " : A(1)", 4:8-4:14 => ""}),
matcher #1: (Diag at 5:8-5:14 = ['B' should be initialized in a member initializer of the constructor]) and (is an object whose given field has 1 element that LSP fix 'B' should be initialized in a member initializer of the constructor {3:11-3:11 => " : B(1)", 5:8-5:14 => ""}),
matcher #2: (Diag at 9:12-9:13 = [variable 'C' is not initialized]) and (is an object whose given field has 1 element that LSP fix variable 'C' is not initialized {9:13-9:13 => " = NAN", 0:0-0:0 => "#include <math.h>\0A\0A"}),
matcher #3: (Diag at 10:13-10:14 = [variable 'D' is not initialized]) and (is an object whose given field has 1 element that LSP fix variable 'D' is not initialized {10:14-10:14 => " = NAN", 0:0-0:0 => "#include <math.h>\0A\0A"})
and where the following elements don't match any matchers:
element #0: [4:8-4:14] 'A' should be initialized in a member initializer of the constructor, fixes: {'A' should be initialized in a member initializer of the constructor {3:11-3:11 => " : A(1)", 4:8-4:14 => ""}, suppress this warning with NOLINT {4:14-4:14 => " // NOLINT(cppcoreguidelines-prefer-member-initializer)"}, suppress this warning with NOLINTNEXTLINE {4:0-4:0 => "        // NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer)\0A"}},
element #1: [5:8-5:14] 'B' should be initialized in a member initializer of the constructor, fixes: {'B' should be initialized in a member initializer of the constructor {3:11-3:11 => " : B(1)", 5:8-5:14 => ""}, suppress this warning with NOLINT {5:14-5:14 => " // NOLINT(cppcoreguidelines-prefer-member-initializer)"}, suppress this warning with NOLINTNEXTLINE {5:0-5:0 => "        // NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer)\0A"}},
element #2: [9:12-9:13] variable 'C' is not initialized, fixes: {variable 'C' is not initialized {9:13-9:13 => " = NAN", 0:0-0:0 => "#include <math.h>\0A\0A"}, suppress this warning with NOLINT {9:14-9:14 => " // NOLINT(cppcoreguidelines-init-variables)"}, suppress this warning with NOLINTNEXTLINE {9:0-9:0 => "      // NOLINTNEXTLINE(cppcoreguidelines-init-variables)\0A"}},
element #3: [10:13-10:14] variable 'D' is not initialized, fixes: {variable 'D' is not initialized {10:14-10:14 => " = NAN", 0:0-0:0 => "#include <math.h>\0A\0A"}, suppress this warning with NOLINT {10:15-10:15 => " // NOLINT(cppcoreguidelines-init-variables)"}, suppress this warning with NOLINTNEXTLINE {10:0-10:0 => "      // NOLINTNEXTLINE(cppcoreguidelines-init-variables)\0A"}}


Clangd Unit Tests._/ClangdTests_exe/DiagnosticTest/ClangTidySelfContainedDiagsFormatting
Script:
--
C:\_work\llvm-project\llvm-project\build\tools\clang\tools\extra\clangd\unittests\.\ClangdTests.exe --gtest_filter=DiagnosticTest.ClangTidySelfContainedDiagsFormatting
--
C:\_work\llvm-project\llvm-project\clang-tools-extra\clangd\unittests\DiagnosticsTests.cpp:978
Value of: TU.build().getDiagnostics()
Expected: has 2 elements and there exists some permutation of elements such that:
 - element #0 (Diag at 8:22-8:28 = [prefer using 'override' or (rarely) 'final' instead of 'virtual']) and (is an object whose given field has 1 element that LSP fix prefer using 'override' or (rarely) 'final' instead of 'virtual' {8:30-8:30 => " override", 8:6-8:17 => ""}), and
 - element #1 (Diag at 9:28-9:34 = [prefer using 'override' or (rarely) 'final' instead of 'virtual']) and (is an object whose given field has 1 element that LSP fix prefer using 'override' or (rarely) 'final' instead of 'virtual' {9:36-9:36 => " override", 9:6-9:19 => ""})
  Actual: { [8:22-8:28] prefer using 'override' or (rarely) 'final' instead of 'virtual', fixes: {prefer using 'override' or (rarely) 'final' instead of 'virtual' {8:30-8:30 => " override", 8:6-8:17 => ""}, suppress this warning with NOLINT {8:31-8:31 => " // NOLINT(cppcoreguidelines-explicit-virtual-functions)"}, suppress this warning with NOLINTNEXTLINE {8:0-8:0 => "      // NOLINTNEXTLINE(cppcoreguidelines-explicit-virtual-functions)\0A"}}, [9:28-9:34] prefer using 'override' or (rarely) 'final' instead of 'virtual', fixes: {prefer using 'override' or (rarely) 'final' instead of 'virtual' {9:36-9:36 => " override", 9:6-9:19 => ""}, suppress this warning with NOLINT {9:37-9:37 => " // NOLINT(cppcoreguidelines-explicit-virtual-functions)"}, suppress this warning with NOLINTNEXTLINE {9:0-9:0 => "      // NOLINTNEXTLINE(cppcoreguidelines-explicit-virtual-functions)\0A"}} }, where the following matchers don't match any elements:
matcher #0: (Diag at 8:22-8:28 = [prefer using 'override' or (rarely) 'final' instead of 'virtual']) and (is an object whose given field has 1 element that LSP fix prefer using 'override' or (rarely) 'final' instead of 'virtual' {8:30-8:30 => " override", 8:6-8:17 => ""}),
matcher #1: (Diag at 9:28-9:34 = [prefer using 'override' or (rarely) 'final' instead of 'virtual']) and (is an object whose given field has 1 element that LSP fix prefer using 'override' or (rarely) 'final' instead of 'virtual' {9:36-9:36 => " override", 9:6-9:19 => ""})
and where the following elements don't match any matchers:
element #0: [8:22-8:28] prefer using 'override' or (rarely) 'final' instead of 'virtual', fixes: {prefer using 'override' or (rarely) 'final' instead of 'virtual' {8:30-8:30 => " override", 8:6-8:17 => ""}, suppress this warning with NOLINT {8:31-8:31 => " // NOLINT(cppcoreguidelines-explicit-virtual-functions)"}, suppress this warning with NOLINTNEXTLINE {8:0-8:0 => "      // NOLINTNEXTLINE(cppcoreguidelines-explicit-virtual-functions)\0A"}},
element #1: [9:28-9:34] prefer using 'override' or (rarely) 'final' instead of 'virtual', fixes: {prefer using 'override' or (rarely) 'final' instead of 'virtual' {9:36-9:36 => " override", 9:6-9:19 => ""}, suppress this warning with NOLINT {9:37-9:37 => " // NOLINT(cppcoreguidelines-explicit-virtual-functions)"}, suppress this warning with NOLINTNEXTLINE {9:0-9:0 => "      // NOLINTNEXTLINE(cppcoreguidelines-explicit-virtual-functions)\0A"}}


Clangd Unit Tests._/ClangdTests_exe/DiagnosticsTest/ClangTidy
Script:
--
C:\_work\llvm-project\llvm-project\build\tools\clang\tools\extra\clangd\unittests\.\ClangdTests.exe --gtest_filter=DiagnosticsTest.ClangTidy
--
C:\_work\llvm-project\llvm-project\clang-tools-extra\clangd\unittests\DiagnosticsTests.cpp:358
Value of: TU.build().getDiagnostics()
Expected: has 6 elements and there exists some permutation of elements such that:
 - element #0 (Diag at 1:13-1:23 = [inclusion of deprecated C++ header 'assert.h'; consider using 'cassert' instead]) and (diag source (S: 2)) and (diag name (N: "modernize-deprecated-headers")) and (is an object whose given field has 1 element that Fix 1:13-1:23 => "<cassert>" = [change '"assert.h"' to '<cassert>']), and
 - element #1 Diag at 7:13-7:32 = [suspicious usage of 'sizeof(sizeof(...))'], and
 - element #2 (Diag at 6:20-6:22 = [side effects in the 1st macro argument 'X' are repeated in macro expansion]) and (diag source (S: 2)) and (diag name (N: "bugprone-macro-repeated-side-effects")) and (is an object whose given field has 1 element that Diag at 3:12-3:18 = [macro 'SQUARE' defined here]), and
 - element #3 (Diag at 4:8-4:12 = [use a trailing return type for this function]) and (diag source (S: 2)) and (diag name (N: "modernize-use-trailing-return-type")) and (is an object whose given field has 1 element that fix message (Message: "use a trailing return type for this function")), and
 - element #4 Diag at 15:9-15:12 = [function 'foo' is within a recursive call chain], and
 - element #5 Diag at 12:9-12:12 = [function 'bar' is within a recursive call chain]
  Actual: { [6:20-6:22] side effects in the 1st macro argument 'X' are repeated in macro expansion, notes: {[3:12-3:18] macro 'SQUARE' defined here}, fixes: {suppress this warning with NOLINT {6:25-6:25 => " // NOLINT(bugprone-macro-repeated-side-effects)"}, suppress this warning with NOLINTNEXTLINE {6:0-6:0 => "      // NOLINTNEXTLINE(bugprone-macro-repeated-side-effects)\0A"}}, [1:13-1:23] inclusion of deprecated C++ header 'assert.h'; consider using 'cassert' instead, fixes: {change '"assert.h"' to '<cassert>' {1:13-1:23 => "<cassert>"}, suppress this warning with NOLINT {1:23-1:23 => " // NOLINT(modernize-deprecated-headers)"}, suppress this warning with NOLINTNEXTLINE {1:0-1:0 => "    // NOLINTNEXTLINE(modernize-deprecated-headers)\0A"}}, [15:9-15:12] function 'foo' is within a recursive call chain, fixes: {suppress this warning with NOLINT {15:16-15:16 => " // NOLINT(misc-no-recursion)"}, suppress this warning with NOLINTNEXTLINE {15:0-15:0 => "    // NOLINTNEXTLINE(misc-no-recursion)\0A"}}, [12:9-12:12] function 'bar' is within a recursive call chain, notes: {[15:9-15:12] example recursive call chain, starting from function 'foo', [16:6-16:9] Frame #1: function 'foo' calls function 'bar' here:, [13:6-13:9] Frame #2: function 'bar' calls function 'foo' here:, [13:6-13:9] ... which was the starting point of the recursive call chain; there may be other cycles}, fixes: {suppress this warning with NOLINT {12:16-12:16 => " // NOLINT(misc-no-recursion)"}, suppress this warning with NOLINTNEXTLINE {12:0-12:0 => "    // NOLINTNEXTLINE(misc-no-recursion)\0A"}}, [4:8-4:12] use a trailing return type for this function, fixes: {use a trailing return type for this function {4:4-4:7 => "auto", 4:14-4:14 => " -> int"}, suppress this warning with NOLINT {4:16-4:16 => " // NOLINT(modernize-use-trailing-return-type)"}, suppress this warning with NOLINTNEXTLINE {4:0-4:0 => "    // NOLINTNEXTLINE(modernize-use-trailing-return-type)\0A"}}, [7:13-7:32] suspicious usage of 'sizeof(sizeof(...))', fixes: {suppress this warning with NOLINT {7:33-7:33 => " // NOLINT(bugprone-sizeof-expression)"}, suppress this warning with NOLINTNEXTLINE {7:0-7:0 => "      // NOLINTNEXTLINE(bugprone-sizeof-expression)\0A"}} }, where the following matchers don't match any elements:
matcher #0: (Diag at 1:13-1:23 = [inclusion of deprecated C++ header 'assert.h'; consider using 'cassert' instead]) and (diag source (S: 2)) and (diag name (N: "modernize-deprecated-headers")) and (is an object whose given field has 1 element that Fix 1:13-1:23 => "<cassert>" = [change '"assert.h"' to '<cassert>']),
matcher #3: (Diag at 4:8-4:12 = [use a trailing return type for this function]) and (diag source (S: 2)) and (diag name (N: "modernize-use-trailing-return-type")) and (is an object whose given field has 1 element that fix message (Message: "use a trailing return type for this function"))
and where the following elements don't match any matchers:
element #1: [1:13-1:23] inclusion of deprecated C++ header 'assert.h'; consider using 'cassert' instead, fixes: {change '"assert.h"' to '<cassert>' {1:13-1:23 => "<cassert>"}, suppress this warning with NOLINT {1:23-1:23 => " // NOLINT(modernize-deprecated-headers)"}, suppress this warning with NOLINTNEXTLINE {1:0-1:0 => "    // NOLINTNEXTLINE(modernize-deprecated-headers)\0A"}},
element #4: [4:8-4:12] use a trailing return type for this function, fixes: {use a trailing return type for this function {4:4-4:7 => "auto", 4:14-4:14 => " -> int"}, suppress this warning with NOLINT {4:16-4:16 => " // NOLINT(modernize-use-trailing-return-type)"}, suppress this warning with NOLINTNEXTLINE {4:0-4:0 => "    // NOLINTNEXTLINE(modernize-use-trailing-return-type)\0A"}}


If these failures are unrelated to your changes (for example tests are broken or flaky at HEAD), please open an issue at https://github.com/llvm/llvm-project/issues and add the infrastructure label.

@github-actions
Copy link
Copy Markdown

🐧 Linux x64 Test Results

  • 3174 tests passed
  • 7 tests skipped
  • 3 tests failed

Failed Tests

(click on a test name to see its output)

Clangd Unit Tests

Clangd Unit Tests._/ClangdTests/DiagnosticTest/ClangTidySelfContainedDiags
Script:
--
/home/gha/actions-runner/_work/llvm-project/llvm-project/build/tools/clang/tools/extra/clangd/unittests/./ClangdTests --gtest_filter=DiagnosticTest.ClangTidySelfContainedDiags
--
/home/gha/actions-runner/_work/llvm-project/llvm-project/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp:936
Value of: TU.build().getDiagnostics()
Expected: has 4 elements and there exists some permutation of elements such that:
 - element #0 (Diag at 4:8-4:14 = ['A' should be initialized in a member initializer of the constructor]) and (is an object whose given field has 1 element that LSP fix 'A' should be initialized in a member initializer of the constructor {3:11-3:11 => " : A(1)", 4:8-4:14 => ""}), and
 - element #1 (Diag at 5:8-5:14 = ['B' should be initialized in a member initializer of the constructor]) and (is an object whose given field has 1 element that LSP fix 'B' should be initialized in a member initializer of the constructor {3:11-3:11 => " : B(1)", 5:8-5:14 => ""}), and
 - element #2 (Diag at 9:12-9:13 = [variable 'C' is not initialized]) and (is an object whose given field has 1 element that LSP fix variable 'C' is not initialized {9:13-9:13 => " = NAN", 0:0-0:0 => "#include <math.h>\0A\0A"}), and
 - element #3 (Diag at 10:13-10:14 = [variable 'D' is not initialized]) and (is an object whose given field has 1 element that LSP fix variable 'D' is not initialized {10:14-10:14 => " = NAN", 0:0-0:0 => "#include <math.h>\0A\0A"})
  Actual: { [4:8-4:14] 'A' should be initialized in a member initializer of the constructor, fixes: {'A' should be initialized in a member initializer of the constructor {3:11-3:11 => " : A(1)", 4:8-4:14 => ""}, suppress this warning with NOLINT {4:14-4:14 => " // NOLINT(cppcoreguidelines-prefer-member-initializer)"}, suppress this warning with NOLINTNEXTLINE {4:0-4:0 => "        // NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer)\0A"}}, [5:8-5:14] 'B' should be initialized in a member initializer of the constructor, fixes: {'B' should be initialized in a member initializer of the constructor {3:11-3:11 => " : B(1)", 5:8-5:14 => ""}, suppress this warning with NOLINT {5:14-5:14 => " // NOLINT(cppcoreguidelines-prefer-member-initializer)"}, suppress this warning with NOLINTNEXTLINE {5:0-5:0 => "        // NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer)\0A"}}, [9:12-9:13] variable 'C' is not initialized, fixes: {variable 'C' is not initialized {9:13-9:13 => " = NAN", 0:0-0:0 => "#include <math.h>\0A\0A"}, suppress this warning with NOLINT {9:14-9:14 => " // NOLINT(cppcoreguidelines-init-variables)"}, suppress this warning with NOLINTNEXTLINE {9:0-9:0 => "      // NOLINTNEXTLINE(cppcoreguidelines-init-variables)\0A"}}, [10:13-10:14] variable 'D' is not initialized, fixes: {variable 'D' is not initialized {10:14-10:14 => " = NAN", 0:0-0:0 => "#include <math.h>\0A\0A"}, suppress this warning with NOLINT {10:15-10:15 => " // NOLINT(cppcoreguidelines-init-variables)"}, suppress this warning with NOLINTNEXTLINE {10:0-10:0 => "      // NOLINTNEXTLINE(cppcoreguidelines-init-variables)\0A"}} }, where the following matchers don't match any elements:
matcher #0: (Diag at 4:8-4:14 = ['A' should be initialized in a member initializer of the constructor]) and (is an object whose given field has 1 element that LSP fix 'A' should be initialized in a member initializer of the constructor {3:11-3:11 => " : A(1)", 4:8-4:14 => ""}),
matcher #1: (Diag at 5:8-5:14 = ['B' should be initialized in a member initializer of the constructor]) and (is an object whose given field has 1 element that LSP fix 'B' should be initialized in a member initializer of the constructor {3:11-3:11 => " : B(1)", 5:8-5:14 => ""}),
matcher #2: (Diag at 9:12-9:13 = [variable 'C' is not initialized]) and (is an object whose given field has 1 element that LSP fix variable 'C' is not initialized {9:13-9:13 => " = NAN", 0:0-0:0 => "#include <math.h>\0A\0A"}),
matcher #3: (Diag at 10:13-10:14 = [variable 'D' is not initialized]) and (is an object whose given field has 1 element that LSP fix variable 'D' is not initialized {10:14-10:14 => " = NAN", 0:0-0:0 => "#include <math.h>\0A\0A"})
and where the following elements don't match any matchers:
element #0: [4:8-4:14] 'A' should be initialized in a member initializer of the constructor, fixes: {'A' should be initialized in a member initializer of the constructor {3:11-3:11 => " : A(1)", 4:8-4:14 => ""}, suppress this warning with NOLINT {4:14-4:14 => " // NOLINT(cppcoreguidelines-prefer-member-initializer)"}, suppress this warning with NOLINTNEXTLINE {4:0-4:0 => "        // NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer)\0A"}},
element #1: [5:8-5:14] 'B' should be initialized in a member initializer of the constructor, fixes: {'B' should be initialized in a member initializer of the constructor {3:11-3:11 => " : B(1)", 5:8-5:14 => ""}, suppress this warning with NOLINT {5:14-5:14 => " // NOLINT(cppcoreguidelines-prefer-member-initializer)"}, suppress this warning with NOLINTNEXTLINE {5:0-5:0 => "        // NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer)\0A"}},
element #2: [9:12-9:13] variable 'C' is not initialized, fixes: {variable 'C' is not initialized {9:13-9:13 => " = NAN", 0:0-0:0 => "#include <math.h>\0A\0A"}, suppress this warning with NOLINT {9:14-9:14 => " // NOLINT(cppcoreguidelines-init-variables)"}, suppress this warning with NOLINTNEXTLINE {9:0-9:0 => "      // NOLINTNEXTLINE(cppcoreguidelines-init-variables)\0A"}},
element #3: [10:13-10:14] variable 'D' is not initialized, fixes: {variable 'D' is not initialized {10:14-10:14 => " = NAN", 0:0-0:0 => "#include <math.h>\0A\0A"}, suppress this warning with NOLINT {10:15-10:15 => " // NOLINT(cppcoreguidelines-init-variables)"}, suppress this warning with NOLINTNEXTLINE {10:0-10:0 => "      // NOLINTNEXTLINE(cppcoreguidelines-init-variables)\0A"}}


Clangd Unit Tests._/ClangdTests/DiagnosticTest/ClangTidySelfContainedDiagsFormatting
Script:
--
/home/gha/actions-runner/_work/llvm-project/llvm-project/build/tools/clang/tools/extra/clangd/unittests/./ClangdTests --gtest_filter=DiagnosticTest.ClangTidySelfContainedDiagsFormatting
--
/home/gha/actions-runner/_work/llvm-project/llvm-project/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp:978
Value of: TU.build().getDiagnostics()
Expected: has 2 elements and there exists some permutation of elements such that:
 - element #0 (Diag at 8:22-8:28 = [prefer using 'override' or (rarely) 'final' instead of 'virtual']) and (is an object whose given field has 1 element that LSP fix prefer using 'override' or (rarely) 'final' instead of 'virtual' {8:30-8:30 => " override", 8:6-8:17 => ""}), and
 - element #1 (Diag at 9:28-9:34 = [prefer using 'override' or (rarely) 'final' instead of 'virtual']) and (is an object whose given field has 1 element that LSP fix prefer using 'override' or (rarely) 'final' instead of 'virtual' {9:36-9:36 => " override", 9:6-9:19 => ""})
  Actual: { [8:22-8:28] prefer using 'override' or (rarely) 'final' instead of 'virtual', fixes: {prefer using 'override' or (rarely) 'final' instead of 'virtual' {8:30-8:30 => " override", 8:6-8:17 => ""}, suppress this warning with NOLINT {8:31-8:31 => " // NOLINT(cppcoreguidelines-explicit-virtual-functions)"}, suppress this warning with NOLINTNEXTLINE {8:0-8:0 => "      // NOLINTNEXTLINE(cppcoreguidelines-explicit-virtual-functions)\0A"}}, [9:28-9:34] prefer using 'override' or (rarely) 'final' instead of 'virtual', fixes: {prefer using 'override' or (rarely) 'final' instead of 'virtual' {9:36-9:36 => " override", 9:6-9:19 => ""}, suppress this warning with NOLINT {9:37-9:37 => " // NOLINT(cppcoreguidelines-explicit-virtual-functions)"}, suppress this warning with NOLINTNEXTLINE {9:0-9:0 => "      // NOLINTNEXTLINE(cppcoreguidelines-explicit-virtual-functions)\0A"}} }, where the following matchers don't match any elements:
matcher #0: (Diag at 8:22-8:28 = [prefer using 'override' or (rarely) 'final' instead of 'virtual']) and (is an object whose given field has 1 element that LSP fix prefer using 'override' or (rarely) 'final' instead of 'virtual' {8:30-8:30 => " override", 8:6-8:17 => ""}),
matcher #1: (Diag at 9:28-9:34 = [prefer using 'override' or (rarely) 'final' instead of 'virtual']) and (is an object whose given field has 1 element that LSP fix prefer using 'override' or (rarely) 'final' instead of 'virtual' {9:36-9:36 => " override", 9:6-9:19 => ""})
and where the following elements don't match any matchers:
element #0: [8:22-8:28] prefer using 'override' or (rarely) 'final' instead of 'virtual', fixes: {prefer using 'override' or (rarely) 'final' instead of 'virtual' {8:30-8:30 => " override", 8:6-8:17 => ""}, suppress this warning with NOLINT {8:31-8:31 => " // NOLINT(cppcoreguidelines-explicit-virtual-functions)"}, suppress this warning with NOLINTNEXTLINE {8:0-8:0 => "      // NOLINTNEXTLINE(cppcoreguidelines-explicit-virtual-functions)\0A"}},
element #1: [9:28-9:34] prefer using 'override' or (rarely) 'final' instead of 'virtual', fixes: {prefer using 'override' or (rarely) 'final' instead of 'virtual' {9:36-9:36 => " override", 9:6-9:19 => ""}, suppress this warning with NOLINT {9:37-9:37 => " // NOLINT(cppcoreguidelines-explicit-virtual-functions)"}, suppress this warning with NOLINTNEXTLINE {9:0-9:0 => "      // NOLINTNEXTLINE(cppcoreguidelines-explicit-virtual-functions)\0A"}}


Clangd Unit Tests._/ClangdTests/DiagnosticsTest/ClangTidy
Script:
--
/home/gha/actions-runner/_work/llvm-project/llvm-project/build/tools/clang/tools/extra/clangd/unittests/./ClangdTests --gtest_filter=DiagnosticsTest.ClangTidy
--
/home/gha/actions-runner/_work/llvm-project/llvm-project/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp:358
Value of: TU.build().getDiagnostics()
Expected: has 6 elements and there exists some permutation of elements such that:
 - element #0 (Diag at 1:13-1:23 = [inclusion of deprecated C++ header 'assert.h'; consider using 'cassert' instead]) and (diag source (S: 2)) and (diag name (N: "modernize-deprecated-headers")) and (is an object whose given field has 1 element that Fix 1:13-1:23 => "<cassert>" = [change '"assert.h"' to '<cassert>']), and
 - element #1 Diag at 7:13-7:32 = [suspicious usage of 'sizeof(sizeof(...))'], and
 - element #2 (Diag at 6:20-6:22 = [side effects in the 1st macro argument 'X' are repeated in macro expansion]) and (diag source (S: 2)) and (diag name (N: "bugprone-macro-repeated-side-effects")) and (is an object whose given field has 1 element that Diag at 3:12-3:18 = [macro 'SQUARE' defined here]), and
 - element #3 (Diag at 4:8-4:12 = [use a trailing return type for this function]) and (diag source (S: 2)) and (diag name (N: "modernize-use-trailing-return-type")) and (is an object whose given field has 1 element that fix message (Message: "use a trailing return type for this function")), and
 - element #4 Diag at 15:9-15:12 = [function 'foo' is within a recursive call chain], and
 - element #5 Diag at 12:9-12:12 = [function 'bar' is within a recursive call chain]
  Actual: { [6:20-6:22] side effects in the 1st macro argument 'X' are repeated in macro expansion, notes: {[3:12-3:18] macro 'SQUARE' defined here}, fixes: {suppress this warning with NOLINT {6:25-6:25 => " // NOLINT(bugprone-macro-repeated-side-effects)"}, suppress this warning with NOLINTNEXTLINE {6:0-6:0 => "      // NOLINTNEXTLINE(bugprone-macro-repeated-side-effects)\0A"}}, [1:13-1:23] inclusion of deprecated C++ header 'assert.h'; consider using 'cassert' instead, fixes: {change '"assert.h"' to '<cassert>' {1:13-1:23 => "<cassert>"}, suppress this warning with NOLINT {1:23-1:23 => " // NOLINT(modernize-deprecated-headers)"}, suppress this warning with NOLINTNEXTLINE {1:0-1:0 => "    // NOLINTNEXTLINE(modernize-deprecated-headers)\0A"}}, [15:9-15:12] function 'foo' is within a recursive call chain, fixes: {suppress this warning with NOLINT {15:16-15:16 => " // NOLINT(misc-no-recursion)"}, suppress this warning with NOLINTNEXTLINE {15:0-15:0 => "    // NOLINTNEXTLINE(misc-no-recursion)\0A"}}, [12:9-12:12] function 'bar' is within a recursive call chain, notes: {[15:9-15:12] example recursive call chain, starting from function 'foo', [16:6-16:9] Frame #1: function 'foo' calls function 'bar' here:, [13:6-13:9] Frame #2: function 'bar' calls function 'foo' here:, [13:6-13:9] ... which was the starting point of the recursive call chain; there may be other cycles}, fixes: {suppress this warning with NOLINT {12:16-12:16 => " // NOLINT(misc-no-recursion)"}, suppress this warning with NOLINTNEXTLINE {12:0-12:0 => "    // NOLINTNEXTLINE(misc-no-recursion)\0A"}}, [4:8-4:12] use a trailing return type for this function, fixes: {use a trailing return type for this function {4:4-4:7 => "auto", 4:14-4:14 => " -> int"}, suppress this warning with NOLINT {4:16-4:16 => " // NOLINT(modernize-use-trailing-return-type)"}, suppress this warning with NOLINTNEXTLINE {4:0-4:0 => "    // NOLINTNEXTLINE(modernize-use-trailing-return-type)\0A"}}, [7:13-7:32] suspicious usage of 'sizeof(sizeof(...))', fixes: {suppress this warning with NOLINT {7:33-7:33 => " // NOLINT(bugprone-sizeof-expression)"}, suppress this warning with NOLINTNEXTLINE {7:0-7:0 => "      // NOLINTNEXTLINE(bugprone-sizeof-expression)\0A"}} }, where the following matchers don't match any elements:
matcher #0: (Diag at 1:13-1:23 = [inclusion of deprecated C++ header 'assert.h'; consider using 'cassert' instead]) and (diag source (S: 2)) and (diag name (N: "modernize-deprecated-headers")) and (is an object whose given field has 1 element that Fix 1:13-1:23 => "<cassert>" = [change '"assert.h"' to '<cassert>']),
matcher #3: (Diag at 4:8-4:12 = [use a trailing return type for this function]) and (diag source (S: 2)) and (diag name (N: "modernize-use-trailing-return-type")) and (is an object whose given field has 1 element that fix message (Message: "use a trailing return type for this function"))
and where the following elements don't match any matchers:
element #1: [1:13-1:23] inclusion of deprecated C++ header 'assert.h'; consider using 'cassert' instead, fixes: {change '"assert.h"' to '<cassert>' {1:13-1:23 => "<cassert>"}, suppress this warning with NOLINT {1:23-1:23 => " // NOLINT(modernize-deprecated-headers)"}, suppress this warning with NOLINTNEXTLINE {1:0-1:0 => "    // NOLINTNEXTLINE(modernize-deprecated-headers)\0A"}},
element #4: [4:8-4:12] use a trailing return type for this function, fixes: {use a trailing return type for this function {4:4-4:7 => "auto", 4:14-4:14 => " -> int"}, suppress this warning with NOLINT {4:16-4:16 => " // NOLINT(modernize-use-trailing-return-type)"}, suppress this warning with NOLINTNEXTLINE {4:0-4:0 => "    // NOLINTNEXTLINE(modernize-use-trailing-return-type)\0A"}}


If these failures are unrelated to your changes (for example tests are broken or flaky at HEAD), please open an issue at https://github.com/llvm/llvm-project/issues and add the infrastructure label.

Copy link
Copy Markdown
Contributor

@ArcsinX ArcsinX left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A previous PR #114661 attempted to land similar feature, but it ends up being closed by its author.

In that patch, in the comments, there was a suggestion to use "feature module" for this implementation #114661 (review). Can you please address it? Example of feature modules implementation can be found in tests (e.g. here https://github.com/llvm/llvm-project/blob/main/clang-tools-extra/clangd/unittests/FeatureModulesTests.cpp#L27 )

Comment on lines +674 to +677
if (auto NolintFix = makeNolintFix(Code, Diag))
Diag.Fixes.push_back(std::move(*NolintFix));
if (auto NolintNextLineFix = makeNolintNextLineFix(Code, Diag))
Diag.Fixes.push_back(std::move(*NolintNextLineFix));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In one of the code bases I am contributing to, we only use NOLINTNEXTLINE and never use NOLINT.
Would be great to have a setting in the .clangd config controlling which suppression style gets suggested (possible values same-line, previous-line, off or both)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool idea. I'll add config entries as you suggested. Thanks.

@Lancern
Copy link
Copy Markdown
Member Author

Lancern commented Mar 30, 2026

A previous PR #114661 attempted to land similar feature, but it ends up being closed by its author.

In that patch, in the comments, there was a suggestion to use "feature module" for this implementation #114661 (review). Can you please address it? Example of feature modules implementation can be found in tests (e.g. here https://github.com/llvm/llvm-project/blob/main/clang-tools-extra/clangd/unittests/FeatureModulesTests.cpp#L27 )

Thanks for the feedback.

I'm trying to migrate changes in this PR to a feature module. The issue is that ParsedAST::build has a non-trivial amount of code building a ClangTidy context for identifying and processing ClangTidy diagnostics, which is not accessible in a feature module. I don't know whether it's possible to build a ClangTidy context in a feature module (specifically, in an AST listener) without a non-trivial refactor attempt.

Also, I notice that there are currently no in-tree usage of feature module. Is it really preferable to introduce the change as a feature module?

@ArcsinX
Copy link
Copy Markdown
Contributor

ArcsinX commented Mar 30, 2026

The issue is that ParsedAST::build has a non-trivial amount of code building a ClangTidy context for identifying and processing ClangTidy diagnostics, which is not accessible in a feature module. I don't know whether it's possible to build a ClangTidy context in a feature module (specifically, in an AST listener) without a non-trivial refactor attempt.

Maybe we can just check that diagnostic ID > DIAG_UPPER_LIMIT, which means that this diagnostic is a custom one. After that we can check diagnostic message and if it ends with [...], this is probably a clang-tidy warning (we can check ClangTidyModuleRegistry entries to make sure)

Also, I notice that there are currently no in-tree usage of feature module. Is it really preferable to introduce the change as a feature module?

As we can see, with this patch tests fail because of new fixits appearing. Changes in StoreDiags::take() prototype leads to several files changes. With this patch these fixits became really builtin into clangd without simple to disable.

Yes, we have no in-tree feature modules, but personally I think that feature modules is underestimated ability and not well documented (only comments in the source code). Maybe if we implement at least one, we can use feature modules more frequent, thus keeping clangd core logic more clean

@vogelsgesang
Copy link
Copy Markdown
Member

The issue is that ParsedAST::build has a non-trivial amount of code building a ClangTidy context for identifying and processing ClangTidy diagnostics, which is not accessible in a feature module.

Would it make sense to move all of clang-tidy into a feature module? And make the ClangTidy context an implementation detail of that
Then you could add the "suppress diagnostic" as part of that clang-tidy feature module. Since you would be inside that module, you would have access to the module-private ClangTidy context.

(That being said, I know next to nothing about feature modules, and hence don't know how much work that refactoring would be or if it's even doable)

@ArcsinX
Copy link
Copy Markdown
Contributor

ArcsinX commented Mar 31, 2026

The issue is that ParsedAST::build has a non-trivial amount of code building a ClangTidy context for identifying and processing ClangTidy diagnostics, which is not accessible in a feature module.

Would it make sense to move all of clang-tidy into a feature module? And make the ClangTidy context an implementation detail of that Then you could add the "suppress diagnostic" as part of that clang-tidy feature module. Since you would be inside that module, you would have access to the module-private ClangTidy context.

(That being said, I know next to nothing about feature modules, and hence don't know how much work that refactoring would be or if it's even doable)

Personally, I think this is a great idea. And maybe the reason it wasn't done this way initially is because feature modules were introduced after clang-tidy had already been integrated into clangd.

I don't have time to do this by myself right now, but if someone wants to implement it, I'll be happy to review it.
The implementation can be based on the use of FeatureModule::ASTListener::beforeExecute() to replace AST consumer in CompilerInstance with MultiplexConsumer which will contain both: current AST consumer and clang-tidy AST consumer.

@Lancern
Copy link
Copy Markdown
Member Author

Lancern commented Mar 31, 2026

The issue is that ParsedAST::build has a non-trivial amount of code building a ClangTidy context for identifying and processing ClangTidy diagnostics, which is not accessible in a feature module.

Would it make sense to move all of clang-tidy into a feature module? And make the ClangTidy context an implementation detail of that Then you could add the "suppress diagnostic" as part of that clang-tidy feature module. Since you would be inside that module, you would have access to the module-private ClangTidy context.

(That being said, I know next to nothing about feature modules, and hence don't know how much work that refactoring would be or if it's even doable)

This sounds like a good idea to me. I'm willing to take some time to investigate into this and I think I would hold this PR on for a while until that approach is attempted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants