Comment.java revision 4ec568b934dab55eca62a72e4b1ed48924f87460
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
17package com.google.doclava;
18
19import java.util.regex.Pattern;
20import java.util.regex.Matcher;
21import java.util.ArrayList;
22
23public 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") || name.equals("@linkplain")) {
314      mInlineTagsList.add(new SeeTagInfo(name, "@see", text, mBase, pos));
315    } else if (name.equals("@value")) {
316      mInlineTagsList.add(new SeeTagInfo(name, "@value", text, mBase, pos));
317    } else if (name.equals("@throws") || name.equals("@exception")) {
318      mThrowsTagsList.add(new ThrowsTagInfo("@throws", "@throws", text, mBase, pos));
319    } else if (name.equals("@return")) {
320      mReturnTagsList.add(new ParsedTagInfo("@return", "@return", text, mBase, pos));
321    } else if (name.equals("@deprecated")) {
322      if (text.length() == 0) {
323        Errors.error(Errors.MISSING_COMMENT, pos, "@deprecated tag with no explanatory comment");
324        text = "No replacement.";
325      }
326      mDeprecatedTagsList.add(new ParsedTagInfo("@deprecated", "@deprecated", text, mBase, pos));
327    } else if (name.equals("@literal")) {
328      mInlineTagsList.add(new LiteralTagInfo(text, pos));
329    } else if (name.equals("@code")) {
330      mInlineTagsList.add(new CodeTagInfo(text, pos));
331    } else if (name.equals("@hide") || name.equals("@pending") || name.equals("@doconly")) {
332      // nothing
333    } else if (name.equals("@attr")) {
334      AttrTagInfo tag = new AttrTagInfo("@attr", "@attr", text, mBase, pos);
335      mAttrTagsList.add(tag);
336      Comment c = tag.description();
337      if (c != null) {
338        for (TagInfo t : c.tags()) {
339          mInlineTagsList.add(t);
340        }
341      }
342    } else if (name.equals("@undeprecate")) {
343      mUndeprecateTagsList.add(new TextTagInfo("@undeprecate", "@undeprecate", text, pos));
344    } else if (name.equals("@include") || name.equals("@sample")) {
345      mInlineTagsList.add(new SampleTagInfo(name, "@include", text, mBase, pos));
346    } else {
347      boolean known = false;
348      for (String s : KNOWN_TAGS) {
349        if (s.equals(name)) {
350          known = true;
351          break;
352        }
353      }
354      if (!known) {
355        known = Doclava.knownTags.contains(name);
356      }
357      if (!known) {
358        Errors.error(Errors.UNKNOWN_TAG, pos == null ? null : new SourcePositionInfo(pos),
359            "Unknown tag: " + name);
360      }
361      TagInfo t = new TextTagInfo(name, name, text, pos);
362      if (isInline) {
363        mInlineTagsList.add(t);
364      } else {
365        mTagsList.add(t);
366      }
367    }
368  }
369
370  private void parseBriefTags() {
371    int N = mInlineTagsList.size();
372
373    // look for "@more" tag, which means that we might go past the first sentence.
374    int more = -1;
375    for (int i = 0; i < N; i++) {
376      if (mInlineTagsList.get(i).name().equals("@more")) {
377        more = i;
378      }
379    }
380    if (more >= 0) {
381      for (int i = 0; i < more; i++) {
382        mBriefTagsList.add(mInlineTagsList.get(i));
383      }
384    } else {
385      for (int i = 0; i < N; i++) {
386        TagInfo t = mInlineTagsList.get(i);
387        if (t.name().equals("Text")) {
388          Matcher m = FIRST_SENTENCE.matcher(t.text());
389          if (m.matches()) {
390            String text = m.group(1);
391            TagInfo firstSentenceTag = new TagInfo(t.name(), t.kind(), text, t.position());
392            mBriefTagsList.add(firstSentenceTag);
393            break;
394          }
395        }
396        mBriefTagsList.add(t);
397
398      }
399    }
400  }
401
402  public TagInfo[] tags() {
403    init();
404    return mInlineTags;
405  }
406
407  public TagInfo[] tags(String name) {
408    init();
409    ArrayList<TagInfo> results = new ArrayList<TagInfo>();
410    int N = mInlineTagsList.size();
411    for (int i = 0; i < N; i++) {
412      TagInfo t = mInlineTagsList.get(i);
413      if (t.name().equals(name)) {
414        results.add(t);
415      }
416    }
417    return results.toArray(new TagInfo[results.size()]);
418  }
419
420  public ParamTagInfo[] paramTags() {
421    init();
422    return mParamTags;
423  }
424
425  public SeeTagInfo[] seeTags() {
426    init();
427    return mSeeTags;
428  }
429
430  public ThrowsTagInfo[] throwsTags() {
431    init();
432    return mThrowsTags;
433  }
434
435  public TagInfo[] returnTags() {
436    init();
437    return mReturnTags;
438  }
439
440  public TagInfo[] deprecatedTags() {
441    init();
442    return mDeprecatedTags;
443  }
444
445  public TagInfo[] undeprecateTags() {
446    init();
447    return mUndeprecateTags;
448  }
449
450  public AttrTagInfo[] attrTags() {
451    init();
452    return mAttrTags;
453  }
454
455  public TagInfo[] briefTags() {
456    init();
457    return mBriefTags;
458  }
459
460  public boolean isHidden() {
461    if (mHidden != -1) {
462      return mHidden != 0;
463    } else {
464      if (Doclava.checkLevel(Doclava.SHOW_HIDDEN)) {
465        mHidden = 0;
466        return false;
467      }
468      boolean b = mText.indexOf("@hide") >= 0 || mText.indexOf("@pending") >= 0;
469      mHidden = b ? 1 : 0;
470      return b;
471    }
472  }
473
474  public boolean isDocOnly() {
475    if (mDocOnly != -1) {
476      return mDocOnly != 0;
477    } else {
478      boolean b = (mText != null) && (mText.indexOf("@doconly") >= 0);
479      mDocOnly = b ? 1 : 0;
480      return b;
481    }
482  }
483
484  public boolean isDeprecated() {
485    if (mDeprecated != -1) {
486      return mDeprecated != 0;
487    } else {
488      boolean b = (mText != null) && (mText.indexOf("@deprecated") >= 0);
489      mDeprecated = b ? 1 : 0;
490      return b;
491    }
492  }
493
494  private void init() {
495    if (!mInitialized) {
496      initImpl();
497    }
498  }
499
500  private void initImpl() {
501    isHidden();
502    isDocOnly();
503    isDeprecated();
504
505    // Don't bother parsing text if we aren't generating documentation.
506    if (Doclava.parseComments()) {
507        parseCommentTags(mText);
508        parseBriefTags();
509    } else {
510      // Forces methods to be recognized by findOverriddenMethods in MethodInfo.
511      mInlineTagsList.add(new TextTagInfo("Text", "Text", mText,
512          SourcePositionInfo.add(mPosition, mText, 0)));
513    }
514
515    mText = null;
516    mInitialized = true;
517
518    mInlineTags = mInlineTagsList.toArray(new TagInfo[mInlineTagsList.size()]);
519    mParamTags = mParamTagsList.toArray(new ParamTagInfo[mParamTagsList.size()]);
520    mSeeTags = mSeeTagsList.toArray(new SeeTagInfo[mSeeTagsList.size()]);
521    mThrowsTags = mThrowsTagsList.toArray(new ThrowsTagInfo[mThrowsTagsList.size()]);
522    mReturnTags =
523        ParsedTagInfo.joinTags(mReturnTagsList.toArray(new ParsedTagInfo[mReturnTagsList.size()]));
524    mDeprecatedTags =
525        ParsedTagInfo.joinTags(mDeprecatedTagsList.toArray(new ParsedTagInfo[mDeprecatedTagsList
526            .size()]));
527    mUndeprecateTags = mUndeprecateTagsList.toArray(new TagInfo[mUndeprecateTagsList.size()]);
528    mAttrTags = mAttrTagsList.toArray(new AttrTagInfo[mAttrTagsList.size()]);
529    mBriefTags = mBriefTagsList.toArray(new TagInfo[mBriefTagsList.size()]);
530
531    mParamTagsList = null;
532    mSeeTagsList = null;
533    mThrowsTagsList = null;
534    mReturnTagsList = null;
535    mDeprecatedTagsList = null;
536    mUndeprecateTagsList = null;
537    mAttrTagsList = null;
538    mBriefTagsList = null;
539  }
540
541  boolean mInitialized;
542  int mHidden = -1;
543  int mDocOnly = -1;
544  int mDeprecated = -1;
545  String mText;
546  ContainerInfo mBase;
547  SourcePositionInfo mPosition;
548  int mLine = 1;
549
550  TagInfo[] mInlineTags;
551  TagInfo[] mTags;
552  ParamTagInfo[] mParamTags;
553  SeeTagInfo[] mSeeTags;
554  ThrowsTagInfo[] mThrowsTags;
555  TagInfo[] mBriefTags;
556  TagInfo[] mReturnTags;
557  TagInfo[] mDeprecatedTags;
558  TagInfo[] mUndeprecateTags;
559  AttrTagInfo[] mAttrTags;
560
561  ArrayList<TagInfo> mInlineTagsList = new ArrayList<TagInfo>();
562  ArrayList<TagInfo> mTagsList = new ArrayList<TagInfo>();
563  ArrayList<ParamTagInfo> mParamTagsList = new ArrayList<ParamTagInfo>();
564  ArrayList<SeeTagInfo> mSeeTagsList = new ArrayList<SeeTagInfo>();
565  ArrayList<ThrowsTagInfo> mThrowsTagsList = new ArrayList<ThrowsTagInfo>();
566  ArrayList<TagInfo> mBriefTagsList = new ArrayList<TagInfo>();
567  ArrayList<ParsedTagInfo> mReturnTagsList = new ArrayList<ParsedTagInfo>();
568  ArrayList<ParsedTagInfo> mDeprecatedTagsList = new ArrayList<ParsedTagInfo>();
569  ArrayList<TagInfo> mUndeprecateTagsList = new ArrayList<TagInfo>();
570  ArrayList<AttrTagInfo> mAttrTagsList = new ArrayList<AttrTagInfo>();
571
572
573}
574