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       String tagName = text.substring(start+1, endOfFirstPart);
    206       String tagText = text.substring(startOfSecondPart, end-1);
    207       tag(tagName, tagText, true, pos);
    208   }
    209 
    210 
    211   /**
    212    * Finds the index of the start of a new block comment or -1 if there are
    213    * no more starts.
    214    * @param text The String to search
    215    * @param start the index of the String to start searching
    216    * @return The index of the start of a new block comment or -1 if there are
    217    * no more starts.
    218    */
    219   private int findStartOfBlock(String text, int start) {
    220       // how to detect we're at a new @
    221       //       if the chars to the left of it are \r or \n, we're at one
    222       //       if the chars to the left of it are ' ' or \t, keep looking
    223       //       otherwise, we're in the middle of a block, keep looking
    224       int index = text.indexOf('@', start);
    225 
    226       // no @ in text or index at first position
    227       if (index == -1 ||
    228               (index == 0 && text.length() > 1 && !isWhitespaceChar(text.charAt(index+1)))) {
    229           return index;
    230       }
    231 
    232       index = getPossibleStartOfBlock(text, index);
    233 
    234       int i = index-1; // start at the character immediately to the left of @
    235       char c;
    236       while (i >= 0) {
    237           c = text.charAt(i--);
    238 
    239           // found a new block comment because we're at the beginning of a line
    240           if (c == '\r' || c == '\n') {
    241               return index;
    242           }
    243 
    244           // there is a non whitespace character to the left of the @
    245           // before finding a new line, keep searching
    246           if (c != ' ' && c != '\t') {
    247               index = getPossibleStartOfBlock(text, index+1);
    248               i = index-1;
    249           }
    250 
    251           // some whitespace character, so keep looking, we might be at a new block comment
    252       }
    253 
    254       return -1;
    255   }
    256 
    257   private int getPossibleStartOfBlock(String text, int index) {
    258       while (isWhitespaceChar(text.charAt(index+1)) || !isWhitespaceChar(text.charAt(index-1))) {
    259           index = text.indexOf('@', index+1);
    260 
    261           if (index == -1 || index == text.length()-1) {
    262               return -1;
    263           }
    264       }
    265 
    266       return index;
    267   }
    268 
    269   private void parseBlock(String text, int startOfBlock, int endOfBlock) {
    270       SourcePositionInfo pos = SourcePositionInfo.add(mPosition, mText, startOfBlock);
    271       int index = startOfBlock;
    272 
    273       for (char c = text.charAt(index);
    274               index < endOfBlock && !isWhitespaceChar(c); c = text.charAt(index++)) {}
    275 
    276       //
    277       if (index == startOfBlock+1) {
    278           return;
    279       }
    280 
    281       int endOfFirstPart = index-1;
    282       if (index == endOfBlock) {
    283           // TODO - should value be null or ""
    284           tag(text.substring(startOfBlock,
    285                   findEndOfMainOrBlock(text, startOfBlock, index)), "", false, pos);
    286           return;
    287       }
    288 
    289 
    290       // get to beginning of tag value
    291       while (index < endOfBlock && isWhitespaceChar(text.charAt(index++))) {}
    292       int startOfSecondPart = index-1;
    293 
    294       tag(text.substring(startOfBlock, endOfFirstPart),
    295               text.substring(startOfSecondPart, endOfBlock), false, pos);
    296   }
    297 
    298   private boolean isWhitespaceChar(char c) {
    299       return c == ' ' || c == '\r' || c == '\t' || c == '\n';
    300   }
    301 
    302   private void tag(String name, String text, boolean isInline, SourcePositionInfo pos) {
    303     /*
    304      * String s = isInline ? "inline" : "outofline"; System.out.println("---> " + s + " name=[" +
    305      * name + "] text=[" + text + "]");
    306      */
    307     if (name == null) {
    308       mInlineTagsList.add(new TextTagInfo("Text", "Text", text, pos));
    309     } else if (name.equals("@param")) {
    310       mParamTagsList.add(new ParamTagInfo("@param", "@param", text, mBase, pos));
    311     } else if (name.equals("@see")) {
    312       mSeeTagsList.add(new SeeTagInfo("@see", "@see", text, mBase, pos));
    313     } else if (name.equals("@link")) {
    314       mInlineTagsList.add(new SeeTagInfo(name, "@see", text, mBase, pos));
    315     } else if (name.equals("@linkplain")) {
    316       mInlineTagsList.add(new SeeTagInfo(name, "@linkplain", text, mBase, pos));
    317     } else if (name.equals("@value")) {
    318       mInlineTagsList.add(new SeeTagInfo(name, "@value", text, mBase, pos));
    319     } else if (name.equals("@throws") || name.equals("@exception")) {
    320       mThrowsTagsList.add(new ThrowsTagInfo("@throws", "@throws", text, mBase, pos));
    321     } else if (name.equals("@return")) {
    322       mReturnTagsList.add(new ParsedTagInfo("@return", "@return", text, mBase, pos));
    323     } else if (name.equals("@deprecated")) {
    324       if (text.length() == 0) {
    325         Errors.error(Errors.MISSING_COMMENT, pos, "@deprecated tag with no explanatory comment");
    326         text = "No replacement.";
    327       }
    328       mDeprecatedTagsList.add(new ParsedTagInfo("@deprecated", "@deprecated", text, mBase, pos));
    329     } else if (name.equals("@literal")) {
    330       mInlineTagsList.add(new LiteralTagInfo(text, pos));
    331     } else if (name.equals("@code")) {
    332       mInlineTagsList.add(new CodeTagInfo(text, pos));
    333     } else if (name.equals("@hide") || name.equals("@pending") || name.equals("@doconly")) {
    334       // nothing
    335     } else if (name.equals("@attr")) {
    336       AttrTagInfo tag = new AttrTagInfo("@attr", "@attr", text, mBase, pos);
    337       mAttrTagsList.add(tag);
    338       Comment c = tag.description();
    339       if (c != null) {
    340         for (TagInfo t : c.tags()) {
    341           mInlineTagsList.add(t);
    342         }
    343       }
    344     } else if (name.equals("@undeprecate")) {
    345       mUndeprecateTagsList.add(new TextTagInfo("@undeprecate", "@undeprecate", text, pos));
    346     } else if (name.equals("@include") || name.equals("@sample")) {
    347       mInlineTagsList.add(new SampleTagInfo(name, "@include", text, mBase, pos));
    348     } else {
    349       boolean known = false;
    350       for (String s : KNOWN_TAGS) {
    351         if (s.equals(name)) {
    352           known = true;
    353           break;
    354         }
    355       }
    356       if (!known) {
    357         known = Doclava.knownTags.contains(name);
    358       }
    359       if (!known) {
    360         Errors.error(Errors.UNKNOWN_TAG, pos == null ? null : new SourcePositionInfo(pos),
    361             "Unknown tag: " + name);
    362       }
    363       TagInfo t = new TextTagInfo(name, name, text, pos);
    364       if (isInline) {
    365         mInlineTagsList.add(t);
    366       } else {
    367         mTagsList.add(t);
    368       }
    369     }
    370   }
    371 
    372   private void parseBriefTags() {
    373     int N = mInlineTagsList.size();
    374 
    375     // look for "@more" tag, which means that we might go past the first sentence.
    376     int more = -1;
    377     for (int i = 0; i < N; i++) {
    378       if (mInlineTagsList.get(i).name().equals("@more")) {
    379         more = i;
    380       }
    381     }
    382     if (more >= 0) {
    383       for (int i = 0; i < more; i++) {
    384         mBriefTagsList.add(mInlineTagsList.get(i));
    385       }
    386     } else {
    387       for (int i = 0; i < N; i++) {
    388         TagInfo t = mInlineTagsList.get(i);
    389         if (t.name().equals("Text")) {
    390           Matcher m = FIRST_SENTENCE.matcher(t.text());
    391           if (m.matches()) {
    392             String text = m.group(1);
    393             TagInfo firstSentenceTag = new TagInfo(t.name(), t.kind(), text, t.position());
    394             mBriefTagsList.add(firstSentenceTag);
    395             break;
    396           }
    397         }
    398         mBriefTagsList.add(t);
    399 
    400       }
    401     }
    402   }
    403 
    404   public TagInfo[] tags() {
    405     init();
    406     return mInlineTags;
    407   }
    408 
    409   public TagInfo[] tags(String name) {
    410     init();
    411     ArrayList<TagInfo> results = new ArrayList<TagInfo>();
    412     int N = mInlineTagsList.size();
    413     for (int i = 0; i < N; i++) {
    414       TagInfo t = mInlineTagsList.get(i);
    415       if (t.name().equals(name)) {
    416         results.add(t);
    417       }
    418     }
    419     return results.toArray(new TagInfo[results.size()]);
    420   }
    421 
    422   public ParamTagInfo[] paramTags() {
    423     init();
    424     return mParamTags;
    425   }
    426 
    427   public SeeTagInfo[] seeTags() {
    428     init();
    429     return mSeeTags;
    430   }
    431 
    432   public ThrowsTagInfo[] throwsTags() {
    433     init();
    434     return mThrowsTags;
    435   }
    436 
    437   public TagInfo[] returnTags() {
    438     init();
    439     return mReturnTags;
    440   }
    441 
    442   public TagInfo[] deprecatedTags() {
    443     init();
    444     return mDeprecatedTags;
    445   }
    446 
    447   public TagInfo[] undeprecateTags() {
    448     init();
    449     return mUndeprecateTags;
    450   }
    451 
    452   public AttrTagInfo[] attrTags() {
    453     init();
    454     return mAttrTags;
    455   }
    456 
    457   public TagInfo[] briefTags() {
    458     init();
    459     return mBriefTags;
    460   }
    461 
    462   public boolean isHidden() {
    463     if (mHidden == null) {
    464       mHidden = !Doclava.checkLevel(Doclava.SHOW_HIDDEN) &&
    465           (mText != null) && (mText.indexOf("@hide") >= 0 || mText.indexOf("@pending") >= 0);
    466     }
    467     return mHidden;
    468   }
    469 
    470   public boolean isRemoved() {
    471     if (mRemoved == null) {
    472         mRemoved = !Doclava.checkLevel(Doclava.SHOW_HIDDEN) &&
    473             (mText != null) && (mText.indexOf("@removed") >= 0);
    474     }
    475 
    476     return mRemoved;
    477   }
    478 
    479   public boolean isDocOnly() {
    480     if (mDocOnly == null) {
    481       mDocOnly = (mText != null) && (mText.indexOf("@doconly") >= 0);
    482     }
    483     return mDocOnly;
    484   }
    485 
    486   public boolean isDeprecated() {
    487     if (mDeprecated == null) {
    488       mDeprecated = (mText != null) && (mText.indexOf("@deprecated") >= 0);
    489     }
    490 
    491     return mDeprecated;
    492   }
    493 
    494   private void init() {
    495     if (!mInitialized) {
    496       initImpl();
    497     }
    498   }
    499 
    500   private void initImpl() {
    501     isHidden();
    502     isRemoved();
    503     isDocOnly();
    504     isDeprecated();
    505 
    506     // Don't bother parsing text if we aren't generating documentation.
    507     if (Doclava.parseComments()) {
    508         parseCommentTags(mText);
    509         parseBriefTags();
    510     } else {
    511       // Forces methods to be recognized by findOverriddenMethods in MethodInfo.
    512       mInlineTagsList.add(new TextTagInfo("Text", "Text", mText,
    513           SourcePositionInfo.add(mPosition, mText, 0)));
    514     }
    515 
    516     mText = null;
    517     mInitialized = true;
    518 
    519     mInlineTags = mInlineTagsList.toArray(new TagInfo[mInlineTagsList.size()]);
    520     mParamTags = mParamTagsList.toArray(new ParamTagInfo[mParamTagsList.size()]);
    521     mSeeTags = mSeeTagsList.toArray(new SeeTagInfo[mSeeTagsList.size()]);
    522     mThrowsTags = mThrowsTagsList.toArray(new ThrowsTagInfo[mThrowsTagsList.size()]);
    523     mReturnTags =
    524         ParsedTagInfo.joinTags(mReturnTagsList.toArray(new ParsedTagInfo[mReturnTagsList.size()]));
    525     mDeprecatedTags =
    526         ParsedTagInfo.joinTags(mDeprecatedTagsList.toArray(new ParsedTagInfo[mDeprecatedTagsList
    527             .size()]));
    528     mUndeprecateTags = mUndeprecateTagsList.toArray(new TagInfo[mUndeprecateTagsList.size()]);
    529     mAttrTags = mAttrTagsList.toArray(new AttrTagInfo[mAttrTagsList.size()]);
    530     mBriefTags = mBriefTagsList.toArray(new TagInfo[mBriefTagsList.size()]);
    531 
    532     mParamTagsList = null;
    533     mSeeTagsList = null;
    534     mThrowsTagsList = null;
    535     mReturnTagsList = null;
    536     mDeprecatedTagsList = null;
    537     mUndeprecateTagsList = null;
    538     mAttrTagsList = null;
    539     mBriefTagsList = null;
    540   }
    541 
    542   boolean mInitialized;
    543   Boolean mHidden = null;
    544   Boolean mRemoved = null;
    545   Boolean mDocOnly = null;
    546   Boolean mDeprecated = null;
    547   String mText;
    548   ContainerInfo mBase;
    549   SourcePositionInfo mPosition;
    550   int mLine = 1;
    551 
    552   TagInfo[] mInlineTags;
    553   TagInfo[] mTags;
    554   ParamTagInfo[] mParamTags;
    555   SeeTagInfo[] mSeeTags;
    556   ThrowsTagInfo[] mThrowsTags;
    557   TagInfo[] mBriefTags;
    558   TagInfo[] mReturnTags;
    559   TagInfo[] mDeprecatedTags;
    560   TagInfo[] mUndeprecateTags;
    561   AttrTagInfo[] mAttrTags;
    562 
    563   ArrayList<TagInfo> mInlineTagsList = new ArrayList<TagInfo>();
    564   ArrayList<TagInfo> mTagsList = new ArrayList<TagInfo>();
    565   ArrayList<ParamTagInfo> mParamTagsList = new ArrayList<ParamTagInfo>();
    566   ArrayList<SeeTagInfo> mSeeTagsList = new ArrayList<SeeTagInfo>();
    567   ArrayList<ThrowsTagInfo> mThrowsTagsList = new ArrayList<ThrowsTagInfo>();
    568   ArrayList<TagInfo> mBriefTagsList = new ArrayList<TagInfo>();
    569   ArrayList<ParsedTagInfo> mReturnTagsList = new ArrayList<ParsedTagInfo>();
    570   ArrayList<ParsedTagInfo> mDeprecatedTagsList = new ArrayList<ParsedTagInfo>();
    571   ArrayList<TagInfo> mUndeprecateTagsList = new ArrayList<TagInfo>();
    572   ArrayList<AttrTagInfo> mAttrTagsList = new ArrayList<AttrTagInfo>();
    573 
    574 
    575 }
    576