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 Comment(String text, ContainerInfo base, SourcePositionInfo sp)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 parseCommentTags(String text)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 findEndOfMainOrBlock(String text, int start, int end)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 parseMainDescription(String mainDescription, int start, int end)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 findStartIndexOfInlineTag(String text, int fromIndex, int toIndex)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 findEndIndexOfInlineTag(String text, int fromIndex, int toIndex)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 parseInlineTag(String text, int start, int end, SourcePositionInfo pos)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 */ findStartOfBlock(String text, int start)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 getPossibleStartOfBlock(String text, int index)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 parseBlock(String text, int startOfBlock, int endOfBlock)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 isWhitespaceChar(char c)298 private boolean isWhitespaceChar(char c) { 299 return c == ' ' || c == '\r' || c == '\t' || c == '\n'; 300 } 301 tag(String name, String text, boolean isInline, SourcePositionInfo pos)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 parseBriefTags()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 tags()402 public TagInfo[] tags() { 403 init(); 404 return mInlineTags; 405 } 406 tags(String name)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 paramTags()420 public ParamTagInfo[] paramTags() { 421 init(); 422 return mParamTags; 423 } 424 seeTags()425 public SeeTagInfo[] seeTags() { 426 init(); 427 return mSeeTags; 428 } 429 throwsTags()430 public ThrowsTagInfo[] throwsTags() { 431 init(); 432 return mThrowsTags; 433 } 434 returnTags()435 public TagInfo[] returnTags() { 436 init(); 437 return mReturnTags; 438 } 439 deprecatedTags()440 public TagInfo[] deprecatedTags() { 441 init(); 442 return mDeprecatedTags; 443 } 444 undeprecateTags()445 public TagInfo[] undeprecateTags() { 446 init(); 447 return mUndeprecateTags; 448 } 449 attrTags()450 public AttrTagInfo[] attrTags() { 451 init(); 452 return mAttrTags; 453 } 454 briefTags()455 public TagInfo[] briefTags() { 456 init(); 457 return mBriefTags; 458 } 459 isHidden()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 isDocOnly()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 isDeprecated()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 init()494 private void init() { 495 if (!mInitialized) { 496 initImpl(); 497 } 498 } 499 initImpl()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