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