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