1 //===--- CommentParser.cpp - Doxygen comment parser -----------------------===// 2 // 3 // The LLVM Compiler Infrastructure 4 // 5 // This file is distributed under the University of Illinois Open Source 6 // License. See LICENSE.TXT for details. 7 // 8 //===----------------------------------------------------------------------===// 9 10 #include "clang/AST/CommentParser.h" 11 #include "clang/AST/CommentCommandTraits.h" 12 #include "clang/AST/CommentDiagnostic.h" 13 #include "clang/AST/CommentSema.h" 14 #include "clang/Basic/SourceManager.h" 15 #include "llvm/Support/ErrorHandling.h" 16 17 namespace clang { 18 namespace comments { 19 20 /// Re-lexes a sequence of tok::text tokens. 21 class TextTokenRetokenizer { 22 llvm::BumpPtrAllocator &Allocator; 23 Parser &P; 24 25 /// This flag is set when there are no more tokens we can fetch from lexer. 26 bool NoMoreInterestingTokens; 27 28 /// Token buffer: tokens we have processed and lookahead. 29 SmallVector<Token, 16> Toks; 30 31 /// A position in \c Toks. 32 struct Position { 33 unsigned CurToken; 34 const char *BufferStart; 35 const char *BufferEnd; 36 const char *BufferPtr; 37 SourceLocation BufferStartLoc; 38 }; 39 40 /// Current position in Toks. 41 Position Pos; 42 43 bool isEnd() const { 44 return Pos.CurToken >= Toks.size(); 45 } 46 47 /// Sets up the buffer pointers to point to current token. 48 void setupBuffer() { 49 assert(!isEnd()); 50 const Token &Tok = Toks[Pos.CurToken]; 51 52 Pos.BufferStart = Tok.getText().begin(); 53 Pos.BufferEnd = Tok.getText().end(); 54 Pos.BufferPtr = Pos.BufferStart; 55 Pos.BufferStartLoc = Tok.getLocation(); 56 } 57 58 SourceLocation getSourceLocation() const { 59 const unsigned CharNo = Pos.BufferPtr - Pos.BufferStart; 60 return Pos.BufferStartLoc.getLocWithOffset(CharNo); 61 } 62 63 char peek() const { 64 assert(!isEnd()); 65 assert(Pos.BufferPtr != Pos.BufferEnd); 66 return *Pos.BufferPtr; 67 } 68 69 void consumeChar() { 70 assert(!isEnd()); 71 assert(Pos.BufferPtr != Pos.BufferEnd); 72 Pos.BufferPtr++; 73 if (Pos.BufferPtr == Pos.BufferEnd) { 74 Pos.CurToken++; 75 if (isEnd() && !addToken()) 76 return; 77 78 assert(!isEnd()); 79 setupBuffer(); 80 } 81 } 82 83 /// Add a token. 84 /// Returns true on success, false if there are no interesting tokens to 85 /// fetch from lexer. 86 bool addToken() { 87 if (NoMoreInterestingTokens) 88 return false; 89 90 if (P.Tok.is(tok::newline)) { 91 // If we see a single newline token between text tokens, skip it. 92 Token Newline = P.Tok; 93 P.consumeToken(); 94 if (P.Tok.isNot(tok::text)) { 95 P.putBack(Newline); 96 NoMoreInterestingTokens = true; 97 return false; 98 } 99 } 100 if (P.Tok.isNot(tok::text)) { 101 NoMoreInterestingTokens = true; 102 return false; 103 } 104 105 Toks.push_back(P.Tok); 106 P.consumeToken(); 107 if (Toks.size() == 1) 108 setupBuffer(); 109 return true; 110 } 111 112 static bool isWhitespace(char C) { 113 return C == ' ' || C == '\n' || C == '\r' || 114 C == '\t' || C == '\f' || C == '\v'; 115 } 116 117 void consumeWhitespace() { 118 while (!isEnd()) { 119 if (isWhitespace(peek())) 120 consumeChar(); 121 else 122 break; 123 } 124 } 125 126 void formTokenWithChars(Token &Result, 127 SourceLocation Loc, 128 const char *TokBegin, 129 unsigned TokLength, 130 StringRef Text) { 131 Result.setLocation(Loc); 132 Result.setKind(tok::text); 133 Result.setLength(TokLength); 134 #ifndef NDEBUG 135 Result.TextPtr = "<UNSET>"; 136 Result.IntVal = 7; 137 #endif 138 Result.setText(Text); 139 } 140 141 public: 142 TextTokenRetokenizer(llvm::BumpPtrAllocator &Allocator, Parser &P): 143 Allocator(Allocator), P(P), NoMoreInterestingTokens(false) { 144 Pos.CurToken = 0; 145 addToken(); 146 } 147 148 /// Extract a word -- sequence of non-whitespace characters. 149 bool lexWord(Token &Tok) { 150 if (isEnd()) 151 return false; 152 153 Position SavedPos = Pos; 154 155 consumeWhitespace(); 156 SmallString<32> WordText; 157 const char *WordBegin = Pos.BufferPtr; 158 SourceLocation Loc = getSourceLocation(); 159 while (!isEnd()) { 160 const char C = peek(); 161 if (!isWhitespace(C)) { 162 WordText.push_back(C); 163 consumeChar(); 164 } else 165 break; 166 } 167 const unsigned Length = WordText.size(); 168 if (Length == 0) { 169 Pos = SavedPos; 170 return false; 171 } 172 173 char *TextPtr = Allocator.Allocate<char>(Length + 1); 174 175 memcpy(TextPtr, WordText.c_str(), Length + 1); 176 StringRef Text = StringRef(TextPtr, Length); 177 178 formTokenWithChars(Tok, Loc, WordBegin, Length, Text); 179 return true; 180 } 181 182 bool lexDelimitedSeq(Token &Tok, char OpenDelim, char CloseDelim) { 183 if (isEnd()) 184 return false; 185 186 Position SavedPos = Pos; 187 188 consumeWhitespace(); 189 SmallString<32> WordText; 190 const char *WordBegin = Pos.BufferPtr; 191 SourceLocation Loc = getSourceLocation(); 192 bool Error = false; 193 if (!isEnd()) { 194 const char C = peek(); 195 if (C == OpenDelim) { 196 WordText.push_back(C); 197 consumeChar(); 198 } else 199 Error = true; 200 } 201 char C = '\0'; 202 while (!Error && !isEnd()) { 203 C = peek(); 204 WordText.push_back(C); 205 consumeChar(); 206 if (C == CloseDelim) 207 break; 208 } 209 if (!Error && C != CloseDelim) 210 Error = true; 211 212 if (Error) { 213 Pos = SavedPos; 214 return false; 215 } 216 217 const unsigned Length = WordText.size(); 218 char *TextPtr = Allocator.Allocate<char>(Length + 1); 219 220 memcpy(TextPtr, WordText.c_str(), Length + 1); 221 StringRef Text = StringRef(TextPtr, Length); 222 223 formTokenWithChars(Tok, Loc, WordBegin, 224 Pos.BufferPtr - WordBegin, Text); 225 return true; 226 } 227 228 /// Put back tokens that we didn't consume. 229 void putBackLeftoverTokens() { 230 if (isEnd()) 231 return; 232 233 bool HavePartialTok = false; 234 Token PartialTok; 235 if (Pos.BufferPtr != Pos.BufferStart) { 236 formTokenWithChars(PartialTok, getSourceLocation(), 237 Pos.BufferPtr, Pos.BufferEnd - Pos.BufferPtr, 238 StringRef(Pos.BufferPtr, 239 Pos.BufferEnd - Pos.BufferPtr)); 240 HavePartialTok = true; 241 Pos.CurToken++; 242 } 243 244 P.putBack(llvm::makeArrayRef(Toks.begin() + Pos.CurToken, Toks.end())); 245 Pos.CurToken = Toks.size(); 246 247 if (HavePartialTok) 248 P.putBack(PartialTok); 249 } 250 }; 251 252 Parser::Parser(Lexer &L, Sema &S, llvm::BumpPtrAllocator &Allocator, 253 const SourceManager &SourceMgr, DiagnosticsEngine &Diags, 254 const CommandTraits &Traits): 255 L(L), S(S), Allocator(Allocator), SourceMgr(SourceMgr), Diags(Diags), 256 Traits(Traits) { 257 consumeToken(); 258 } 259 260 void Parser::parseParamCommandArgs(ParamCommandComment *PC, 261 TextTokenRetokenizer &Retokenizer) { 262 Token Arg; 263 // Check if argument looks like direction specification: [dir] 264 // e.g., [in], [out], [in,out] 265 if (Retokenizer.lexDelimitedSeq(Arg, '[', ']')) 266 S.actOnParamCommandDirectionArg(PC, 267 Arg.getLocation(), 268 Arg.getEndLocation(), 269 Arg.getText()); 270 271 if (Retokenizer.lexWord(Arg)) 272 S.actOnParamCommandParamNameArg(PC, 273 Arg.getLocation(), 274 Arg.getEndLocation(), 275 Arg.getText()); 276 } 277 278 void Parser::parseTParamCommandArgs(TParamCommandComment *TPC, 279 TextTokenRetokenizer &Retokenizer) { 280 Token Arg; 281 if (Retokenizer.lexWord(Arg)) 282 S.actOnTParamCommandParamNameArg(TPC, 283 Arg.getLocation(), 284 Arg.getEndLocation(), 285 Arg.getText()); 286 } 287 288 void Parser::parseBlockCommandArgs(BlockCommandComment *BC, 289 TextTokenRetokenizer &Retokenizer, 290 unsigned NumArgs) { 291 typedef BlockCommandComment::Argument Argument; 292 Argument *Args = 293 new (Allocator.Allocate<Argument>(NumArgs)) Argument[NumArgs]; 294 unsigned ParsedArgs = 0; 295 Token Arg; 296 while (ParsedArgs < NumArgs && Retokenizer.lexWord(Arg)) { 297 Args[ParsedArgs] = Argument(SourceRange(Arg.getLocation(), 298 Arg.getEndLocation()), 299 Arg.getText()); 300 ParsedArgs++; 301 } 302 303 S.actOnBlockCommandArgs(BC, llvm::makeArrayRef(Args, ParsedArgs)); 304 } 305 306 BlockCommandComment *Parser::parseBlockCommand() { 307 assert(Tok.is(tok::command)); 308 309 ParamCommandComment *PC; 310 TParamCommandComment *TPC; 311 BlockCommandComment *BC; 312 bool IsParam = false; 313 bool IsTParam = false; 314 const CommandInfo *Info = Traits.getCommandInfo(Tok.getCommandID()); 315 if (Info->IsParamCommand) { 316 IsParam = true; 317 PC = S.actOnParamCommandStart(Tok.getLocation(), 318 Tok.getEndLocation(), 319 Tok.getCommandID()); 320 } else if (Info->IsTParamCommand) { 321 IsTParam = true; 322 TPC = S.actOnTParamCommandStart(Tok.getLocation(), 323 Tok.getEndLocation(), 324 Tok.getCommandID()); 325 } else { 326 BC = S.actOnBlockCommandStart(Tok.getLocation(), 327 Tok.getEndLocation(), 328 Tok.getCommandID()); 329 } 330 consumeToken(); 331 332 if (Tok.is(tok::command) && 333 Traits.getCommandInfo(Tok.getCommandID())->IsBlockCommand) { 334 // Block command ahead. We can't nest block commands, so pretend that this 335 // command has an empty argument. 336 ParagraphComment *Paragraph = S.actOnParagraphComment( 337 ArrayRef<InlineContentComment *>()); 338 if (IsParam) { 339 S.actOnParamCommandFinish(PC, Paragraph); 340 return PC; 341 } else if (IsTParam) { 342 S.actOnTParamCommandFinish(TPC, Paragraph); 343 return TPC; 344 } else { 345 S.actOnBlockCommandFinish(BC, Paragraph); 346 return BC; 347 } 348 } 349 350 if (IsParam || IsTParam || Info->NumArgs > 0) { 351 // In order to parse command arguments we need to retokenize a few 352 // following text tokens. 353 TextTokenRetokenizer Retokenizer(Allocator, *this); 354 355 if (IsParam) 356 parseParamCommandArgs(PC, Retokenizer); 357 else if (IsTParam) 358 parseTParamCommandArgs(TPC, Retokenizer); 359 else 360 parseBlockCommandArgs(BC, Retokenizer, Info->NumArgs); 361 362 Retokenizer.putBackLeftoverTokens(); 363 } 364 365 BlockContentComment *Block = parseParagraphOrBlockCommand(); 366 // Since we have checked for a block command, we should have parsed a 367 // paragraph. 368 ParagraphComment *Paragraph = cast<ParagraphComment>(Block); 369 if (IsParam) { 370 S.actOnParamCommandFinish(PC, Paragraph); 371 return PC; 372 } else if (IsTParam) { 373 S.actOnTParamCommandFinish(TPC, Paragraph); 374 return TPC; 375 } else { 376 S.actOnBlockCommandFinish(BC, Paragraph); 377 return BC; 378 } 379 } 380 381 InlineCommandComment *Parser::parseInlineCommand() { 382 assert(Tok.is(tok::command)); 383 384 const Token CommandTok = Tok; 385 consumeToken(); 386 387 TextTokenRetokenizer Retokenizer(Allocator, *this); 388 389 Token ArgTok; 390 bool ArgTokValid = Retokenizer.lexWord(ArgTok); 391 392 InlineCommandComment *IC; 393 if (ArgTokValid) { 394 IC = S.actOnInlineCommand(CommandTok.getLocation(), 395 CommandTok.getEndLocation(), 396 CommandTok.getCommandID(), 397 ArgTok.getLocation(), 398 ArgTok.getEndLocation(), 399 ArgTok.getText()); 400 } else { 401 IC = S.actOnInlineCommand(CommandTok.getLocation(), 402 CommandTok.getEndLocation(), 403 CommandTok.getCommandID()); 404 } 405 406 Retokenizer.putBackLeftoverTokens(); 407 408 return IC; 409 } 410 411 HTMLStartTagComment *Parser::parseHTMLStartTag() { 412 assert(Tok.is(tok::html_start_tag)); 413 HTMLStartTagComment *HST = 414 S.actOnHTMLStartTagStart(Tok.getLocation(), 415 Tok.getHTMLTagStartName()); 416 consumeToken(); 417 418 SmallVector<HTMLStartTagComment::Attribute, 2> Attrs; 419 while (true) { 420 switch (Tok.getKind()) { 421 case tok::html_ident: { 422 Token Ident = Tok; 423 consumeToken(); 424 if (Tok.isNot(tok::html_equals)) { 425 Attrs.push_back(HTMLStartTagComment::Attribute(Ident.getLocation(), 426 Ident.getHTMLIdent())); 427 continue; 428 } 429 Token Equals = Tok; 430 consumeToken(); 431 if (Tok.isNot(tok::html_quoted_string)) { 432 Diag(Tok.getLocation(), 433 diag::warn_doc_html_start_tag_expected_quoted_string) 434 << SourceRange(Equals.getLocation()); 435 Attrs.push_back(HTMLStartTagComment::Attribute(Ident.getLocation(), 436 Ident.getHTMLIdent())); 437 while (Tok.is(tok::html_equals) || 438 Tok.is(tok::html_quoted_string)) 439 consumeToken(); 440 continue; 441 } 442 Attrs.push_back(HTMLStartTagComment::Attribute( 443 Ident.getLocation(), 444 Ident.getHTMLIdent(), 445 Equals.getLocation(), 446 SourceRange(Tok.getLocation(), 447 Tok.getEndLocation()), 448 Tok.getHTMLQuotedString())); 449 consumeToken(); 450 continue; 451 } 452 453 case tok::html_greater: 454 S.actOnHTMLStartTagFinish(HST, 455 S.copyArray(llvm::makeArrayRef(Attrs)), 456 Tok.getLocation(), 457 /* IsSelfClosing = */ false); 458 consumeToken(); 459 return HST; 460 461 case tok::html_slash_greater: 462 S.actOnHTMLStartTagFinish(HST, 463 S.copyArray(llvm::makeArrayRef(Attrs)), 464 Tok.getLocation(), 465 /* IsSelfClosing = */ true); 466 consumeToken(); 467 return HST; 468 469 case tok::html_equals: 470 case tok::html_quoted_string: 471 Diag(Tok.getLocation(), 472 diag::warn_doc_html_start_tag_expected_ident_or_greater); 473 while (Tok.is(tok::html_equals) || 474 Tok.is(tok::html_quoted_string)) 475 consumeToken(); 476 if (Tok.is(tok::html_ident) || 477 Tok.is(tok::html_greater) || 478 Tok.is(tok::html_slash_greater)) 479 continue; 480 481 S.actOnHTMLStartTagFinish(HST, 482 S.copyArray(llvm::makeArrayRef(Attrs)), 483 SourceLocation(), 484 /* IsSelfClosing = */ false); 485 return HST; 486 487 default: 488 // Not a token from an HTML start tag. Thus HTML tag prematurely ended. 489 S.actOnHTMLStartTagFinish(HST, 490 S.copyArray(llvm::makeArrayRef(Attrs)), 491 SourceLocation(), 492 /* IsSelfClosing = */ false); 493 bool StartLineInvalid; 494 const unsigned StartLine = SourceMgr.getPresumedLineNumber( 495 HST->getLocation(), 496 &StartLineInvalid); 497 bool EndLineInvalid; 498 const unsigned EndLine = SourceMgr.getPresumedLineNumber( 499 Tok.getLocation(), 500 &EndLineInvalid); 501 if (StartLineInvalid || EndLineInvalid || StartLine == EndLine) 502 Diag(Tok.getLocation(), 503 diag::warn_doc_html_start_tag_expected_ident_or_greater) 504 << HST->getSourceRange(); 505 else { 506 Diag(Tok.getLocation(), 507 diag::warn_doc_html_start_tag_expected_ident_or_greater); 508 Diag(HST->getLocation(), diag::note_doc_html_tag_started_here) 509 << HST->getSourceRange(); 510 } 511 return HST; 512 } 513 } 514 } 515 516 HTMLEndTagComment *Parser::parseHTMLEndTag() { 517 assert(Tok.is(tok::html_end_tag)); 518 Token TokEndTag = Tok; 519 consumeToken(); 520 SourceLocation Loc; 521 if (Tok.is(tok::html_greater)) { 522 Loc = Tok.getLocation(); 523 consumeToken(); 524 } 525 526 return S.actOnHTMLEndTag(TokEndTag.getLocation(), 527 Loc, 528 TokEndTag.getHTMLTagEndName()); 529 } 530 531 BlockContentComment *Parser::parseParagraphOrBlockCommand() { 532 SmallVector<InlineContentComment *, 8> Content; 533 534 while (true) { 535 switch (Tok.getKind()) { 536 case tok::verbatim_block_begin: 537 case tok::verbatim_line_name: 538 case tok::eof: 539 assert(Content.size() != 0); 540 break; // Block content or EOF ahead, finish this parapgaph. 541 542 case tok::unknown_command: 543 Content.push_back(S.actOnUnknownCommand(Tok.getLocation(), 544 Tok.getEndLocation(), 545 Tok.getUnknownCommandName())); 546 consumeToken(); 547 continue; 548 549 case tok::command: { 550 const CommandInfo *Info = Traits.getCommandInfo(Tok.getCommandID()); 551 if (Info->IsBlockCommand) { 552 if (Content.size() == 0) 553 return parseBlockCommand(); 554 break; // Block command ahead, finish this parapgaph. 555 } 556 if (Info->IsVerbatimBlockEndCommand) { 557 Diag(Tok.getLocation(), 558 diag::warn_verbatim_block_end_without_start) 559 << Info->Name 560 << SourceRange(Tok.getLocation(), Tok.getEndLocation()); 561 consumeToken(); 562 continue; 563 } 564 if (Info->IsUnknownCommand) { 565 Content.push_back(S.actOnUnknownCommand(Tok.getLocation(), 566 Tok.getEndLocation(), 567 Info->getID())); 568 consumeToken(); 569 continue; 570 } 571 assert(Info->IsInlineCommand); 572 Content.push_back(parseInlineCommand()); 573 continue; 574 } 575 576 case tok::newline: { 577 consumeToken(); 578 if (Tok.is(tok::newline) || Tok.is(tok::eof)) { 579 consumeToken(); 580 break; // Two newlines -- end of paragraph. 581 } 582 if (Content.size() > 0) 583 Content.back()->addTrailingNewline(); 584 continue; 585 } 586 587 // Don't deal with HTML tag soup now. 588 case tok::html_start_tag: 589 Content.push_back(parseHTMLStartTag()); 590 continue; 591 592 case tok::html_end_tag: 593 Content.push_back(parseHTMLEndTag()); 594 continue; 595 596 case tok::text: 597 Content.push_back(S.actOnText(Tok.getLocation(), 598 Tok.getEndLocation(), 599 Tok.getText())); 600 consumeToken(); 601 continue; 602 603 case tok::verbatim_block_line: 604 case tok::verbatim_block_end: 605 case tok::verbatim_line_text: 606 case tok::html_ident: 607 case tok::html_equals: 608 case tok::html_quoted_string: 609 case tok::html_greater: 610 case tok::html_slash_greater: 611 llvm_unreachable("should not see this token"); 612 } 613 break; 614 } 615 616 return S.actOnParagraphComment(S.copyArray(llvm::makeArrayRef(Content))); 617 } 618 619 VerbatimBlockComment *Parser::parseVerbatimBlock() { 620 assert(Tok.is(tok::verbatim_block_begin)); 621 622 VerbatimBlockComment *VB = 623 S.actOnVerbatimBlockStart(Tok.getLocation(), 624 Tok.getVerbatimBlockID()); 625 consumeToken(); 626 627 // Don't create an empty line if verbatim opening command is followed 628 // by a newline. 629 if (Tok.is(tok::newline)) 630 consumeToken(); 631 632 SmallVector<VerbatimBlockLineComment *, 8> Lines; 633 while (Tok.is(tok::verbatim_block_line) || 634 Tok.is(tok::newline)) { 635 VerbatimBlockLineComment *Line; 636 if (Tok.is(tok::verbatim_block_line)) { 637 Line = S.actOnVerbatimBlockLine(Tok.getLocation(), 638 Tok.getVerbatimBlockText()); 639 consumeToken(); 640 if (Tok.is(tok::newline)) { 641 consumeToken(); 642 } 643 } else { 644 // Empty line, just a tok::newline. 645 Line = S.actOnVerbatimBlockLine(Tok.getLocation(), ""); 646 consumeToken(); 647 } 648 Lines.push_back(Line); 649 } 650 651 if (Tok.is(tok::verbatim_block_end)) { 652 const CommandInfo *Info = Traits.getCommandInfo(Tok.getVerbatimBlockID()); 653 S.actOnVerbatimBlockFinish(VB, Tok.getLocation(), 654 Info->Name, 655 S.copyArray(llvm::makeArrayRef(Lines))); 656 consumeToken(); 657 } else { 658 // Unterminated \\verbatim block 659 S.actOnVerbatimBlockFinish(VB, SourceLocation(), "", 660 S.copyArray(llvm::makeArrayRef(Lines))); 661 } 662 663 return VB; 664 } 665 666 VerbatimLineComment *Parser::parseVerbatimLine() { 667 assert(Tok.is(tok::verbatim_line_name)); 668 669 Token NameTok = Tok; 670 consumeToken(); 671 672 SourceLocation TextBegin; 673 StringRef Text; 674 // Next token might not be a tok::verbatim_line_text if verbatim line 675 // starting command comes just before a newline or comment end. 676 if (Tok.is(tok::verbatim_line_text)) { 677 TextBegin = Tok.getLocation(); 678 Text = Tok.getVerbatimLineText(); 679 } else { 680 TextBegin = NameTok.getEndLocation(); 681 Text = ""; 682 } 683 684 VerbatimLineComment *VL = S.actOnVerbatimLine(NameTok.getLocation(), 685 NameTok.getVerbatimLineID(), 686 TextBegin, 687 Text); 688 consumeToken(); 689 return VL; 690 } 691 692 BlockContentComment *Parser::parseBlockContent() { 693 switch (Tok.getKind()) { 694 case tok::text: 695 case tok::unknown_command: 696 case tok::command: 697 case tok::html_start_tag: 698 case tok::html_end_tag: 699 return parseParagraphOrBlockCommand(); 700 701 case tok::verbatim_block_begin: 702 return parseVerbatimBlock(); 703 704 case tok::verbatim_line_name: 705 return parseVerbatimLine(); 706 707 case tok::eof: 708 case tok::newline: 709 case tok::verbatim_block_line: 710 case tok::verbatim_block_end: 711 case tok::verbatim_line_text: 712 case tok::html_ident: 713 case tok::html_equals: 714 case tok::html_quoted_string: 715 case tok::html_greater: 716 case tok::html_slash_greater: 717 llvm_unreachable("should not see this token"); 718 } 719 llvm_unreachable("bogus token kind"); 720 } 721 722 FullComment *Parser::parseFullComment() { 723 // Skip newlines at the beginning of the comment. 724 while (Tok.is(tok::newline)) 725 consumeToken(); 726 727 SmallVector<BlockContentComment *, 8> Blocks; 728 while (Tok.isNot(tok::eof)) { 729 Blocks.push_back(parseBlockContent()); 730 731 // Skip extra newlines after paragraph end. 732 while (Tok.is(tok::newline)) 733 consumeToken(); 734 } 735 return S.actOnFullComment(S.copyArray(llvm::makeArrayRef(Blocks))); 736 } 737 738 } // end namespace comments 739 } // end namespace clang 740