diff --git a/clang-tools-extra/clangd/CMakeLists.txt b/clang-tools-extra/clangd/CMakeLists.txt --- a/clang-tools-extra/clangd/CMakeLists.txt +++ b/clang-tools-extra/clangd/CMakeLists.txt @@ -97,6 +97,7 @@ SemanticHighlighting.cpp SemanticSelection.cpp SourceCode.cpp + SymbolDocumentation.cpp QueryDriverDatabase.cpp TidyProvider.cpp TUScheduler.cpp diff --git a/clang-tools-extra/clangd/CodeComplete.cpp b/clang-tools-extra/clangd/CodeComplete.cpp --- a/clang-tools-extra/clangd/CodeComplete.cpp +++ b/clang-tools-extra/clangd/CodeComplete.cpp @@ -424,7 +424,7 @@ } else if (C.SemaResult) { const auto DocComment = getDocComment(*ASTCtx, *C.SemaResult, /*CommentsFromHeaders=*/false); - SetDoc(formatDocumentation(*SemaCCS, DocComment)); + SetDoc(formatDocumentation(*SemaCCS, DocComment.CommentText)); } } if (Completion.Deprecated) { @@ -978,7 +978,7 @@ Candidate, *CCS, Candidate.getFunction() ? getDeclComment(S.getASTContext(), *Candidate.getFunction()) - : "")); + : SymbolDocumentation{})); } // Sema does not load the docs from the preamble, so we need to fetch extra @@ -1100,15 +1100,17 @@ // FIXME(ioeric): consider moving CodeCompletionString logic here to // CompletionString.h. - ScoredSignature processOverloadCandidate(const OverloadCandidate &Candidate, - const CodeCompletionString &CCS, - llvm::StringRef DocComment) const { + ScoredSignature + processOverloadCandidate(const OverloadCandidate &Candidate, + const CodeCompletionString &CCS, + const SymbolDocumentation &DocComment) const { SignatureInformation Signature; SignatureQualitySignals Signal; const char *ReturnType = nullptr; markup::Document OverloadComment; - parseDocumentation(formatDocumentation(CCS, DocComment), OverloadComment); + parseDocumentation(formatDocumentation(CCS, DocComment.CommentText), + OverloadComment); Signature.documentation = renderDoc(OverloadComment, DocumentationFormat); Signal.Kind = Candidate.getKind(); diff --git a/clang-tools-extra/clangd/CodeCompletionStrings.h b/clang-tools-extra/clangd/CodeCompletionStrings.h --- a/clang-tools-extra/clangd/CodeCompletionStrings.h +++ b/clang-tools-extra/clangd/CodeCompletionStrings.h @@ -14,13 +14,17 @@ #ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_CODECOMPLETIONSTRINGS_H #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_CODECOMPLETIONSTRINGS_H +#include "clang/AST/CommentVisitor.h" #include "clang/Sema/CodeCompleteConsumer.h" +#include "SymbolDocumentation.h" + namespace clang { class ASTContext; namespace clangd { +/// TODO Update docs /// Gets a minimally formatted documentation comment of \p Result, with comment /// markers stripped. See clang::RawComment::getFormattedText() for the detailed /// explanation of how the comment text is transformed. @@ -28,12 +32,11 @@ /// If \p CommentsFromHeaders parameter is set, only comments from the main /// file will be returned. It is used to workaround crashes when parsing /// comments in the stale headers, coming from completion preamble. -std::string getDocComment(const ASTContext &Ctx, - const CodeCompletionResult &Result, - bool CommentsFromHeaders); +SymbolDocumentation getDocComment(const ASTContext &Ctx, + const CodeCompletionResult &Result, + bool CommentsFromHeaders); -/// Similar to getDocComment, but returns the comment for a NamedDecl. -std::string getDeclComment(const ASTContext &Ctx, const NamedDecl &D); +SymbolDocumentation getDeclComment(const ASTContext &Ctx, const NamedDecl &D); /// Formats the signature for an item, as a display string and snippet. /// e.g. for const_reference std::vector::at(size_type) const, this returns: diff --git a/clang-tools-extra/clangd/CodeCompletionStrings.cpp b/clang-tools-extra/clangd/CodeCompletionStrings.cpp --- a/clang-tools-extra/clangd/CodeCompletionStrings.cpp +++ b/clang-tools-extra/clangd/CodeCompletionStrings.cpp @@ -58,39 +58,48 @@ } // namespace -std::string getDocComment(const ASTContext &Ctx, - const CodeCompletionResult &Result, - bool CommentsFromHeaders) { +SymbolDocumentation getDocComment(const ASTContext &Ctx, + const CodeCompletionResult &Result, + bool CommentsFromHeaders) { + // FIXME: CommentsFromHeaders seems to be unused? Is this a bug? + // FIXME: clang's completion also returns documentation for RK_Pattern if they // contain a pattern for ObjC properties. Unfortunately, there is no API to // get this declaration, so we don't show documentation in that case. if (Result.Kind != CodeCompletionResult::RK_Declaration) - return ""; + return {}; return Result.getDeclaration() ? getDeclComment(Ctx, *Result.getDeclaration()) - : ""; + : SymbolDocumentation{}; } -std::string getDeclComment(const ASTContext &Ctx, const NamedDecl &Decl) { +SymbolDocumentation getDeclComment(const ASTContext &Ctx, + const NamedDecl &Decl) { if (isa(Decl)) { // Namespaces often have too many redecls for any particular redecl comment // to be useful. Moreover, we often confuse file headers or generated // comments with namespace comments. Therefore we choose to just ignore // the comments for namespaces. - return ""; + return {}; } const RawComment *RC = getCompletionComment(Ctx, &Decl); if (!RC) - return ""; + 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 ""; + + // TODO Figure out where to get the Preprocessor from (3rd arg)...? + SymbolDocumentation Doc(*RC, Ctx, nullptr, &Decl); + + if (!looksLikeDocComment(Doc.CommentText)) + return {}; + + // TODO where should be put this check now? // Clang requires source to be UTF-8, but doesn't enforce this in comments. - if (!llvm::json::isUTF8(Doc)) - Doc = llvm::json::fixUTF8(Doc); + // + // if (!llvm::json::isUTF8(Doc)) + // Doc = llvm::json::fixUTF8(Doc); + return Doc; } diff --git a/clang-tools-extra/clangd/Hover.h b/clang-tools-extra/clangd/Hover.h --- a/clang-tools-extra/clangd/Hover.h +++ b/clang-tools-extra/clangd/Hover.h @@ -9,6 +9,7 @@ #ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_HOVER_H #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_HOVER_H +#include "CodeCompletionStrings.h" #include "ParsedAST.h" #include "Protocol.h" #include "support/Markup.h" @@ -68,7 +69,7 @@ std::string Name; llvm::Optional SymRange; index::SymbolKind Kind = index::SymbolKind::Unknown; - std::string Documentation; + SymbolDocumentation Documentation; /// Source code containing the definition of the symbol. std::string Definition; const char *DefinitionLanguage = "cpp"; diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp --- a/clang-tools-extra/clangd/Hover.cpp +++ b/clang-tools-extra/clangd/Hover.cpp @@ -332,7 +332,7 @@ LookupRequest Req; Req.IDs.insert(ID); Index->lookup(Req, [&](const Symbol &S) { - Hover.Documentation = std::string(S.Documentation); + Hover.Documentation.CommentText = std::string(S.Documentation); }); } @@ -1089,6 +1089,10 @@ // Put a linebreak after header to increase readability. Output.addRuler(); + + if (!Documentation.Brief.empty()) + Output.addParagraph().appendText(Documentation.Brief); + // Print Types on their own lines to reduce chances of getting line-wrapped by // editor, as they might be long. if (ReturnType) { @@ -1097,15 +1101,36 @@ // Parameters: // - `bool param1` // - `int param2 = 5` - Output.addParagraph().appendText("→ ").appendCode( - llvm::to_string(*ReturnType)); + Output.addParagraph() + .appendText("→ ") + .appendCode(llvm::to_string(*ReturnType)) + .appendText(": ") + .appendText(Documentation.Returns); } - 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)); + unsigned ParamIndex = 0; + for (const auto &Param : *Parameters) { + auto &Paragraph = L.addItem().addParagraph(); + Paragraph.appendCode(llvm::to_string(Param)); + + const auto Parameter = + Documentation.MatchingParameterDocs.find(ParamIndex); + if (Parameter != Documentation.MatchingParameterDocs.end()) { + Paragraph.appendText(": ").appendText(Parameter->second.Description); + } + + ++ParamIndex; + } + + for (const auto &ParamDoc : Documentation.UnmatchedParameterDocs) { + L.addItem() + .addParagraph() + .appendCode(ParamDoc.Name) + .appendText(": ") + .appendText(ParamDoc.Description); + } } // Don't print Type after Parameters or ReturnType as this will just duplicate @@ -1149,8 +1174,42 @@ Output.addParagraph().appendText(OS.str()); } - if (!Documentation.empty()) - parseDocumentation(Documentation, Output); + if (!Documentation.Description.empty()) + parseDocumentation(Documentation.Description, Output); + + if (!Documentation.Throws.empty()) { + Output.addRuler(); + Output.addParagraph().appendText("Throws: "); + markup::BulletList &L = Output.addBulletList(); + for (const auto &Throws : Documentation.Throws) + L.addItem() + .addParagraph() + .appendCode(Throws.Exception) + .appendText(": ") + .appendText(Throws.Description); + } + + if (!Documentation.Warnings.empty()) { + Output.addRuler(); + Output.addParagraph() + .appendText("Warning") + .appendText(Documentation.Warnings.size() > 1 ? "s" : "") + .appendText(": "); + markup::BulletList &L = Output.addBulletList(); + for (const auto &Warning : Documentation.Warnings) + L.addItem().addParagraph().appendText(Warning); + } + + if (!Documentation.Notes.empty()) { + Output.addRuler(); + Output.addParagraph() + .appendText("Note") + .appendText(Documentation.Notes.size() > 1 ? "s" : "") + .appendText(": "); + markup::BulletList &L = Output.addBulletList(); + for (const auto &Note : Documentation.Notes) + L.addItem().addParagraph().appendText(Note); + } if (!Definition.empty()) { Output.addRuler(); diff --git a/clang-tools-extra/clangd/SymbolDocumentation.h b/clang-tools-extra/clangd/SymbolDocumentation.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/SymbolDocumentation.h @@ -0,0 +1,71 @@ +//===--- 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 "clang/AST/ASTContext.h" +#include "clang/AST/Comment.h" +#include "clang/AST/CommentVisitor.h" + +namespace clang { +namespace clangd { + +struct ParameterDocumentation { + llvm::StringRef Name; + std::string Description; + llvm::Optional PassDirection; +}; + +struct ExceptionDocumentation { + llvm::StringRef Exception; + std::string Description; +}; + +struct SymbolDocumentation { + SymbolDocumentation(const RawComment &RC, const ASTContext &Ctx, + const Preprocessor *PP, const Decl *D); + + void visitFullComment(const comments::FullComment *FC); + void visitParamCommandComment(const comments::ParamCommandComment *P); + + SymbolDocumentation() = default; + SymbolDocumentation(const std::string &Doc) : CommentText(Doc) {} + + bool empty() const { return CommentText.empty(); } + + std::string Brief; + + std::string Returns; + + llvm::SmallVector Notes; + llvm::SmallVector Warnings; + + /// \\param commands that match a parameter in the function declaration + std::map MatchingParameterDocs; + + llvm::SmallVector UnmatchedParameterDocs; + + llvm::SmallVector Throws; + + /// All the paragraphs we don't have any special handling for, e.g. \details + std::string Description; + + /// The full text of the doxygen comment + std::string CommentText; +}; + +} // namespace clangd +} // namespace clang + +#endif // LLVM_CLANG_TOOLS_EXTRA_CLANGD_SYMBOLDOCUMENTATION_H diff --git a/clang-tools-extra/clangd/SymbolDocumentation.cpp b/clang-tools-extra/clangd/SymbolDocumentation.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/SymbolDocumentation.cpp @@ -0,0 +1,149 @@ +//===--- 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 "clang/AST/TextNodeDumper.h" +#include "llvm/Support/JSON.h" + +namespace clang { +namespace clangd { + +/// Converts a BlockCommentToString, +class BlockCommentToString + : public comments::ConstCommentVisitor { +public: + BlockCommentToString(std::string &Out, const ASTContext &Ctx) + : Out(Out), Ctx(Ctx) {} + + void visitParagraphComment(const comments::ParagraphComment *C) { + for (const auto *Child = C->child_begin(); Child != C->child_end(); + ++Child) { + visit(*Child); + } + Out << "\n"; + } + + void visitBlockCommandComment(const comments::BlockCommandComment *B) { + Out << (B->getCommandMarker() == (comments::CommandMarkerKind::CMK_At) + ? '@' + : '\\') + << B->getCommandName(Ctx.getCommentCommandTraits()); + + visit(B->getParagraph()); + } + + void visitTextComment(const comments::TextComment *C) { Out << C->getText(); } + + void visitInlineCommandComment(const comments::InlineCommandComment *C) { + const std::string SurroundWith = [C] { + switch (C->getRenderKind()) { + case comments::InlineCommandComment::RenderKind::RenderMonospaced: + return "`"; + case comments::InlineCommandComment::RenderKind::RenderBold: + return "**"; + case comments::InlineCommandComment::RenderKind::RenderEmphasized: + return "*"; + default: + return ""; + } + }(); + + Out << SurroundWith; + for (unsigned I = 0; I < C->getNumArgs(); ++I) { + Out << C->getArgText(I); + } + Out << SurroundWith; + } + +private: + llvm::raw_string_ostream Out; + const ASTContext &Ctx; +}; + +class CommentToSymbolDocumentation + : public comments::ConstCommentVisitor { +public: + CommentToSymbolDocumentation(const RawComment &RC, const ASTContext &Ctx, + const Preprocessor *PP, const Decl *D, + SymbolDocumentation &Doc) + : FullComment(RC.parse(Ctx, PP, D)), Output(Doc), Ctx(Ctx) { + + Output.CommentText = + RC.getFormattedText(Ctx.getSourceManager(), Ctx.getDiagnostics()); + + for (auto *Block : FullComment->getBlocks()) { + visit(Block); + } + } + + void visitBlockCommandComment(const comments::BlockCommandComment *B) { + const llvm::StringRef CommandName = + B->getCommandName(Ctx.getCommentCommandTraits()); + + // 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 (CommandName == "brief") + BlockCommentToString(Output.Brief, Ctx).visit(B->getParagraph()); + else if (CommandName == "return") + BlockCommentToString(Output.Returns, Ctx).visit(B->getParagraph()); + else if (CommandName == "throw" || CommandName == "throws" || + CommandName == "exception") { + ExceptionDocumentation Doc; + // TODO Looks like comments::Parser does not parse this correctly, + // getNumArgs() is always zero and the exception argument is contained in + // the Paragraph instead. + if (B->getNumArgs() > 0) + Doc.Exception = B->getArgText(0); + BlockCommentToString(Doc.Description, Ctx).visit(B->getParagraph()); + Output.Throws.emplace_back(std::move(Doc)); + } else if (CommandName == "warning") + BlockCommentToString(Output.Warnings.emplace_back(), Ctx) + .visit(B->getParagraph()); + else if (CommandName == "note") + BlockCommentToString(Output.Notes.emplace_back(), Ctx) + .visit(B->getParagraph()); + else + BlockCommentToString(Output.Description, Ctx).visit(B); + } + + void visitParagraphComment(const comments::ParagraphComment *P) { + BlockCommentToString(Output.Description, Ctx).visit(P); + } + + void visitParamCommandComment(const comments::ParamCommandComment *P) { + ParameterDocumentation ParamDoc; + ParamDoc.Name = P->getParamNameAsWritten(); + if (P->isDirectionExplicit()) + ParamDoc.PassDirection = P->getDirection(); + + BlockCommentToString(ParamDoc.Description, Ctx).visit(P->getParagraph()); + + if (P->isParamIndexValid() && !P->isVarArgParam()) { + Output.MatchingParameterDocs.insert( + {P->getParamIndex(), std::move(ParamDoc)}); + } else { + Output.UnmatchedParameterDocs.emplace_back(std::move(ParamDoc)); + } + } + +private: + comments::FullComment *FullComment; + SymbolDocumentation &Output; + const ASTContext &Ctx; +}; + +SymbolDocumentation::SymbolDocumentation(const RawComment &RC, + const ASTContext &Ctx, + const Preprocessor *PP, + const Decl *D) { + CommentToSymbolDocumentation(RC, Ctx, PP, D, *this); +} + +} // namespace clangd +} // namespace clang diff --git a/clang-tools-extra/clangd/index/SymbolCollector.cpp b/clang-tools-extra/clangd/index/SymbolCollector.cpp --- a/clang-tools-extra/clangd/index/SymbolCollector.cpp +++ b/clang-tools-extra/clangd/index/SymbolCollector.cpp @@ -914,7 +914,8 @@ /*IncludeBriefComments*/ false); std::string Documentation = formatDocumentation(*CCS, getDocComment(Ctx, SymbolCompletion, - /*CommentsFromHeaders=*/true)); + /*CommentsFromHeaders=*/true) + .CommentText); if (!(S.Flags & Symbol::IndexedForCodeCompletion)) { if (Opts.StoreAllDocumentation) S.Documentation = Documentation; diff --git a/llvm/utils/gn/secondary/clang-tools-extra/clangd/BUILD.gn b/llvm/utils/gn/secondary/clang-tools-extra/clangd/BUILD.gn --- a/llvm/utils/gn/secondary/clang-tools-extra/clangd/BUILD.gn +++ b/llvm/utils/gn/secondary/clang-tools-extra/clangd/BUILD.gn @@ -113,6 +113,7 @@ "SemanticHighlighting.cpp", "SemanticSelection.cpp", "SourceCode.cpp", + "SymbolDocumentation.cpp" "TUScheduler.cpp", "TidyProvider.cpp", "URI.cpp",