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 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 tag(text.substring(start+1, endOfFirstPart), 206 text.substring(startOfSecondPart, end-1), true, pos); 207 } 208 209 210 /** 211 * Finds the index of the start of a new block comment or -1 if there are 212 * no more starts. 213 * @param text The String to search 214 * @param start the index of the String to start searching 215 * @return The index of the start of a new block comment or -1 if there are 216 * no more starts. 217 */ 218 private int findStartOfBlock(String text, int start) { 219 // how to detect we're at a new @ 220 // if the chars to the left of it are \r or \n, we're at one 221 // if the chars to the left of it are ' ' or \t, keep looking 222 // otherwise, we're in the middle of a block, keep looking 223 int index = text.indexOf('@', start); 224 225 // no @ in text or index at first position 226 if (index == -1 || 227 (index == 0 && text.length() > 1 && !isWhitespaceChar(text.charAt(index+1)))) { 228 return index; 229 } 230 231 index = getPossibleStartOfBlock(text, index); 232 233 int i = index-1; // start at the character immediately to the left of @ 234 char c; 235 while (i >= 0) { 236 c = text.charAt(i--); 237 238 // found a new block comment because we're at the beginning of a line 239 if (c == '\r' || c == '\n') { 240 return index; 241 } 242 243 // there is a non whitespace character to the left of the @ 244 // before finding a new line, keep searching 245 if (c != ' ' && c != '\t') { 246 index = getPossibleStartOfBlock(text, index+1); 247 i = index-1; 248 } 249 250 // some whitespace character, so keep looking, we might be at a new block comment 251 } 252 253 return -1; 254 } 255 256 private int getPossibleStartOfBlock(String text, int index) { 257 while (isWhitespaceChar(text.charAt(index+1)) || !isWhitespaceChar(text.charAt(index-1))) { 258 index = text.indexOf('@', index+1); 259 260 if (index == -1 || index == text.length()-1) { 261 return -1; 262 } 263 } 264 265 return index; 266 } 267 268 private void parseBlock(String text, int startOfBlock, int endOfBlock) { 269 SourcePositionInfo pos = SourcePositionInfo.add(mPosition, mText, startOfBlock); 270 int index = startOfBlock; 271 272 for (char c = text.charAt(index); 273 index < endOfBlock && !isWhitespaceChar(c); c = text.charAt(index++)) {} 274 275 // 276 if (index == startOfBlock+1) { 277 return; 278 } 279 280 int endOfFirstPart = index-1; 281 if (index == endOfBlock) { 282 // TODO - should value be null or "" 283 tag(text.substring(startOfBlock, 284 findEndOfMainOrBlock(text, startOfBlock, index)), "", false, pos); 285 return; 286 } 287 288 289 // get to beginning of tag value 290 while (index < endOfBlock && isWhitespaceChar(text.charAt(index++))) {} 291 int startOfSecondPart = index-1; 292 293 tag(text.substring(startOfBlock, endOfFirstPart), 294 text.substring(startOfSecondPart, endOfBlock), false, pos); 295 } 296 297 private boolean isWhitespaceChar(char c) { 298 return c == ' ' || c == '\r' || c == '\t' || c == '\n'; 299 } 300 301 private void tag(String name, String text, boolean isInline, SourcePositionInfo pos) { 302 /* 303 * String s = isInline ? "inline" : "outofline"; System.out.println("---> " + s + " name=[" + 304 * name + "] text=[" + text + "]"); 305 */ 306 if (name == null) { 307 mInlineTagsList.add(new TextTagInfo("Text", "Text", text, pos)); 308 } else if (name.equals("@param")) { 309 mParamTagsList.add(new ParamTagInfo("@param", "@param", text, mBase, pos)); 310 } else if (name.equals("@see")) { 311 mSeeTagsList.add(new SeeTagInfo("@see", "@see", text, mBase, pos)); 312 } else if (name.equals("@link") || name.equals("@linkplain")) { 313 mInlineTagsList.add(new SeeTagInfo(name, "@see", text, mBase, pos)); 314 } else if (name.equals("@throws") || name.equals("@exception")) { 315 mThrowsTagsList.add(new ThrowsTagInfo("@throws", "@throws", text, mBase, pos)); 316 } else if (name.equals("@return")) { 317 mReturnTagsList.add(new ParsedTagInfo("@return", "@return", text, mBase, pos)); 318 } else if (name.equals("@deprecated")) { 319 if (text.length() == 0) { 320 Errors.error(Errors.MISSING_COMMENT, pos, "@deprecated tag with no explanatory comment"); 321 text = "No replacement."; 322 } 323 mDeprecatedTagsList.add(new ParsedTagInfo("@deprecated", "@deprecated", text, mBase, pos)); 324 } else if (name.equals("@literal")) { 325 mInlineTagsList.add(new LiteralTagInfo(text, pos)); 326 } else if (name.equals("@code")) { 327 mInlineTagsList.add(new CodeTagInfo(text, pos)); 328 } else if (name.equals("@hide") || name.equals("@pending") || name.equals("@doconly")) { 329 // nothing 330 } else if (name.equals("@attr")) { 331 AttrTagInfo tag = new AttrTagInfo("@attr", "@attr", text, mBase, pos); 332 mAttrTagsList.add(tag); 333 Comment c = tag.description(); 334 if (c != null) { 335 for (TagInfo t : c.tags()) { 336 mInlineTagsList.add(t); 337 } 338 } 339 } else if (name.equals("@undeprecate")) { 340 mUndeprecateTagsList.add(new TextTagInfo("@undeprecate", "@undeprecate", text, pos)); 341 } else if (name.equals("@include") || name.equals("@sample")) { 342 mInlineTagsList.add(new SampleTagInfo(name, "@include", text, mBase, pos)); 343 } else { 344 boolean known = false; 345 for (String s : KNOWN_TAGS) { 346 if (s.equals(name)) { 347 known = true; 348 break; 349 } 350 } 351 if (!known) { 352 known = Doclava.knownTags.contains(name); 353 } 354 if (!known) { 355 Errors.error(Errors.UNKNOWN_TAG, pos == null ? null : new SourcePositionInfo(pos), 356 "Unknown tag: " + name); 357 } 358 TagInfo t = new TextTagInfo(name, name, text, pos); 359 if (isInline) { 360 mInlineTagsList.add(t); 361 } else { 362 mTagsList.add(t); 363 } 364 } 365 } 366 367 private void parseBriefTags() { 368 int N = mInlineTagsList.size(); 369 370 // look for "@more" tag, which means that we might go past the first sentence. 371 int more = -1; 372 for (int i = 0; i < N; i++) { 373 if (mInlineTagsList.get(i).name().equals("@more")) { 374 more = i; 375 } 376 } 377 if (more >= 0) { 378 for (int i = 0; i < more; i++) { 379 mBriefTagsList.add(mInlineTagsList.get(i)); 380 } 381 } else { 382 for (int i = 0; i < N; i++) { 383 TagInfo t = mInlineTagsList.get(i); 384 if (t.name().equals("Text")) { 385 Matcher m = FIRST_SENTENCE.matcher(t.text()); 386 if (m.matches()) { 387 String text = m.group(1); 388 TagInfo firstSentenceTag = new TagInfo(t.name(), t.kind(), text, t.position()); 389 mBriefTagsList.add(firstSentenceTag); 390 break; 391 } 392 } 393 mBriefTagsList.add(t); 394 395 } 396 } 397 } 398 399 public TagInfo[] tags() { 400 init(); 401 return mInlineTags; 402 } 403 404 public TagInfo[] tags(String name) { 405 init(); 406 ArrayList<TagInfo> results = new ArrayList<TagInfo>(); 407 int N = mInlineTagsList.size(); 408 for (int i = 0; i < N; i++) { 409 TagInfo t = mInlineTagsList.get(i); 410 if (t.name().equals(name)) { 411 results.add(t); 412 } 413 } 414 return results.toArray(new TagInfo[results.size()]); 415 } 416 417 public ParamTagInfo[] paramTags() { 418 init(); 419 return mParamTags; 420 } 421 422 public SeeTagInfo[] seeTags() { 423 init(); 424 return mSeeTags; 425 } 426 427 public ThrowsTagInfo[] throwsTags() { 428 init(); 429 return mThrowsTags; 430 } 431 432 public TagInfo[] returnTags() { 433 init(); 434 return mReturnTags; 435 } 436 437 public TagInfo[] deprecatedTags() { 438 init(); 439 return mDeprecatedTags; 440 } 441 442 public TagInfo[] undeprecateTags() { 443 init(); 444 return mUndeprecateTags; 445 } 446 447 public AttrTagInfo[] attrTags() { 448 init(); 449 return mAttrTags; 450 } 451 452 public TagInfo[] briefTags() { 453 init(); 454 return mBriefTags; 455 } 456 457 public boolean isHidden() { 458 if (mHidden != -1) { 459 return mHidden != 0; 460 } else { 461 if (Doclava.checkLevel(Doclava.SHOW_HIDDEN)) { 462 mHidden = 0; 463 return false; 464 } 465 boolean b = mText.indexOf("@hide") >= 0 || mText.indexOf("@pending") >= 0; 466 mHidden = b ? 1 : 0; 467 return b; 468 } 469 } 470 471 public boolean isDocOnly() { 472 if (mDocOnly != -1) { 473 return mDocOnly != 0; 474 } else { 475 boolean b = (mText != null) && (mText.indexOf("@doconly") >= 0); 476 mDocOnly = b ? 1 : 0; 477 return b; 478 } 479 } 480 481 public boolean isDeprecated() { 482 if (mDeprecated != -1) { 483 return mDeprecated != 0; 484 } else { 485 boolean b = (mText != null) && (mText.indexOf("@deprecated") >= 0); 486 mDeprecated = b ? 1 : 0; 487 return b; 488 } 489 } 490 491 private void init() { 492 if (!mInitialized) { 493 initImpl(); 494 } 495 } 496 497 private void initImpl() { 498 isHidden(); 499 isDocOnly(); 500 isDeprecated(); 501 502 // Don't bother parsing text if we aren't generating documentation. 503 if (Doclava.parseComments()) { 504 parseCommentTags(mText); 505 parseBriefTags(); 506 } else { 507 // Forces methods to be recognized by findOverriddenMethods in MethodInfo. 508 mInlineTagsList.add(new TextTagInfo("Text", "Text", mText, 509 SourcePositionInfo.add(mPosition, mText, 0))); 510 } 511 512 mText = null; 513 mInitialized = true; 514 515 mInlineTags = mInlineTagsList.toArray(new TagInfo[mInlineTagsList.size()]); 516 mParamTags = mParamTagsList.toArray(new ParamTagInfo[mParamTagsList.size()]); 517 mSeeTags = mSeeTagsList.toArray(new SeeTagInfo[mSeeTagsList.size()]); 518 mThrowsTags = mThrowsTagsList.toArray(new ThrowsTagInfo[mThrowsTagsList.size()]); 519 mReturnTags = 520 ParsedTagInfo.joinTags(mReturnTagsList.toArray(new ParsedTagInfo[mReturnTagsList.size()])); 521 mDeprecatedTags = 522 ParsedTagInfo.joinTags(mDeprecatedTagsList.toArray(new ParsedTagInfo[mDeprecatedTagsList 523 .size()])); 524 mUndeprecateTags = mUndeprecateTagsList.toArray(new TagInfo[mUndeprecateTagsList.size()]); 525 mAttrTags = mAttrTagsList.toArray(new AttrTagInfo[mAttrTagsList.size()]); 526 mBriefTags = mBriefTagsList.toArray(new TagInfo[mBriefTagsList.size()]); 527 528 mParamTagsList = null; 529 mSeeTagsList = null; 530 mThrowsTagsList = null; 531 mReturnTagsList = null; 532 mDeprecatedTagsList = null; 533 mUndeprecateTagsList = null; 534 mAttrTagsList = null; 535 mBriefTagsList = null; 536 } 537 538 boolean mInitialized; 539 int mHidden = -1; 540 int mDocOnly = -1; 541 int mDeprecated = -1; 542 String mText; 543 ContainerInfo mBase; 544 SourcePositionInfo mPosition; 545 int mLine = 1; 546 547 TagInfo[] mInlineTags; 548 TagInfo[] mTags; 549 ParamTagInfo[] mParamTags; 550 SeeTagInfo[] mSeeTags; 551 ThrowsTagInfo[] mThrowsTags; 552 TagInfo[] mBriefTags; 553 TagInfo[] mReturnTags; 554 TagInfo[] mDeprecatedTags; 555 TagInfo[] mUndeprecateTags; 556 AttrTagInfo[] mAttrTags; 557 558 ArrayList<TagInfo> mInlineTagsList = new ArrayList<TagInfo>(); 559 ArrayList<TagInfo> mTagsList = new ArrayList<TagInfo>(); 560 ArrayList<ParamTagInfo> mParamTagsList = new ArrayList<ParamTagInfo>(); 561 ArrayList<SeeTagInfo> mSeeTagsList = new ArrayList<SeeTagInfo>(); 562 ArrayList<ThrowsTagInfo> mThrowsTagsList = new ArrayList<ThrowsTagInfo>(); 563 ArrayList<TagInfo> mBriefTagsList = new ArrayList<TagInfo>(); 564 ArrayList<ParsedTagInfo> mReturnTagsList = new ArrayList<ParsedTagInfo>(); 565 ArrayList<ParsedTagInfo> mDeprecatedTagsList = new ArrayList<ParsedTagInfo>(); 566 ArrayList<TagInfo> mUndeprecateTagsList = new ArrayList<TagInfo>(); 567 ArrayList<AttrTagInfo> mAttrTagsList = new ArrayList<AttrTagInfo>(); 568 569 570 } 571