Home | History | Annotate | Download | only in doclava
      1 /*
      2  * Copyright (C) 2010 Google Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.google.doclava;
     18 
     19 import java.util.regex.Pattern;
     20 import java.util.regex.Matcher;
     21 import java.util.ArrayList;
     22 import java.util.Arrays;
     23 import java.util.HashSet;
     24 import java.util.Set;
     25 
     26 public class Comment {
     27   static final Pattern FIRST_SENTENCE =
     28       Pattern.compile("((.*?)\\.)[ \t\r\n\\<](.*)", Pattern.DOTALL);
     29 
     30   private static final Set<String> KNOWN_TAGS = new HashSet<String>(Arrays.asList(new String[] {
     31           "@apiNote",
     32           "@author",
     33           "@since",
     34           "@version",
     35           "@deprecated",
     36           "@undeprecate",
     37           "@docRoot",
     38           "@sdkCurrent",
     39           "@inheritDoc",
     40           "@more",
     41           "@samplecode",
     42           "@sample",
     43           "@include",
     44           "@serial",
     45           "@implNote",
     46           "@implSpec",
     47           "@usesMathJax",
     48       }));
     49 
     50   public Comment(String text, ContainerInfo base, SourcePositionInfo sp) {
     51     mText = text;
     52     mBase = base;
     53     // sp now points to the end of the text, not the beginning!
     54     mPosition = SourcePositionInfo.findBeginning(sp, text);
     55   }
     56 
     57   private void parseCommentTags(String text) {
     58       int i = 0;
     59       int length = text.length();
     60       while (i < length  && isWhitespaceChar(text.charAt(i++))) {}
     61 
     62       if (i <=  0) {
     63           return;
     64       }
     65 
     66       text = text.substring(i-1);
     67       length = text.length();
     68 
     69       if ("".equals(text)) {
     70           return;
     71       }
     72 
     73       int start = 0;
     74       int end = findStartOfBlock(text, start);
     75 
     76 
     77       // possible scenarios
     78       //    main and block(s)
     79       //    main only (end == -1)
     80       //    block(s) only (end == 0)
     81 
     82       switch (end) {
     83           case -1: // main only
     84               parseMainDescription(text, start, length);
     85               return;
     86           case 0: // block(s) only
     87               break;
     88           default: // main and block
     89 
     90               // find end of main because end is really the beginning of @
     91               parseMainDescription(text, start, findEndOfMainOrBlock(text, start, end));
     92               break;
     93       }
     94 
     95       // parse blocks
     96       for (start = end; start < length; start = end) {
     97           end = findStartOfBlock(text, start+1);
     98 
     99           if (end == -1) {
    100               parseBlock(text, start, length);
    101               break;
    102           } else {
    103               parseBlock(text, start, findEndOfMainOrBlock(text, start, end));
    104           }
    105       }
    106 
    107       // for each block
    108       //    make block parts
    109       //        end is either next @ at beginning of line or end of text
    110   }
    111 
    112   private int findEndOfMainOrBlock(String text, int start, int end) {
    113       for (int i = end-1; i >= start; i--) {
    114           if (!isWhitespaceChar(text.charAt(i))) {
    115               end = i+1;
    116               break;
    117           }
    118       }
    119       return end;
    120   }
    121 
    122   private void parseMainDescription(String mainDescription, int start, int end) {
    123       if (mainDescription == null) {
    124           return;
    125       }
    126 
    127       SourcePositionInfo pos = SourcePositionInfo.add(mPosition, mText, 0);
    128       while (start < end) {
    129           int startOfInlineTag = findStartIndexOfInlineTag(mainDescription, start, end);
    130 
    131           // if there are no more tags
    132           if (startOfInlineTag == -1) {
    133               tag(null, mainDescription.substring(start, end), true, pos);
    134               return;
    135           }
    136 
    137           //int endOfInlineTag = mainDescription.indexOf('}', startOfInlineTag);
    138           int endOfInlineTag = findEndIndexOfInlineTag(mainDescription, startOfInlineTag, end);
    139 
    140           // if there was only beginning tag
    141           if (endOfInlineTag == -1) {
    142               // parse all of main as one tag
    143               tag(null, mainDescription.substring(start, end), true, pos);
    144               return;
    145           }
    146 
    147           endOfInlineTag++; // add one to make it a proper ending index
    148 
    149           // do first part without an inline tag - ie, just plaintext
    150           tag(null, mainDescription.substring(start, startOfInlineTag), true, pos);
    151 
    152           // parse the rest of this section, the inline tag
    153           parseInlineTag(mainDescription, startOfInlineTag, endOfInlineTag, pos);
    154 
    155           // keep going
    156           start = endOfInlineTag;
    157       }
    158   }
    159 
    160   private int findStartIndexOfInlineTag(String text, int fromIndex, int toIndex) {
    161       for (int i = fromIndex; i < (toIndex-3); i++) {
    162           if (text.charAt(i) == '{' && text.charAt(i+1) == '@' && !isWhitespaceChar(text.charAt(i+2))) {
    163               return i;
    164           }
    165       }
    166 
    167       return -1;
    168   }
    169 
    170   private int findEndIndexOfInlineTag(String text, int fromIndex, int toIndex) {
    171       int braceDepth = 0;
    172       for (int i = fromIndex; i < toIndex; i++) {
    173           if (text.charAt(i) == '{') {
    174               braceDepth++;
    175           } else if (text.charAt(i) == '}') {
    176               braceDepth--;
    177               if (braceDepth == 0) {
    178                   return i;
    179               }
    180           }
    181       }
    182 
    183       return -1;
    184   }
    185 
    186   private void parseInlineTag(String text, int start, int end, SourcePositionInfo pos) {
    187       int index = start+1;
    188       //int len = text.length();
    189       char c = text.charAt(index);
    190       // find the end of the tag name "@something"
    191       // need to do something special if we have '}'
    192       while (index < end && !isWhitespaceChar(c)) {
    193 
    194           // if this tag has no value, just return with tag name only
    195           if (c == '}') {
    196               // TODO - should value be "" or null?
    197               tag(text.substring(start+1, end), null, true, pos);
    198               return;
    199           }
    200           c = text.charAt(index++);
    201       }
    202 
    203       // don't parse things that don't have at least one extra character after @
    204       // probably should be plus 3
    205       // TODO - remove this - think it's fixed by change in parseMainDescription
    206       if (index == start+3) {
    207           return;
    208       }
    209 
    210       int endOfFirstPart = index-1;
    211 
    212       // get to beginning of tag value
    213       while (index < end && isWhitespaceChar(text.charAt(index++))) {}
    214       int startOfSecondPart = index-1;
    215 
    216       // +1 to get rid of opening brace and -1 to get rid of closing brace
    217       // maybe i wanna make this more elegant
    218       String tagName = text.substring(start+1, endOfFirstPart);
    219       String tagText = text.substring(startOfSecondPart, end-1);
    220       tag(tagName, tagText, true, pos);
    221   }
    222 
    223 
    224   /**
    225    * Finds the index of the start of a new block comment or -1 if there are
    226    * no more starts.
    227    * @param text The String to search
    228    * @param start the index of the String to start searching
    229    * @return The index of the start of a new block comment or -1 if there are
    230    * no more starts.
    231    */
    232   private int findStartOfBlock(String text, int start) {
    233       // how to detect we're at a new @
    234       //       if the chars to the left of it are \r or \n, we're at one
    235       //       if the chars to the left of it are ' ' or \t, keep looking
    236       //       otherwise, we're in the middle of a block, keep looking
    237       int index = text.indexOf('@', start);
    238 
    239       // no @ in text or index at first position
    240       if (index == -1 ||
    241               (index == 0 && text.length() > 1 && !isWhitespaceChar(text.charAt(index+1)))) {
    242           return index;
    243       }
    244 
    245       index = getPossibleStartOfBlock(text, index);
    246 
    247       int i = index-1; // start at the character immediately to the left of @
    248       char c;
    249       while (i >= 0) {
    250           c = text.charAt(i--);
    251 
    252           // found a new block comment because we're at the beginning of a line
    253           if (c == '\r' || c == '\n') {
    254               return index;
    255           }
    256 
    257           // there is a non whitespace character to the left of the @
    258           // before finding a new line, keep searching
    259           if (c != ' ' && c != '\t') {
    260               index = getPossibleStartOfBlock(text, index+1);
    261               i = index-1;
    262           }
    263 
    264           // some whitespace character, so keep looking, we might be at a new block comment
    265       }
    266 
    267       return -1;
    268   }
    269 
    270   private int getPossibleStartOfBlock(String text, int index) {
    271       while (isWhitespaceChar(text.charAt(index+1)) || !isWhitespaceChar(text.charAt(index-1))) {
    272           index = text.indexOf('@', index+1);
    273 
    274           if (index == -1 || index == text.length()-1) {
    275               return -1;
    276           }
    277       }
    278 
    279       return index;
    280   }
    281 
    282   private void parseBlock(String text, int startOfBlock, int endOfBlock) {
    283       SourcePositionInfo pos = SourcePositionInfo.add(mPosition, mText, startOfBlock);
    284       int index = startOfBlock;
    285 
    286       for (char c = text.charAt(index);
    287               index < endOfBlock && !isWhitespaceChar(c); c = text.charAt(index++)) {}
    288 
    289       //
    290       if (index == startOfBlock+1) {
    291           return;
    292       }
    293 
    294       int endOfFirstPart = index-1;
    295       if (index == endOfBlock) {
    296           // TODO - should value be null or ""
    297           tag(text.substring(startOfBlock,
    298                   findEndOfMainOrBlock(text, startOfBlock, index)), "", false, pos);
    299           return;
    300       }
    301 
    302 
    303       // get to beginning of tag value
    304       while (index < endOfBlock && isWhitespaceChar(text.charAt(index++))) {}
    305       int startOfSecondPart = index-1;
    306 
    307       tag(text.substring(startOfBlock, endOfFirstPart),
    308               text.substring(startOfSecondPart, endOfBlock), false, pos);
    309   }
    310 
    311   private boolean isWhitespaceChar(char c) {
    312       switch (c) {
    313           case ' ':
    314           case '\r':
    315           case '\t':
    316           case '\n':
    317               return true;
    318       }
    319       return false;
    320   }
    321 
    322   private void tag(String name, String text, boolean isInline, SourcePositionInfo pos) {
    323     /*
    324      * String s = isInline ? "inline" : "outofline"; System.out.println("---> " + s + " name=[" +
    325      * name + "] text=[" + text + "]");
    326      */
    327     if (name == null) {
    328       mInlineTagsList.add(new TextTagInfo("Text", "Text", text, pos));
    329     } else if (name.equals("@param")) {
    330       mParamTagsList.add(new ParamTagInfo("@param", "@param", text, mBase, pos));
    331     } else if (name.equals("@see")) {
    332       mSeeTagsList.add(new SeeTagInfo("@see", "@see", text, mBase, pos));
    333     } else if (name.equals("@link")) {
    334       if (Doclava.DEVSITE_IGNORE_JDLINKS) {
    335         TagInfo linkTag = new TextTagInfo(name, name, text, pos);
    336         mInlineTagsList.add(linkTag);
    337       } else {
    338         mInlineTagsList.add(new SeeTagInfo(name, "@see", text, mBase, pos));
    339       }
    340     } else if (name.equals("@linkplain")) {
    341       mInlineTagsList.add(new SeeTagInfo(name, "@linkplain", text, mBase, pos));
    342     } else if (name.equals("@value")) {
    343       mInlineTagsList.add(new SeeTagInfo(name, "@value", text, mBase, pos));
    344     } else if (name.equals("@throws") || name.equals("@exception")) {
    345       mThrowsTagsList.add(new ThrowsTagInfo("@throws", "@throws", text, mBase, pos));
    346     } else if (name.equals("@return")) {
    347       mReturnTagsList.add(new ParsedTagInfo("@return", "@return", text, mBase, pos));
    348     } else if (name.equals("@deprecated")) {
    349       if (text.length() == 0) {
    350         Errors.error(Errors.MISSING_COMMENT, pos, "@deprecated tag with no explanatory comment");
    351         text = "No replacement.";
    352       }
    353       mDeprecatedTagsList.add(new ParsedTagInfo("@deprecated", "@deprecated", text, mBase, pos));
    354     } else if (name.equals("@literal")) {
    355       mInlineTagsList.add(new LiteralTagInfo(text, pos));
    356     } else if (name.equals("@code")) {
    357       mInlineTagsList.add(new CodeTagInfo(text, pos));
    358     } else if (name.equals("@hide") || name.equals("@removed")
    359             || name.equals("@pending") || name.equals("@doconly")) {
    360       // nothing
    361     } else if (name.equals("@attr")) {
    362       AttrTagInfo tag = new AttrTagInfo("@attr", "@attr", text, mBase, pos);
    363       mAttrTagsList.add(tag);
    364       Comment c = tag.description();
    365       if (c != null) {
    366         for (TagInfo t : c.tags()) {
    367           mInlineTagsList.add(t);
    368         }
    369       }
    370     } else if (name.equals("@undeprecate")) {
    371       mUndeprecateTagsList.add(new TextTagInfo("@undeprecate", "@undeprecate", text, pos));
    372     } else if (name.equals("@include") || name.equals("@sample")) {
    373       mInlineTagsList.add(new SampleTagInfo(name, "@include", text, mBase, pos));
    374     } else if (name.equals("@apiNote") || name.equals("@implSpec") || name.equals("@implNote")) {
    375       mTagsList.add(new ParsedTagInfo(name, name, text, mBase, pos));
    376     } else if (name.equals("@memberDoc")) {
    377       mMemberDocTagsList.add(new ParsedTagInfo("@memberDoc", "@memberDoc", text, mBase, pos));
    378     } else if (name.equals("@paramDoc")) {
    379       mParamDocTagsList.add(new ParsedTagInfo("@paramDoc", "@paramDoc", text, mBase, pos));
    380     } else if (name.equals("@returnDoc")) {
    381       mReturnDocTagsList.add(new ParsedTagInfo("@returnDoc", "@returnDoc", text, mBase, pos));
    382     } else {
    383       boolean known = KNOWN_TAGS.contains(name);
    384       if (!known) {
    385         known = Doclava.knownTags.contains(name);
    386       }
    387       if (!known) {
    388         Errors.error(Errors.UNKNOWN_TAG, pos == null ? null : new SourcePositionInfo(pos),
    389             "Unknown tag: " + name);
    390       }
    391       TagInfo t = new TextTagInfo(name, name, text, pos);
    392       if (isInline) {
    393         mInlineTagsList.add(t);
    394       } else {
    395         mTagsList.add(t);
    396       }
    397     }
    398   }
    399 
    400   private void parseBriefTags() {
    401     int N = mInlineTagsList.size();
    402 
    403     // look for "@more" tag, which means that we might go past the first sentence.
    404     int more = -1;
    405     for (int i = 0; i < N; i++) {
    406       if (mInlineTagsList.get(i).name().equals("@more")) {
    407         more = i;
    408       }
    409     }
    410     if (more >= 0) {
    411       for (int i = 0; i < more; i++) {
    412         mBriefTagsList.add(mInlineTagsList.get(i));
    413       }
    414     } else {
    415       for (int i = 0; i < N; i++) {
    416         TagInfo t = mInlineTagsList.get(i);
    417         if (t.name().equals("Text")) {
    418           Matcher m = FIRST_SENTENCE.matcher(t.text());
    419           if (m.matches()) {
    420             String text = m.group(1);
    421             TagInfo firstSentenceTag = new TagInfo(t.name(), t.kind(), text, t.position());
    422             mBriefTagsList.add(firstSentenceTag);
    423             break;
    424           }
    425         }
    426         mBriefTagsList.add(t);
    427 
    428       }
    429     }
    430   }
    431 
    432   public TagInfo[] tags() {
    433     init();
    434     return mInlineTags;
    435   }
    436 
    437   public TagInfo[] tags(String name) {
    438     init();
    439     ArrayList<TagInfo> results = new ArrayList<TagInfo>();
    440     int N = mInlineTagsList.size();
    441     for (int i = 0; i < N; i++) {
    442       TagInfo t = mInlineTagsList.get(i);
    443       if (t.name().equals(name)) {
    444         results.add(t);
    445       }
    446     }
    447     return results.toArray(TagInfo.getArray(results.size()));
    448   }
    449 
    450   public TagInfo[] blockTags() {
    451     init();
    452     return mTags;
    453   }
    454 
    455   public ParamTagInfo[] paramTags() {
    456     init();
    457     return mParamTags;
    458   }
    459 
    460   public SeeTagInfo[] seeTags() {
    461     init();
    462     return mSeeTags;
    463   }
    464 
    465   public ThrowsTagInfo[] throwsTags() {
    466     init();
    467     return mThrowsTags;
    468   }
    469 
    470   public TagInfo[] returnTags() {
    471     init();
    472     return mReturnTags;
    473   }
    474 
    475   public TagInfo[] deprecatedTags() {
    476     init();
    477     return mDeprecatedTags;
    478   }
    479 
    480   public TagInfo[] undeprecateTags() {
    481     init();
    482     return mUndeprecateTags;
    483   }
    484 
    485   public AttrTagInfo[] attrTags() {
    486     init();
    487     return mAttrTags;
    488   }
    489 
    490   public TagInfo[] briefTags() {
    491     init();
    492     return mBriefTags;
    493   }
    494 
    495   public ParsedTagInfo[] memberDocTags() {
    496     init();
    497     return mMemberDocTags;
    498   }
    499 
    500   public ParsedTagInfo[] paramDocTags() {
    501     init();
    502     return mParamDocTags;
    503   }
    504 
    505   public ParsedTagInfo[] returnDocTags() {
    506     init();
    507     return mReturnDocTags;
    508   }
    509 
    510   public boolean isHidden() {
    511     if (mHidden == null) {
    512       mHidden = !Doclava.checkLevel(Doclava.SHOW_HIDDEN) &&
    513           (mText != null) && (mText.indexOf("@hide") >= 0 || mText.indexOf("@pending") >= 0);
    514     }
    515     return mHidden;
    516   }
    517 
    518   public boolean isRemoved() {
    519     if (mRemoved == null) {
    520         mRemoved = !Doclava.checkLevel(Doclava.SHOW_HIDDEN) &&
    521             (mText != null) && (mText.indexOf("@removed") >= 0);
    522     }
    523 
    524     return mRemoved;
    525   }
    526 
    527   public boolean isDocOnly() {
    528     if (mDocOnly == null) {
    529       mDocOnly = (mText != null) && (mText.indexOf("@doconly") >= 0);
    530     }
    531     return mDocOnly;
    532   }
    533 
    534   public boolean isDeprecated() {
    535     if (mDeprecated == null) {
    536       mDeprecated = (mText != null) && (mText.indexOf("@deprecated") >= 0);
    537     }
    538 
    539     return mDeprecated;
    540   }
    541 
    542   private void init() {
    543     if (!mInitialized) {
    544       initImpl();
    545     }
    546   }
    547 
    548   private void initImpl() {
    549     isHidden();
    550     isRemoved();
    551     isDocOnly();
    552     isDeprecated();
    553 
    554     // Don't bother parsing text if we aren't generating documentation.
    555     if (Doclava.parseComments()) {
    556         parseCommentTags(mText);
    557         parseBriefTags();
    558     } else {
    559       // Forces methods to be recognized by findOverriddenMethods in MethodInfo.
    560       mInlineTagsList.add(new TextTagInfo("Text", "Text", mText,
    561           SourcePositionInfo.add(mPosition, mText, 0)));
    562     }
    563 
    564     mText = null;
    565     mInitialized = true;
    566 
    567     mInlineTags = mInlineTagsList.toArray(TagInfo.getArray(mInlineTagsList.size()));
    568     mTags = mTagsList.toArray(TagInfo.getArray(mTagsList.size()));
    569     mParamTags = mParamTagsList.toArray(ParamTagInfo.getArray(mParamTagsList.size()));
    570     mSeeTags = mSeeTagsList.toArray(SeeTagInfo.getArray(mSeeTagsList.size()));
    571     mThrowsTags = mThrowsTagsList.toArray(ThrowsTagInfo.getArray(mThrowsTagsList.size()));
    572     mReturnTags = ParsedTagInfo.joinTags(
    573         mReturnTagsList.toArray(ParsedTagInfo.getArray(mReturnTagsList.size())));
    574     mDeprecatedTags = ParsedTagInfo.joinTags(
    575         mDeprecatedTagsList.toArray(ParsedTagInfo.getArray(mDeprecatedTagsList.size())));
    576     mUndeprecateTags = mUndeprecateTagsList.toArray(TagInfo.getArray(mUndeprecateTagsList.size()));
    577     mAttrTags = mAttrTagsList.toArray(AttrTagInfo.getArray(mAttrTagsList.size()));
    578     mBriefTags = mBriefTagsList.toArray(TagInfo.getArray(mBriefTagsList.size()));
    579     mMemberDocTags = mMemberDocTagsList.toArray(ParsedTagInfo.getArray(mMemberDocTagsList.size()));
    580     mParamDocTags = mParamDocTagsList.toArray(ParsedTagInfo.getArray(mParamDocTagsList.size()));
    581     mReturnDocTags = mReturnDocTagsList.toArray(ParsedTagInfo.getArray(mReturnDocTagsList.size()));
    582 
    583     mTagsList = null;
    584     mParamTagsList = null;
    585     mSeeTagsList = null;
    586     mThrowsTagsList = null;
    587     mReturnTagsList = null;
    588     mDeprecatedTagsList = null;
    589     mUndeprecateTagsList = null;
    590     mAttrTagsList = null;
    591     mBriefTagsList = null;
    592     mMemberDocTagsList = null;
    593     mParamDocTagsList = null;
    594     mReturnDocTagsList = null;
    595   }
    596 
    597   boolean mInitialized;
    598   Boolean mHidden = null;
    599   Boolean mRemoved = null;
    600   Boolean mDocOnly = null;
    601   Boolean mDeprecated = null;
    602   String mText;
    603   ContainerInfo mBase;
    604   SourcePositionInfo mPosition;
    605   int mLine = 1;
    606 
    607   TagInfo[] mInlineTags;
    608   TagInfo[] mTags;
    609   ParamTagInfo[] mParamTags;
    610   SeeTagInfo[] mSeeTags;
    611   ThrowsTagInfo[] mThrowsTags;
    612   TagInfo[] mBriefTags;
    613   TagInfo[] mReturnTags;
    614   TagInfo[] mDeprecatedTags;
    615   TagInfo[] mUndeprecateTags;
    616   AttrTagInfo[] mAttrTags;
    617   ParsedTagInfo[] mMemberDocTags;
    618   ParsedTagInfo[] mParamDocTags;
    619   ParsedTagInfo[] mReturnDocTags;
    620 
    621   ArrayList<TagInfo> mInlineTagsList = new ArrayList<TagInfo>();
    622   ArrayList<TagInfo> mTagsList = new ArrayList<TagInfo>();
    623   ArrayList<ParamTagInfo> mParamTagsList = new ArrayList<ParamTagInfo>();
    624   ArrayList<SeeTagInfo> mSeeTagsList = new ArrayList<SeeTagInfo>();
    625   ArrayList<ThrowsTagInfo> mThrowsTagsList = new ArrayList<ThrowsTagInfo>();
    626   ArrayList<TagInfo> mBriefTagsList = new ArrayList<TagInfo>();
    627   ArrayList<ParsedTagInfo> mReturnTagsList = new ArrayList<ParsedTagInfo>();
    628   ArrayList<ParsedTagInfo> mDeprecatedTagsList = new ArrayList<ParsedTagInfo>();
    629   ArrayList<TagInfo> mUndeprecateTagsList = new ArrayList<TagInfo>();
    630   ArrayList<AttrTagInfo> mAttrTagsList = new ArrayList<AttrTagInfo>();
    631   ArrayList<ParsedTagInfo> mMemberDocTagsList = new ArrayList<ParsedTagInfo>();
    632   ArrayList<ParsedTagInfo> mParamDocTagsList = new ArrayList<ParsedTagInfo>();
    633   ArrayList<ParsedTagInfo> mReturnDocTagsList = new ArrayList<ParsedTagInfo>();
    634 
    635 }
    636