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/CommentSema.h" 12#include "clang/AST/CommentDiagnostic.h" 13#include "clang/AST/CommentCommandTraits.h" 14#include "clang/Basic/SourceManager.h" 15#include "llvm/Support/ErrorHandling.h" 16 17namespace clang { 18namespace comments { 19 20/// Re-lexes a sequence of tok::text tokens. 21class 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 141public: 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, 179 Pos.BufferPtr - WordBegin, Text); 180 return true; 181 } 182 183 bool lexDelimitedSeq(Token &Tok, char OpenDelim, char CloseDelim) { 184 if (isEnd()) 185 return false; 186 187 Position SavedPos = Pos; 188 189 consumeWhitespace(); 190 SmallString<32> WordText; 191 const char *WordBegin = Pos.BufferPtr; 192 SourceLocation Loc = getSourceLocation(); 193 bool Error = false; 194 if (!isEnd()) { 195 const char C = peek(); 196 if (C == OpenDelim) { 197 WordText.push_back(C); 198 consumeChar(); 199 } else 200 Error = true; 201 } 202 char C = '\0'; 203 while (!Error && !isEnd()) { 204 C = peek(); 205 WordText.push_back(C); 206 consumeChar(); 207 if (C == CloseDelim) 208 break; 209 } 210 if (!Error && C != CloseDelim) 211 Error = true; 212 213 if (Error) { 214 Pos = SavedPos; 215 return false; 216 } 217 218 const unsigned Length = WordText.size(); 219 char *TextPtr = Allocator.Allocate<char>(Length + 1); 220 221 memcpy(TextPtr, WordText.c_str(), Length + 1); 222 StringRef Text = StringRef(TextPtr, Length); 223 224 formTokenWithChars(Tok, Loc, WordBegin, 225 Pos.BufferPtr - WordBegin, Text); 226 return true; 227 } 228 229 /// Put back tokens that we didn't consume. 230 void putBackLeftoverTokens() { 231 if (isEnd()) 232 return; 233 234 bool HavePartialTok = false; 235 Token PartialTok; 236 if (Pos.BufferPtr != Pos.BufferStart) { 237 formTokenWithChars(PartialTok, getSourceLocation(), 238 Pos.BufferPtr, Pos.BufferEnd - Pos.BufferPtr, 239 StringRef(Pos.BufferPtr, 240 Pos.BufferEnd - Pos.BufferPtr)); 241 HavePartialTok = true; 242 Pos.CurToken++; 243 } 244 245 P.putBack(llvm::makeArrayRef(Toks.begin() + Pos.CurToken, Toks.end())); 246 Pos.CurToken = Toks.size(); 247 248 if (HavePartialTok) 249 P.putBack(PartialTok); 250 } 251}; 252 253Parser::Parser(Lexer &L, Sema &S, llvm::BumpPtrAllocator &Allocator, 254 const SourceManager &SourceMgr, DiagnosticsEngine &Diags, 255 const CommandTraits &Traits): 256 L(L), S(S), Allocator(Allocator), SourceMgr(SourceMgr), Diags(Diags), 257 Traits(Traits) { 258 consumeToken(); 259} 260 261void Parser::parseParamCommandArgs(ParamCommandComment *PC, 262 TextTokenRetokenizer &Retokenizer) { 263 Token Arg; 264 // Check if argument looks like direction specification: [dir] 265 // e.g., [in], [out], [in,out] 266 if (Retokenizer.lexDelimitedSeq(Arg, '[', ']')) 267 S.actOnParamCommandDirectionArg(PC, 268 Arg.getLocation(), 269 Arg.getEndLocation(), 270 Arg.getText()); 271 272 if (Retokenizer.lexWord(Arg)) 273 S.actOnParamCommandParamNameArg(PC, 274 Arg.getLocation(), 275 Arg.getEndLocation(), 276 Arg.getText()); 277} 278 279void Parser::parseTParamCommandArgs(TParamCommandComment *TPC, 280 TextTokenRetokenizer &Retokenizer) { 281 Token Arg; 282 if (Retokenizer.lexWord(Arg)) 283 S.actOnTParamCommandParamNameArg(TPC, 284 Arg.getLocation(), 285 Arg.getEndLocation(), 286 Arg.getText()); 287} 288 289void Parser::parseBlockCommandArgs(BlockCommandComment *BC, 290 TextTokenRetokenizer &Retokenizer, 291 unsigned NumArgs) { 292 typedef BlockCommandComment::Argument Argument; 293 Argument *Args = 294 new (Allocator.Allocate<Argument>(NumArgs)) Argument[NumArgs]; 295 unsigned ParsedArgs = 0; 296 Token Arg; 297 while (ParsedArgs < NumArgs && Retokenizer.lexWord(Arg)) { 298 Args[ParsedArgs] = Argument(SourceRange(Arg.getLocation(), 299 Arg.getEndLocation()), 300 Arg.getText()); 301 ParsedArgs++; 302 } 303 304 S.actOnBlockCommandArgs(BC, llvm::makeArrayRef(Args, ParsedArgs)); 305} 306 307BlockCommandComment *Parser::parseBlockCommand() { 308 assert(Tok.is(tok::command)); 309 310 ParamCommandComment *PC; 311 TParamCommandComment *TPC; 312 BlockCommandComment *BC; 313 bool IsParam = false; 314 bool IsTParam = false; 315 const CommandInfo *Info = Traits.getCommandInfo(Tok.getCommandID()); 316 if (Info->IsParamCommand) { 317 IsParam = true; 318 PC = S.actOnParamCommandStart(Tok.getLocation(), 319 Tok.getEndLocation(), 320 Tok.getCommandID()); 321 } if (Info->IsTParamCommand) { 322 IsTParam = true; 323 TPC = S.actOnTParamCommandStart(Tok.getLocation(), 324 Tok.getEndLocation(), 325 Tok.getCommandID()); 326 } else { 327 BC = S.actOnBlockCommandStart(Tok.getLocation(), 328 Tok.getEndLocation(), 329 Tok.getCommandID()); 330 } 331 consumeToken(); 332 333 if (Tok.is(tok::command) && 334 Traits.getCommandInfo(Tok.getCommandID())->IsBlockCommand) { 335 // Block command ahead. We can't nest block commands, so pretend that this 336 // command has an empty argument. 337 ParagraphComment *Paragraph = S.actOnParagraphComment( 338 ArrayRef<InlineContentComment *>()); 339 if (IsParam) { 340 S.actOnParamCommandFinish(PC, Paragraph); 341 return PC; 342 } else if (IsTParam) { 343 S.actOnTParamCommandFinish(TPC, Paragraph); 344 return TPC; 345 } else { 346 S.actOnBlockCommandFinish(BC, Paragraph); 347 return BC; 348 } 349 } 350 351 if (IsParam || IsTParam || Info->NumArgs > 0) { 352 // In order to parse command arguments we need to retokenize a few 353 // following text tokens. 354 TextTokenRetokenizer Retokenizer(Allocator, *this); 355 356 if (IsParam) 357 parseParamCommandArgs(PC, Retokenizer); 358 else if (IsTParam) 359 parseTParamCommandArgs(TPC, Retokenizer); 360 else 361 parseBlockCommandArgs(BC, Retokenizer, Info->NumArgs); 362 363 Retokenizer.putBackLeftoverTokens(); 364 } 365 366 BlockContentComment *Block = parseParagraphOrBlockCommand(); 367 // Since we have checked for a block command, we should have parsed a 368 // paragraph. 369 ParagraphComment *Paragraph = cast<ParagraphComment>(Block); 370 if (IsParam) { 371 S.actOnParamCommandFinish(PC, Paragraph); 372 return PC; 373 } else if (IsTParam) { 374 S.actOnTParamCommandFinish(TPC, Paragraph); 375 return TPC; 376 } else { 377 S.actOnBlockCommandFinish(BC, Paragraph); 378 return BC; 379 } 380} 381 382InlineCommandComment *Parser::parseInlineCommand() { 383 assert(Tok.is(tok::command)); 384 385 const Token CommandTok = Tok; 386 consumeToken(); 387 388 TextTokenRetokenizer Retokenizer(Allocator, *this); 389 390 Token ArgTok; 391 bool ArgTokValid = Retokenizer.lexWord(ArgTok); 392 393 InlineCommandComment *IC; 394 if (ArgTokValid) { 395 IC = S.actOnInlineCommand(CommandTok.getLocation(), 396 CommandTok.getEndLocation(), 397 CommandTok.getCommandID(), 398 ArgTok.getLocation(), 399 ArgTok.getEndLocation(), 400 ArgTok.getText()); 401 } else { 402 IC = S.actOnInlineCommand(CommandTok.getLocation(), 403 CommandTok.getEndLocation(), 404 CommandTok.getCommandID()); 405 } 406 407 Retokenizer.putBackLeftoverTokens(); 408 409 return IC; 410} 411 412HTMLStartTagComment *Parser::parseHTMLStartTag() { 413 assert(Tok.is(tok::html_start_tag)); 414 HTMLStartTagComment *HST = 415 S.actOnHTMLStartTagStart(Tok.getLocation(), 416 Tok.getHTMLTagStartName()); 417 consumeToken(); 418 419 SmallVector<HTMLStartTagComment::Attribute, 2> Attrs; 420 while (true) { 421 switch (Tok.getKind()) { 422 case tok::html_ident: { 423 Token Ident = Tok; 424 consumeToken(); 425 if (Tok.isNot(tok::html_equals)) { 426 Attrs.push_back(HTMLStartTagComment::Attribute(Ident.getLocation(), 427 Ident.getHTMLIdent())); 428 continue; 429 } 430 Token Equals = Tok; 431 consumeToken(); 432 if (Tok.isNot(tok::html_quoted_string)) { 433 Diag(Tok.getLocation(), 434 diag::warn_doc_html_start_tag_expected_quoted_string) 435 << SourceRange(Equals.getLocation()); 436 Attrs.push_back(HTMLStartTagComment::Attribute(Ident.getLocation(), 437 Ident.getHTMLIdent())); 438 while (Tok.is(tok::html_equals) || 439 Tok.is(tok::html_quoted_string)) 440 consumeToken(); 441 continue; 442 } 443 Attrs.push_back(HTMLStartTagComment::Attribute( 444 Ident.getLocation(), 445 Ident.getHTMLIdent(), 446 Equals.getLocation(), 447 SourceRange(Tok.getLocation(), 448 Tok.getEndLocation()), 449 Tok.getHTMLQuotedString())); 450 consumeToken(); 451 continue; 452 } 453 454 case tok::html_greater: 455 S.actOnHTMLStartTagFinish(HST, 456 S.copyArray(llvm::makeArrayRef(Attrs)), 457 Tok.getLocation(), 458 /* IsSelfClosing = */ false); 459 consumeToken(); 460 return HST; 461 462 case tok::html_slash_greater: 463 S.actOnHTMLStartTagFinish(HST, 464 S.copyArray(llvm::makeArrayRef(Attrs)), 465 Tok.getLocation(), 466 /* IsSelfClosing = */ true); 467 consumeToken(); 468 return HST; 469 470 case tok::html_equals: 471 case tok::html_quoted_string: 472 Diag(Tok.getLocation(), 473 diag::warn_doc_html_start_tag_expected_ident_or_greater); 474 while (Tok.is(tok::html_equals) || 475 Tok.is(tok::html_quoted_string)) 476 consumeToken(); 477 if (Tok.is(tok::html_ident) || 478 Tok.is(tok::html_greater) || 479 Tok.is(tok::html_slash_greater)) 480 continue; 481 482 S.actOnHTMLStartTagFinish(HST, 483 S.copyArray(llvm::makeArrayRef(Attrs)), 484 SourceLocation(), 485 /* IsSelfClosing = */ false); 486 return HST; 487 488 default: 489 // Not a token from an HTML start tag. Thus HTML tag prematurely ended. 490 S.actOnHTMLStartTagFinish(HST, 491 S.copyArray(llvm::makeArrayRef(Attrs)), 492 SourceLocation(), 493 /* IsSelfClosing = */ false); 494 bool StartLineInvalid; 495 const unsigned StartLine = SourceMgr.getPresumedLineNumber( 496 HST->getLocation(), 497 &StartLineInvalid); 498 bool EndLineInvalid; 499 const unsigned EndLine = SourceMgr.getPresumedLineNumber( 500 Tok.getLocation(), 501 &EndLineInvalid); 502 if (StartLineInvalid || EndLineInvalid || StartLine == EndLine) 503 Diag(Tok.getLocation(), 504 diag::warn_doc_html_start_tag_expected_ident_or_greater) 505 << HST->getSourceRange(); 506 else { 507 Diag(Tok.getLocation(), 508 diag::warn_doc_html_start_tag_expected_ident_or_greater); 509 Diag(HST->getLocation(), diag::note_doc_html_tag_started_here) 510 << HST->getSourceRange(); 511 } 512 return HST; 513 } 514 } 515} 516 517HTMLEndTagComment *Parser::parseHTMLEndTag() { 518 assert(Tok.is(tok::html_end_tag)); 519 Token TokEndTag = Tok; 520 consumeToken(); 521 SourceLocation Loc; 522 if (Tok.is(tok::html_greater)) { 523 Loc = Tok.getLocation(); 524 consumeToken(); 525 } 526 527 return S.actOnHTMLEndTag(TokEndTag.getLocation(), 528 Loc, 529 TokEndTag.getHTMLTagEndName()); 530} 531 532BlockContentComment *Parser::parseParagraphOrBlockCommand() { 533 SmallVector<InlineContentComment *, 8> Content; 534 535 while (true) { 536 switch (Tok.getKind()) { 537 case tok::verbatim_block_begin: 538 case tok::verbatim_line_name: 539 case tok::eof: 540 assert(Content.size() != 0); 541 break; // Block content or EOF ahead, finish this parapgaph. 542 543 case tok::unknown_command: 544 Content.push_back(S.actOnUnknownCommand(Tok.getLocation(), 545 Tok.getEndLocation(), 546 Tok.getUnknownCommandName())); 547 consumeToken(); 548 continue; 549 550 case tok::command: { 551 const CommandInfo *Info = Traits.getCommandInfo(Tok.getCommandID()); 552 if (Info->IsBlockCommand) { 553 if (Content.size() == 0) 554 return parseBlockCommand(); 555 break; // Block command ahead, finish this parapgaph. 556 } 557 assert(Info->IsInlineCommand); 558 Content.push_back(parseInlineCommand()); 559 continue; 560 } 561 562 case tok::newline: { 563 consumeToken(); 564 if (Tok.is(tok::newline) || Tok.is(tok::eof)) { 565 consumeToken(); 566 break; // Two newlines -- end of paragraph. 567 } 568 if (Content.size() > 0) 569 Content.back()->addTrailingNewline(); 570 continue; 571 } 572 573 // Don't deal with HTML tag soup now. 574 case tok::html_start_tag: 575 Content.push_back(parseHTMLStartTag()); 576 continue; 577 578 case tok::html_end_tag: 579 Content.push_back(parseHTMLEndTag()); 580 continue; 581 582 case tok::text: 583 Content.push_back(S.actOnText(Tok.getLocation(), 584 Tok.getEndLocation(), 585 Tok.getText())); 586 consumeToken(); 587 continue; 588 589 case tok::verbatim_block_line: 590 case tok::verbatim_block_end: 591 case tok::verbatim_line_text: 592 case tok::html_ident: 593 case tok::html_equals: 594 case tok::html_quoted_string: 595 case tok::html_greater: 596 case tok::html_slash_greater: 597 llvm_unreachable("should not see this token"); 598 } 599 break; 600 } 601 602 return S.actOnParagraphComment(S.copyArray(llvm::makeArrayRef(Content))); 603} 604 605VerbatimBlockComment *Parser::parseVerbatimBlock() { 606 assert(Tok.is(tok::verbatim_block_begin)); 607 608 VerbatimBlockComment *VB = 609 S.actOnVerbatimBlockStart(Tok.getLocation(), 610 Tok.getVerbatimBlockID()); 611 consumeToken(); 612 613 // Don't create an empty line if verbatim opening command is followed 614 // by a newline. 615 if (Tok.is(tok::newline)) 616 consumeToken(); 617 618 SmallVector<VerbatimBlockLineComment *, 8> Lines; 619 while (Tok.is(tok::verbatim_block_line) || 620 Tok.is(tok::newline)) { 621 VerbatimBlockLineComment *Line; 622 if (Tok.is(tok::verbatim_block_line)) { 623 Line = S.actOnVerbatimBlockLine(Tok.getLocation(), 624 Tok.getVerbatimBlockText()); 625 consumeToken(); 626 if (Tok.is(tok::newline)) { 627 consumeToken(); 628 } 629 } else { 630 // Empty line, just a tok::newline. 631 Line = S.actOnVerbatimBlockLine(Tok.getLocation(), ""); 632 consumeToken(); 633 } 634 Lines.push_back(Line); 635 } 636 637 if (Tok.is(tok::verbatim_block_end)) { 638 const CommandInfo *Info = Traits.getCommandInfo(Tok.getVerbatimBlockID()); 639 S.actOnVerbatimBlockFinish(VB, Tok.getLocation(), 640 Info->Name, 641 S.copyArray(llvm::makeArrayRef(Lines))); 642 consumeToken(); 643 } else { 644 // Unterminated \\verbatim block 645 S.actOnVerbatimBlockFinish(VB, SourceLocation(), "", 646 S.copyArray(llvm::makeArrayRef(Lines))); 647 } 648 649 return VB; 650} 651 652VerbatimLineComment *Parser::parseVerbatimLine() { 653 assert(Tok.is(tok::verbatim_line_name)); 654 655 Token NameTok = Tok; 656 consumeToken(); 657 658 SourceLocation TextBegin; 659 StringRef Text; 660 // Next token might not be a tok::verbatim_line_text if verbatim line 661 // starting command comes just before a newline or comment end. 662 if (Tok.is(tok::verbatim_line_text)) { 663 TextBegin = Tok.getLocation(); 664 Text = Tok.getVerbatimLineText(); 665 } else { 666 TextBegin = NameTok.getEndLocation(); 667 Text = ""; 668 } 669 670 VerbatimLineComment *VL = S.actOnVerbatimLine(NameTok.getLocation(), 671 NameTok.getVerbatimLineID(), 672 TextBegin, 673 Text); 674 consumeToken(); 675 return VL; 676} 677 678BlockContentComment *Parser::parseBlockContent() { 679 switch (Tok.getKind()) { 680 case tok::text: 681 case tok::unknown_command: 682 case tok::command: 683 case tok::html_start_tag: 684 case tok::html_end_tag: 685 return parseParagraphOrBlockCommand(); 686 687 case tok::verbatim_block_begin: 688 return parseVerbatimBlock(); 689 690 case tok::verbatim_line_name: 691 return parseVerbatimLine(); 692 693 case tok::eof: 694 case tok::newline: 695 case tok::verbatim_block_line: 696 case tok::verbatim_block_end: 697 case tok::verbatim_line_text: 698 case tok::html_ident: 699 case tok::html_equals: 700 case tok::html_quoted_string: 701 case tok::html_greater: 702 case tok::html_slash_greater: 703 llvm_unreachable("should not see this token"); 704 } 705 llvm_unreachable("bogus token kind"); 706} 707 708FullComment *Parser::parseFullComment() { 709 // Skip newlines at the beginning of the comment. 710 while (Tok.is(tok::newline)) 711 consumeToken(); 712 713 SmallVector<BlockContentComment *, 8> Blocks; 714 while (Tok.isNot(tok::eof)) { 715 Blocks.push_back(parseBlockContent()); 716 717 // Skip extra newlines after paragraph end. 718 while (Tok.is(tok::newline)) 719 consumeToken(); 720 } 721 return S.actOnFullComment(S.copyArray(llvm::makeArrayRef(Blocks))); 722} 723 724} // end namespace comments 725} // end namespace clang 726