https://github.com/tcottin updated https://github.com/llvm/llvm-project/pull/128591
>From 18fa9e58b04b4e41e2b810fcc4a0c6be48258a1f Mon Sep 17 00:00:00 2001 From: Tim Cottin <timcot...@gmx.de> Date: Tue, 11 Mar 2025 19:00:49 +0000 Subject: [PATCH] [clangd] Add doxygen parsing for hover information --- clang-tools-extra/clangd/CMakeLists.txt | 1 + .../clangd/CodeCompletionStrings.cpp | 74 ++- .../clangd/CodeCompletionStrings.h | 23 + clang-tools-extra/clangd/Hover.cpp | 135 ++--- clang-tools-extra/clangd/Hover.h | 54 +- .../clangd/SymbolDocumentation.cpp | 503 ++++++++++++++++++ .../clangd/SymbolDocumentation.h | 92 ++++ clang-tools-extra/clangd/support/Markup.cpp | 36 ++ clang-tools-extra/clangd/support/Markup.h | 8 + .../unittests/CodeCompletionStringsTests.cpp | 219 ++++++++ .../clangd/unittests/HoverTests.cpp | 127 ++++- .../clangd/unittests/support/MarkupTests.cpp | 30 ++ clang/include/clang/AST/Comment.h | 22 + clang/include/clang/AST/CommentSema.h | 1 + clang/lib/AST/CommentParser.cpp | 5 +- clang/lib/AST/CommentSema.cpp | 3 +- .../clang-tools-extra/clangd/BUILD.gn | 1 + 17 files changed, 1177 insertions(+), 157 deletions(-) create mode 100644 clang-tools-extra/clangd/SymbolDocumentation.cpp create mode 100644 clang-tools-extra/clangd/SymbolDocumentation.h diff --git a/clang-tools-extra/clangd/CMakeLists.txt b/clang-tools-extra/clangd/CMakeLists.txt index 6f10afe4a5625..2fda21510f046 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..f307866176ca5 100644 --- a/clang-tools-extra/clangd/CodeCompletionStrings.cpp +++ b/clang-tools-extra/clangd/CodeCompletionStrings.cpp @@ -9,6 +9,12 @@ #include "CodeCompletionStrings.h" #include "clang-c/Index.h" #include "clang/AST/ASTContext.h" +#include "clang/AST/Comment.h" +#include "clang/AST/CommentCommandTraits.h" +#include "clang/AST/CommentLexer.h" +#include "clang/AST/CommentParser.h" +#include "clang/AST/CommentSema.h" +#include "clang/AST/Decl.h" #include "clang/AST/RawCommentList.h" #include "clang/Basic/SourceManager.h" #include "clang/Sema/CodeCompleteConsumer.h" @@ -100,14 +106,25 @@ std::string getDeclComment(const ASTContext &Ctx, const NamedDecl &Decl) { // the comments for namespaces. return ""; } - const RawComment *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())); - std::string Doc = - RC->getFormattedText(Ctx.getSourceManager(), Ctx.getDiagnostics()); + + std::string Doc; + + if (isa<ParmVarDecl>(Decl)) { + // Parameters are documented in the function comment. + if (const auto *FD = dyn_cast<FunctionDecl>(Decl.getDeclContext())) + Doc = getParamDocString(Ctx.getCommentForDecl(FD, nullptr), + Decl.getName(), Ctx.getCommentCommandTraits()); + } else { + + const RawComment *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 ""; // Clang requires source to be UTF-8, but doesn't enforce this in comments. @@ -316,5 +333,46 @@ std::string getReturnType(const CodeCompletionString &CCS) { return ""; } +void docCommentToMarkup( + markup::Document &Doc, llvm::StringRef Comment, + llvm::BumpPtrAllocator &Allocator, comments::CommandTraits &Traits, + std::optional<SymbolPrintedType> SymbolType, + std::optional<SymbolPrintedType> SymbolReturnType, + const std::optional<std::vector<SymbolParam>> &SymbolParameters) { + + // 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 "/** */". + std::string CommentWithMarkers = "///"; + for (char C : Comment) { + if (C == '\n') { + CommentWithMarkers += "\n///"; + } else { + CommentWithMarkers += C; + } + } + 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); + + fullCommentToMarkupDocument(Doc, P.parseFullComment(), Traits, SymbolType, + SymbolReturnType, SymbolParameters); +} + } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/CodeCompletionStrings.h b/clang-tools-extra/clangd/CodeCompletionStrings.h index fa81ad64d406c..b440849f0eb49 100644 --- a/clang-tools-extra/clangd/CodeCompletionStrings.h +++ b/clang-tools-extra/clangd/CodeCompletionStrings.h @@ -14,11 +14,17 @@ #ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_CODECOMPLETIONSTRINGS_H #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_CODECOMPLETIONSTRINGS_H +#include "SymbolDocumentation.h" #include "clang/Sema/CodeCompleteConsumer.h" namespace clang { class ASTContext; +namespace comments { +class CommandTraits; +class FullComment; +} // namespace comments + namespace clangd { /// Gets a minimally formatted documentation comment of \p Result, with comment @@ -67,6 +73,23 @@ std::string formatDocumentation(const CodeCompletionString &CCS, /// is usually the return type of a function. std::string getReturnType(const CodeCompletionString &CCS); +/// \brief Parse the \p Comment as doxygen comment and save the result in the +/// given markup Document \p Doc. +/// +/// It is assumed that comment markers have already been stripped (e.g. via +/// getDocComment()). +/// +/// This uses the Clang doxygen comment parser to parse the comment, and then +/// converts the parsed comment to a markup::Document. The resulting document is +/// a combination of the symbol information \p SymbolType, \p SymbolReturnType, +/// and \p SymbolParameters and the parsed doxygen comment. +void docCommentToMarkup( + markup::Document &Doc, llvm::StringRef Comment, + llvm::BumpPtrAllocator &Allocator, comments::CommandTraits &Traits, + std::optional<SymbolPrintedType> SymbolType, + std::optional<SymbolPrintedType> SymbolReturnType, + const std::optional<std::vector<SymbolParam>> &SymbolParameters); + } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp index 3ab3d89030520..b488011f8abfc 100644 --- a/clang-tools-extra/clangd/Hover.cpp +++ b/clang-tools-extra/clangd/Hover.cpp @@ -17,6 +17,7 @@ #include "ParsedAST.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" @@ -40,6 +41,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" @@ -160,14 +162,14 @@ const char *getMarkdownLanguage(const ASTContext &Ctx) { return LangOpts.ObjC ? "objective-c" : "cpp"; } -HoverInfo::PrintedType printType(QualType QT, ASTContext &ASTCtx, +SymbolPrintedType printType(QualType QT, ASTContext &ASTCtx, const PrintingPolicy &PP) { // TypePrinter doesn't resolve decltypes, so resolve them here. // FIXME: This doesn't handle composite types that contain a decltype in them. // We should rather have a printing policy for that. while (!QT.isNull() && QT->isDecltypeType()) QT = QT->castAs<DecltypeType>()->getUnderlyingType(); - HoverInfo::PrintedType Result; + SymbolPrintedType Result; llvm::raw_string_ostream OS(Result.Type); // Special case: if the outer type is a tag type without qualifiers, then // include the tag for extra clarity. @@ -189,15 +191,15 @@ HoverInfo::PrintedType printType(QualType QT, ASTContext &ASTCtx, return Result; } -HoverInfo::PrintedType printType(const TemplateTypeParmDecl *TTP) { - HoverInfo::PrintedType Result; +SymbolPrintedType printType(const TemplateTypeParmDecl *TTP) { + SymbolPrintedType Result; Result.Type = TTP->wasDeclaredWithTypename() ? "typename" : "class"; if (TTP->isParameterPack()) Result.Type += "..."; return Result; } -HoverInfo::PrintedType printType(const NonTypeTemplateParmDecl *NTTP, +SymbolPrintedType printType(const NonTypeTemplateParmDecl *NTTP, const PrintingPolicy &PP) { auto PrintedType = printType(NTTP->getType(), NTTP->getASTContext(), PP); if (NTTP->isParameterPack()) { @@ -208,9 +210,9 @@ HoverInfo::PrintedType printType(const NonTypeTemplateParmDecl *NTTP, return PrintedType; } -HoverInfo::PrintedType printType(const TemplateTemplateParmDecl *TTP, +SymbolPrintedType printType(const TemplateTemplateParmDecl *TTP, const PrintingPolicy &PP) { - HoverInfo::PrintedType Result; + SymbolPrintedType Result; llvm::raw_string_ostream OS(Result.Type); OS << "template <"; llvm::StringRef Sep = ""; @@ -230,14 +232,14 @@ HoverInfo::PrintedType printType(const TemplateTemplateParmDecl *TTP, return Result; } -std::vector<HoverInfo::Param> +std::vector<SymbolParam> fetchTemplateParameters(const TemplateParameterList *Params, const PrintingPolicy &PP) { assert(Params); - std::vector<HoverInfo::Param> TempParameters; + std::vector<SymbolParam> TempParameters; for (const Decl *Param : *Params) { - HoverInfo::Param P; + SymbolParam P; if (const auto *TTP = dyn_cast<TemplateTypeParmDecl>(Param)) { P.Type = printType(TTP); @@ -351,41 +353,13 @@ void enhanceFromIndex(HoverInfo &Hover, const NamedDecl &ND, }); } -// Default argument might exist but be unavailable, in the case of unparsed -// arguments for example. This function returns the default argument if it is -// available. -const Expr *getDefaultArg(const ParmVarDecl *PVD) { - // Default argument can be unparsed or uninstantiated. For the former we - // can't do much, as token information is only stored in Sema and not - // attached to the AST node. For the latter though, it is safe to proceed as - // the expression is still valid. - if (!PVD->hasDefaultArg() || PVD->hasUnparsedDefaultArg()) - return nullptr; - return PVD->hasUninstantiatedDefaultArg() ? PVD->getUninstantiatedDefaultArg() - : PVD->getDefaultArg(); -} - -HoverInfo::Param toHoverInfoParam(const ParmVarDecl *PVD, - const PrintingPolicy &PP) { - HoverInfo::Param Out; - Out.Type = printType(PVD->getType(), PVD->getASTContext(), PP); - if (!PVD->getName().empty()) - Out.Name = PVD->getNameAsString(); - if (const Expr *DefArg = getDefaultArg(PVD)) { - Out.Default.emplace(); - llvm::raw_string_ostream OS(*Out.Default); - DefArg->printPretty(OS, nullptr, PP); - } - return Out; -} - // Populates Type, ReturnType, and Parameters for function-like decls. void fillFunctionTypeAndParams(HoverInfo &HI, const Decl *D, const FunctionDecl *FD, const PrintingPolicy &PP) { HI.Parameters.emplace(); for (const ParmVarDecl *PVD : FD->parameters()) - HI.Parameters->emplace_back(toHoverInfoParam(PVD, PP)); + HI.Parameters->emplace_back(createSymbolParam(PVD, PP)); // We don't want any type info, if name already contains it. This is true for // constructors/destructors and conversion operators. @@ -626,6 +600,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); @@ -812,7 +789,7 @@ HoverInfo getHoverContents(const DefinedMacro &Macro, const syntax::Token &Tok, return HI; } -std::string typeAsDefinition(const HoverInfo::PrintedType &PType) { +std::string typeAsDefinition(const SymbolPrintedType &PType) { std::string Result; llvm::raw_string_ostream OS(Result); OS << PType.Type; @@ -1095,7 +1072,7 @@ void maybeAddCalleeArgInfo(const SelectionTree::Node *N, HoverInfo &HI, // Extract matching argument from function declaration. if (const ParmVarDecl *PVD = Parameters[I]) { - HI.CalleeArgInfo.emplace(toHoverInfoParam(PVD, PP)); + HI.CalleeArgInfo.emplace(createSymbolParam(PVD, PP)); if (N == &OuterNode) PassType.PassBy = getPassMode(PVD->getType()); } @@ -1455,30 +1432,38 @@ markup::Document HoverInfo::present() const { // 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)); - } - if (Parameters && !Parameters->empty()) { - Output.addParagraph().appendText("Parameters: "); - markup::BulletList &L = Output.addBulletList(); - for (const auto &Param : *Parameters) - L.addItem().addParagraph().appendCode(llvm::to_string(Param)); - } + if (!Documentation.empty()) { + llvm::BumpPtrAllocator Allocator; + comments::CommandTraits Traits(Allocator, CommentOpts); + docCommentToMarkup(Output, Documentation, Allocator, Traits, Type, ReturnType, + Parameters); + } else { + // 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)); + } - // 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 (Parameters && !Parameters->empty()) { + Output.addParagraph().appendText("Parameters: "); + markup::BulletList &L = Output.addBulletList(); + for (const auto &Param : *Parameters) + L.addItem().addParagraph().appendCode(llvm::to_string(Param)); + } + + // 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(); @@ -1518,9 +1503,6 @@ markup::Document HoverInfo::present() const { Output.addParagraph().appendText(OS.str()); } - if (!Documentation.empty()) - parseDocumentation(Documentation, Output); - if (!Definition.empty()) { Output.addRuler(); std::string Buffer; @@ -1646,26 +1628,5 @@ void parseDocumentation(llvm::StringRef Input, markup::Document &Output) { FlushParagraph(); } -llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, - const HoverInfo::PrintedType &T) { - OS << T.Type; - if (T.AKA) - OS << " (aka " << *T.AKA << ")"; - return OS; -} - -llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, - const HoverInfo::Param &P) { - if (P.Type) - OS << P.Type->Type; - if (P.Name) - OS << " " << *P.Name; - if (P.Default) - OS << " = " << *P.Default; - if (P.Type && P.Type->AKA) - OS << " (aka " << *P.Type->AKA << ")"; - return OS; -} - } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/Hover.h b/clang-tools-extra/clangd/Hover.h index fe689de44732e..f485f02f3d516 100644 --- a/clang-tools-extra/clangd/Hover.h +++ b/clang-tools-extra/clangd/Hover.h @@ -11,6 +11,7 @@ #include "ParsedAST.h" #include "Protocol.h" +#include "SymbolDocumentation.h" #include "support/Markup.h" #include "clang/Index/IndexSymbol.h" #include <optional> @@ -25,33 +26,6 @@ namespace clangd { /// embedding clients can use the structured information to provide their own /// UI. struct HoverInfo { - /// Contains pretty-printed type and desugared type - struct PrintedType { - PrintedType() = default; - PrintedType(const char *Type) : Type(Type) {} - PrintedType(const char *Type, const char *AKAType) - : Type(Type), AKA(AKAType) {} - - /// Pretty-printed type - std::string Type; - /// Desugared type - std::optional<std::string> AKA; - }; - - /// Represents parameters of a function, a template or a macro. - /// For example: - /// - void foo(ParamType Name = DefaultValue) - /// - #define FOO(Name) - /// - template <ParamType Name = DefaultType> class Foo {}; - struct Param { - /// The printable parameter type, e.g. "int", or "typename" (in - /// TemplateParameters), might be std::nullopt for macro parameters. - std::optional<PrintedType> Type; - /// std::nullopt for unnamed parameters. - std::optional<std::string> Name; - /// std::nullopt if no default is provided. - std::optional<std::string> Default; - }; /// For a variable named Bar, declared in clang::clangd::Foo::getFoo the /// following fields will hold: @@ -74,6 +48,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"; @@ -82,13 +58,13 @@ struct HoverInfo { std::string AccessSpecifier; /// Printable variable type. /// Set only for variables. - std::optional<PrintedType> Type; + std::optional<SymbolPrintedType> Type; /// Set for functions and lambdas. - std::optional<PrintedType> ReturnType; + std::optional<SymbolPrintedType> ReturnType; /// Set for functions, lambdas and macros with parameters. - std::optional<std::vector<Param>> Parameters; + std::optional<std::vector<SymbolParam>> Parameters; /// Set for all templates(function, class, variable). - std::optional<std::vector<Param>> TemplateParameters; + std::optional<std::vector<SymbolParam>> TemplateParameters; /// Contains the evaluated value of the symbol if available. std::optional<std::string> Value; /// Contains the bit-size of fields and types where it's interesting. @@ -101,7 +77,7 @@ struct HoverInfo { std::optional<uint64_t> Align; // Set when symbol is inside function call. Contains information extracted // from the callee definition about the argument this is passed as. - std::optional<Param> CalleeArgInfo; + std::optional<SymbolParam> CalleeArgInfo; struct PassType { // How the variable is passed to callee. enum PassMode { Ref, ConstRef, Value }; @@ -122,11 +98,6 @@ struct HoverInfo { markup::Document present() const; }; -inline bool operator==(const HoverInfo::PrintedType &LHS, - const HoverInfo::PrintedType &RHS) { - return std::tie(LHS.Type, LHS.AKA) == std::tie(RHS.Type, RHS.AKA); -} - inline bool operator==(const HoverInfo::PassType &LHS, const HoverInfo::PassType &RHS) { return std::tie(LHS.PassBy, LHS.Converted) == @@ -137,15 +108,6 @@ inline bool operator==(const HoverInfo::PassType &LHS, // FIXME: move to another file so CodeComplete doesn't depend on Hover. void parseDocumentation(llvm::StringRef Input, markup::Document &Output); -llvm::raw_ostream &operator<<(llvm::raw_ostream &, - const HoverInfo::PrintedType &); -llvm::raw_ostream &operator<<(llvm::raw_ostream &, const HoverInfo::Param &); -inline bool operator==(const HoverInfo::Param &LHS, - const HoverInfo::Param &RHS) { - return std::tie(LHS.Type, LHS.Name, LHS.Default) == - std::tie(RHS.Type, RHS.Name, RHS.Default); -} - /// Get the hover information when hovering at \p Pos. std::optional<HoverInfo> getHover(ParsedAST &AST, Position Pos, const format::FormatStyle &Style, diff --git a/clang-tools-extra/clangd/SymbolDocumentation.cpp b/clang-tools-extra/clangd/SymbolDocumentation.cpp new file mode 100644 index 0000000000000..771f1d99e639e --- /dev/null +++ b/clang-tools-extra/clangd/SymbolDocumentation.cpp @@ -0,0 +1,503 @@ +//===--- 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 "Config.h" +#include "support/Markup.h" +#include "clang/AST/ASTDiagnostic.h" +#include "clang/AST/Comment.h" +#include "clang/AST/CommentCommandTraits.h" +#include "clang/AST/CommentVisitor.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/ScopedPrinter.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) { + for (const auto *Child = C->child_begin(); Child != C->child_end(); + ++Child) { + visit(*Child); + } + } + + void visitTextComment(const comments::TextComment *C) { + Out.appendText(C->getText().str()); + // A paragraph may have multiple TextComments seperated by a newline. + // We need to add a space to separate them in the markup::Document. + if (C->hasTrailingNewline()) + Out.appendSpace(); + } + + 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 { + commandToMarkup(Out, C->getCommandName(Traits), C->getCommandMarker(), + ""); + } + } + +private: + markup::Paragraph &Out; + const comments::CommandTraits &Traits; +}; + +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) { + // 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()->isWhitespace()) { + P.appendSpace(); + ParagraphToMarkupDocument(P, Traits).visit(B->getParagraph()); + } + } + +private: + markup::Document &Out; + const comments::CommandTraits &Traits; +}; + +class FullCommentToMarkupDocument + : public comments::ConstCommentVisitor<FullCommentToMarkupDocument> { +public: + FullCommentToMarkupDocument( + const comments::FullComment &FC, const comments::CommandTraits &Traits, + markup::Document &Doc, std::optional<SymbolPrintedType> SymbolType, + std::optional<SymbolPrintedType> SymbolReturnType, + const std::optional<std::vector<SymbolParam>> &SymbolParameters) + : Traits(Traits), Output(Doc) { + + for (auto *Block : FC.getBlocks()) { + visit(Block); + } + + for (const auto *BP : BriefParagraphs) + ParagraphToMarkupDocument(Output.addParagraph(), Traits).visit(BP); + + if (!BriefParagraphs.empty()) + Output.addRuler(); + + if (SymbolReturnType.has_value()) { + std::string RT = llvm::to_string(*SymbolReturnType); + if (!RT.empty()) { + auto &P = Output.addParagraph().appendText("→ ").appendCode(RT); + if (!ReturnParagraphs.empty()) { + P.appendText(": "); + for (const auto *RP : ReturnParagraphs) + ParagraphToMarkupDocument(P, Traits).visit(RP); + } + } + } + + if (SymbolParameters.has_value() && !SymbolParameters->empty()) { + Output.addParagraph().appendText("Parameters:"); + markup::BulletList &L = Output.addBulletList(); + for (const auto &P : *SymbolParameters) { + markup::Paragraph &PP = + L.addItem().addParagraph().appendCode(llvm::to_string(P)); + + if (!P.Name.has_value()) + continue; + + if (const auto *PCC = Parameters[*P.Name]) { + PP.appendText(": "); + ParagraphToMarkupDocument(PP, Traits).visit(PCC->getParagraph()); + Parameters.erase(*P.Name); + } + } + } + + // Don't print Type after Parameters or ReturnType as this will just + // duplicate the information + if (SymbolType.has_value() && !SymbolReturnType.has_value() && + (!SymbolParameters.has_value() || SymbolParameters->empty())) { + Output.addParagraph().appendText("Type: ").appendCode( + llvm::to_string(*SymbolType)); + } + + if (!WarningParagraphs.empty()) { + Output.addParagraph().appendText("Warning").appendText( + WarningParagraphs.size() > 1 ? "s:" : ":"); + markup::BulletList &L = Output.addBulletList(); + for (const auto *WP : WarningParagraphs) + ParagraphToMarkupDocument(L.addItem().addParagraph(), Traits).visit(WP); + Output.addRuler(); + } + + if (!NoteParagraphs.empty()) { + if (WarningParagraphs.empty()) + Output.addRuler(); + Output.addParagraph().appendText("Note").appendText( + WarningParagraphs.size() > 1 ? "s:" : ":"); + markup::BulletList &L = Output.addBulletList(); + for (const auto *WP : NoteParagraphs) + ParagraphToMarkupDocument(L.addItem().addParagraph(), Traits).visit(WP); + Output.addRuler(); + } + + for (unsigned I = 0; I < CommentPartIndex; ++I) { + if (const auto *UnhandledCommand = UnhandledCommands.lookup(I)) { + BlockCommentToMarkupDocument(Output, Traits).visit(UnhandledCommand); + continue; + } + if (const auto *FreeText = FreeParagraphs.lookup(I)) { + ParagraphToMarkupDocument(Output.addParagraph(), Traits) + .visit(FreeText); + continue; + } + } + } + + void visitBlockCommandComment(const comments::BlockCommandComment *B) { + const comments::CommandInfo *Info = + Traits.getCommandInfo(B->getCommandID()); + + // Visit B->getParagraph() for commands that we have special fields for, + // so that the command name won't be included in the string. + // Otherwise, we want to keep the command name, so visit B itself. + if (Info->IsBriefCommand) { + BriefParagraphs.push_back(B->getParagraph()); + } else if (Info->IsReturnsCommand) { + ReturnParagraphs.push_back(B->getParagraph()); + } else { + const llvm::StringRef CommandName = B->getCommandName(Traits); + if (CommandName == "warning") { + WarningParagraphs.push_back(B->getParagraph()); + } else if (CommandName == "note") { + NoteParagraphs.push_back(B->getParagraph()); + } else { + UnhandledCommands[CommentPartIndex] = B; + } + CommentPartIndex++; + } + } + + void visitParagraphComment(const comments::ParagraphComment *P) { + FreeParagraphs[CommentPartIndex] = P; + CommentPartIndex++; + } + + void visitParamCommandComment(const comments::ParamCommandComment *P) { + Parameters[P->getParamNameAsWritten()] = P; + } + +private: + const comments::CommandTraits &Traits; + markup::Document &Output; + + unsigned CommentPartIndex = 0; + + /// Paragraph of the "brief" command. + llvm::SmallVector<const comments::ParagraphComment *, 1> BriefParagraphs; + + /// Paragraph of the "return" command. + llvm::SmallVector<const comments::ParagraphComment *, 1> ReturnParagraphs; + + /// Paragraph(s) of the "note" command(s) + llvm::SmallVector<const comments::ParagraphComment *> NoteParagraphs; + + /// Paragraph(s) of the "warning" command(s) + llvm::SmallVector<const comments::ParagraphComment *> WarningParagraphs; + + /// Parsed paragaph(s) of the "param" comamnd(s) + llvm::SmallDenseMap<StringRef, const comments::ParamCommandComment *> + Parameters; + + /// All the paragraphs we don't have any special handling for, + /// e.g. "details". + llvm::SmallDenseMap<unsigned, const comments::BlockCommandComment *> + UnhandledCommands; + + /// All free text paragraphs. + llvm::SmallDenseMap<unsigned, const comments::ParagraphComment *> + FreeParagraphs; +}; + +void fullCommentToMarkupDocument( + markup::Document &Doc, const comments::FullComment *FC, + const comments::CommandTraits &Traits, + std::optional<SymbolPrintedType> SymbolType, + std::optional<SymbolPrintedType> SymbolReturnType, + const std::optional<std::vector<SymbolParam>> &SymbolParameters) { + if (!FC) + return; + FullCommentToMarkupDocument(*FC, Traits, Doc, SymbolType, SymbolReturnType, + SymbolParameters); +} + +class ParamDocStringVisitor + : public comments::ConstCommentVisitor<ParamDocStringVisitor> { +public: + ParamDocStringVisitor(const comments::FullComment *FC, StringRef ParamName, + const comments::CommandTraits &Traits) + : ParamName(ParamName), Traits(Traits) { + if (!FC) + return; + + for (const auto *Block : FC->getBlocks()) { + visit(Block); + if (ParamCommentFound) + break; + } + } + + void visitParamCommandComment(const comments::ParamCommandComment *P) { + if (P->getParamNameAsWritten() == ParamName) { + ParamCommentFound = true; + visit(P->getParagraph()); + } + } + + void visitParagraphComment(const comments::ParagraphComment *C) { + if (!ParamCommentFound) + return; + for (const auto *Child = C->child_begin(); Child != C->child_end(); + ++Child) { + visit(*Child); + } + } + + void visitTextComment(const comments::TextComment *C) { + if (!ParamCommentFound) + return; + if (Result.empty()) + // When hovering over the parameter, the documentation is parsed the same + // way as any other comment. This means that the brief command is shown + // more prominently as the paragraph in the hover. + // Hence we add the brief command to the beginning of the documentation to + // reuse the same behaviour. + Result = "\\brief"; + + Result += " " + C->getText().trim().str(); + } + + void visitInlineCommandComment(const comments::InlineCommandComment *C) { + if (!ParamCommentFound) + return; + + if (Result.empty()) + // When hovering over the parameter, the documentation is parsed the same + // way as any other comment. This means that the brief command is shown + // more prominently as the paragraph in the hover. + // Hence we add the brief command to the beginning of the documentation to + // reuse the same behaviour. + Result = "\\brief"; + + Result += " "; + Result += C->getCommandMarker() == (comments::CommandMarkerKind::CMK_At) + ? "@" + : "\\" + C->getCommandName(Traits).str(); + + if (C->getNumArgs() > 0) { + for (unsigned I = 0; I < C->getNumArgs(); ++I) { + Result += " " + C->getArgText(I).str(); + } + } + } + + StringRef getParamDocString() const { return Result; } + +private: + StringRef ParamName; + std::string Result; + bool ParamCommentFound = false; + const comments::CommandTraits &Traits; +}; + +std::string getParamDocString(const comments::FullComment *FC, + StringRef ParamName, + const comments::CommandTraits &Traits) { + if (!FC) + return ""; + ParamDocStringVisitor PDSV(FC, ParamName, Traits); + return PDSV.getParamDocString().str(); +} + +llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, + const SymbolPrintedType &T) { + OS << T.Type; + if (T.AKA) + OS << " (aka " << *T.AKA << ")"; + return OS; +} + +llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const SymbolParam &P) { + if (P.Type) + OS << P.Type->Type; + if (P.Name) + OS << " " << *P.Name; + if (P.Default) + OS << " = " << *P.Default; + if (P.Type && P.Type->AKA) + OS << " (aka " << *P.Type->AKA << ")"; + return OS; +} + +SymbolPrintedType printType(QualType QT, ASTContext &ASTCtx, + const PrintingPolicy &PP) { + // TypePrinter doesn't resolve decltypes, so resolve them here. + // FIXME: This doesn't handle composite types that contain a decltype in them. + // We should rather have a printing policy for that. + while (!QT.isNull() && QT->isDecltypeType()) + QT = QT->castAs<DecltypeType>()->getUnderlyingType(); + SymbolPrintedType Result; + llvm::raw_string_ostream OS(Result.Type); + // Special case: if the outer type is a tag type without qualifiers, then + // include the tag for extra clarity. + // This isn't very idiomatic, so don't attempt it for complex cases, including + // pointers/references, template specializations, etc. + if (!QT.isNull() && !QT.hasQualifiers() && PP.SuppressTagKeyword) { + if (auto *TT = llvm::dyn_cast<TagType>(QT.getTypePtr())) + OS << TT->getDecl()->getKindName() << " "; + } + QT.print(OS, PP); + + const Config &Cfg = Config::current(); + if (!QT.isNull() && Cfg.Hover.ShowAKA) { + bool ShouldAKA = false; + QualType DesugaredTy = clang::desugarForDiagnostic(ASTCtx, QT, ShouldAKA); + if (ShouldAKA) + Result.AKA = DesugaredTy.getAsString(PP); + } + return Result; +} + +SymbolPrintedType printType(const TemplateTypeParmDecl *TTP) { + SymbolPrintedType Result; + Result.Type = TTP->wasDeclaredWithTypename() ? "typename" : "class"; + if (TTP->isParameterPack()) + Result.Type += "..."; + return Result; +} + +SymbolPrintedType printType(const NonTypeTemplateParmDecl *NTTP, + const PrintingPolicy &PP) { + auto PrintedType = printType(NTTP->getType(), NTTP->getASTContext(), PP); + if (NTTP->isParameterPack()) { + PrintedType.Type += "..."; + if (PrintedType.AKA) + *PrintedType.AKA += "..."; + } + return PrintedType; +} + +SymbolPrintedType printType(const TemplateTemplateParmDecl *TTP, + const PrintingPolicy &PP) { + SymbolPrintedType Result; + llvm::raw_string_ostream OS(Result.Type); + OS << "template <"; + llvm::StringRef Sep = ""; + for (const Decl *Param : *TTP->getTemplateParameters()) { + OS << Sep; + Sep = ", "; + if (const auto *TTP = dyn_cast<TemplateTypeParmDecl>(Param)) + OS << printType(TTP).Type; + else if (const auto *NTTP = dyn_cast<NonTypeTemplateParmDecl>(Param)) + OS << printType(NTTP, PP).Type; + else if (const auto *TTPD = dyn_cast<TemplateTemplateParmDecl>(Param)) + OS << printType(TTPD, PP).Type; + } + // FIXME: TemplateTemplateParameter doesn't store the info on whether this + // param was a "typename" or "class". + OS << "> class"; + return Result; +} + +// Default argument might exist but be unavailable, in the case of unparsed +// arguments for example. This function returns the default argument if it is +// available. +const Expr *getDefaultArg(const ParmVarDecl *PVD) { + // Default argument can be unparsed or uninstantiated. For the former we + // can't do much, as token information is only stored in Sema and not + // attached to the AST node. For the latter though, it is safe to proceed as + // the expression is still valid. + if (!PVD->hasDefaultArg() || PVD->hasUnparsedDefaultArg()) + return nullptr; + return PVD->hasUninstantiatedDefaultArg() ? PVD->getUninstantiatedDefaultArg() + : PVD->getDefaultArg(); +} + +SymbolParam createSymbolParam(const ParmVarDecl *PVD, + const PrintingPolicy &PP) { + SymbolParam Out; + Out.Type = printType(PVD->getType(), PVD->getASTContext(), PP); + if (!PVD->getName().empty()) + Out.Name = PVD->getNameAsString(); + if (const Expr *DefArg = getDefaultArg(PVD)) { + Out.Default.emplace(); + llvm::raw_string_ostream OS(*Out.Default); + DefArg->printPretty(OS, nullptr, PP); + } + return Out; +} + +} // 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..7da2fffb51e05 --- /dev/null +++ b/clang-tools-extra/clangd/SymbolDocumentation.h @@ -0,0 +1,92 @@ +//===--- 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/Decl.h" +#include "clang/AST/DeclTemplate.h" +#include "llvm/Support/raw_ostream.h" +#include <optional> +#include <string> +#include <tuple> + +namespace clang { +namespace clangd { + +/// Contains pretty-printed type and desugared type +struct SymbolPrintedType { + SymbolPrintedType() = default; + SymbolPrintedType(const char *Type) : Type(Type) {} + SymbolPrintedType(const char *Type, const char *AKAType) + : Type(Type), AKA(AKAType) {} + + /// Pretty-printed type + std::string Type; + /// Desugared type + std::optional<std::string> AKA; +}; + +/// Represents parameters of a function, a template or a macro. +/// For example: +/// - void foo(ParamType Name = DefaultValue) +/// - #define FOO(Name) +/// - template <ParamType Name = DefaultType> class Foo {}; +struct SymbolParam { + /// The printable parameter type, e.g. "int", or "typename" (in + /// TemplateParameters), might be std::nullopt for macro parameters. + std::optional<SymbolPrintedType> Type; + /// std::nullopt for unnamed parameters. + std::optional<std::string> Name; + /// std::nullopt if no default is provided. + std::optional<std::string> Default; +}; + +llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, + const SymbolPrintedType &T); +inline bool operator==(const SymbolPrintedType &LHS, + const SymbolPrintedType &RHS) { + return std::tie(LHS.Type, LHS.AKA) == std::tie(RHS.Type, RHS.AKA); +} + +llvm::raw_ostream &operator<<(llvm::raw_ostream &, const SymbolParam &); +inline bool operator==(const SymbolParam &LHS, const SymbolParam &RHS) { + return std::tie(LHS.Type, LHS.Name, LHS.Default) == + std::tie(RHS.Type, RHS.Name, RHS.Default); +} + +SymbolPrintedType printType(const TemplateTemplateParmDecl *TTP, + const PrintingPolicy &PP); +SymbolPrintedType printType(const NonTypeTemplateParmDecl *NTTP, + const PrintingPolicy &PP); +SymbolPrintedType printType(QualType QT, ASTContext &ASTCtx, + const PrintingPolicy &PP); + +SymbolParam createSymbolParam(const ParmVarDecl *PVD, const PrintingPolicy &PP); + +void fullCommentToMarkupDocument( + markup::Document &Doc, const comments::FullComment *FC, + const comments::CommandTraits &Traits, + std::optional<SymbolPrintedType> SymbolType, + std::optional<SymbolPrintedType> SymbolReturnType, + const std::optional<std::vector<SymbolParam>> &SymbolParameters); + +std::string getParamDocString(const comments::FullComment *FC, + StringRef ParamName, + const comments::CommandTraits &Traits); + +} // 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 63aff96b02056..d025f078c6d8d 100644 --- a/clang-tools-extra/clangd/support/Markup.cpp +++ b/clang-tools-extra/clangd/support/Markup.cpp @@ -354,6 +354,12 @@ void Paragraph::renderMarkdown(llvm::raw_ostream &OS) const { case Chunk::InlineCode: OS << renderInlineBlock(C.Contents); break; + case Chunk::Bold: + OS << "**" << renderText(C.Contents, !HasChunks) << "**"; + break; + case Chunk::Emphasized: + OS << "*" << renderText(C.Contents, !HasChunks) << "*"; + break; } HasChunks = true; NeedsSpace = C.SpaceAfter; @@ -387,6 +393,10 @@ void Paragraph::renderPlainText(llvm::raw_ostream &OS) const { llvm::StringRef Marker = ""; if (C.Preserve && C.Kind == Chunk::InlineCode) Marker = chooseMarker({"`", "'", "\""}, C.Contents); + else if (C.Kind == Chunk::Bold) + Marker = "**"; + else if (C.Kind == Chunk::Emphasized) + Marker = "*"; OS << Marker << C.Contents << Marker; NeedsSpace = C.SpaceAfter; } @@ -433,6 +443,32 @@ Paragraph &Paragraph::appendText(llvm::StringRef Text) { return *this; } +Paragraph &Paragraph::appendEmphasizedText(llvm::StringRef Text) { + std::string Norm = canonicalizeSpaces(Text); + if (Norm.empty()) + return *this; + Chunks.emplace_back(); + Chunk &C = Chunks.back(); + C.Contents = std::move(Norm); + C.Kind = Chunk::Emphasized; + C.SpaceBefore = llvm::isSpace(Text.front()); + C.SpaceAfter = llvm::isSpace(Text.back()); + return *this; +} + +Paragraph &Paragraph::appendBoldText(llvm::StringRef Text) { + std::string Norm = canonicalizeSpaces(Text); + if (Norm.empty()) + return *this; + Chunks.emplace_back(); + Chunk &C = Chunks.back(); + C.Contents = std::move(Norm); + C.Kind = Chunk::Bold; + C.SpaceBefore = llvm::isSpace(Text.front()); + C.SpaceAfter = llvm::isSpace(Text.back()); + return *this; +} + Paragraph &Paragraph::appendCode(llvm::StringRef Code, bool Preserve) { bool AdjacentCode = !Chunks.empty() && Chunks.back().Kind == Chunk::InlineCode; diff --git a/clang-tools-extra/clangd/support/Markup.h b/clang-tools-extra/clangd/support/Markup.h index 3a869c49a2cbb..de327b459f957 100644 --- a/clang-tools-extra/clangd/support/Markup.h +++ b/clang-tools-extra/clangd/support/Markup.h @@ -49,6 +49,12 @@ class Paragraph : public Block { /// Append plain text to the end of the string. Paragraph &appendText(llvm::StringRef Text); + /// Append emphasized text, this translates to the * block in markdown. + Paragraph &appendEmphasizedText(llvm::StringRef Text); + + /// Append bold text, this translates to the ** block in markdown. + Paragraph &appendBoldText(llvm::StringRef Text); + /// Append inline code, this translates to the ` block in markdown. /// \p Preserve indicates the code span must be apparent even in plaintext. Paragraph &appendCode(llvm::StringRef Code, bool Preserve = false); @@ -62,6 +68,8 @@ class Paragraph : public Block { enum { PlainText, InlineCode, + Bold, + Emphasized } Kind = PlainText; // Preserve chunk markers in plaintext. bool Preserve = false; diff --git a/clang-tools-extra/clangd/unittests/CodeCompletionStringsTests.cpp b/clang-tools-extra/clangd/unittests/CodeCompletionStringsTests.cpp index de5f533d31645..81a14e069bd82 100644 --- a/clang-tools-extra/clangd/unittests/CodeCompletionStringsTests.cpp +++ b/clang-tools-extra/clangd/unittests/CodeCompletionStringsTests.cpp @@ -11,6 +11,7 @@ #include "clang/Sema/CodeCompleteConsumer.h" #include "gmock/gmock.h" #include "gtest/gtest.h" +#include <optional> namespace clang { namespace clangd { @@ -236,6 +237,224 @@ TEST_F(CompletionStringTest, ObjectiveCMethodTwoArgumentsFromMiddle) { EXPECT_EQ(Snippet, "${1:(type2)}"); } +TEST(CompletionString, ParseDocumentation) { + + llvm::BumpPtrAllocator Allocator; + CommentOptions CommentOpts; + comments::CommandTraits Traits(Allocator, CommentOpts); + + struct Case { + llvm::StringRef Documentation; + std::optional<SymbolPrintedType> SymbolType; + std::optional<SymbolPrintedType> SymbolReturnType; + std::optional<std::vector<SymbolParam>> SymbolParameters; + llvm::StringRef ExpectedRenderMarkdown; + llvm::StringRef ExpectedRenderPlainText; + } Cases[] = { + { + "foo bar", + std::nullopt, + std::nullopt, + std::nullopt, + "foo bar", + "foo bar", + }, + { + "foo\nbar\n", + std::nullopt, + std::nullopt, + std::nullopt, + "foo bar", + "foo bar", + }, + { + "foo\n\nbar\n", + std::nullopt, + std::nullopt, + std::nullopt, + "foo \nbar", + "foo\nbar", + }, + { + "foo \\p bar baz", + std::nullopt, + std::nullopt, + std::nullopt, + "foo `bar` baz", + "foo bar baz", + }, + { + "foo \\e bar baz", + std::nullopt, + std::nullopt, + std::nullopt, + "foo *bar* baz", + "foo *bar* baz", + }, + { + "foo \\b bar baz", + std::nullopt, + std::nullopt, + std::nullopt, + "foo **bar** baz", + "foo **bar** baz", + }, + { + "foo \\ref bar baz", + std::nullopt, + std::nullopt, + std::nullopt, + "foo **\\\\ref** *bar* baz", + "foo **\\ref** *bar* baz", + }, + { + "foo @ref bar baz", + std::nullopt, + std::nullopt, + std::nullopt, + "foo **@ref** *bar* baz", + "foo **@ref** *bar* baz", + }, + { + "\\throw exception foo", + std::nullopt, + std::nullopt, + std::nullopt, + "**\\\\throw** *exception* foo", + "**\\throw** *exception* foo", + }, + {"@throws exception foo\n\n@note bar\n\n@warning baz\n\n@details " + "qux\n\nfree standing paragraph", + std::nullopt, std::nullopt, std::nullopt, + "Warning: \n- baz\n\n---\n" + "Note: \n- bar\n\n---\n" + "**@throws** *exception* foo \n" + "**@details** qux \n" + "free standing paragraph", + "Warning:\n- baz\n\n" + "Note:\n- bar\n\n" + "**@throws** *exception* foo\n" + "**@details** qux\n" + "free standing paragraph"}, + {"@throws exception foo\n\n@note bar\n\n@warning baz\n\n@note another " + "note\n\n@warning another warning\n\n@details qux\n\nfree standing " + "paragraph", + std::nullopt, std::nullopt, std::nullopt, + "Warnings: \n- baz\n- another warning\n\n---\n" + "Notes: \n- bar\n- another note\n\n---\n" + "**@throws** *exception* foo \n" + "**@details** qux \n" + "free standing paragraph", + "Warnings:\n- baz\n- another warning\n\n" + "Notes:\n- bar\n- another note\n\n" + "**@throws** *exception* foo\n" + "**@details** qux\n" + "free standing paragraph"}, + { + "", + SymbolPrintedType("my_type", "type"), + std::nullopt, + std::nullopt, + "Type: `my_type (aka type)`", + "Type: my_type (aka type)", + }, + { + "", + SymbolPrintedType("my_type", "type"), + SymbolPrintedType("my_ret_type", "type"), + std::nullopt, + "→ `my_ret_type (aka type)`", + "→ my_ret_type (aka type)", + }, + { + "\\return foo", + SymbolPrintedType("my_type", "type"), + SymbolPrintedType("my_ret_type", "type"), + std::nullopt, + "→ `my_ret_type (aka type)`: foo", + "→ my_ret_type (aka type): foo", + }, + { + "\\returns foo", + SymbolPrintedType("my_type", "type"), + SymbolPrintedType("my_ret_type", "type"), + std::nullopt, + "→ `my_ret_type (aka type)`: foo", + "→ my_ret_type (aka type): foo", + }, + { + "", + std::nullopt, + std::nullopt, + std::vector<SymbolParam>{ + {SymbolPrintedType("my_type", "type"), "foo", "default"}}, + "Parameters: \n- `my_type foo = default (aka type)`", + "Parameters:\n- my_type foo = default (aka type)", + }, + { + "\\param foo bar", + std::nullopt, + std::nullopt, + std::vector<SymbolParam>{ + {SymbolPrintedType("my_type", "type"), "foo", "default"}}, + "Parameters: \n- `my_type foo = default (aka type)`: bar", + "Parameters:\n- my_type foo = default (aka type): bar", + }, + { + "\\param foo bar\n\n\\param baz qux", + std::nullopt, + std::nullopt, + std::vector<SymbolParam>{ + {SymbolPrintedType("my_type", "type"), "foo", "default"}}, + "Parameters: \n- `my_type foo = default (aka type)`: bar", + "Parameters:\n- my_type foo = default (aka type): bar", + }, + { + "\\brief This is a brief description\n\nlonger description " + "paragraph\n\n\\note foo\n\n\\warning warning\n\njust another " + "paragraph\\param foo bar\\return baz", + SymbolPrintedType("my_type", "type"), + SymbolPrintedType("my_ret_type", "type"), + std::vector<SymbolParam>{ + {SymbolPrintedType("my_type", "type"), "foo", "default"}}, + "This is a brief description \n\n" + "---\n" + "→ `my_ret_type (aka type)`: baz \n" + "Parameters: \n" + "- `my_type foo = default (aka type)`: bar\n\n" + "Warning: \n" + "- warning\n\n" + "---\n" + "Note: \n" + "- foo\n\n" + "---\n" + "longer description paragraph \n" + "just another paragraph", + + R"(This is a brief description + +→ my_ret_type (aka type): baz +Parameters: +- my_type foo = default (aka type): bar +Warning: +- warning + +Note: +- foo + +longer description paragraph +just another paragraph)", + }}; + + for (const auto &C : Cases) { + markup::Document Doc; + docCommentToMarkup(Doc, C.Documentation, Allocator, Traits, C.SymbolType, + C.SymbolReturnType, C.SymbolParameters); + EXPECT_EQ(Doc.asPlainText(), C.ExpectedRenderPlainText); + EXPECT_EQ(Doc.asMarkdown(), C.ExpectedRenderMarkdown); + } +} + } // namespace } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/unittests/HoverTests.cpp b/clang-tools-extra/clangd/unittests/HoverTests.cpp index 69f6df46c87ce..fdb3bfa0797b2 100644 --- a/clang-tools-extra/clangd/unittests/HoverTests.cpp +++ b/clang-tools-extra/clangd/unittests/HoverTests.cpp @@ -10,6 +10,7 @@ #include "Annotations.h" #include "Config.h" #include "Hover.h" +#include "SymbolDocumentation.h" #include "TestFS.h" #include "TestIndex.h" #include "TestTU.h" @@ -639,7 +640,7 @@ class Foo final {})cpp"; [](HoverInfo &HI) { HI.Name = "DECL_STR"; HI.Kind = index::SymbolKind::Macro; - HI.Type = HoverInfo::PrintedType("const char *"); + HI.Type = SymbolPrintedType("const char *"); HI.Definition = "#define DECL_STR(NAME, VALUE) const char *v_##NAME = " "STRINGIFY(VALUE)\n\n" "// Expands to\n" @@ -1848,7 +1849,7 @@ TEST(Hover, All) { HI.Type = "int ()"; HI.Definition = "int x()"; HI.ReturnType = "int"; - HI.Parameters = std::vector<HoverInfo::Param>{}; + HI.Parameters = std::vector<SymbolParam>{}; }}, { R"cpp(// Static method call @@ -1865,7 +1866,7 @@ TEST(Hover, All) { HI.Type = "int ()"; HI.Definition = "static int x()"; HI.ReturnType = "int"; - HI.Parameters = std::vector<HoverInfo::Param>{}; + HI.Parameters = std::vector<SymbolParam>{}; }}, { R"cpp(// Typedef @@ -1944,7 +1945,7 @@ TEST(Hover, All) { HI.Definition = "void foo()"; HI.Documentation = ""; HI.ReturnType = "void"; - HI.Parameters = std::vector<HoverInfo::Param>{}; + HI.Parameters = std::vector<SymbolParam>{}; }}, { R"cpp( // using declaration and two possible function declarations @@ -2032,7 +2033,7 @@ TEST(Hover, All) { HI.Definition = "void foo()"; HI.Documentation = "Function declaration"; HI.ReturnType = "void"; - HI.Parameters = std::vector<HoverInfo::Param>{}; + HI.Parameters = std::vector<SymbolParam>{}; }}, { R"cpp(// Enum declaration @@ -2173,7 +2174,7 @@ TEST(Hover, All) { HI.Definition = "template <> int foo<int>()"; HI.Documentation = "Templated function"; HI.ReturnType = "int"; - HI.Parameters = std::vector<HoverInfo::Param>{}; + HI.Parameters = std::vector<SymbolParam>{}; // FIXME: We should populate template parameters with arguments in // case of instantiations. }}, @@ -2207,7 +2208,7 @@ TEST(Hover, All) { HI.Type = "void ()"; HI.Definition = "void indexSymbol()"; HI.ReturnType = "void"; - HI.Parameters = std::vector<HoverInfo::Param>{}; + HI.Parameters = std::vector<SymbolParam>{}; HI.Documentation = "comment from index"; }}, { @@ -3440,8 +3441,8 @@ TEST(Hover, Present) { }, R"(class foo -Size: 10 bytes documentation +Size: 10 bytes template <typename T, typename C = bool> class Foo {})", }, @@ -3452,7 +3453,7 @@ template <typename T, typename C = bool> class Foo {})", HI.Type = {"type", "c_type"}; HI.ReturnType = {"ret_type", "can_ret_type"}; HI.Parameters.emplace(); - HoverInfo::Param P; + SymbolParam P; HI.Parameters->push_back(P); P.Type = {"type", "can_type"}; HI.Parameters->push_back(P); @@ -3955,7 +3956,7 @@ TEST(Hover, DisableShowAKA) { auto H = getHover(AST, T.point(), format::getLLVMStyle(), nullptr); ASSERT_TRUE(H); - EXPECT_EQ(H->Type, HoverInfo::PrintedType("m_int")); + EXPECT_EQ(H->Type, SymbolPrintedType("m_int")); } TEST(Hover, HideBigInitializers) { @@ -4093,7 +4094,7 @@ constexpr u64 pow_with_mod(u64 a, u64 b, u64 p) { /*Validator=*/ [](std::optional<HoverInfo> HI, size_t) { EXPECT_EQ(HI->Value, "42 (0x2a)"); - EXPECT_EQ(HI->Type, HoverInfo::PrintedType("int")); + EXPECT_EQ(HI->Type, SymbolPrintedType("int")); }, }, { @@ -4184,7 +4185,7 @@ constexpr u64 pow_with_mod(u64 a, u64 b, u64 p) { [](std::optional<HoverInfo> HI, size_t Id) { switch (Id) { case 0: - EXPECT_EQ(HI->Type, HoverInfo::PrintedType("int[10]")); + EXPECT_EQ(HI->Type, SymbolPrintedType("int[10]")); break; case 1: case 2: @@ -4210,11 +4211,11 @@ constexpr u64 pow_with_mod(u64 a, u64 b, u64 p) { switch (Id) { case 0: EXPECT_FALSE(HI->Value); - EXPECT_EQ(HI->Type, HoverInfo::PrintedType("const (lambda)")); + EXPECT_EQ(HI->Type, SymbolPrintedType("const (lambda)")); break; case 1: EXPECT_EQ(HI->Value, "0"); - EXPECT_EQ(HI->Type, HoverInfo::PrintedType("u64")); + EXPECT_EQ(HI->Type, SymbolPrintedType("u64")); break; case 2: EXPECT_FALSE(HI); @@ -4264,6 +4265,104 @@ 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; + } 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 = ""; + }}, + {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 = "\\brief this is doc for a"; + }}, + {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 = ""; + }}, + {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 = "\\brief this is doc for \\p b"; + }}, + }; + + // 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; + 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); + + SCOPED_TRACE(H->present().asPlainText()); + 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/support/MarkupTests.cpp b/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp index 2d86c91c7ec08..c52d0996893a3 100644 --- a/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp +++ b/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp @@ -210,6 +210,36 @@ TEST(Paragraph, NewLines) { EXPECT_EQ(P.asPlainText(), "foo bar foo bar"); } +TEST(Paragraph, BoldText) { + Paragraph P; + P.appendBoldText(""); + EXPECT_EQ(P.asMarkdown(), ""); + EXPECT_EQ(P.asPlainText(), ""); + + P.appendBoldText(" \n foo\nbar\n "); + EXPECT_EQ(P.asMarkdown(), "**foo bar**"); + EXPECT_EQ(P.asPlainText(), "**foo bar**"); + + P.appendSpace().appendBoldText("foobar"); + EXPECT_EQ(P.asMarkdown(), "**foo bar** **foobar**"); + EXPECT_EQ(P.asPlainText(), "**foo bar** **foobar**"); +} + +TEST(Paragraph, EmphasizedText) { + Paragraph P; + P.appendEmphasizedText(""); + EXPECT_EQ(P.asMarkdown(), ""); + EXPECT_EQ(P.asPlainText(), ""); + + P.appendEmphasizedText(" \n foo\nbar\n "); + EXPECT_EQ(P.asMarkdown(), "*foo bar*"); + EXPECT_EQ(P.asPlainText(), "*foo bar*"); + + P.appendSpace().appendEmphasizedText("foobar"); + EXPECT_EQ(P.asMarkdown(), "*foo bar* *foobar*"); + EXPECT_EQ(P.asPlainText(), "*foo bar* *foobar*"); +} + TEST(Document, Separators) { Document D; D.addParagraph().appendText("foo"); diff --git a/clang/include/clang/AST/Comment.h b/clang/include/clang/AST/Comment.h index dd9906727293f..8a0216bf51a30 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,17 @@ 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 +401,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 12ed8e3f1b79a..a3c22f81eb590 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,7 @@ 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 +582,8 @@ 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 bd2206bb8a3bc..ca3813b8c891d 100644 --- a/clang/lib/AST/CommentSema.cpp +++ b/clang/lib/AST/CommentSema.cpp @@ -360,12 +360,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); + 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", _______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits