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