/* * Copyright (C) 2010 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.doclava; import java.util.regex.Pattern; import java.util.regex.Matcher; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Set; public class Comment { static final Pattern FIRST_SENTENCE = Pattern.compile("((.*?)\\.)[ \t\r\n\\<](.*)", Pattern.DOTALL); private static final Set KNOWN_TAGS = new HashSet(Arrays.asList(new String[] { "@apiNote", "@author", "@version", //not used by metalava for Android docs (see @apiSince) "@since", //value is an Android API level (set automatically by metalava) "@apiSince", // value is " " (set automatically by metalava) "@sdkExtSince", "@deprecated", //value is an Android API level (set automatically by metalava) "@deprecatedSince", "@undeprecate", "@docRoot", "@sdkCurrent", "@inheritDoc", "@more", "@samplecode", "@sample", "@include", "@serial", "@implNote", "@implSpec", "@usesMathJax", })); public Comment(String text, ContainerInfo base, SourcePositionInfo sp) { mText = text; mBase = base; // sp now points to the end of the text, not the beginning! mPosition = SourcePositionInfo.findBeginning(sp, text); } private void parseCommentTags(String text) { int i = 0; int length = text.length(); while (i < length && isWhitespaceChar(text.charAt(i++))) {} if (i <= 0) { return; } text = text.substring(i-1); length = text.length(); if ("".equals(text)) { return; } int start = 0; int end = findStartOfBlock(text, start); // possible scenarios // main and block(s) // main only (end == -1) // block(s) only (end == 0) switch (end) { case -1: // main only parseMainDescription(text, start, length); return; case 0: // block(s) only break; default: // main and block // find end of main because end is really the beginning of @ parseMainDescription(text, start, findEndOfMainOrBlock(text, start, end)); break; } // parse blocks for (start = end; start < length; start = end) { end = findStartOfBlock(text, start+1); if (end == -1) { parseBlock(text, start, length); break; } else { parseBlock(text, start, findEndOfMainOrBlock(text, start, end)); } } // for each block // make block parts // end is either next @ at beginning of line or end of text } private int findEndOfMainOrBlock(String text, int start, int end) { for (int i = end-1; i >= start; i--) { if (!isWhitespaceChar(text.charAt(i))) { end = i+1; break; } } return end; } private void parseMainDescription(String mainDescription, int start, int end) { if (mainDescription == null) { return; } SourcePositionInfo pos = SourcePositionInfo.add(mPosition, mText, 0); while (start < end) { int startOfInlineTag = findStartIndexOfInlineTag(mainDescription, start, end); // if there are no more tags if (startOfInlineTag == -1) { tag(null, mainDescription.substring(start, end), true, pos); return; } //int endOfInlineTag = mainDescription.indexOf('}', startOfInlineTag); int endOfInlineTag = findEndIndexOfInlineTag(mainDescription, startOfInlineTag, end); // if there was only beginning tag if (endOfInlineTag == -1) { // parse all of main as one tag tag(null, mainDescription.substring(start, end), true, pos); return; } endOfInlineTag++; // add one to make it a proper ending index // do first part without an inline tag - ie, just plaintext tag(null, mainDescription.substring(start, startOfInlineTag), true, pos); // parse the rest of this section, the inline tag parseInlineTag(mainDescription, startOfInlineTag, endOfInlineTag, pos); // keep going start = endOfInlineTag; } } private int findStartIndexOfInlineTag(String text, int fromIndex, int toIndex) { for (int i = fromIndex; i < (toIndex-3); i++) { if (text.charAt(i) == '{' && text.charAt(i+1) == '@' && !isWhitespaceChar(text.charAt(i+2))) { return i; } } return -1; } private int findEndIndexOfInlineTag(String text, int fromIndex, int toIndex) { int braceDepth = 0; for (int i = fromIndex; i < toIndex; i++) { if (text.charAt(i) == '{') { braceDepth++; } else if (text.charAt(i) == '}') { braceDepth--; if (braceDepth == 0) { return i; } } } return -1; } private void parseInlineTag(String text, int start, int end, SourcePositionInfo pos) { int index = start+1; //int len = text.length(); char c = text.charAt(index); // find the end of the tag name "@something" // need to do something special if we have '}' while (index < end && !isWhitespaceChar(c)) { // if this tag has no value, just return with tag name only if (c == '}') { // TODO - should value be "" or null? tag(text.substring(start+1, end), null, true, pos); return; } c = text.charAt(index++); } // don't parse things that don't have at least one extra character after @ // probably should be plus 3 // TODO - remove this - think it's fixed by change in parseMainDescription if (index == start+3) { return; } int endOfFirstPart = index-1; // get to beginning of tag value while (index < end && isWhitespaceChar(text.charAt(index++))) {} int startOfSecondPart = index-1; // +1 to get rid of opening brace and -1 to get rid of closing brace // maybe i wanna make this more elegant String tagName = text.substring(start+1, endOfFirstPart); String tagText = text.substring(startOfSecondPart, end-1); tag(tagName, tagText, true, pos); } /** * Finds the index of the start of a new block comment or -1 if there are * no more starts. * @param text The String to search * @param start the index of the String to start searching * @return The index of the start of a new block comment or -1 if there are * no more starts. */ private int findStartOfBlock(String text, int start) { // how to detect we're at a new @ // if the chars to the left of it are \r or \n, we're at one // if the chars to the left of it are ' ' or \t, keep looking // otherwise, we're in the middle of a block, keep looking int index = text.indexOf('@', start); // no @ in text or index at first position if (index == -1 || (index == 0 && text.length() > 1 && !isWhitespaceChar(text.charAt(index+1)))) { return index; } index = getPossibleStartOfBlock(text, index); int i = index-1; // start at the character immediately to the left of @ char c; while (i >= 0) { c = text.charAt(i--); // found a new block comment because we're at the beginning of a line if (c == '\r' || c == '\n') { return index; } // there is a non whitespace character to the left of the @ // before finding a new line, keep searching if (c != ' ' && c != '\t') { index = getPossibleStartOfBlock(text, index+1); i = index-1; } // some whitespace character, so keep looking, we might be at a new block comment } return -1; } private int getPossibleStartOfBlock(String text, int index) { while (isWhitespaceChar(text.charAt(index+1)) || !isWhitespaceChar(text.charAt(index-1))) { index = text.indexOf('@', index+1); if (index == -1 || index == text.length()-1) { return -1; } } return index; } private void parseBlock(String text, int startOfBlock, int endOfBlock) { SourcePositionInfo pos = SourcePositionInfo.add(mPosition, mText, startOfBlock); int index = startOfBlock; for (char c = text.charAt(index); index < endOfBlock && !isWhitespaceChar(c); c = text.charAt(index++)) {} if (index == startOfBlock+1) { return; } int endOfFirstPart = index-1; if (index == endOfBlock) { // TODO - should value be null or "" tag(text.substring(startOfBlock, findEndOfMainOrBlock(text, startOfBlock, index)), "", false, pos); return; } // get to beginning of tag value while (index < endOfBlock && isWhitespaceChar(text.charAt(index++))) {} int startOfSecondPart = index-1; tag(text.substring(startOfBlock, endOfFirstPart), text.substring(startOfSecondPart, endOfBlock), false, pos); } private boolean isWhitespaceChar(char c) { switch (c) { case ' ': case '\r': case '\t': case '\n': return true; } return false; } private void tag(String name, String text, boolean isInline, SourcePositionInfo pos) { /* * String s = isInline ? "inline" : "outofline"; System.out.println("---> " + s + " name=[" + * name + "] text=[" + text + "]"); */ if (name == null) { mInlineTagsList.add(new TextTagInfo("Text", "Text", text, pos)); } else if (name.equals("@param")) { mParamTagsList.add(new ParamTagInfo("@param", "@param", text, mBase, pos)); } else if (name.equals("@apiSince")) { setApiSince(text); } else if (name.equals("@sdkExtSince")) { if (getSdkExtSince() != null) { Errors.error(Errors.MULTIPLE_SDK_EXT_INFO, pos, "API symbol has multiple @sdkExtSince javadoc comments"); } setSdkExtSince(text); } else if (name.equals("@deprecatedSince")) { setDeprecatedSince(text); } else if (name.equals("@see")) { mSeeTagsList.add(new SeeTagInfo("@see", "@see", text, mBase, pos)); } else if (name.equals("@link")) { if (Doclava.DEVSITE_IGNORE_JDLINKS) { TagInfo linkTag = new TextTagInfo(name, name, text, pos); mInlineTagsList.add(linkTag); } else { mInlineTagsList.add(new SeeTagInfo(name, "@see", text, mBase, pos)); } } else if (name.equals("@linkplain")) { mInlineTagsList.add(new SeeTagInfo(name, "@linkplain", text, mBase, pos)); } else if (name.equals("@value")) { mInlineTagsList.add(new SeeTagInfo(name, "@value", text, mBase, pos)); } else if (name.equals("@throws") || name.equals("@exception")) { mThrowsTagsList.add(new ThrowsTagInfo("@throws", "@throws", text, mBase, pos)); } else if (name.equals("@return")) { mReturnTagsList.add(new ParsedTagInfo("@return", "@return", text, mBase, pos)); } else if (name.equals("@deprecated")) { if (text.length() == 0) { Errors.error(Errors.MISSING_COMMENT, pos, "@deprecated tag with no explanatory comment"); text = "No replacement."; } mDeprecatedTagsList.add(new ParsedTagInfo("@deprecated", "@deprecated", text, mBase, pos)); } else if (name.equals("@literal")) { mInlineTagsList.add(new LiteralTagInfo(text, pos)); } else if (name.equals("@code") || name.equals("@snippet")) { mInlineTagsList.add(new CodeTagInfo(text, pos)); } else if (name.equals("@hide") || name.equals("@removed") || name.equals("@pending") || name.equals("@doconly")) { // nothing } else if (name.equals("@attr")) { AttrTagInfo tag = new AttrTagInfo("@attr", "@attr", text, mBase, pos); mAttrTagsList.add(tag); Comment c = tag.description(); if (c != null) { for (TagInfo t : c.tags()) { mInlineTagsList.add(t); } } } else if (name.equals("@undeprecate")) { mUndeprecateTagsList.add(new TextTagInfo("@undeprecate", "@undeprecate", text, pos)); } else if (name.equals("@include") || name.equals("@sample")) { mInlineTagsList.add(new SampleTagInfo(name, "@include", text, mBase, pos)); } else if (name.equals("@apiNote") || name.equals("@implSpec") || name.equals("@implNote")) { mTagsList.add(new ParsedTagInfo(name, name, text, mBase, pos)); } else if (name.equals("@memberDoc")) { mMemberDocTagsList.add(new ParsedTagInfo("@memberDoc", "@memberDoc", text, mBase, pos)); } else if (name.equals("@paramDoc")) { mParamDocTagsList.add(new ParsedTagInfo("@paramDoc", "@paramDoc", text, mBase, pos)); } else if (name.equals("@returnDoc")) { mReturnDocTagsList.add(new ParsedTagInfo("@returnDoc", "@returnDoc", text, mBase, pos)); } else { boolean known = KNOWN_TAGS.contains(name); if (!known) { known = Doclava.knownTags.contains(name); } if (!known) { if (name.length() >= 2 && Character.isUpperCase(name.charAt(1))) { // This is a workaround for b/135928616 where parsing of comments fails when there is // a Java annotation and not a tag. Errors.error(Errors.JAVA_TAG_IN_COMMENT, pos == null ? null : new SourcePositionInfo(pos), "Invalid tag: " + name); } else { Errors.error(Errors.UNKNOWN_TAG, pos == null ? null : new SourcePositionInfo(pos), "Unknown tag: " + name); } } TagInfo t = new TextTagInfo(name, name, text, pos); if (isInline) { mInlineTagsList.add(t); } else { mTagsList.add(t); } } } private void parseBriefTags() { int N = mInlineTagsList.size(); // look for "@more" tag, which means that we might go past the first sentence. int more = -1; for (int i = 0; i < N; i++) { if (mInlineTagsList.get(i).name().equals("@more")) { more = i; } } if (more >= 0) { for (int i = 0; i < more; i++) { mBriefTagsList.add(mInlineTagsList.get(i)); } } else { for (int i = 0; i < N; i++) { TagInfo t = mInlineTagsList.get(i); if (t.name().equals("Text")) { Matcher m = FIRST_SENTENCE.matcher(t.text()); if (m.matches()) { String text = m.group(1); TagInfo firstSentenceTag = new TagInfo(t.name(), t.kind(), text, t.position()); mBriefTagsList.add(firstSentenceTag); break; } } mBriefTagsList.add(t); } } } public TagInfo[] tags() { init(); return mInlineTags; } public TagInfo[] tags(String name) { init(); ArrayList results = new ArrayList(); int N = mInlineTagsList.size(); for (int i = 0; i < N; i++) { TagInfo t = mInlineTagsList.get(i); if (t.name().equals(name)) { results.add(t); } } return results.toArray(TagInfo.getArray(results.size())); } public TagInfo[] blockTags() { init(); return mTags; } public ParamTagInfo[] paramTags() { init(); return mParamTags; } public SeeTagInfo[] seeTags() { init(); return mSeeTags; } public ThrowsTagInfo[] throwsTags() { init(); return mThrowsTags; } public TagInfo[] returnTags() { init(); return mReturnTags; } public TagInfo[] deprecatedTags() { init(); return mDeprecatedTags; } public TagInfo[] undeprecateTags() { init(); return mUndeprecateTags; } public AttrTagInfo[] attrTags() { init(); return mAttrTags; } public TagInfo[] briefTags() { init(); return mBriefTags; } public ParsedTagInfo[] memberDocTags() { init(); return mMemberDocTags; } public ParsedTagInfo[] paramDocTags() { init(); return mParamDocTags; } public ParsedTagInfo[] returnDocTags() { init(); return mReturnDocTags; } public boolean isHidden() { if (mHidden == null) { mHidden = !Doclava.checkLevel(Doclava.SHOW_HIDDEN) && (mText != null) && (mText.indexOf("@hide") >= 0 || mText.indexOf("@pending") >= 0); } return mHidden; } public boolean isRemoved() { if (mRemoved == null) { mRemoved = !Doclava.checkLevel(Doclava.SHOW_HIDDEN) && (mText != null) && (mText.indexOf("@removed") >= 0); } return mRemoved; } public void setDeprecatedSince(String since) { if (since != null) { since = since.trim(); } mDeprecatedSince = since; } public String getDeprecatedSince() { return mDeprecatedSince; } public void setApiSince(String since) { if (since != null) { since = since.trim(); } mApiSince = since; } public String getApiSince() { //return the value of @apiSince, an API level in Android return mApiSince; } public void setSdkExtSince(String sdkextsince) { if (sdkextsince != null) { sdkextsince = sdkextsince.trim(); } mSdkExtSince = sdkextsince; } public String getSdkExtSince() { return mSdkExtSince; } public boolean isDocOnly() { if (mDocOnly == null) { mDocOnly = (mText != null) && (mText.indexOf("@doconly") >= 0); } return mDocOnly; } public boolean isDeprecated() { if (mDeprecated == null) { mDeprecated = (mText != null) && (mText.indexOf("@deprecated") >= 0); } return mDeprecated; } private void init() { if (!mInitialized) { initImpl(); } } private void initImpl() { isHidden(); isRemoved(); isDocOnly(); isDeprecated(); // Don't bother parsing text if we aren't generating documentation. if (Doclava.parseComments()) { parseCommentTags(mText); parseBriefTags(); } else { // Forces methods to be recognized by findOverriddenMethods in MethodInfo. mInlineTagsList.add(new TextTagInfo("Text", "Text", mText, SourcePositionInfo.add(mPosition, mText, 0))); } mText = null; mInitialized = true; mInlineTags = mInlineTagsList.toArray(TagInfo.getArray(mInlineTagsList.size())); mTags = mTagsList.toArray(TagInfo.getArray(mTagsList.size())); mParamTags = mParamTagsList.toArray(ParamTagInfo.getArray(mParamTagsList.size())); mSeeTags = mSeeTagsList.toArray(SeeTagInfo.getArray(mSeeTagsList.size())); mThrowsTags = mThrowsTagsList.toArray(ThrowsTagInfo.getArray(mThrowsTagsList.size())); mReturnTags = ParsedTagInfo.joinTags( mReturnTagsList.toArray(ParsedTagInfo.getArray(mReturnTagsList.size()))); mDeprecatedTags = ParsedTagInfo.joinTags( mDeprecatedTagsList.toArray(ParsedTagInfo.getArray(mDeprecatedTagsList.size()))); mUndeprecateTags = mUndeprecateTagsList.toArray(TagInfo.getArray(mUndeprecateTagsList.size())); mAttrTags = mAttrTagsList.toArray(AttrTagInfo.getArray(mAttrTagsList.size())); mBriefTags = mBriefTagsList.toArray(TagInfo.getArray(mBriefTagsList.size())); mMemberDocTags = mMemberDocTagsList.toArray(ParsedTagInfo.getArray(mMemberDocTagsList.size())); mParamDocTags = mParamDocTagsList.toArray(ParsedTagInfo.getArray(mParamDocTagsList.size())); mReturnDocTags = mReturnDocTagsList.toArray(ParsedTagInfo.getArray(mReturnDocTagsList.size())); mTagsList = null; mParamTagsList = null; mSeeTagsList = null; mThrowsTagsList = null; mReturnTagsList = null; mDeprecatedTagsList = null; mUndeprecateTagsList = null; mAttrTagsList = null; mBriefTagsList = null; mMemberDocTagsList = null; mParamDocTagsList = null; mReturnDocTagsList = null; } boolean mInitialized; Boolean mHidden = null; Boolean mRemoved = null; Boolean mDocOnly = null; Boolean mDeprecated = null; String mDeprecatedSince; String mApiSince; String mSdkExtSince; String mText; ContainerInfo mBase; SourcePositionInfo mPosition; int mLine = 1; TagInfo[] mInlineTags; TagInfo[] mTags; ParamTagInfo[] mParamTags; SeeTagInfo[] mSeeTags; ThrowsTagInfo[] mThrowsTags; TagInfo[] mBriefTags; TagInfo[] mReturnTags; TagInfo[] mDeprecatedTags; TagInfo[] mUndeprecateTags; AttrTagInfo[] mAttrTags; ParsedTagInfo[] mMemberDocTags; ParsedTagInfo[] mParamDocTags; ParsedTagInfo[] mReturnDocTags; ArrayList mInlineTagsList = new ArrayList(); ArrayList mTagsList = new ArrayList(); ArrayList mParamTagsList = new ArrayList(); ArrayList mSeeTagsList = new ArrayList(); ArrayList mThrowsTagsList = new ArrayList(); ArrayList mBriefTagsList = new ArrayList(); ArrayList mReturnTagsList = new ArrayList(); ArrayList mDeprecatedTagsList = new ArrayList(); ArrayList mUndeprecateTagsList = new ArrayList(); ArrayList mAttrTagsList = new ArrayList(); ArrayList mMemberDocTagsList = new ArrayList(); ArrayList mParamDocTagsList = new ArrayList(); ArrayList mReturnDocTagsList = new ArrayList(); }