[clang] [clang-tools-extra] [llvm] [clangd] introduce doxygen parser (PR #150790)
via llvm-commits
llvm-commits at lists.llvm.org
Wed Aug 6 14:24:45 PDT 2025
https://github.com/tcottin updated https://github.com/llvm/llvm-project/pull/150790
>From 78c7cc2441b9395fd2a0b3ac6b25090dd7937098 Mon Sep 17 00:00:00 2001
From: Tim Cottin <timcottin at gmx.de>
Date: Sat, 26 Jul 2025 18:25:45 +0000
Subject: [PATCH 1/4] [clangd] introduce doxygen parser
---
clang-tools-extra/clangd/CMakeLists.txt | 1 +
.../clangd/CodeCompletionStrings.cpp | 15 +-
clang-tools-extra/clangd/Hover.cpp | 187 +++++++++++++-
clang-tools-extra/clangd/Hover.h | 13 +-
.../clangd/SymbolDocumentation.cpp | 221 +++++++++++++++++
.../clangd/SymbolDocumentation.h | 140 +++++++++++
clang-tools-extra/clangd/support/Markup.cpp | 7 +-
.../clangd/unittests/CMakeLists.txt | 1 +
.../clangd/unittests/HoverTests.cpp | 229 ++++++++++++++++++
.../unittests/SymbolDocumentationTests.cpp | 161 ++++++++++++
.../clangd/unittests/support/MarkupTests.cpp | 2 +
clang/include/clang/AST/Comment.h | 21 ++
clang/include/clang/AST/CommentSema.h | 1 +
clang/lib/AST/CommentParser.cpp | 5 +-
clang/lib/AST/CommentSema.cpp | 7 +-
.../clang-tools-extra/clangd/BUILD.gn | 1 +
16 files changed, 993 insertions(+), 19 deletions(-)
create mode 100644 clang-tools-extra/clangd/SymbolDocumentation.cpp
create mode 100644 clang-tools-extra/clangd/SymbolDocumentation.h
create mode 100644 clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp
diff --git a/clang-tools-extra/clangd/CMakeLists.txt b/clang-tools-extra/clangd/CMakeLists.txt
index a1e9da41b4b32..06920a97ddc88 100644
--- a/clang-tools-extra/clangd/CMakeLists.txt
+++ b/clang-tools-extra/clangd/CMakeLists.txt
@@ -108,6 +108,7 @@ add_clang_library(clangDaemon STATIC
SemanticHighlighting.cpp
SemanticSelection.cpp
SourceCode.cpp
+ SymbolDocumentation.cpp
SystemIncludeExtractor.cpp
TidyProvider.cpp
TUScheduler.cpp
diff --git a/clang-tools-extra/clangd/CodeCompletionStrings.cpp b/clang-tools-extra/clangd/CodeCompletionStrings.cpp
index 9b4442b0bb76f..196a1624e1c04 100644
--- a/clang-tools-extra/clangd/CodeCompletionStrings.cpp
+++ b/clang-tools-extra/clangd/CodeCompletionStrings.cpp
@@ -7,6 +7,7 @@
//===----------------------------------------------------------------------===//
#include "CodeCompletionStrings.h"
+#include "Config.h"
#include "clang-c/Index.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/RawCommentList.h"
@@ -100,7 +101,19 @@ std::string getDeclComment(const ASTContext &Ctx, const NamedDecl &Decl) {
// the comments for namespaces.
return "";
}
- const RawComment *RC = getCompletionComment(Ctx, &Decl);
+
+ const RawComment *RC = nullptr;
+ const Config &Cfg = Config::current();
+
+ if (Cfg.Documentation.CommentFormat == Config::CommentFormatPolicy::Doxygen &&
+ isa<ParmVarDecl>(Decl)) {
+ // Parameters are documented in the function comment.
+ if (const auto *FD = dyn_cast<FunctionDecl>(Decl.getDeclContext()))
+ RC = getCompletionComment(Ctx, FD);
+ } else {
+ RC = getCompletionComment(Ctx, &Decl);
+ }
+
if (!RC)
return "";
// Sanity check that the comment does not come from the PCH. We choose to not
diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp
index 1e0718d673260..63fdc7c24a7a8 100644
--- a/clang-tools-extra/clangd/Hover.cpp
+++ b/clang-tools-extra/clangd/Hover.cpp
@@ -18,6 +18,7 @@
#include "Protocol.h"
#include "Selection.h"
#include "SourceCode.h"
+#include "SymbolDocumentation.h"
#include "clang-include-cleaner/Analysis.h"
#include "clang-include-cleaner/IncludeSpeller.h"
#include "clang-include-cleaner/Types.h"
@@ -41,6 +42,7 @@
#include "clang/AST/Type.h"
#include "clang/Basic/CharInfo.h"
#include "clang/Basic/LLVM.h"
+#include "clang/Basic/LangOptions.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Basic/Specifiers.h"
@@ -627,6 +629,9 @@ HoverInfo getHoverContents(const NamedDecl *D, const PrintingPolicy &PP,
HI.Name = printName(Ctx, *D);
const auto *CommentD = getDeclForComment(D);
HI.Documentation = getDeclComment(Ctx, *CommentD);
+ // safe the language options to be able to create the comment::CommandTraits
+ // to parse the documentation
+ HI.CommentOpts = D->getASTContext().getLangOpts().CommentOpts;
enhanceFromIndex(HI, *CommentD, Index);
if (HI.Documentation.empty())
HI.Documentation = synthesizeDocumentation(D);
@@ -1388,9 +1393,170 @@ static std::string formatOffset(uint64_t OffsetInBits) {
return Offset;
}
-markup::Document HoverInfo::present() const {
+markup::Document HoverInfo::presentDoxygen() const {
+ // NOTE: this function is currently almost identical to presentDefault().
+ // This is to have a minimal change when introducing the doxygen parser.
+ // This function will be changed when rearranging the output for doxygen
+ // parsed documentation.
+
markup::Document Output;
+ // Header contains a text of the form:
+ // variable `var`
+ //
+ // class `X`
+ //
+ // function `foo`
+ //
+ // expression
+ //
+ // Note that we are making use of a level-3 heading because VSCode renders
+ // level 1 and 2 headers in a huge font, see
+ // https://github.com/microsoft/vscode/issues/88417 for details.
+ markup::Paragraph &Header = Output.addHeading(3);
+ if (Kind != index::SymbolKind::Unknown)
+ Header.appendText(index::getSymbolKindString(Kind)).appendSpace();
+ assert(!Name.empty() && "hover triggered on a nameless symbol");
+
+ Header.appendCode(Name);
+
+ if (!Provider.empty()) {
+ markup::Paragraph &DI = Output.addParagraph();
+ DI.appendText("provided by");
+ DI.appendSpace();
+ DI.appendCode(Provider);
+ Output.addRuler();
+ }
+
+ // Put a linebreak after header to increase readability.
+ Output.addRuler();
+ // Print Types on their own lines to reduce chances of getting line-wrapped by
+ // editor, as they might be long.
+ if (ReturnType) {
+ // For functions we display signature in a list form, e.g.:
+ // → `x`
+ // Parameters:
+ // - `bool param1`
+ // - `int param2 = 5`
+ Output.addParagraph().appendText("→ ").appendCode(
+ llvm::to_string(*ReturnType));
+ }
+
+ SymbolDocCommentVisitor SymbolDoc(Documentation, CommentOpts);
+
+ if (Parameters && !Parameters->empty()) {
+ Output.addParagraph().appendText("Parameters:");
+ markup::BulletList &L = Output.addBulletList();
+ for (const auto &Param : *Parameters) {
+ markup::Paragraph &P = L.addItem().addParagraph();
+ P.appendCode(llvm::to_string(Param));
+
+ if (SymbolDoc.isParameterDocumented(llvm::to_string(Param.Name))) {
+ P.appendText(" -");
+ SymbolDoc.parameterDocToMarkup(llvm::to_string(Param.Name), P);
+ }
+ }
+ }
+ // Don't print Type after Parameters or ReturnType as this will just duplicate
+ // the information
+ if (Type && !ReturnType && !Parameters)
+ Output.addParagraph().appendText("Type: ").appendCode(
+ llvm::to_string(*Type));
+
+ if (Value) {
+ markup::Paragraph &P = Output.addParagraph();
+ P.appendText("Value = ");
+ P.appendCode(*Value);
+ }
+
+ if (Offset)
+ Output.addParagraph().appendText("Offset: " + formatOffset(*Offset));
+ if (Size) {
+ auto &P = Output.addParagraph().appendText("Size: " + formatSize(*Size));
+ if (Padding && *Padding != 0) {
+ P.appendText(
+ llvm::formatv(" (+{0} padding)", formatSize(*Padding)).str());
+ }
+ if (Align)
+ P.appendText(", alignment " + formatSize(*Align));
+ }
+
+ if (CalleeArgInfo) {
+ assert(CallPassType);
+ std::string Buffer;
+ llvm::raw_string_ostream OS(Buffer);
+ OS << "Passed ";
+ if (CallPassType->PassBy != HoverInfo::PassType::Value) {
+ OS << "by ";
+ if (CallPassType->PassBy == HoverInfo::PassType::ConstRef)
+ OS << "const ";
+ OS << "reference ";
+ }
+ if (CalleeArgInfo->Name)
+ OS << "as " << CalleeArgInfo->Name;
+ else if (CallPassType->PassBy == HoverInfo::PassType::Value)
+ OS << "by value";
+ if (CallPassType->Converted && CalleeArgInfo->Type)
+ OS << " (converted to " << CalleeArgInfo->Type->Type << ")";
+ Output.addParagraph().appendText(OS.str());
+ }
+ if (Kind == index::SymbolKind::Parameter) {
+ if (SymbolDoc.isParameterDocumented(Name))
+ SymbolDoc.parameterDocToMarkup(Name, Output.addParagraph());
+ } else
+ SymbolDoc.docToMarkup(Output);
+
+ if (!Definition.empty()) {
+ Output.addRuler();
+ std::string Buffer;
+
+ if (!Definition.empty()) {
+ // Append scope comment, dropping trailing "::".
+ // Note that we don't print anything for global namespace, to not annoy
+ // non-c++ projects or projects that are not making use of namespaces.
+ if (!LocalScope.empty()) {
+ // Container name, e.g. class, method, function.
+ // We might want to propagate some info about container type to print
+ // function foo, class X, method X::bar, etc.
+ Buffer +=
+ "// In " + llvm::StringRef(LocalScope).rtrim(':').str() + '\n';
+ } else if (NamespaceScope && !NamespaceScope->empty()) {
+ Buffer += "// In namespace " +
+ llvm::StringRef(*NamespaceScope).rtrim(':').str() + '\n';
+ }
+
+ if (!AccessSpecifier.empty()) {
+ Buffer += AccessSpecifier + ": ";
+ }
+
+ Buffer += Definition;
+ }
+
+ Output.addCodeBlock(Buffer, DefinitionLanguage);
+ }
+
+ if (!UsedSymbolNames.empty()) {
+ Output.addRuler();
+ markup::Paragraph &P = Output.addParagraph();
+ P.appendText("provides ");
+
+ const std::vector<std::string>::size_type SymbolNamesLimit = 5;
+ auto Front = llvm::ArrayRef(UsedSymbolNames).take_front(SymbolNamesLimit);
+
+ llvm::interleave(
+ Front, [&](llvm::StringRef Sym) { P.appendCode(Sym); },
+ [&] { P.appendText(", "); });
+ if (UsedSymbolNames.size() > Front.size()) {
+ P.appendText(" and ");
+ P.appendText(std::to_string(UsedSymbolNames.size() - Front.size()));
+ P.appendText(" more");
+ }
+ }
+ return Output;
+}
+
+markup::Document HoverInfo::presentDefault() const {
+ markup::Document Output;
// Header contains a text of the form:
// variable `var`
//
@@ -1538,21 +1704,22 @@ markup::Document HoverInfo::present() const {
std::string HoverInfo::present(MarkupKind Kind) const {
if (Kind == MarkupKind::Markdown) {
const Config &Cfg = Config::current();
- if ((Cfg.Documentation.CommentFormat ==
- Config::CommentFormatPolicy::Markdown) ||
- (Cfg.Documentation.CommentFormat ==
- Config::CommentFormatPolicy::Doxygen))
- // If the user prefers Markdown, we use the present() method to generate
- // the Markdown output.
- return present().asMarkdown();
+ if (Cfg.Documentation.CommentFormat ==
+ Config::CommentFormatPolicy::Markdown)
+ return presentDefault().asMarkdown();
+ if (Cfg.Documentation.CommentFormat ==
+ Config::CommentFormatPolicy::Doxygen) {
+ std::string T = presentDoxygen().asMarkdown();
+ return T;
+ }
if (Cfg.Documentation.CommentFormat ==
Config::CommentFormatPolicy::PlainText)
// If the user prefers plain text, we use the present() method to generate
// the plain text output.
- return present().asEscapedMarkdown();
+ return presentDefault().asEscapedMarkdown();
}
- return present().asPlainText();
+ return presentDefault().asPlainText();
}
// If the backtick at `Offset` starts a probable quoted range, return the range
diff --git a/clang-tools-extra/clangd/Hover.h b/clang-tools-extra/clangd/Hover.h
index 2f65431bd72de..2578e7a4339d0 100644
--- a/clang-tools-extra/clangd/Hover.h
+++ b/clang-tools-extra/clangd/Hover.h
@@ -74,6 +74,8 @@ struct HoverInfo {
std::optional<Range> SymRange;
index::SymbolKind Kind = index::SymbolKind::Unknown;
std::string Documentation;
+ // required to create a comments::CommandTraits object without the ASTContext
+ CommentOptions CommentOpts;
/// Source code containing the definition of the symbol.
std::string Definition;
const char *DefinitionLanguage = "cpp";
@@ -118,10 +120,15 @@ struct HoverInfo {
// alphabetical order.
std::vector<std::string> UsedSymbolNames;
- /// Produce a user-readable information.
- markup::Document present() const;
-
+ /// Produce a user-readable information based on the specified markup kind.
std::string present(MarkupKind Kind) const;
+
+private:
+ /// Parse and render the hover information as Doxygen documentation.
+ markup::Document presentDoxygen() const;
+
+ /// Render the hover information as a default documentation.
+ markup::Document presentDefault() const;
};
inline bool operator==(const HoverInfo::PrintedType &LHS,
diff --git a/clang-tools-extra/clangd/SymbolDocumentation.cpp b/clang-tools-extra/clangd/SymbolDocumentation.cpp
new file mode 100644
index 0000000000000..1c14ccb01fc26
--- /dev/null
+++ b/clang-tools-extra/clangd/SymbolDocumentation.cpp
@@ -0,0 +1,221 @@
+//===--- SymbolDocumentation.cpp ==-------------------------------*- C++-*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "SymbolDocumentation.h"
+
+#include "support/Markup.h"
+#include "clang/AST/Comment.h"
+#include "clang/AST/CommentCommandTraits.h"
+#include "clang/AST/CommentVisitor.h"
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/StringRef.h"
+
+namespace clang {
+namespace clangd {
+
+void commandToMarkup(markup::Paragraph &Out, StringRef Command,
+ comments::CommandMarkerKind CommandMarker,
+ StringRef Args) {
+ Out.appendBoldText(
+ (CommandMarker == (comments::CommandMarkerKind::CMK_At) ? "@" : "\\") +
+ Command.str());
+ if (!Args.empty()) {
+ Out.appendSpace();
+ Out.appendEmphasizedText(Args.str());
+ }
+}
+
+class ParagraphToMarkupDocument
+ : public comments::ConstCommentVisitor<ParagraphToMarkupDocument> {
+public:
+ ParagraphToMarkupDocument(markup::Paragraph &Out,
+ const comments::CommandTraits &Traits)
+ : Out(Out), Traits(Traits) {}
+
+ void visitParagraphComment(const comments::ParagraphComment *C) {
+ if (!C)
+ return;
+
+ for (const auto *Child = C->child_begin(); Child != C->child_end();
+ ++Child) {
+ visit(*Child);
+ }
+ }
+
+ void visitTextComment(const comments::TextComment *C) {
+ // Always trim leading space after a newline.
+ StringRef Text = C->getText();
+ if (LastChunkEndsWithNewline && C->getText().starts_with(' '))
+ Text = Text.drop_front();
+
+ LastChunkEndsWithNewline = C->hasTrailingNewline();
+ Out.appendText(Text.str() + (LastChunkEndsWithNewline ? "\n" : ""));
+ }
+
+ void visitInlineCommandComment(const comments::InlineCommandComment *C) {
+
+ if (C->getNumArgs() > 0) {
+ std::string ArgText;
+ for (unsigned I = 0; I < C->getNumArgs(); ++I) {
+ if (!ArgText.empty())
+ ArgText += " ";
+ ArgText += C->getArgText(I);
+ }
+
+ switch (C->getRenderKind()) {
+ case comments::InlineCommandRenderKind::Monospaced:
+ Out.appendCode(ArgText);
+ break;
+ case comments::InlineCommandRenderKind::Bold:
+ Out.appendBoldText(ArgText);
+ break;
+ case comments::InlineCommandRenderKind::Emphasized:
+ Out.appendEmphasizedText(ArgText);
+ break;
+ default:
+ commandToMarkup(Out, C->getCommandName(Traits), C->getCommandMarker(),
+ ArgText);
+ break;
+ }
+ } else {
+ if (C->getCommandName(Traits) == "n") {
+ // \n is a special case, it is used to create a new line.
+ Out.appendText(" \n");
+ LastChunkEndsWithNewline = true;
+ return;
+ }
+
+ commandToMarkup(Out, C->getCommandName(Traits), C->getCommandMarker(),
+ "");
+ }
+ }
+
+ void visitHTMLStartTagComment(const comments::HTMLStartTagComment *STC) {
+ std::string TagText = "<" + STC->getTagName().str();
+
+ for (unsigned I = 0; I < STC->getNumAttrs(); ++I) {
+ const comments::HTMLStartTagComment::Attribute &Attr = STC->getAttr(I);
+ TagText += " " + Attr.Name.str() + "=\"" + Attr.Value.str() + "\"";
+ }
+
+ if (STC->isSelfClosing())
+ TagText += " /";
+ TagText += ">";
+
+ LastChunkEndsWithNewline = STC->hasTrailingNewline();
+ Out.appendText(TagText + (LastChunkEndsWithNewline ? "\n" : ""));
+ }
+
+ void visitHTMLEndTagComment(const comments::HTMLEndTagComment *ETC) {
+ LastChunkEndsWithNewline = ETC->hasTrailingNewline();
+ Out.appendText("</" + ETC->getTagName().str() + ">" +
+ (LastChunkEndsWithNewline ? "\n" : ""));
+ }
+
+private:
+ markup::Paragraph &Out;
+ const comments::CommandTraits &Traits;
+
+ /// If true, the next leading space after a new line is trimmed.
+ bool LastChunkEndsWithNewline = false;
+};
+
+class BlockCommentToMarkupDocument
+ : public comments::ConstCommentVisitor<BlockCommentToMarkupDocument> {
+public:
+ BlockCommentToMarkupDocument(markup::Document &Out,
+ const comments::CommandTraits &Traits)
+ : Out(Out), Traits(Traits) {}
+
+ void visitBlockCommandComment(const comments::BlockCommandComment *B) {
+
+ switch (B->getCommandID()) {
+ case comments::CommandTraits::KCI_arg:
+ case comments::CommandTraits::KCI_li:
+ // \li and \arg are special cases, they are used to create a list item.
+ // In markdown it is a bullet list.
+ ParagraphToMarkupDocument(Out.addBulletList().addItem().addParagraph(),
+ Traits)
+ .visit(B->getParagraph());
+ break;
+ default: {
+ // Some commands have arguments, like \throws.
+ // The arguments are not part of the paragraph.
+ // We need reconstruct them here.
+ std::string ArgText;
+ for (unsigned I = 0; I < B->getNumArgs(); ++I) {
+ if (!ArgText.empty())
+ ArgText += " ";
+ ArgText += B->getArgText(I);
+ }
+ auto &P = Out.addParagraph();
+ commandToMarkup(P, B->getCommandName(Traits), B->getCommandMarker(),
+ ArgText);
+ if (B->getParagraph() && !B->getParagraph()->isWhitespace()) {
+ // For commands with arguments, the paragraph starts after the first
+ // space. Therefore we need to append a space manually in this case.
+ if (!ArgText.empty())
+ P.appendSpace();
+ ParagraphToMarkupDocument(P, Traits).visit(B->getParagraph());
+ }
+ }
+ }
+ }
+
+ void visitVerbatimBlockComment(const comments::VerbatimBlockComment *VB) {
+ commandToMarkup(Out.addParagraph(), VB->getCommandName(Traits),
+ VB->getCommandMarker(), "");
+
+ std::string VerbatimText;
+
+ for (const auto *LI = VB->child_begin(); LI != VB->child_end(); ++LI) {
+ if (const auto *Line = cast<comments::VerbatimBlockLineComment>(*LI)) {
+ VerbatimText += Line->getText().str() + "\n";
+ }
+ }
+
+ Out.addCodeBlock(VerbatimText, "");
+
+ commandToMarkup(Out.addParagraph(), VB->getCloseName(),
+ VB->getCommandMarker(), "");
+ }
+
+ void visitVerbatimLineComment(const comments::VerbatimLineComment *VL) {
+ auto &P = Out.addParagraph();
+ commandToMarkup(P, VL->getCommandName(Traits), VL->getCommandMarker(), "");
+ P.appendSpace().appendCode(VL->getText().str(), true).appendSpace();
+ }
+
+private:
+ markup::Document &Out;
+ const comments::CommandTraits &Traits;
+ StringRef CommentEscapeMarker;
+};
+
+void SymbolDocCommentVisitor::parameterDocToMarkup(StringRef ParamName,
+ markup::Paragraph &Out) {
+ if (ParamName.empty())
+ return;
+
+ if (const auto *P = Parameters.lookup(ParamName)) {
+ ParagraphToMarkupDocument(Out, Traits).visit(P->getParagraph());
+ }
+}
+
+void SymbolDocCommentVisitor::docToMarkup(markup::Document &Out) {
+ for (unsigned I = 0; I < CommentPartIndex; ++I) {
+ if (const auto *BC = BlockCommands.lookup(I)) {
+ BlockCommentToMarkupDocument(Out, Traits).visit(BC);
+ } else if (const auto *P = FreeParagraphs.lookup(I)) {
+ ParagraphToMarkupDocument(Out.addParagraph(), Traits).visit(P);
+ }
+ }
+}
+
+} // namespace clangd
+} // namespace clang
diff --git a/clang-tools-extra/clangd/SymbolDocumentation.h b/clang-tools-extra/clangd/SymbolDocumentation.h
new file mode 100644
index 0000000000000..f1ab349858398
--- /dev/null
+++ b/clang-tools-extra/clangd/SymbolDocumentation.h
@@ -0,0 +1,140 @@
+//===--- SymbolDocumentation.h ==---------------------------------*- C++-*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Class to parse doxygen comments into a flat structure for consumption
+// in e.g. Hover and Code Completion
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_SYMBOLDOCUMENTATION_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_SYMBOLDOCUMENTATION_H
+
+#include "support/Markup.h"
+#include "clang/AST/Comment.h"
+#include "clang/AST/CommentLexer.h"
+#include "clang/AST/CommentParser.h"
+#include "clang/AST/CommentSema.h"
+#include "clang/AST/CommentVisitor.h"
+#include "clang/Basic/SourceManager.h"
+#include <string>
+
+namespace clang {
+namespace clangd {
+
+class SymbolDocCommentVisitor
+ : public comments::ConstCommentVisitor<SymbolDocCommentVisitor> {
+public:
+ SymbolDocCommentVisitor(llvm::StringRef Documentation,
+ const CommentOptions &CommentOpts)
+ : Traits(Allocator, CommentOpts), Allocator() {
+
+ if (Documentation.empty())
+ return;
+
+ CommentWithMarkers.reserve(Documentation.size() +
+ Documentation.count('\n') * 3);
+
+ // The comment lexer expects doxygen markers, so add them back.
+ // We need to use the /// style doxygen markers because the comment could
+ // contain the closing the closing tag "*/" of a C Style "/** */" comment
+ // which would break the parsing if we would just enclose the comment text
+ // with "/** */".
+ CommentWithMarkers = "///";
+ bool NewLine = true;
+ for (char C : Documentation) {
+ if (C == '\n') {
+ CommentWithMarkers += "\n///";
+ NewLine = true;
+ } else {
+ if (NewLine && (C == '<')) {
+ // A comment line starting with '///<' is treated as a doxygen
+ // comment. Therefore add a space to separate the '<' from the comment
+ // marker. This allows to parse html tags at the beginning of a line
+ // and the escape marker prevents adding the artificial space in the
+ // markup documentation. The extra space will not be rendered, since
+ // we render it as markdown.
+ CommentWithMarkers += ' ';
+ }
+ CommentWithMarkers += C;
+ NewLine = false;
+ }
+ }
+ SourceManagerForFile SourceMgrForFile("mock_file.cpp", CommentWithMarkers);
+
+ SourceManager &SourceMgr = SourceMgrForFile.get();
+ // The doxygen Sema requires a Diagostics consumer, since it reports
+ // warnings e.g. when parameters are not documented correctly. These
+ // warnings are not relevant for us, so we can ignore them.
+ SourceMgr.getDiagnostics().setClient(new IgnoringDiagConsumer);
+
+ comments::Sema S(Allocator, SourceMgr, SourceMgr.getDiagnostics(), Traits,
+ /*PP=*/nullptr);
+ comments::Lexer L(Allocator, SourceMgr.getDiagnostics(), Traits,
+ SourceMgr.getLocForStartOfFile(SourceMgr.getMainFileID()),
+ CommentWithMarkers.data(),
+ CommentWithMarkers.data() + CommentWithMarkers.size());
+ comments::Parser P(L, S, Allocator, SourceMgr, SourceMgr.getDiagnostics(),
+ Traits);
+ comments::FullComment *FC = P.parseFullComment();
+
+ if (FC) {
+ for (auto *Block : FC->getBlocks()) {
+ visit(Block);
+ }
+ }
+ }
+
+ bool isParameterDocumented(StringRef ParamName) const {
+ return Parameters.contains(ParamName);
+ }
+
+ void parameterDocToMarkup(StringRef ParamName, markup::Paragraph &Out);
+
+ void docToMarkup(markup::Document &Out);
+
+ void visitBlockCommandComment(const comments::BlockCommandComment *B) {
+ BlockCommands[CommentPartIndex] = std::move(B);
+ CommentPartIndex++;
+ }
+
+ void visitParagraphComment(const comments::ParagraphComment *P) {
+ FreeParagraphs[CommentPartIndex] = std::move(P);
+ CommentPartIndex++;
+ }
+
+ void visitParamCommandComment(const comments::ParamCommandComment *P) {
+ Parameters[P->getParamNameAsWritten()] = std::move(P);
+ }
+
+private:
+ comments::CommandTraits Traits;
+ llvm::BumpPtrAllocator Allocator;
+ std::string CommentWithMarkers;
+
+ /// Index to keep track of the order of the comments.
+ /// We want to rearange some commands like \\param.
+ /// This index allows us to keep the order of the other comment parts.
+ unsigned CommentPartIndex = 0;
+
+ /// Parsed paragaph(s) of the "param" comamnd(s)
+ llvm::SmallDenseMap<StringRef, const comments::ParamCommandComment *>
+ Parameters;
+
+ /// All the block commands.
+ llvm::SmallDenseMap<unsigned, const comments::BlockCommandComment *>
+ BlockCommands;
+
+ /// All "free" text paragraphs.
+ llvm::SmallDenseMap<unsigned, const comments::ParagraphComment *>
+ FreeParagraphs;
+};
+
+} // namespace clangd
+} // namespace clang
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_CLANGD_SYMBOLDOCUMENTATION_H
diff --git a/clang-tools-extra/clangd/support/Markup.cpp b/clang-tools-extra/clangd/support/Markup.cpp
index a13083026f26b..152863191dad1 100644
--- a/clang-tools-extra/clangd/support/Markup.cpp
+++ b/clang-tools-extra/clangd/support/Markup.cpp
@@ -363,7 +363,12 @@ class CodeBlock : public Block {
void renderMarkdown(llvm::raw_ostream &OS) const override {
std::string Marker = getMarkerForCodeBlock(Contents);
// No need to pad from previous blocks, as they should end with a new line.
- OS << Marker << Language << '\n' << Contents << '\n' << Marker << '\n';
+ OS << Marker << Language << '\n' << Contents;
+ if (Contents.back() != '\n')
+ OS << '\n';
+ // Always end with an empty line to separate code blocks from following
+ // paragraphs.
+ OS << Marker << "\n\n";
}
void renderPlainText(llvm::raw_ostream &OS) const override {
diff --git a/clang-tools-extra/clangd/unittests/CMakeLists.txt b/clang-tools-extra/clangd/unittests/CMakeLists.txt
index dffdcd5d014ca..bc457a8241fa7 100644
--- a/clang-tools-extra/clangd/unittests/CMakeLists.txt
+++ b/clang-tools-extra/clangd/unittests/CMakeLists.txt
@@ -92,6 +92,7 @@ add_unittest(ClangdUnitTests ClangdTests
SourceCodeTests.cpp
StdLibTests.cpp
SymbolCollectorTests.cpp
+ SymbolDocumentationTests.cpp
SymbolInfoTests.cpp
SyncAPI.cpp
TUSchedulerTests.cpp
diff --git a/clang-tools-extra/clangd/unittests/HoverTests.cpp b/clang-tools-extra/clangd/unittests/HoverTests.cpp
index 12d260db7ea11..fb6f49c987d46 100644
--- a/clang-tools-extra/clangd/unittests/HoverTests.cpp
+++ b/clang-tools-extra/clangd/unittests/HoverTests.cpp
@@ -3762,6 +3762,127 @@ provides Foo, Bar, Baz, Foobar, Qux and 1 more)"}};
}
}
+TEST(Hover, PresentDocumentation) {
+ struct {
+ const std::function<void(HoverInfo &)> Builder;
+ llvm::StringRef ExpectedRender;
+ } Cases[] = {
+ {[](HoverInfo &HI) {
+ HI.Kind = index::SymbolKind::Function;
+ HI.Documentation = "@brief brief doc\n\n"
+ "longer doc";
+ HI.Definition = "void foo()";
+ HI.Name = "foo";
+ },
+ R"(### function `foo`
+
+---
+**@brief** brief doc
+
+longer doc
+
+---
+```cpp
+void foo()
+```)"},
+ {[](HoverInfo &HI) {
+ HI.Kind = index::SymbolKind::Function;
+ HI.Documentation = "@brief brief doc\n\n"
+ "longer doc";
+ HI.Definition = "int foo()";
+ HI.ReturnType = "int";
+ HI.Name = "foo";
+ },
+ R"(### function `foo`
+
+---
+→ `int`
+
+**@brief** brief doc
+
+longer doc
+
+---
+```cpp
+int foo()
+```)"},
+ {[](HoverInfo &HI) {
+ HI.Kind = index::SymbolKind::Function;
+ HI.Documentation = "@brief brief doc\n\n"
+ "longer doc\n at param a this is a param\n at return it "
+ "returns something";
+ HI.Definition = "int foo(int a)";
+ HI.ReturnType = "int";
+ HI.Name = "foo";
+ HI.Parameters.emplace();
+ HI.Parameters->emplace_back();
+ HI.Parameters->back().Type = "int";
+ HI.Parameters->back().Name = "a";
+ },
+ R"(### function `foo`
+
+---
+→ `int`
+
+Parameters:
+
+- `int a` - this is a param
+
+**@brief** brief doc
+
+longer doc
+
+**@return** it returns something
+
+---
+```cpp
+int foo(int a)
+```)"},
+ {[](HoverInfo &HI) {
+ HI.Kind = index::SymbolKind::Function;
+ HI.Documentation = "@brief brief doc\n\n"
+ "longer doc\n at param a this is a param\n at param b "
+ "does not exist\n at return it returns something";
+ HI.Definition = "int foo(int a)";
+ HI.ReturnType = "int";
+ HI.Name = "foo";
+ HI.Parameters.emplace();
+ HI.Parameters->emplace_back();
+ HI.Parameters->back().Type = "int";
+ HI.Parameters->back().Name = "a";
+ },
+ R"(### function `foo`
+
+---
+→ `int`
+
+Parameters:
+
+- `int a` - this is a param
+
+**@brief** brief doc
+
+longer doc
+
+**@return** it returns something
+
+---
+```cpp
+int foo(int a)
+```)"},
+ };
+
+ for (const auto &C : Cases) {
+ HoverInfo HI;
+ C.Builder(HI);
+ Config Cfg;
+ Cfg.Hover.ShowAKA = true;
+ Cfg.Documentation.CommentFormat = Config::CommentFormatPolicy::Doxygen;
+ WithContextValue WithCfg(Config::Key, std::move(Cfg));
+ EXPECT_EQ(HI.present(MarkupKind::Markdown), C.ExpectedRender);
+ }
+}
+
TEST(Hover, ParseDocumentation) {
struct Case {
llvm::StringRef Documentation;
@@ -4339,6 +4460,114 @@ constexpr u64 pow_with_mod(u64 a, u64 b, u64 p) {
EXPECT_TRUE(H->Value);
EXPECT_TRUE(H->Type);
}
+
+TEST(Hover, FunctionParameters) {
+ struct {
+ const char *const Code;
+ const std::function<void(HoverInfo &)> ExpectedBuilder;
+ std::string ExpectedRender;
+ } Cases[] = {
+ {R"cpp(/// Function doc
+ void foo(int [[^a]]);
+ )cpp",
+ [](HoverInfo &HI) {
+ HI.Name = "a";
+ HI.Kind = index::SymbolKind::Parameter;
+ HI.NamespaceScope = "";
+ HI.LocalScope = "foo::";
+ HI.Type = "int";
+ HI.Definition = "int a";
+ HI.Documentation = "Function doc";
+ },
+ "### param `a`\n\n---\nType: `int`\n\n---\n```cpp\n// In foo\nint "
+ "a\n```"},
+ {R"cpp(/// Function doc
+ /// @param a this is doc for a
+ void foo(int [[^a]]);
+ )cpp",
+ [](HoverInfo &HI) {
+ HI.Name = "a";
+ HI.Kind = index::SymbolKind::Parameter;
+ HI.NamespaceScope = "";
+ HI.LocalScope = "foo::";
+ HI.Type = "int";
+ HI.Definition = "int a";
+ HI.Documentation = "Function doc\n @param a this is doc for a";
+ },
+ "### param `a`\n\n---\nType: `int`\n\n this is doc for "
+ "a\n\n---\n```cpp\n// In foo\nint a\n```"},
+ {R"cpp(/// Function doc
+ /// @param b this is doc for b
+ void foo(int [[^a]], int b);
+ )cpp",
+ [](HoverInfo &HI) {
+ HI.Name = "a";
+ HI.Kind = index::SymbolKind::Parameter;
+ HI.NamespaceScope = "";
+ HI.LocalScope = "foo::";
+ HI.Type = "int";
+ HI.Definition = "int a";
+ HI.Documentation = "Function doc\n @param b this is doc for b";
+ },
+ "### param `a`\n\n---\nType: `int`\n\n---\n```cpp\n// In foo\nint "
+ "a\n```"},
+ {R"cpp(/// Function doc
+ /// @param b this is doc for \p b
+ void foo(int a, int [[^b]]);
+ )cpp",
+ [](HoverInfo &HI) {
+ HI.Name = "b";
+ HI.Kind = index::SymbolKind::Parameter;
+ HI.NamespaceScope = "";
+ HI.LocalScope = "foo::";
+ HI.Type = "int";
+ HI.Definition = "int b";
+ HI.Documentation = "Function doc\n @param b this is doc for \\p b";
+ },
+ "### param `b`\n\n---\nType: `int`\n\n this is doc for "
+ "`b`\n\n---\n```cpp\n// In foo\nint b\n```"},
+ };
+
+ // Create a tiny index, so tests above can verify documentation is fetched.
+ Symbol IndexSym = func("indexSymbol");
+ IndexSym.Documentation = "comment from index";
+ SymbolSlab::Builder Symbols;
+ Symbols.insert(IndexSym);
+ auto Index =
+ MemIndex::build(std::move(Symbols).build(), RefSlab(), RelationSlab());
+
+ for (const auto &Case : Cases) {
+ SCOPED_TRACE(Case.Code);
+
+ Annotations T(Case.Code);
+ TestTU TU = TestTU::withCode(T.code());
+ auto AST = TU.build();
+ Config Cfg;
+ Cfg.Hover.ShowAKA = true;
+ Cfg.Documentation.CommentFormat = Config::CommentFormatPolicy::Doxygen;
+ WithContextValue WithCfg(Config::Key, std::move(Cfg));
+ auto H = getHover(AST, T.point(), format::getLLVMStyle(), Index.get());
+ ASSERT_TRUE(H);
+ HoverInfo Expected;
+ Expected.SymRange = T.range();
+ Case.ExpectedBuilder(Expected);
+
+ EXPECT_EQ(H->present(MarkupKind::Markdown), Case.ExpectedRender);
+ EXPECT_EQ(H->NamespaceScope, Expected.NamespaceScope);
+ EXPECT_EQ(H->LocalScope, Expected.LocalScope);
+ EXPECT_EQ(H->Name, Expected.Name);
+ EXPECT_EQ(H->Kind, Expected.Kind);
+ EXPECT_EQ(H->Documentation, Expected.Documentation);
+ EXPECT_EQ(H->Definition, Expected.Definition);
+ EXPECT_EQ(H->Type, Expected.Type);
+ EXPECT_EQ(H->ReturnType, Expected.ReturnType);
+ EXPECT_EQ(H->Parameters, Expected.Parameters);
+ EXPECT_EQ(H->TemplateParameters, Expected.TemplateParameters);
+ EXPECT_EQ(H->SymRange, Expected.SymRange);
+ EXPECT_EQ(H->Value, Expected.Value);
+ }
+}
+
} // namespace
} // namespace clangd
} // namespace clang
diff --git a/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp b/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp
new file mode 100644
index 0000000000000..69eb13b2142d2
--- /dev/null
+++ b/clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp
@@ -0,0 +1,161 @@
+//===-- SymbolDocumentationTests.cpp --------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+#include "SymbolDocumentation.h"
+
+#include "support/Markup.h"
+#include "clang/Basic/CommentOptions.h"
+#include "llvm/ADT/StringRef.h"
+#include "gtest/gtest.h"
+
+namespace clang {
+namespace clangd {
+
+TEST(SymbolDocumentation, Parse) {
+
+ CommentOptions CommentOpts;
+
+ struct Case {
+ llvm::StringRef Documentation;
+ llvm::StringRef ExpectedRenderEscapedMarkdown;
+ llvm::StringRef ExpectedRenderMarkdown;
+ llvm::StringRef ExpectedRenderPlainText;
+ } Cases[] = {
+ {
+ "foo bar",
+ "foo bar",
+ "foo bar",
+ "foo bar",
+ },
+ {
+ "foo\nbar\n",
+ "foo\nbar",
+ "foo\nbar",
+ "foo bar",
+ },
+ {
+ "foo\n\nbar\n",
+ "foo\n\nbar",
+ "foo\n\nbar",
+ "foo\n\nbar",
+ },
+ {
+ "foo \\p bar baz",
+ "foo `bar` baz",
+ "foo `bar` baz",
+ "foo bar baz",
+ },
+ {
+ "foo \\e bar baz",
+ "foo \\*bar\\* baz",
+ "foo *bar* baz",
+ "foo *bar* baz",
+ },
+ {
+ "foo \\b bar baz",
+ "foo \\*\\*bar\\*\\* baz",
+ "foo **bar** baz",
+ "foo **bar** baz",
+ },
+ {
+ "foo \\ref bar baz",
+ "foo \\*\\*\\\\ref\\*\\* \\*bar\\* baz",
+ "foo **\\ref** *bar* baz",
+ "foo **\\ref** *bar* baz",
+ },
+ {
+ "foo @ref bar baz",
+ "foo \\*\\*@ref\\*\\* \\*bar\\* baz",
+ "foo **@ref** *bar* baz",
+ "foo **@ref** *bar* baz",
+ },
+ {
+ "\\brief this is a \\n\nbrief description",
+ "\\*\\*\\\\brief\\*\\* this is a \nbrief description",
+ "**\\brief** this is a \nbrief description",
+ "**\\brief** this is a\nbrief description",
+ },
+ {
+ "\\throw exception foo",
+ "\\*\\*\\\\throw\\*\\* \\*exception\\* foo",
+ "**\\throw** *exception* foo",
+ "**\\throw** *exception* foo",
+ },
+ {
+ "\\brief this is a brief description\n\n\\li item 1\n\\li item "
+ "2\n\\arg item 3",
+ "\\*\\*\\\\brief\\*\\* this is a brief description\n\n- item 1\n\n- "
+ "item "
+ "2\n\n- "
+ "item 3",
+ "**\\brief** this is a brief description\n\n- item 1\n\n- item "
+ "2\n\n- "
+ "item 3",
+ "**\\brief** this is a brief description\n\n- item 1\n\n- item "
+ "2\n\n- "
+ "item 3",
+ },
+ {
+ "\\defgroup mygroup this is a group\nthis is not a group description",
+ "\\*\\*@defgroup\\*\\* `mygroup this is a group`\n\nthis is not a "
+ "group "
+ "description",
+ "**@defgroup** `mygroup this is a group`\n\nthis is not a group "
+ "description",
+ "**@defgroup** `mygroup this is a group`\n\nthis is not a group "
+ "description",
+ },
+ {
+ "\\verbatim\nthis is a\nverbatim block containing\nsome verbatim "
+ "text\n\\endverbatim",
+ "\\*\\*@verbatim\\*\\*\n\n```\nthis is a\nverbatim block "
+ "containing\nsome "
+ "verbatim text\n```\n\n\\*\\*@endverbatim\\*\\*",
+ "**@verbatim**\n\n```\nthis is a\nverbatim block containing\nsome "
+ "verbatim text\n```\n\n**@endverbatim**",
+ "**@verbatim**\n\nthis is a\nverbatim block containing\nsome "
+ "verbatim text\n\n**@endverbatim**",
+ },
+ {
+ "@param foo this is a parameter\n at param bar this is another "
+ "parameter",
+ "",
+ "",
+ "",
+ },
+ {
+ "@brief brief docs\n\n at param foo this is a parameter\n\nMore "
+ "description\ndocumentation",
+ "\\*\\*@brief\\*\\* brief docs\n\nMore description\ndocumentation",
+ "**@brief** brief docs\n\nMore description\ndocumentation",
+ "**@brief** brief docs\n\nMore description documentation",
+ },
+ {
+ "<b>this is a bold text</b>\nnormal text\n<i>this is an italic "
+ "text</i>\n<code>this is a code block</code>",
+ "\\<b>this is a bold text\\</b>\nnormal text\n\\<i>this is an italic "
+ "text\\</i>\n\\<code>this is a code block\\</code>",
+ "\\<b>this is a bold text\\</b>\nnormal text\n\\<i>this is an italic "
+ "text\\</i>\n\\<code>this is a code block\\</code>",
+ "<b>this is a bold text</b> normal text <i>this is an italic "
+ "text</i> <code>this is a code block</code>",
+ },
+ };
+ for (const auto &C : Cases) {
+ markup::Document Doc;
+ SymbolDocCommentVisitor SymbolDoc(C.Documentation, CommentOpts);
+
+ SymbolDoc.docToMarkup(Doc);
+
+ EXPECT_EQ(Doc.asPlainText(), C.ExpectedRenderPlainText);
+ EXPECT_EQ(Doc.asMarkdown(), C.ExpectedRenderMarkdown);
+ EXPECT_EQ(Doc.asEscapedMarkdown(), C.ExpectedRenderEscapedMarkdown);
+ }
+}
+
+} // namespace clangd
+} // namespace clang
diff --git a/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp b/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp
index 482f230fb86fe..9c17db067f398 100644
--- a/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp
+++ b/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp
@@ -463,6 +463,7 @@ TEST(Document, Separators) {
```cpp
test
```
+
bar)md";
EXPECT_EQ(D.asEscapedMarkdown(), ExpectedMarkdown);
EXPECT_EQ(D.asMarkdown(), ExpectedMarkdown);
@@ -559,6 +560,7 @@ foo
bar
baz
```
+
```cpp
foo
```)md";
diff --git a/clang/include/clang/AST/Comment.h b/clang/include/clang/AST/Comment.h
index dd9906727293f..42686ff24076a 100644
--- a/clang/include/clang/AST/Comment.h
+++ b/clang/include/clang/AST/Comment.h
@@ -19,6 +19,7 @@
#include "clang/Basic/SourceLocation.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Compiler.h"
namespace clang {
class Decl;
@@ -119,6 +120,11 @@ class Comment {
LLVM_PREFERRED_TYPE(CommandTraits::KnownCommandIDs)
unsigned CommandID : CommandInfo::NumCommandIDBits;
+
+ /// Describes the syntax that was used in a documentation command.
+ /// Contains values from CommandMarkerKind enum.
+ LLVM_PREFERRED_TYPE(CommandMarkerKind)
+ unsigned CommandMarker : 1;
};
enum { NumInlineCommandCommentBits = NumInlineContentCommentBits + 3 +
CommandInfo::NumCommandIDBits };
@@ -347,6 +353,16 @@ class InlineCommandComment : public InlineContentComment {
InlineCommandCommentBits.RenderKind = llvm::to_underlying(RK);
InlineCommandCommentBits.CommandID = CommandID;
}
+ InlineCommandComment(SourceLocation LocBegin, SourceLocation LocEnd,
+ unsigned CommandID, InlineCommandRenderKind RK,
+ CommandMarkerKind CommandMarker, ArrayRef<Argument> Args)
+ : InlineContentComment(CommentKind::InlineCommandComment, LocBegin,
+ LocEnd),
+ Args(Args) {
+ InlineCommandCommentBits.RenderKind = llvm::to_underlying(RK);
+ InlineCommandCommentBits.CommandID = CommandID;
+ InlineCommandCommentBits.CommandMarker = llvm::to_underlying(CommandMarker);
+ }
static bool classof(const Comment *C) {
return C->getCommentKind() == CommentKind::InlineCommandComment;
@@ -384,6 +400,11 @@ class InlineCommandComment : public InlineContentComment {
SourceRange getArgRange(unsigned Idx) const {
return Args[Idx].Range;
}
+
+ CommandMarkerKind getCommandMarker() const LLVM_READONLY {
+ return static_cast<CommandMarkerKind>(
+ InlineCommandCommentBits.CommandMarker);
+ }
};
/// Abstract class for opening and closing HTML tags. HTML tags are always
diff --git a/clang/include/clang/AST/CommentSema.h b/clang/include/clang/AST/CommentSema.h
index 916d7945329c5..3169e2b0d86b9 100644
--- a/clang/include/clang/AST/CommentSema.h
+++ b/clang/include/clang/AST/CommentSema.h
@@ -131,6 +131,7 @@ class Sema {
InlineCommandComment *actOnInlineCommand(SourceLocation CommandLocBegin,
SourceLocation CommandLocEnd,
unsigned CommandID,
+ CommandMarkerKind CommandMarker,
ArrayRef<Comment::Argument> Args);
InlineContentComment *actOnUnknownCommand(SourceLocation LocBegin,
diff --git a/clang/lib/AST/CommentParser.cpp b/clang/lib/AST/CommentParser.cpp
index e61846d241915..2e5821a8e4436 100644
--- a/clang/lib/AST/CommentParser.cpp
+++ b/clang/lib/AST/CommentParser.cpp
@@ -7,6 +7,7 @@
//===----------------------------------------------------------------------===//
#include "clang/AST/CommentParser.h"
+#include "clang/AST/Comment.h"
#include "clang/AST/CommentCommandTraits.h"
#include "clang/AST/CommentSema.h"
#include "clang/Basic/CharInfo.h"
@@ -569,6 +570,8 @@ BlockCommandComment *Parser::parseBlockCommand() {
InlineCommandComment *Parser::parseInlineCommand() {
assert(Tok.is(tok::backslash_command) || Tok.is(tok::at_command));
+ CommandMarkerKind CMK =
+ Tok.is(tok::backslash_command) ? CMK_Backslash : CMK_At;
const CommandInfo *Info = Traits.getCommandInfo(Tok.getCommandID());
const Token CommandTok = Tok;
@@ -580,7 +583,7 @@ InlineCommandComment *Parser::parseInlineCommand() {
InlineCommandComment *IC = S.actOnInlineCommand(
CommandTok.getLocation(), CommandTok.getEndLocation(),
- CommandTok.getCommandID(), Args);
+ CommandTok.getCommandID(), CMK, Args);
if (Args.size() < Info->NumArgs) {
Diag(CommandTok.getEndLocation().getLocWithOffset(1),
diff --git a/clang/lib/AST/CommentSema.cpp b/clang/lib/AST/CommentSema.cpp
index 88520d7940e34..c02983b03163f 100644
--- a/clang/lib/AST/CommentSema.cpp
+++ b/clang/lib/AST/CommentSema.cpp
@@ -363,12 +363,13 @@ void Sema::actOnTParamCommandFinish(TParamCommandComment *Command,
InlineCommandComment *
Sema::actOnInlineCommand(SourceLocation CommandLocBegin,
SourceLocation CommandLocEnd, unsigned CommandID,
+ CommandMarkerKind CommandMarker,
ArrayRef<Comment::Argument> Args) {
StringRef CommandName = Traits.getCommandInfo(CommandID)->Name;
- return new (Allocator)
- InlineCommandComment(CommandLocBegin, CommandLocEnd, CommandID,
- getInlineCommandRenderKind(CommandName), Args);
+ return new (Allocator) InlineCommandComment(
+ CommandLocBegin, CommandLocEnd, CommandID,
+ getInlineCommandRenderKind(CommandName), CommandMarker, Args);
}
InlineContentComment *Sema::actOnUnknownCommand(SourceLocation LocBegin,
diff --git a/llvm/utils/gn/secondary/clang-tools-extra/clangd/BUILD.gn b/llvm/utils/gn/secondary/clang-tools-extra/clangd/BUILD.gn
index b609d4a7462fb..f8c4838ab7ee3 100644
--- a/llvm/utils/gn/secondary/clang-tools-extra/clangd/BUILD.gn
+++ b/llvm/utils/gn/secondary/clang-tools-extra/clangd/BUILD.gn
@@ -122,6 +122,7 @@ static_library("clangd") {
"SemanticHighlighting.cpp",
"SemanticSelection.cpp",
"SourceCode.cpp",
+ "SymbolDocumentation.cpp",
"SystemIncludeExtractor.cpp",
"TUScheduler.cpp",
"TidyProvider.cpp",
>From dfa8278bf07a5ad3d94e5bf79536fca12d0b7cc7 Mon Sep 17 00:00:00 2001
From: Tim Cottin <timcottin at gmx.de>
Date: Wed, 6 Aug 2025 14:10:29 +0000
Subject: [PATCH 2/4] [clangd] fix review findings
---
.../clangd/CodeCompletionStrings.cpp | 51 ++++++++++----
clang-tools-extra/clangd/Hover.cpp | 8 +--
.../clangd/SymbolDocumentation.cpp | 70 +++++++++++++++++++
.../clangd/SymbolDocumentation.h | 29 ++++++--
.../clangd/unittests/HoverTests.cpp | 28 ++++++--
5 files changed, 155 insertions(+), 31 deletions(-)
diff --git a/clang-tools-extra/clangd/CodeCompletionStrings.cpp b/clang-tools-extra/clangd/CodeCompletionStrings.cpp
index 196a1624e1c04..d6579640cb0fb 100644
--- a/clang-tools-extra/clangd/CodeCompletionStrings.cpp
+++ b/clang-tools-extra/clangd/CodeCompletionStrings.cpp
@@ -8,13 +8,17 @@
#include "CodeCompletionStrings.h"
#include "Config.h"
+#include "SymbolDocumentation.h"
#include "clang-c/Index.h"
#include "clang/AST/ASTContext.h"
+#include "clang/AST/Comment.h"
+#include "clang/AST/Decl.h"
#include "clang/AST/RawCommentList.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Sema/CodeCompleteConsumer.h"
#include "llvm/Support/Compiler.h"
#include "llvm/Support/JSON.h"
+#include "llvm/Support/raw_ostream.h"
#include <limits>
#include <utility>
@@ -105,24 +109,47 @@ std::string getDeclComment(const ASTContext &Ctx, const NamedDecl &Decl) {
const RawComment *RC = nullptr;
const Config &Cfg = Config::current();
+ std::string Doc;
+
if (Cfg.Documentation.CommentFormat == Config::CommentFormatPolicy::Doxygen &&
isa<ParmVarDecl>(Decl)) {
- // Parameters are documented in the function comment.
- if (const auto *FD = dyn_cast<FunctionDecl>(Decl.getDeclContext()))
- RC = getCompletionComment(Ctx, FD);
+ // Parameters are documented in their declaration context (function or
+ // template function).
+ const NamedDecl *ND = dyn_cast<NamedDecl>(Decl.getDeclContext());
+ if (!ND)
+ return "";
+
+ RC = getCompletionComment(Ctx, ND);
+ if (!RC)
+ return "";
+
+ // Sanity check that the comment does not come from the PCH. We choose to
+ // not write them into PCH, because they are racy and slow to load.
+ assert(!Ctx.getSourceManager().isLoadedSourceLocation(RC->getBeginLoc()));
+
+ comments::FullComment *FC = RC->parse(Ctx, /*PP=*/nullptr, ND);
+ if (!FC)
+ return "";
+
+ SymbolDocCommentVisitor V(FC, Ctx.getLangOpts().CommentOpts);
+ std::string RawDoc;
+ llvm::raw_string_ostream OS(RawDoc);
+
+ V.parameterDocToString(dyn_cast<ParmVarDecl>(&Decl)->getName(), OS);
+
+ Doc = StringRef(RawDoc).trim().str();
} else {
RC = getCompletionComment(Ctx, &Decl);
+ if (!RC)
+ return "";
+ // Sanity check that the comment does not come from the PCH. We choose to
+ // not write them into PCH, because they are racy and slow to load.
+ assert(!Ctx.getSourceManager().isLoadedSourceLocation(RC->getBeginLoc()));
+ Doc = RC->getFormattedText(Ctx.getSourceManager(), Ctx.getDiagnostics());
+ if (!looksLikeDocComment(Doc))
+ return "";
}
- if (!RC)
- return "";
- // Sanity check that the comment does not come from the PCH. We choose to not
- // write them into PCH, because they are racy and slow to load.
- assert(!Ctx.getSourceManager().isLoadedSourceLocation(RC->getBeginLoc()));
- std::string Doc =
- RC->getFormattedText(Ctx.getSourceManager(), Ctx.getDiagnostics());
- if (!looksLikeDocComment(Doc))
- return "";
// Clang requires source to be UTF-8, but doesn't enforce this in comments.
if (!llvm::json::isUTF8(Doc))
Doc = llvm::json::fixUTF8(Doc);
diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp
index 63fdc7c24a7a8..529ef499b9fb0 100644
--- a/clang-tools-extra/clangd/Hover.cpp
+++ b/clang-tools-extra/clangd/Hover.cpp
@@ -629,7 +629,7 @@ HoverInfo getHoverContents(const NamedDecl *D, const PrintingPolicy &PP,
HI.Name = printName(Ctx, *D);
const auto *CommentD = getDeclForComment(D);
HI.Documentation = getDeclComment(Ctx, *CommentD);
- // safe the language options to be able to create the comment::CommandTraits
+ // save the language options to be able to create the comment::CommandTraits
// to parse the documentation
HI.CommentOpts = D->getASTContext().getLangOpts().CommentOpts;
enhanceFromIndex(HI, *CommentD, Index);
@@ -1500,11 +1500,7 @@ markup::Document HoverInfo::presentDoxygen() const {
Output.addParagraph().appendText(OS.str());
}
- if (Kind == index::SymbolKind::Parameter) {
- if (SymbolDoc.isParameterDocumented(Name))
- SymbolDoc.parameterDocToMarkup(Name, Output.addParagraph());
- } else
- SymbolDoc.docToMarkup(Output);
+ SymbolDoc.docToMarkup(Output);
if (!Definition.empty()) {
Output.addRuler();
diff --git a/clang-tools-extra/clangd/SymbolDocumentation.cpp b/clang-tools-extra/clangd/SymbolDocumentation.cpp
index 1c14ccb01fc26..0c8b09e2506b0 100644
--- a/clang-tools-extra/clangd/SymbolDocumentation.cpp
+++ b/clang-tools-extra/clangd/SymbolDocumentation.cpp
@@ -125,6 +125,66 @@ class ParagraphToMarkupDocument
bool LastChunkEndsWithNewline = false;
};
+class ParagraphToString
+ : public comments::ConstCommentVisitor<ParagraphToString> {
+public:
+ ParagraphToString(llvm::raw_string_ostream &Out,
+ const comments::CommandTraits &Traits)
+ : Out(Out), Traits(Traits) {}
+
+ void visitParagraphComment(const comments::ParagraphComment *C) {
+ if (!C)
+ return;
+
+ for (const auto *Child = C->child_begin(); Child != C->child_end();
+ ++Child) {
+ visit(*Child);
+ }
+ }
+
+ void visitTextComment(const comments::TextComment *C) { Out << C->getText(); }
+
+ void visitInlineCommandComment(const comments::InlineCommandComment *C) {
+ Out << (C->getCommandMarker() == comments::CommandMarkerKind::CMK_At
+ ? "@"
+ : "\\");
+ Out << C->getCommandName(Traits);
+ if (C->getNumArgs() > 0) {
+ Out << " ";
+ for (unsigned I = 0; I < C->getNumArgs(); ++I) {
+ if (I > 0)
+ Out << " ";
+ Out << C->getArgText(I);
+ }
+ }
+ Out << " ";
+ }
+
+ void visitHTMLStartTagComment(const comments::HTMLStartTagComment *STC) {
+ Out << "<" + STC->getTagName().str();
+
+ for (unsigned I = 0; I < STC->getNumAttrs(); ++I) {
+ const comments::HTMLStartTagComment::Attribute &Attr = STC->getAttr(I);
+ Out << " " + Attr.Name.str() + "=\"" + Attr.Value.str() + "\"";
+ }
+
+ if (STC->isSelfClosing())
+ Out << " /";
+ Out << ">";
+
+ Out << (STC->hasTrailingNewline() ? "\n" : "");
+ }
+
+ void visitHTMLEndTagComment(const comments::HTMLEndTagComment *ETC) {
+ Out << "</" + ETC->getTagName().str() + ">" +
+ (ETC->hasTrailingNewline() ? "\n" : "");
+ }
+
+private:
+ llvm::raw_string_ostream &Out;
+ const comments::CommandTraits &Traits;
+};
+
class BlockCommentToMarkupDocument
: public comments::ConstCommentVisitor<BlockCommentToMarkupDocument> {
public:
@@ -207,6 +267,16 @@ void SymbolDocCommentVisitor::parameterDocToMarkup(StringRef ParamName,
}
}
+void SymbolDocCommentVisitor::parameterDocToString(
+ StringRef ParamName, llvm::raw_string_ostream &Out) {
+ if (ParamName.empty())
+ return;
+
+ if (const auto *P = Parameters.lookup(ParamName)) {
+ ParagraphToString(Out, Traits).visit(P->getParagraph());
+ }
+}
+
void SymbolDocCommentVisitor::docToMarkup(markup::Document &Out) {
for (unsigned I = 0; I < CommentPartIndex; ++I) {
if (const auto *BC = BlockCommands.lookup(I)) {
diff --git a/clang-tools-extra/clangd/SymbolDocumentation.h b/clang-tools-extra/clangd/SymbolDocumentation.h
index f1ab349858398..b5120ba04e8f1 100644
--- a/clang-tools-extra/clangd/SymbolDocumentation.h
+++ b/clang-tools-extra/clangd/SymbolDocumentation.h
@@ -21,6 +21,7 @@
#include "clang/AST/CommentSema.h"
#include "clang/AST/CommentVisitor.h"
#include "clang/Basic/SourceManager.h"
+#include "llvm/Support/raw_ostream.h"
#include <string>
namespace clang {
@@ -29,6 +30,17 @@ namespace clangd {
class SymbolDocCommentVisitor
: public comments::ConstCommentVisitor<SymbolDocCommentVisitor> {
public:
+ SymbolDocCommentVisitor(comments::FullComment *FC,
+ const CommentOptions &CommentOpts)
+ : Traits(Allocator, CommentOpts), Allocator() {
+ if (!FC)
+ return;
+
+ for (auto *Block : FC->getBlocks()) {
+ visit(Block);
+ }
+ }
+
SymbolDocCommentVisitor(llvm::StringRef Documentation,
const CommentOptions &CommentOpts)
: Traits(Allocator, CommentOpts), Allocator() {
@@ -82,10 +94,11 @@ class SymbolDocCommentVisitor
Traits);
comments::FullComment *FC = P.parseFullComment();
- if (FC) {
- for (auto *Block : FC->getBlocks()) {
- visit(Block);
- }
+ if (!FC)
+ return;
+
+ for (auto *Block : FC->getBlocks()) {
+ visit(Block);
}
}
@@ -95,20 +108,22 @@ class SymbolDocCommentVisitor
void parameterDocToMarkup(StringRef ParamName, markup::Paragraph &Out);
+ void parameterDocToString(StringRef ParamName, llvm::raw_string_ostream &Out);
+
void docToMarkup(markup::Document &Out);
void visitBlockCommandComment(const comments::BlockCommandComment *B) {
- BlockCommands[CommentPartIndex] = std::move(B);
+ BlockCommands[CommentPartIndex] = B;
CommentPartIndex++;
}
void visitParagraphComment(const comments::ParagraphComment *P) {
- FreeParagraphs[CommentPartIndex] = std::move(P);
+ FreeParagraphs[CommentPartIndex] = P;
CommentPartIndex++;
}
void visitParamCommandComment(const comments::ParamCommandComment *P) {
- Parameters[P->getParamNameAsWritten()] = std::move(P);
+ Parameters[P->getParamNameAsWritten()] = P;
}
private:
diff --git a/clang-tools-extra/clangd/unittests/HoverTests.cpp b/clang-tools-extra/clangd/unittests/HoverTests.cpp
index fb6f49c987d46..29e28fa6711eb 100644
--- a/clang-tools-extra/clangd/unittests/HoverTests.cpp
+++ b/clang-tools-extra/clangd/unittests/HoverTests.cpp
@@ -4477,7 +4477,7 @@ TEST(Hover, FunctionParameters) {
HI.LocalScope = "foo::";
HI.Type = "int";
HI.Definition = "int a";
- HI.Documentation = "Function doc";
+ HI.Documentation = "";
},
"### param `a`\n\n---\nType: `int`\n\n---\n```cpp\n// In foo\nint "
"a\n```"},
@@ -4492,9 +4492,9 @@ TEST(Hover, FunctionParameters) {
HI.LocalScope = "foo::";
HI.Type = "int";
HI.Definition = "int a";
- HI.Documentation = "Function doc\n @param a this is doc for a";
+ HI.Documentation = "this is doc for a";
},
- "### param `a`\n\n---\nType: `int`\n\n this is doc for "
+ "### param `a`\n\n---\nType: `int`\n\nthis is doc for "
"a\n\n---\n```cpp\n// In foo\nint a\n```"},
{R"cpp(/// Function doc
/// @param b this is doc for b
@@ -4507,7 +4507,7 @@ TEST(Hover, FunctionParameters) {
HI.LocalScope = "foo::";
HI.Type = "int";
HI.Definition = "int a";
- HI.Documentation = "Function doc\n @param b this is doc for b";
+ HI.Documentation = "";
},
"### param `a`\n\n---\nType: `int`\n\n---\n```cpp\n// In foo\nint "
"a\n```"},
@@ -4522,10 +4522,26 @@ TEST(Hover, FunctionParameters) {
HI.LocalScope = "foo::";
HI.Type = "int";
HI.Definition = "int b";
- HI.Documentation = "Function doc\n @param b this is doc for \\p b";
+ HI.Documentation = "this is doc for \\p b";
},
- "### param `b`\n\n---\nType: `int`\n\n this is doc for "
+ "### param `b`\n\n---\nType: `int`\n\nthis is doc for "
"`b`\n\n---\n```cpp\n// In foo\nint b\n```"},
+ {R"cpp(/// Function doc
+ /// @param b this is doc for \p b
+ template <typename T>
+ void foo(T a, T [[^b]]);
+ )cpp",
+ [](HoverInfo &HI) {
+ HI.Name = "b";
+ HI.Kind = index::SymbolKind::Parameter;
+ HI.NamespaceScope = "";
+ HI.LocalScope = "foo::";
+ HI.Type = "T";
+ HI.Definition = "T b";
+ HI.Documentation = "this is doc for \\p b";
+ },
+ "### param `b`\n\n---\nType: `T`\n\nthis is doc for "
+ "`b`\n\n---\n```cpp\n// In foo\nT b\n```"},
};
// Create a tiny index, so tests above can verify documentation is fetched.
>From f34993dbaef533dbdeffbcbdf977939899451eb8 Mon Sep 17 00:00:00 2001
From: Tim Cottin <timcottin at gmx.de>
Date: Wed, 6 Aug 2025 14:42:39 +0000
Subject: [PATCH 3/4] [clangd] fix review findings
---
clang-tools-extra/clangd/Hover.cpp | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp
index 529ef499b9fb0..8e2bdf13e84bb 100644
--- a/clang-tools-extra/clangd/Hover.cpp
+++ b/clang-tools-extra/clangd/Hover.cpp
@@ -1703,11 +1703,8 @@ std::string HoverInfo::present(MarkupKind Kind) const {
if (Cfg.Documentation.CommentFormat ==
Config::CommentFormatPolicy::Markdown)
return presentDefault().asMarkdown();
- if (Cfg.Documentation.CommentFormat ==
- Config::CommentFormatPolicy::Doxygen) {
- std::string T = presentDoxygen().asMarkdown();
- return T;
- }
+ if (Cfg.Documentation.CommentFormat == Config::CommentFormatPolicy::Doxygen)
+ return presentDoxygen().asMarkdown();
if (Cfg.Documentation.CommentFormat ==
Config::CommentFormatPolicy::PlainText)
// If the user prefers plain text, we use the present() method to generate
>From 8abf5aef7715ce729f4ed7f7aff65df0d2a9e0d4 Mon Sep 17 00:00:00 2001
From: Tim Cottin <timcottin at gmx.de>
Date: Wed, 6 Aug 2025 21:24:10 +0000
Subject: [PATCH 4/4] [clangd] fix more review findings
---
clang-tools-extra/clangd/Hover.cpp | 246 ++++++++----------
clang-tools-extra/clangd/Hover.h | 8 +
.../clangd/SymbolDocumentation.cpp | 38 +--
clang-tools-extra/clangd/support/Markup.cpp | 2 +-
.../clangd/unittests/HoverTests.cpp | 19 ++
.../clangd/unittests/support/MarkupTests.cpp | 6 +
clang/include/clang/AST/Comment.h | 2 +-
7 files changed, 159 insertions(+), 162 deletions(-)
diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp
index 8e2bdf13e84bb..0afa90285db52 100644
--- a/clang-tools-extra/clangd/Hover.cpp
+++ b/clang-tools-extra/clangd/Hover.cpp
@@ -1393,6 +1393,93 @@ static std::string formatOffset(uint64_t OffsetInBits) {
return Offset;
}
+void HoverInfo::calleeArgInfoToMarkupParagraph(markup::Paragraph &P) const {
+ assert(CallPassType);
+ std::string Buffer;
+ llvm::raw_string_ostream OS(Buffer);
+ OS << "Passed ";
+ if (CallPassType->PassBy != HoverInfo::PassType::Value) {
+ OS << "by ";
+ if (CallPassType->PassBy == HoverInfo::PassType::ConstRef)
+ OS << "const ";
+ OS << "reference ";
+ }
+ if (CalleeArgInfo->Name)
+ OS << "as " << CalleeArgInfo->Name;
+ else if (CallPassType->PassBy == HoverInfo::PassType::Value)
+ OS << "by value";
+ if (CallPassType->Converted && CalleeArgInfo->Type)
+ OS << " (converted to " << CalleeArgInfo->Type->Type << ")";
+ P.appendText(OS.str());
+}
+
+void HoverInfo::usedSymbolNamesToMarkup(markup::Document &Output) const {
+ markup::Paragraph &P = Output.addParagraph();
+ P.appendText("provides ");
+
+ const std::vector<std::string>::size_type SymbolNamesLimit = 5;
+ auto Front = llvm::ArrayRef(UsedSymbolNames).take_front(SymbolNamesLimit);
+
+ llvm::interleave(
+ Front, [&](llvm::StringRef Sym) { P.appendCode(Sym); },
+ [&] { P.appendText(", "); });
+ if (UsedSymbolNames.size() > Front.size()) {
+ P.appendText(" and ");
+ P.appendText(std::to_string(UsedSymbolNames.size() - Front.size()));
+ P.appendText(" more");
+ }
+}
+
+void HoverInfo::providerToMarkupParagraph(markup::Document &Output) const {
+ markup::Paragraph &DI = Output.addParagraph();
+ DI.appendText("provided by");
+ DI.appendSpace();
+ DI.appendCode(Provider);
+}
+
+void HoverInfo::definitionScopeToMarkup(markup::Document &Output) const {
+ std::string Buffer;
+
+ // Append scope comment, dropping trailing "::".
+ // Note that we don't print anything for global namespace, to not annoy
+ // non-c++ projects or projects that are not making use of namespaces.
+ if (!LocalScope.empty()) {
+ // Container name, e.g. class, method, function.
+ // We might want to propagate some info about container type to print
+ // function foo, class X, method X::bar, etc.
+ Buffer += "// In " + llvm::StringRef(LocalScope).rtrim(':').str() + '\n';
+ } else if (NamespaceScope && !NamespaceScope->empty()) {
+ Buffer += "// In namespace " +
+ llvm::StringRef(*NamespaceScope).rtrim(':').str() + '\n';
+ }
+
+ if (!AccessSpecifier.empty()) {
+ Buffer += AccessSpecifier + ": ";
+ }
+
+ Buffer += Definition;
+
+ Output.addCodeBlock(Buffer, DefinitionLanguage);
+}
+
+void HoverInfo::valueToMarkupParagraph(markup::Paragraph &P) const {
+ P.appendText("Value = ");
+ P.appendCode(*Value);
+}
+
+void HoverInfo::offsetToMarkupParagraph(markup::Paragraph &P) const {
+ P.appendText("Offset: " + formatOffset(*Offset));
+}
+
+void HoverInfo::sizeToMarkupParagraph(markup::Paragraph &P) const {
+ P.appendText("Size: " + formatSize(*Size));
+ if (Padding && *Padding != 0) {
+ P.appendText(llvm::formatv(" (+{0} padding)", formatSize(*Padding)).str());
+ }
+ if (Align)
+ P.appendText(", alignment " + formatSize(*Align));
+}
+
markup::Document HoverInfo::presentDoxygen() const {
// NOTE: this function is currently almost identical to presentDefault().
// This is to have a minimal change when introducing the doxygen parser.
@@ -1420,11 +1507,7 @@ markup::Document HoverInfo::presentDoxygen() const {
Header.appendCode(Name);
if (!Provider.empty()) {
- markup::Paragraph &DI = Output.addParagraph();
- DI.appendText("provided by");
- DI.appendSpace();
- DI.appendCode(Provider);
- Output.addRuler();
+ providerToMarkupParagraph(Output);
}
// Put a linebreak after header to increase readability.
@@ -1463,91 +1546,31 @@ markup::Document HoverInfo::presentDoxygen() const {
llvm::to_string(*Type));
if (Value) {
- markup::Paragraph &P = Output.addParagraph();
- P.appendText("Value = ");
- P.appendCode(*Value);
+ valueToMarkupParagraph(Output.addParagraph());
}
if (Offset)
- Output.addParagraph().appendText("Offset: " + formatOffset(*Offset));
+ offsetToMarkupParagraph(Output.addParagraph());
if (Size) {
- auto &P = Output.addParagraph().appendText("Size: " + formatSize(*Size));
- if (Padding && *Padding != 0) {
- P.appendText(
- llvm::formatv(" (+{0} padding)", formatSize(*Padding)).str());
- }
- if (Align)
- P.appendText(", alignment " + formatSize(*Align));
+ sizeToMarkupParagraph(Output.addParagraph());
}
if (CalleeArgInfo) {
- assert(CallPassType);
- std::string Buffer;
- llvm::raw_string_ostream OS(Buffer);
- OS << "Passed ";
- if (CallPassType->PassBy != HoverInfo::PassType::Value) {
- OS << "by ";
- if (CallPassType->PassBy == HoverInfo::PassType::ConstRef)
- OS << "const ";
- OS << "reference ";
- }
- if (CalleeArgInfo->Name)
- OS << "as " << CalleeArgInfo->Name;
- else if (CallPassType->PassBy == HoverInfo::PassType::Value)
- OS << "by value";
- if (CallPassType->Converted && CalleeArgInfo->Type)
- OS << " (converted to " << CalleeArgInfo->Type->Type << ")";
- Output.addParagraph().appendText(OS.str());
+ calleeArgInfoToMarkupParagraph(Output.addParagraph());
}
SymbolDoc.docToMarkup(Output);
if (!Definition.empty()) {
Output.addRuler();
- std::string Buffer;
-
- if (!Definition.empty()) {
- // Append scope comment, dropping trailing "::".
- // Note that we don't print anything for global namespace, to not annoy
- // non-c++ projects or projects that are not making use of namespaces.
- if (!LocalScope.empty()) {
- // Container name, e.g. class, method, function.
- // We might want to propagate some info about container type to print
- // function foo, class X, method X::bar, etc.
- Buffer +=
- "// In " + llvm::StringRef(LocalScope).rtrim(':').str() + '\n';
- } else if (NamespaceScope && !NamespaceScope->empty()) {
- Buffer += "// In namespace " +
- llvm::StringRef(*NamespaceScope).rtrim(':').str() + '\n';
- }
-
- if (!AccessSpecifier.empty()) {
- Buffer += AccessSpecifier + ": ";
- }
-
- Buffer += Definition;
- }
-
- Output.addCodeBlock(Buffer, DefinitionLanguage);
+ definitionScopeToMarkup(Output);
}
if (!UsedSymbolNames.empty()) {
Output.addRuler();
- markup::Paragraph &P = Output.addParagraph();
- P.appendText("provides ");
-
- const std::vector<std::string>::size_type SymbolNamesLimit = 5;
- auto Front = llvm::ArrayRef(UsedSymbolNames).take_front(SymbolNamesLimit);
-
- llvm::interleave(
- Front, [&](llvm::StringRef Sym) { P.appendCode(Sym); },
- [&] { P.appendText(", "); });
- if (UsedSymbolNames.size() > Front.size()) {
- P.appendText(" and ");
- P.appendText(std::to_string(UsedSymbolNames.size() - Front.size()));
- P.appendText(" more");
- }
+ usedSymbolNamesToMarkup(Output);
}
+
return Output;
}
@@ -1572,11 +1595,7 @@ markup::Document HoverInfo::presentDefault() const {
Header.appendCode(Name);
if (!Provider.empty()) {
- markup::Paragraph &DI = Output.addParagraph();
- DI.appendText("provided by");
- DI.appendSpace();
- DI.appendCode(Provider);
- Output.addRuler();
+ providerToMarkupParagraph(Output);
}
// Put a linebreak after header to increase readability.
@@ -1607,41 +1626,17 @@ markup::Document HoverInfo::presentDefault() const {
llvm::to_string(*Type));
if (Value) {
- markup::Paragraph &P = Output.addParagraph();
- P.appendText("Value = ");
- P.appendCode(*Value);
+ valueToMarkupParagraph(Output.addParagraph());
}
if (Offset)
- Output.addParagraph().appendText("Offset: " + formatOffset(*Offset));
+ offsetToMarkupParagraph(Output.addParagraph());
if (Size) {
- auto &P = Output.addParagraph().appendText("Size: " + formatSize(*Size));
- if (Padding && *Padding != 0) {
- P.appendText(
- llvm::formatv(" (+{0} padding)", formatSize(*Padding)).str());
- }
- if (Align)
- P.appendText(", alignment " + formatSize(*Align));
+ sizeToMarkupParagraph(Output.addParagraph());
}
if (CalleeArgInfo) {
- assert(CallPassType);
- std::string Buffer;
- llvm::raw_string_ostream OS(Buffer);
- OS << "Passed ";
- if (CallPassType->PassBy != HoverInfo::PassType::Value) {
- OS << "by ";
- if (CallPassType->PassBy == HoverInfo::PassType::ConstRef)
- OS << "const ";
- OS << "reference ";
- }
- if (CalleeArgInfo->Name)
- OS << "as " << CalleeArgInfo->Name;
- else if (CallPassType->PassBy == HoverInfo::PassType::Value)
- OS << "by value";
- if (CallPassType->Converted && CalleeArgInfo->Type)
- OS << " (converted to " << CalleeArgInfo->Type->Type << ")";
- Output.addParagraph().appendText(OS.str());
+ calleeArgInfoToMarkupParagraph(Output.addParagraph());
}
if (!Documentation.empty())
@@ -1649,49 +1644,12 @@ markup::Document HoverInfo::presentDefault() const {
if (!Definition.empty()) {
Output.addRuler();
- std::string Buffer;
-
- if (!Definition.empty()) {
- // Append scope comment, dropping trailing "::".
- // Note that we don't print anything for global namespace, to not annoy
- // non-c++ projects or projects that are not making use of namespaces.
- if (!LocalScope.empty()) {
- // Container name, e.g. class, method, function.
- // We might want to propagate some info about container type to print
- // function foo, class X, method X::bar, etc.
- Buffer +=
- "// In " + llvm::StringRef(LocalScope).rtrim(':').str() + '\n';
- } else if (NamespaceScope && !NamespaceScope->empty()) {
- Buffer += "// In namespace " +
- llvm::StringRef(*NamespaceScope).rtrim(':').str() + '\n';
- }
-
- if (!AccessSpecifier.empty()) {
- Buffer += AccessSpecifier + ": ";
- }
-
- Buffer += Definition;
- }
-
- Output.addCodeBlock(Buffer, DefinitionLanguage);
+ definitionScopeToMarkup(Output);
}
if (!UsedSymbolNames.empty()) {
Output.addRuler();
- markup::Paragraph &P = Output.addParagraph();
- P.appendText("provides ");
-
- const std::vector<std::string>::size_type SymbolNamesLimit = 5;
- auto Front = llvm::ArrayRef(UsedSymbolNames).take_front(SymbolNamesLimit);
-
- llvm::interleave(
- Front, [&](llvm::StringRef Sym) { P.appendCode(Sym); },
- [&] { P.appendText(", "); });
- if (UsedSymbolNames.size() > Front.size()) {
- P.appendText(" and ");
- P.appendText(std::to_string(UsedSymbolNames.size() - Front.size()));
- P.appendText(" more");
- }
+ usedSymbolNamesToMarkup(Output);
}
return Output;
diff --git a/clang-tools-extra/clangd/Hover.h b/clang-tools-extra/clangd/Hover.h
index 2578e7a4339d0..614180a7b9846 100644
--- a/clang-tools-extra/clangd/Hover.h
+++ b/clang-tools-extra/clangd/Hover.h
@@ -124,6 +124,14 @@ struct HoverInfo {
std::string present(MarkupKind Kind) const;
private:
+ void usedSymbolNamesToMarkup(markup::Document &Output) const;
+ void providerToMarkupParagraph(markup::Document &Output) const;
+ void definitionScopeToMarkup(markup::Document &Output) const;
+ void calleeArgInfoToMarkupParagraph(markup::Paragraph &P) const;
+ void valueToMarkupParagraph(markup::Paragraph &P) const;
+ void offsetToMarkupParagraph(markup::Paragraph &P) const;
+ void sizeToMarkupParagraph(markup::Paragraph &P) const;
+
/// Parse and render the hover information as Doxygen documentation.
markup::Document presentDoxygen() const;
diff --git a/clang-tools-extra/clangd/SymbolDocumentation.cpp b/clang-tools-extra/clangd/SymbolDocumentation.cpp
index 0c8b09e2506b0..dea637b9100da 100644
--- a/clang-tools-extra/clangd/SymbolDocumentation.cpp
+++ b/clang-tools-extra/clangd/SymbolDocumentation.cpp
@@ -17,18 +17,28 @@
namespace clang {
namespace clangd {
+namespace {
+
+std::string commandMarkerAsString(comments::CommandMarkerKind CommandMarker) {
+ switch (CommandMarker) {
+ case comments::CommandMarkerKind::CMK_At:
+ return "@";
+ case comments::CommandMarkerKind::CMK_Backslash:
+ return "\\";
+ }
+ llvm_unreachable("Unknown command marker kind");
+}
void commandToMarkup(markup::Paragraph &Out, StringRef Command,
comments::CommandMarkerKind CommandMarker,
StringRef Args) {
- Out.appendBoldText(
- (CommandMarker == (comments::CommandMarkerKind::CMK_At) ? "@" : "\\") +
- Command.str());
+ Out.appendBoldText(commandMarkerAsString(CommandMarker) + Command.str());
if (!Args.empty()) {
Out.appendSpace();
Out.appendEmphasizedText(Args.str());
}
}
+} // namespace
class ParagraphToMarkupDocument
: public comments::ConstCommentVisitor<ParagraphToMarkupDocument> {
@@ -145,27 +155,23 @@ class ParagraphToString
void visitTextComment(const comments::TextComment *C) { Out << C->getText(); }
void visitInlineCommandComment(const comments::InlineCommandComment *C) {
- Out << (C->getCommandMarker() == comments::CommandMarkerKind::CMK_At
- ? "@"
- : "\\");
+ Out << commandMarkerAsString(C->getCommandMarker());
Out << C->getCommandName(Traits);
if (C->getNumArgs() > 0) {
- Out << " ";
- for (unsigned I = 0; I < C->getNumArgs(); ++I) {
- if (I > 0)
- Out << " ";
- Out << C->getArgText(I);
- }
+ for (unsigned I = 0; I < C->getNumArgs(); ++I)
+ Out << " " << C->getArgText(I);
}
Out << " ";
}
void visitHTMLStartTagComment(const comments::HTMLStartTagComment *STC) {
- Out << "<" + STC->getTagName().str();
+ Out << "<" << STC->getTagName().str();
for (unsigned I = 0; I < STC->getNumAttrs(); ++I) {
const comments::HTMLStartTagComment::Attribute &Attr = STC->getAttr(I);
- Out << " " + Attr.Name.str() + "=\"" + Attr.Value.str() + "\"";
+ Out << " " << Attr.Name.str();
+ if (!Attr.Value.str().empty())
+ Out << "=\"" << Attr.Value.str() << "\"";
}
if (STC->isSelfClosing())
@@ -176,8 +182,8 @@ class ParagraphToString
}
void visitHTMLEndTagComment(const comments::HTMLEndTagComment *ETC) {
- Out << "</" + ETC->getTagName().str() + ">" +
- (ETC->hasTrailingNewline() ? "\n" : "");
+ Out << "</" << ETC->getTagName().str() << ">"
+ << (ETC->hasTrailingNewline() ? "\n" : "");
}
private:
diff --git a/clang-tools-extra/clangd/support/Markup.cpp b/clang-tools-extra/clangd/support/Markup.cpp
index 152863191dad1..89bdc656d440f 100644
--- a/clang-tools-extra/clangd/support/Markup.cpp
+++ b/clang-tools-extra/clangd/support/Markup.cpp
@@ -364,7 +364,7 @@ class CodeBlock : public Block {
std::string Marker = getMarkerForCodeBlock(Contents);
// No need to pad from previous blocks, as they should end with a new line.
OS << Marker << Language << '\n' << Contents;
- if (Contents.back() != '\n')
+ if (!Contents.empty() && Contents.back() != '\n')
OS << '\n';
// Always end with an empty line to separate code blocks from following
// paragraphs.
diff --git a/clang-tools-extra/clangd/unittests/HoverTests.cpp b/clang-tools-extra/clangd/unittests/HoverTests.cpp
index 29e28fa6711eb..3331164ab0024 100644
--- a/clang-tools-extra/clangd/unittests/HoverTests.cpp
+++ b/clang-tools-extra/clangd/unittests/HoverTests.cpp
@@ -4542,6 +4542,25 @@ TEST(Hover, FunctionParameters) {
},
"### param `b`\n\n---\nType: `T`\n\nthis is doc for "
"`b`\n\n---\n```cpp\n// In foo\nT b\n```"},
+ {R"cpp(/// Function doc
+ /// @param b this is <b>doc</b> <html-tag attribute/> <another-html-tag attribute="value">for</another-html-tag> \p b
+ void foo(int a, int [[^b]]);
+ )cpp",
+ [](HoverInfo &HI) {
+ HI.Name = "b";
+ HI.Kind = index::SymbolKind::Parameter;
+ HI.NamespaceScope = "";
+ HI.LocalScope = "foo::";
+ HI.Type = "int";
+ HI.Definition = "int b";
+ HI.Documentation =
+ "this is <b>doc</b> <html-tag attribute/> <another-html-tag "
+ "attribute=\"value\">for</another-html-tag> \\p b";
+ },
+ "### param `b`\n\n---\nType: `int`\n\nthis is \\<b>doc\\</b> "
+ "\\<html-tag attribute/> \\<another-html-tag "
+ "attribute=\"value\">for\\</another-html-tag> "
+ "`b`\n\n---\n```cpp\n// In foo\nint b\n```"},
};
// Create a tiny index, so tests above can verify documentation is fetched.
diff --git a/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp b/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp
index 9c17db067f398..5f91f31557176 100644
--- a/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp
+++ b/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp
@@ -573,6 +573,12 @@ foo
foo)pt";
EXPECT_EQ(D.asPlainText(), ExpectedPlainText);
+
+ Document D2;
+ D2.addCodeBlock("");
+ EXPECT_EQ(D2.asEscapedMarkdown(), "```cpp\n```");
+ EXPECT_EQ(D2.asMarkdown(), "```cpp\n```");
+ EXPECT_EQ(D2.asPlainText(), "");
}
TEST(BulletList, Render) {
diff --git a/clang/include/clang/AST/Comment.h b/clang/include/clang/AST/Comment.h
index 42686ff24076a..5ba95c8291d38 100644
--- a/clang/include/clang/AST/Comment.h
+++ b/clang/include/clang/AST/Comment.h
@@ -401,7 +401,7 @@ class InlineCommandComment : public InlineContentComment {
return Args[Idx].Range;
}
- CommandMarkerKind getCommandMarker() const LLVM_READONLY {
+ CommandMarkerKind getCommandMarker() const {
return static_cast<CommandMarkerKind>(
InlineCommandCommentBits.CommandMarker);
}
More information about the llvm-commits
mailing list