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