clang-tools 23.0.0git
SymbolDocumentation.cpp
Go to the documentation of this file.
1//===--- SymbolDocumentation.cpp ==-------------------------------*- C++-*-===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8
10
11#include "support/Markup.h"
12#include "clang/AST/Comment.h"
13#include "clang/AST/CommentCommandTraits.h"
14#include "clang/AST/CommentVisitor.h"
15#include "llvm/ADT/DenseMap.h"
16#include "llvm/ADT/StringExtras.h"
17#include "llvm/ADT/StringRef.h"
18
19namespace clang {
20namespace clangd {
21namespace {
22
23std::string commandMarkerAsString(comments::CommandMarkerKind CommandMarker) {
24 switch (CommandMarker) {
25 case comments::CommandMarkerKind::CMK_At:
26 return "@";
27 case comments::CommandMarkerKind::CMK_Backslash:
28 return "\\";
29 }
30 llvm_unreachable("Unknown command marker kind");
31}
32
33void commandToMarkup(markup::Paragraph &Out, StringRef Command,
34 comments::CommandMarkerKind CommandMarker,
35 StringRef Args) {
36 Out.appendBoldText(commandMarkerAsString(CommandMarker) + Command.str());
37 if (!Args.empty()) {
38 Out.appendSpace();
39 Out.appendCode(Args.str());
40 }
41}
42
43template <typename T> std::string getArgText(const T *Command) {
44 std::string ArgText;
45 for (unsigned I = 0; I < Command->getNumArgs(); ++I) {
46 if (!ArgText.empty())
47 ArgText += " ";
48 ArgText += Command->getArgText(I);
49 }
50 return ArgText;
51}
52
53} // namespace
54
56 : public comments::ConstCommentVisitor<ParagraphToMarkupDocument> {
57public:
59 const comments::CommandTraits &Traits)
60 : Out(Out), Traits(Traits) {}
61
62 void visitParagraphComment(const comments::ParagraphComment *C) {
63 if (!C)
64 return;
65
66 for (const auto *Child = C->child_begin(); Child != C->child_end();
67 ++Child) {
68 visit(*Child);
69 }
70 }
71
72 void visitTextComment(const comments::TextComment *C) {
73 // Always trim leading space after a newline.
74 StringRef Text = C->getText();
75 if (LastChunkEndsWithNewline && C->getText().starts_with(' '))
76 Text = Text.drop_front();
77
78 LastChunkEndsWithNewline = C->hasTrailingNewline();
79 Out.appendText(Text.str() + (LastChunkEndsWithNewline ? "\n" : ""));
80 }
81
82 void visitInlineCommandComment(const comments::InlineCommandComment *C) {
83
84 if (C->getNumArgs() > 0) {
85 std::string ArgText = getArgText(C);
86
87 switch (C->getRenderKind()) {
88 case comments::InlineCommandRenderKind::Monospaced:
89 Out.appendCode(ArgText);
90 break;
91 case comments::InlineCommandRenderKind::Bold:
92 Out.appendBoldText(ArgText);
93 break;
94 case comments::InlineCommandRenderKind::Emphasized:
95 Out.appendEmphasizedText(ArgText);
96 break;
97 default:
98 commandToMarkup(Out, C->getCommandName(Traits), C->getCommandMarker(),
99 ArgText);
100 break;
101 }
102 } else {
103 if (C->getCommandName(Traits) == "n") {
104 // \n is a special case, it is used to create a new line.
105 Out.appendText(" \n");
106 LastChunkEndsWithNewline = true;
107 return;
108 }
109
110 commandToMarkup(Out, C->getCommandName(Traits), C->getCommandMarker(),
111 "");
112 LastChunkEndsWithNewline = false;
113 }
114 }
115
116 void visitHTMLStartTagComment(const comments::HTMLStartTagComment *STC) {
117 std::string TagText = "<" + STC->getTagName().str();
118
119 for (unsigned I = 0; I < STC->getNumAttrs(); ++I) {
120 const comments::HTMLStartTagComment::Attribute &Attr = STC->getAttr(I);
121 TagText += " " + Attr.Name.str() + "=\"" + Attr.Value.str() + "\"";
122 }
123
124 if (STC->isSelfClosing())
125 TagText += " /";
126 TagText += ">";
127
128 LastChunkEndsWithNewline = STC->hasTrailingNewline();
129 Out.appendText(TagText + (LastChunkEndsWithNewline ? "\n" : ""));
130 }
131
132 void visitHTMLEndTagComment(const comments::HTMLEndTagComment *ETC) {
133 LastChunkEndsWithNewline = ETC->hasTrailingNewline();
134 Out.appendText("</" + ETC->getTagName().str() + ">" +
135 (LastChunkEndsWithNewline ? "\n" : ""));
136 }
137
138private:
140 const comments::CommandTraits &Traits;
141
142 /// If true, the next leading space after a new line is trimmed.
143 /// Initially set it to true, to always trim the first text line.
144 bool LastChunkEndsWithNewline = true;
145};
146
148 : public comments::ConstCommentVisitor<ParagraphToString> {
149public:
150 ParagraphToString(llvm::raw_string_ostream &Out,
151 const comments::CommandTraits &Traits)
152 : Out(Out), Traits(Traits) {}
153
154 void visitParagraphComment(const comments::ParagraphComment *C) {
155 if (!C)
156 return;
157
158 for (const auto *Child = C->child_begin(); Child != C->child_end();
159 ++Child) {
160 visit(*Child);
161 }
162 }
163
164 void visitTextComment(const comments::TextComment *C) {
165 Out << C->getText();
166 if (C->hasTrailingNewline())
167 Out << "\n";
168 }
169
170 void visitInlineCommandComment(const comments::InlineCommandComment *C) {
171 Out << commandMarkerAsString(C->getCommandMarker());
172 Out << C->getCommandName(Traits);
173 std::string ArgText = getArgText(C);
174 if (!ArgText.empty())
175 Out << " " << ArgText;
176 }
177
178 void visitHTMLStartTagComment(const comments::HTMLStartTagComment *STC) {
179 Out << "<" << STC->getTagName().str();
180
181 for (unsigned I = 0; I < STC->getNumAttrs(); ++I) {
182 const comments::HTMLStartTagComment::Attribute &Attr = STC->getAttr(I);
183 Out << " " << Attr.Name.str();
184 if (!Attr.Value.str().empty())
185 Out << "=\"" << Attr.Value.str() << "\"";
186 }
187
188 if (STC->isSelfClosing())
189 Out << " /";
190 Out << ">";
191
192 Out << (STC->hasTrailingNewline() ? "\n" : "");
193 }
194
195 void visitHTMLEndTagComment(const comments::HTMLEndTagComment *ETC) {
196 Out << "</" << ETC->getTagName().str() << ">"
197 << (ETC->hasTrailingNewline() ? "\n" : "");
198 }
199
200private:
201 llvm::raw_string_ostream &Out;
202 const comments::CommandTraits &Traits;
203};
204
206 : public comments::ConstCommentVisitor<BlockCommentToMarkupDocument> {
207public:
208 BlockCommentToMarkupDocument(markup::Document &Out,
209 const comments::CommandTraits &Traits)
210 : Out(Out), Traits(Traits) {}
211
212 void visitBlockCommandComment(const comments::BlockCommandComment *B) {
213
214 switch (B->getCommandID()) {
215 case comments::CommandTraits::KCI_arg:
216 case comments::CommandTraits::KCI_li:
217 // \li and \arg are special cases, they are used to create a list item.
218 // In markdown it is a bullet list.
219 ParagraphToMarkupDocument(Out.addBulletList().addItem().addParagraph(),
220 Traits)
221 .visit(B->getParagraph());
222 break;
223 case comments::CommandTraits::KCI_note:
224 case comments::CommandTraits::KCI_warning:
225 commandToHeadedParagraph(B);
226 break;
227 case comments::CommandTraits::KCI_retval: {
228 // The \retval command describes the return value given as its single
229 // argument in the corresponding paragraph.
230 // Note: We know that we have exactly one argument but not if it has an
231 // associated paragraph.
232 auto &P = Out.addParagraph().appendCode(getArgText(B));
233 if (B->getParagraph() && !B->getParagraph()->isWhitespace()) {
234 P.appendText(" - ");
235 ParagraphToMarkupDocument(P, Traits).visit(B->getParagraph());
236 }
237 return;
238 }
239 case comments::CommandTraits::KCI_details: {
240 // The \details command is just used to separate the brief from the
241 // detailed description. This separation is already done in the
242 // SymbolDocCommentVisitor. Therefore we can omit the command itself
243 // here and just process the paragraph.
244 if (B->getParagraph() && !B->getParagraph()->isWhitespace()) {
245 ParagraphToMarkupDocument(Out.addParagraph(), Traits)
246 .visit(B->getParagraph());
247 }
248 return;
249 }
250 default: {
251 // Some commands have arguments, like \throws.
252 // The arguments are not part of the paragraph.
253 // We need reconstruct them here.
254 std::string ArgText = getArgText(B);
255 auto &P = Out.addParagraph();
256 commandToMarkup(P, B->getCommandName(Traits), B->getCommandMarker(),
257 ArgText);
258 if (B->getParagraph() && !B->getParagraph()->isWhitespace()) {
259 P.appendSpace();
260 ParagraphToMarkupDocument(P, Traits).visit(B->getParagraph());
261 }
262 }
263 }
264 }
265
266 void visitCodeCommand(const comments::VerbatimBlockComment *VB) {
267 std::string CodeLang = "";
268 auto *FirstLine = VB->child_begin();
269 // The \\code command has an optional language argument.
270 // This argument is currently not parsed by the clang doxygen parser.
271 // Therefore we try to extract it from the first line of the verbatim
272 // block.
273 if (VB->getNumLines() > 0) {
274 if (const auto *Line =
275 cast<comments::VerbatimBlockLineComment>(*FirstLine)) {
276 llvm::StringRef Text = Line->getText();
277 // Language is a single word enclosed in {}.
278 if (llvm::none_of(Text, llvm::isSpace) && Text.consume_front("{") &&
279 Text.consume_back("}")) {
280 // drop a potential . since this is not supported in Markdown
281 // fenced code blocks.
282 Text.consume_front(".");
283 // Language is alphanumeric or '+'.
284 CodeLang = Text.take_while([](char C) {
285 return llvm::isAlnum(C) || C == '+';
286 })
287 .str();
288 // Skip the first line for the verbatim text.
289 ++FirstLine;
290 }
291 }
292 }
293
294 std::string CodeBlockText;
295
296 for (const auto *LI = FirstLine; LI != VB->child_end(); ++LI) {
297 if (const auto *Line = cast<comments::VerbatimBlockLineComment>(*LI)) {
298 CodeBlockText += Line->getText().str() + "\n";
299 }
300 }
301
302 Out.addCodeBlock(CodeBlockText, CodeLang);
303 }
304
305 void visitVerbatimBlockComment(const comments::VerbatimBlockComment *VB) {
306 // The \\code command is a special verbatim block command which we handle
307 // separately.
308 if (VB->getCommandID() == comments::CommandTraits::KCI_code) {
310 return;
311 }
312
313 commandToMarkup(Out.addParagraph(), VB->getCommandName(Traits),
314 VB->getCommandMarker(), "");
315
316 std::string VerbatimText;
317
318 for (const auto *LI = VB->child_begin(); LI != VB->child_end(); ++LI) {
319 if (const auto *Line = cast<comments::VerbatimBlockLineComment>(*LI)) {
320 VerbatimText += Line->getText().str() + "\n";
321 }
322 }
323
324 Out.addCodeBlock(VerbatimText, "");
325
326 commandToMarkup(Out.addParagraph(), VB->getCloseName(),
327 VB->getCommandMarker(), "");
328 }
329
330 void visitVerbatimLineComment(const comments::VerbatimLineComment *VL) {
331 auto &P = Out.addParagraph();
332 commandToMarkup(P, VL->getCommandName(Traits), VL->getCommandMarker(), "");
333 P.appendSpace().appendCode(VL->getText().str(), true).appendSpace();
334 }
335
336private:
337 markup::Document &Out;
338 const comments::CommandTraits &Traits;
339 StringRef CommentEscapeMarker;
340
341 /// Emphasize the given command in a paragraph.
342 /// Uses the command name with the first letter capitalized as the heading.
343 void commandToHeadedParagraph(const comments::BlockCommandComment *B) {
344 auto &P = Out.addParagraph();
345 std::string Heading = B->getCommandName(Traits).slice(0, 1).upper() +
346 B->getCommandName(Traits).drop_front().str();
347 P.appendBoldText(Heading + ":");
348 P.appendText(" \n");
349 ParagraphToMarkupDocument(P, Traits).visit(B->getParagraph());
350 }
351};
352
354 enum State {
355 Normal,
356 FencedCodeblock,
357 } State = Normal;
358 std::string CodeFence;
359
360 llvm::raw_string_ostream OS(CommentWithMarkers);
361
362 // The documentation string is processed line by line.
363 // The raw documentation string does not contain the comment markers
364 // (e.g. /// or /** */).
365 // But the comment lexer expects doxygen markers, so add them back.
366 // We need to use the /// style doxygen markers because the comment could
367 // contain the closing tag "*/" of a C Style "/** */" comment
368 // which would break the parsing if we would just enclose the comment text
369 // with "/** */".
370
371 // Escape doxygen commands inside markdown inline code spans.
372 // This is required to not let the doxygen parser interpret them as
373 // commands.
374 // Note: This is a heuristic which may fail in some cases.
375 bool InCodeSpan = false;
376
377 llvm::StringRef Line, Rest;
378 for (std::tie(Line, Rest) = Doc.split('\n'); !(Line.empty() && Rest.empty());
379 std::tie(Line, Rest) = Rest.split('\n')) {
380
381 // Detect code fence (``` or ~~~)
382 if (State == Normal) {
383 llvm::StringRef Trimmed = Line.ltrim();
384 if (Trimmed.starts_with("```") || Trimmed.starts_with("~~~")) {
385 // https://www.doxygen.nl/manual/markdown.html#md_fenced
386 CodeFence =
387 Trimmed.take_while([](char C) { return C == '`' || C == '~'; })
388 .str();
389 // Try to detect language: first word after fence. Could also be
390 // enclosed in {}
391 llvm::StringRef AfterFence =
392 Trimmed.drop_front(CodeFence.size()).ltrim();
393 // ignore '{' at the beginning of the language name to not duplicate it
394 // for the doxygen command
395 AfterFence.consume_front("{");
396 // The name is alphanumeric or '.' or '+'
397 StringRef CodeLang = AfterFence.take_while(
398 [](char C) { return llvm::isAlnum(C) || C == '.' || C == '+'; });
399
400 OS << "///@code";
401
402 if (!CodeLang.empty())
403 OS << "{" << CodeLang.str() << "}";
404
405 OS << "\n";
406
407 State = FencedCodeblock;
408 continue;
409 }
410
411 // FIXME: handle indented code blocks too?
412 // In doxygen, the indentation which triggers a code block depends on the
413 // indentation of the previous paragraph.
414 // https://www.doxygen.nl/manual/markdown.html#mddox_code_blocks
415 } else if (State == FencedCodeblock) {
416 // End of code fence
417 if (Line.ltrim().starts_with(CodeFence)) {
418 OS << "///@endcode\n";
419 State = Normal;
420 continue;
421 }
422 OS << "///" << Line << "\n";
423 continue;
424 }
425
426 // Normal line preprocessing (add doxygen markers, handle escaping)
427 OS << "///";
428
429 if (Line.empty() || Line.trim().empty()) {
430 OS << "\n";
431 // Empty lines reset the InCodeSpan state.
432 InCodeSpan = false;
433 continue;
434 }
435
436 if (Line.starts_with("<"))
437 // A comment line starting with '///<' is treated as a doxygen
438 // command. To avoid this, we add a space before the '<'.
439 OS << ' ';
440
441 for (char C : Line) {
442 if (C == '`')
443 InCodeSpan = !InCodeSpan;
444 else if (InCodeSpan && (C == '@' || C == '\\'))
445 OS << '\\';
446 OS << C;
447 }
448
449 OS << "\n";
450 }
451
452 // Close any unclosed code block
453 if (State == FencedCodeblock)
454 OS << "///@endcode\n";
455}
456
458 const comments::BlockCommandComment *B) {
459 switch (B->getCommandID()) {
460 case comments::CommandTraits::KCI_brief: {
461 if (!BriefParagraph) {
462 BriefParagraph = B->getParagraph();
463 return;
464 }
465 break;
466 }
467 case comments::CommandTraits::KCI_return:
468 case comments::CommandTraits::KCI_returns:
469 if (!ReturnParagraph) {
470 ReturnParagraph = B->getParagraph();
471 return;
472 }
473 break;
474 case comments::CommandTraits::KCI_retval:
475 // Only consider retval commands having an argument.
476 // The argument contains the described return value which is needed to
477 // convert it to markup.
478 if (B->getNumArgs() == 1)
479 RetvalCommands.push_back(B);
480 return;
481 default:
482 break;
483 }
484
485 // For all other commands, we store them in the BlockCommands map.
486 // This allows us to keep the order of the comments.
487 BlockCommands[CommentPartIndex] = B;
488 CommentPartIndex++;
489}
490
492 if (!BriefParagraph)
493 return;
494 ParagraphToMarkupDocument(Out, Traits).visit(BriefParagraph);
495}
496
498 if (!ReturnParagraph)
499 return;
500 ParagraphToMarkupDocument(Out, Traits).visit(ReturnParagraph);
501}
502
504 StringRef ParamName, markup::Paragraph &Out) const {
505 if (ParamName.empty())
506 return;
507
508 if (const auto *P = Parameters.lookup(ParamName)) {
509 ParagraphToMarkupDocument(Out, Traits).visit(P->getParagraph());
510 }
511}
512
514 StringRef ParamName, llvm::raw_string_ostream &Out) const {
515 if (ParamName.empty())
516 return;
517
518 if (const auto *P = Parameters.lookup(ParamName)) {
519 ParagraphToString(Out, Traits).visit(P->getParagraph());
520 }
521}
522
523void SymbolDocCommentVisitor::detailedDocToMarkup(markup::Document &Out) const {
524 for (unsigned I = 0; I < CommentPartIndex; ++I) {
525 if (const auto *BC = BlockCommands.lookup(I)) {
526 BlockCommentToMarkupDocument(Out, Traits).visit(BC);
527 } else if (const auto *P = FreeParagraphs.lookup(I)) {
528 ParagraphToMarkupDocument(Out.addParagraph(), Traits).visit(P);
529 }
530 }
531}
532
534 StringRef TemplateParamName, markup::Paragraph &Out) const {
535 if (TemplateParamName.empty())
536 return;
537
538 if (const auto *TP = TemplateParameters.lookup(TemplateParamName)) {
539 ParagraphToMarkupDocument(Out, Traits).visit(TP->getParagraph());
540 }
541}
542
544 StringRef TemplateParamName, llvm::raw_string_ostream &Out) const {
545 if (TemplateParamName.empty())
546 return;
547
548 if (const auto *P = TemplateParameters.lookup(TemplateParamName)) {
549 ParagraphToString(Out, Traits).visit(P->getParagraph());
550 }
551}
552
553void SymbolDocCommentVisitor::retvalsToMarkup(markup::Document &Out) const {
554 if (RetvalCommands.empty())
555 return;
556 markup::BulletList &BL = Out.addBulletList();
557 for (const auto *P : RetvalCommands) {
558 BlockCommentToMarkupDocument(BL.addItem(), Traits).visit(P);
559 }
560}
561
562} // namespace clangd
563} // namespace clang
void visitVerbatimLineComment(const comments::VerbatimLineComment *VL)
void visitCodeCommand(const comments::VerbatimBlockComment *VB)
void visitVerbatimBlockComment(const comments::VerbatimBlockComment *VB)
BlockCommentToMarkupDocument(markup::Document &Out, const comments::CommandTraits &Traits)
void visitBlockCommandComment(const comments::BlockCommandComment *B)
void visitHTMLEndTagComment(const comments::HTMLEndTagComment *ETC)
void visitTextComment(const comments::TextComment *C)
void visitHTMLStartTagComment(const comments::HTMLStartTagComment *STC)
void visitParagraphComment(const comments::ParagraphComment *C)
void visitInlineCommandComment(const comments::InlineCommandComment *C)
ParagraphToMarkupDocument(markup::Paragraph &Out, const comments::CommandTraits &Traits)
void visitInlineCommandComment(const comments::InlineCommandComment *C)
void visitHTMLEndTagComment(const comments::HTMLEndTagComment *ETC)
void visitParagraphComment(const comments::ParagraphComment *C)
ParagraphToString(llvm::raw_string_ostream &Out, const comments::CommandTraits &Traits)
void visitTextComment(const comments::TextComment *C)
void visitHTMLStartTagComment(const comments::HTMLStartTagComment *STC)
void detailedDocToMarkup(markup::Document &Out) const
Converts all unhandled comment commands to a markup document.
void templateTypeParmDocToMarkup(StringRef TemplateParamName, markup::Paragraph &Out) const
void returnToMarkup(markup::Paragraph &Out) const
Converts the "return" command(s) to a markup document.
void parameterDocToMarkup(StringRef ParamName, markup::Paragraph &Out) const
void templateTypeParmDocToString(StringRef TemplateParamName, llvm::raw_string_ostream &Out) const
void preprocessDocumentation(StringRef Doc)
Preprocesses the raw documentation string to prepare it for doxygen parsing.
void parameterDocToString(StringRef ParamName, llvm::raw_string_ostream &Out) const
void visitBlockCommandComment(const comments::BlockCommandComment *B)
void briefToMarkup(markup::Paragraph &Out) const
Converts the "brief" command(s) to a markup document.
void retvalsToMarkup(markup::Document &Out) const
Converts the "retval" command(s) to a markup document.
Represents parts of the markup that can contain strings, like inline code, code block or plain text.
Definition Markup.h:45
FIXME: Skip testing on windows temporarily due to the different escaping code mode.
Definition AST.cpp:44
===– Representation.cpp - ClangDoc Representation --------—*- C++ -*-===//