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 <timcot...@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@param a this is a param\n@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@param a this is a param\n@param b " + "does not exist\n@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@param bar this is another " + "parameter", + "", + "", + "", + }, + { + "@brief brief docs\n\n@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 <timcot...@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 <timcot...@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 <timcot...@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); } _______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits