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