[clang-tools-extra] [clangd] Ignore section-divider comments in hover documentation (PR #225404)
Anthony Gaudino via cfe-commits
cfe-commits at lists.llvm.org
Tue Sep 22 07:05:43 PDT 2026
https://github.com/Anthony-Gaudino created https://github.com/llvm/llvm-project/pull/225404
## Summary
A comment block containing a section-divider line was reported as documentation for the next declaration. In practice this meant the title of a banner was shown on hover, e.g.:
```cpp
// Per-frame pump
// ========================================================================
void update(); // hover showed "Per-frame pump\n===..."
```
`getDeclComment` already filtered comments consisting solely of special characters (`looksLikeDocComment`), but a divider combined with a title line passed the filter and the title was shown as documentation.
## What this changes
- `looksLikeDocComment` (`CodeCompletionStrings.cpp`) now also rejects any comment containing a divider line: a line that, trimmed, is a run of 10+ repetitions of a single character. The check runs on the formatted text, so it applies regardless of comment style (`//`, `///`, `/* ... */`).
- The length threshold keeps short runs (markdown `---` rules and similar adornments) working as documentation; a regression test pins this boundary.
This affects hover and code-completion documentation, which share `getDeclComment`.
## Testing
- New cases in `TEST(Hover, Structured)`: titled `===` divider, same with a blank line before the declaration, `---` divider, `/* ### */` block banner, `+++` divider, and a short-run negative control.
- Verified the divider cases fail without the fix (hover showed the banner title) and pass with it.
- Full `ClangdTests` suite passes (1434 tests).
## Related
- Related to clangd/clangd#974 (divider banners matched across blank lines).
From 9738df685a6e7d9932298bcb08fd148dd4aaabbc Mon Sep 17 00:00:00 2001
From: Anthony Gaudino <anthonygaudino.45 at gmail.com>
Date: Tue, 22 Sep 2026 14:44:33 +0100
Subject: [PATCH] [clangd] Ignore section-divider comments in hover
documentation
Comments containing a divider line (a run of 10+ identical characters,
e.g. // ===... or /* ###... */ banners) are never documentation, even
when combined with a title line. Previously the title of such a block
was reported as documentation for the next declaration.
Related to clangd/clangd#974.
---
.../clangd/CodeCompletionStrings.cpp | 28 ++++-
.../clangd/unittests/HoverTests.cpp | 100 ++++++++++++++++++
2 files changed, 127 insertions(+), 1 deletion(-)
diff --git a/clang-tools-extra/clangd/CodeCompletionStrings.cpp b/clang-tools-extra/clangd/CodeCompletionStrings.cpp
index dc86be60a876f..61c86f7b1d82c 100644
--- a/clang-tools-extra/clangd/CodeCompletionStrings.cpp
+++ b/clang-tools-extra/clangd/CodeCompletionStrings.cpp
@@ -54,13 +54,39 @@ void appendOptionalChunk(const CodeCompletionString &CCS, std::string *Out) {
}
}
+/// A divider line is a long run of a single repeated character, as used in
+/// section banners. These are never documentation, even when combined with
+/// a title line, as in:
+/// // Per-frame pump
+/// // ====================================================================
+/// The length threshold keeps short runs (e.g. markdown `---` rules or RST
+/// adornments) working as documentation.
+bool isDividerLine(llvm::StringRef Line) {
+ constexpr unsigned MinDividerLength = 10;
+ Line = Line.trim(" \t\r\n");
+ if (Line.size() < MinDividerLength)
+ return false;
+ return Line.find_first_not_of(Line.front()) == llvm::StringRef::npos;
+}
+
bool looksLikeDocComment(llvm::StringRef CommentText) {
// We don't report comments that only contain "special" chars.
// This avoids reporting various delimiters, like:
// =================
// -----------------
// *****************
- return CommentText.find_first_not_of("/*-= \t\r\n") != llvm::StringRef::npos;
+ if (CommentText.find_first_not_of("/*-= \t\r\n") == llvm::StringRef::npos)
+ return false;
+ // Nor comments containing a section-divider line. Without this, the title
+ // of a divider block is reported as documentation for the next declaration.
+ llvm::StringRef Rest = CommentText;
+ while (!Rest.empty()) {
+ const auto Split = Rest.split('\n');
+ if (isDividerLine(Split.first))
+ return false;
+ Rest = Split.second;
+ }
+ return true;
}
// Determine whether the completion string should be patched
diff --git a/clang-tools-extra/clangd/unittests/HoverTests.cpp b/clang-tools-extra/clangd/unittests/HoverTests.cpp
index 15b03e6bb6ece..0c2d667a45589 100644
--- a/clang-tools-extra/clangd/unittests/HoverTests.cpp
+++ b/clang-tools-extra/clangd/unittests/HoverTests.cpp
@@ -57,6 +57,106 @@ TEST(Hover, Structured) {
HI.Type = "void ()";
HI.Parameters.emplace();
}},
+ // A section divider with a title is not documentation.
+ {R"cpp(
+ // Per-frame pump
+ // ========================================================================
+ void [[fo^o]]() {}
+ )cpp",
+ [](HoverInfo &HI) {
+ HI.NamespaceScope = "";
+ HI.Name = "foo";
+ HI.Kind = index::SymbolKind::Function;
+ HI.Documentation = "";
+ HI.Definition = "void foo()";
+ HI.ReturnType = "void";
+ HI.Type = "void ()";
+ HI.Parameters.emplace();
+ }},
+ // Same, with a blank line between the divider and the declaration.
+ {R"cpp(
+ // Per-frame pump
+ // ========================================================================
+
+ void [[fo^o]]() {}
+ )cpp",
+ [](HoverInfo &HI) {
+ HI.NamespaceScope = "";
+ HI.Name = "foo";
+ HI.Kind = index::SymbolKind::Function;
+ HI.Documentation = "";
+ HI.Definition = "void foo()";
+ HI.ReturnType = "void";
+ HI.Type = "void ()";
+ HI.Parameters.emplace();
+ }},
+ // Other divider styles are not documentation either.
+ {R"cpp(
+ // Appearance
+ // ------------------------------------------------------------------------
+ void [[fo^o]]() {}
+ )cpp",
+ [](HoverInfo &HI) {
+ HI.NamespaceScope = "";
+ HI.Name = "foo";
+ HI.Kind = index::SymbolKind::Function;
+ HI.Documentation = "";
+ HI.Definition = "void foo()";
+ HI.ReturnType = "void";
+ HI.Type = "void ()";
+ HI.Parameters.emplace();
+ }},
+ // Block-comment banners are not documentation either.
+ {R"cpp(
+ /*
+ ##########################################################################
+ Private
+ ##########################################################################
+ */
+ void [[fo^o]]() {}
+ )cpp",
+ [](HoverInfo &HI) {
+ HI.NamespaceScope = "";
+ HI.Name = "foo";
+ HI.Kind = index::SymbolKind::Function;
+ HI.Documentation = "";
+ HI.Definition = "void foo()";
+ HI.ReturnType = "void";
+ HI.Type = "void ()";
+ HI.Parameters.emplace();
+ }},
+ // Plus-run dividers are not documentation either.
+ {R"cpp(
+ // Helpers
+ // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ void [[fo^o]]() {}
+ )cpp",
+ [](HoverInfo &HI) {
+ HI.NamespaceScope = "";
+ HI.Name = "foo";
+ HI.Kind = index::SymbolKind::Function;
+ HI.Documentation = "";
+ HI.Definition = "void foo()";
+ HI.ReturnType = "void";
+ HI.Type = "void ()";
+ HI.Parameters.emplace();
+ }},
+ // Short runs (e.g. markdown rules) are still documentation.
+ {R"cpp(
+ // Best foo ever.
+ // ---
+ void [[fo^o]]() {}
+ )cpp",
+ [](HoverInfo &HI) {
+ HI.NamespaceScope = "";
+ HI.Name = "foo";
+ HI.Kind = index::SymbolKind::Function;
+ HI.Documentation = "Best foo ever.\n---";
+ HI.Definition = "void foo()";
+ HI.ReturnType = "void";
+ HI.Type = "void ()";
+ HI.Parameters.emplace();
+ }},
{R"cpp(
// Best foo ever.
void [[fo^o]](auto x) {}
More information about the cfe-commits
mailing list