Skip to content

[clangd] introduce doxygen parser #150790

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

tcottin
Copy link
Contributor

@tcottin tcottin commented Jul 26, 2025

Followup work of #140498 to continue the work on clangd/clangd#529

Introduce the use of the Clang doxygen parser to parse the documentation of hovered code.

  • ASTContext independent doxygen parsing
  • Parsing doxygen commands to markdown for hover information

Note: after this PR I have planned another patch to rearrange the information shown in the hover info.
This PR is just for the basic introduction of doxygen parsing for hover information.

@llvmbot llvmbot added clang Clang issues not falling into any other category clang-tools-extra clangd clang:frontend Language frontend issues, e.g. anything involving "Sema" labels Jul 26, 2025
@llvmbot
Copy link
Member

llvmbot commented Jul 26, 2025

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

@llvm/pr-subscribers-clang

Author: None (tcottin)

Changes

Followup work of #140498 to continue the work on clangd/clangd#529

Introduce the use of the Clang doxygen parser to parse the documentation of hovered code.

  • ASTContext independent doxygen parsing
  • Parsing doxygen commands to markdown for hover information

Note: after this PR I have planned another patch to rearrange the information shown in the hover info.
This PR is just for the basic introduction of doxygen parsing for hover information.


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

16 Files Affected:

  • (modified) clang-tools-extra/clangd/CMakeLists.txt (+1)
  • (modified) clang-tools-extra/clangd/CodeCompletionStrings.cpp (+14-1)
  • (modified) clang-tools-extra/clangd/Hover.cpp (+177-10)
  • (modified) clang-tools-extra/clangd/Hover.h (+10-3)
  • (added) clang-tools-extra/clangd/SymbolDocumentation.cpp (+221)
  • (added) clang-tools-extra/clangd/SymbolDocumentation.h (+140)
  • (modified) clang-tools-extra/clangd/support/Markup.cpp (+6-1)
  • (modified) clang-tools-extra/clangd/unittests/CMakeLists.txt (+1)
  • (modified) clang-tools-extra/clangd/unittests/HoverTests.cpp (+229)
  • (added) clang-tools-extra/clangd/unittests/SymbolDocumentationTests.cpp (+161)
  • (modified) clang-tools-extra/clangd/unittests/support/MarkupTests.cpp (+2)
  • (modified) clang/include/clang/AST/Comment.h (+21)
  • (modified) clang/include/clang/AST/CommentSema.h (+1)
  • (modified) clang/lib/AST/CommentParser.cpp (+4-1)
  • (modified) clang/lib/AST/CommentSema.cpp (+4-3)
  • (modified) llvm/utils/gn/secondary/clang-tools-extra/clangd/BUILD.gn (+1)
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"
+...
[truncated]

@mccakit
Copy link

mccakit commented Jul 27, 2025

Hey, I merged this PR into main and build it, but doxygen comments don't work.
Am I supposed to tweak something in my .clangd config?
Or do you intend on enabling doxygen hower on the third patch?

@tcottin
Copy link
Contributor Author

tcottin commented Jul 27, 2025

Hey, I merged this PR into main and build it, but doxygen comments don't work. Am I supposed to tweak something in my .clangd config? Or do you intend on enabling doxygen hower on the third patch?

Yes, you need to enable this in your .clangd config:

Documentation:
  CommentFormat: Doxygen

This PR is parsing doxygen commands and highlights them as bold text.
Additionally, some inline commands like \p will render their argument according to their render kind, e.g. monospaced for \p.
Also hovering over a documented parameter shows the parameter documentation.

The next PR will additionally rearrange and highlight commands like \brief, \return etc differently in the hover.

@mccakit
Copy link

mccakit commented Jul 27, 2025

Yeah, that changed the output though I'm still seeing this

/**
 * @brief Adds two numbers
 * @param a First number
 * @param b Second number
 * @return The sum
 */
int add(int a, int b);
 
->
  function add
  → int
  Parameters:
  int a - First number
  int b - Second number
  @brief Adds two numbers
  @return The sum
  int add(int a, int b)

May I ask you to update the third patch?
I tried merging it myself but there were too many conflicts.
I really want to merge and build it right now, and I will be able to report any issues that arise.

The OG patch worked perfectly though, before you split it down to 3 parts, so I doubt I'll face anything serious

@tcottin
Copy link
Contributor Author

tcottin commented Jul 27, 2025

Yes, as I wrote, this is the expected output for this PR.

Before rebasing the next patch I would like to get this PR reviewed.
If there is something bigger we need to change, I think it is easier to first change it for this PR only and then rebase the last patch.

@emaxx-google emaxx-google self-requested a review July 29, 2025 00:40
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()))
Copy link
Contributor

Choose a reason for hiding this comment

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

For a function template, you'd probably need to go up to the enclosing FunctionTemplateDecl. (Ideally we wouldn't hardcode such low-level AST details here, but I don't have a suggestion for an existing helper function.)


if (Cfg.Documentation.CommentFormat == Config::CommentFormatPolicy::Doxygen &&
isa<ParmVarDecl>(Decl)) {
// Parameters are documented in the function comment.
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this mean the whole function-level comment is going to be duplicated for each of the parameters? Can we extract relevant portions from it instead?

@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: "save"

@@ -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().
Copy link
Contributor

Choose a reason for hiding this comment

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

There's a lot of code that's duplicated here - this might eventually lead to subtle divergence between two branches and bugs. How big are the doxygen-specific bits going to be, and would it be possible to fit them into a single common implementation (maybe with a help of helper functions)?

Copy link
Contributor Author

@tcottin tcottin Jul 29, 2025

Choose a reason for hiding this comment

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

Some smaller parts of the function could be shared.

But overall, the functions will differ in the "flow" of when which part of the documentation will be streamed to the markup document.

The implementation of HoverInfo::presentDoxygen in the last patch is already available here:
tcottin@b4920ed?diff=split

This version is still based on the assumption that we dont need to differentiate between documentation styles.
Meaning, in the diff of the next patch, the HoverInfo::present method corresponds to the HoverInfo::presentDoxygen of this PR.

return presentDefault().asMarkdown();
if (Cfg.Documentation.CommentFormat ==
Config::CommentFormatPolicy::Doxygen) {
std::string T = presentDoxygen().asMarkdown();
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: The variable isn't needed.

void docToMarkup(markup::Document &Out);

void visitBlockCommandComment(const comments::BlockCommandComment *B) {
BlockCommands[CommentPartIndex] = std::move(B);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: std::move() doesn't do anything for pointers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category clang-tools-extra clangd
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants