| //////////////////////////////////////////////////////////////////////////////// |
| // checkstyle: Checks Java source code for adherence to a set of rules. |
| // Copyright (C) 2001-2017 the original author or authors. |
| // |
| // This library is free software; you can redistribute it and/or |
| // modify it under the terms of the GNU Lesser General Public |
| // License as published by the Free Software Foundation; either |
| // version 2.1 of the License, or (at your option) any later version. |
| // |
| // This library is distributed in the hope that it will be useful, |
| // but WITHOUT ANY WARRANTY; without even the implied warranty of |
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| // Lesser General Public License for more details. |
| // |
| // You should have received a copy of the GNU Lesser General Public |
| // License along with this library; if not, write to the Free Software |
| // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| package com.puppycrawl.tools.checkstyle.utils; |
| |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Modifier; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.regex.Pattern; |
| |
| import com.google.common.collect.ImmutableMap; |
| import com.puppycrawl.tools.checkstyle.api.DetailAST; |
| import com.puppycrawl.tools.checkstyle.api.DetailNode; |
| import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes; |
| import com.puppycrawl.tools.checkstyle.api.TextBlock; |
| import com.puppycrawl.tools.checkstyle.api.TokenTypes; |
| import com.puppycrawl.tools.checkstyle.checks.javadoc.InvalidJavadocTag; |
| import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTag; |
| import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTagInfo; |
| import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTags; |
| import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.BlockTagUtils; |
| import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.InlineTagUtils; |
| import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.TagInfo; |
| |
| /** |
| * Contains utility methods for working with Javadoc. |
| * @author Lyle Hanson |
| */ |
| public final class JavadocUtils { |
| |
| /** |
| * The type of Javadoc tag we want returned. |
| */ |
| public enum JavadocTagType { |
| /** Block type. */ |
| BLOCK, |
| /** Inline type. */ |
| INLINE, |
| /** All validTags. */ |
| ALL |
| } |
| |
| /** Maps from a token name to value. */ |
| private static final ImmutableMap<String, Integer> TOKEN_NAME_TO_VALUE; |
| /** Maps from a token value to name. */ |
| private static final String[] TOKEN_VALUE_TO_NAME; |
| |
| /** Exception message for unknown JavaDoc token id. */ |
| private static final String UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE = "Unknown javadoc" |
| + " token id. Given id: "; |
| |
| /** Newline pattern. */ |
| private static final Pattern NEWLINE = Pattern.compile("\n"); |
| |
| /** Return pattern. */ |
| private static final Pattern RETURN = Pattern.compile("\r"); |
| |
| /** Tab pattern. */ |
| private static final Pattern TAB = Pattern.compile("\t"); |
| |
| // Using reflection gets all token names and values from JavadocTokenTypes class |
| // and saves to TOKEN_NAME_TO_VALUE and TOKEN_VALUE_TO_NAME collections. |
| static { |
| final ImmutableMap.Builder<String, Integer> builder = ImmutableMap.builder(); |
| |
| final Field[] fields = JavadocTokenTypes.class.getDeclaredFields(); |
| |
| String[] tempTokenValueToName = CommonUtils.EMPTY_STRING_ARRAY; |
| |
| for (final Field field : fields) { |
| |
| // Only process public int fields. |
| if (!Modifier.isPublic(field.getModifiers()) |
| || field.getType() != Integer.TYPE) { |
| continue; |
| } |
| |
| final String name = field.getName(); |
| |
| final int tokenValue = TokenUtils.getIntFromField(field, name); |
| builder.put(name, tokenValue); |
| if (tokenValue > tempTokenValueToName.length - 1) { |
| final String[] temp = new String[tokenValue + 1]; |
| System.arraycopy(tempTokenValueToName, 0, temp, 0, tempTokenValueToName.length); |
| tempTokenValueToName = temp; |
| } |
| if (tokenValue == -1) { |
| tempTokenValueToName[0] = name; |
| } |
| else { |
| tempTokenValueToName[tokenValue] = name; |
| } |
| } |
| |
| TOKEN_NAME_TO_VALUE = builder.build(); |
| TOKEN_VALUE_TO_NAME = tempTokenValueToName; |
| } |
| |
| /** Prevent instantiation. */ |
| private JavadocUtils() { |
| } |
| |
| /** |
| * Gets validTags from a given piece of Javadoc. |
| * @param textBlock |
| * the Javadoc comment to process. |
| * @param tagType |
| * the type of validTags we're interested in |
| * @return all standalone validTags from the given javadoc. |
| */ |
| public static JavadocTags getJavadocTags(TextBlock textBlock, |
| JavadocTagType tagType) { |
| |
| final boolean getBlockTags = tagType == JavadocTagType.ALL |
| || tagType == JavadocTagType.BLOCK; |
| final boolean getInlineTags = tagType == JavadocTagType.ALL |
| || tagType == JavadocTagType.INLINE; |
| |
| final List<TagInfo> tags = new ArrayList<>(); |
| |
| if (getBlockTags) { |
| tags.addAll(BlockTagUtils.extractBlockTags(textBlock.getText())); |
| } |
| |
| if (getInlineTags) { |
| tags.addAll(InlineTagUtils.extractInlineTags(textBlock.getText())); |
| } |
| |
| final List<JavadocTag> validTags = new ArrayList<>(); |
| final List<InvalidJavadocTag> invalidTags = new ArrayList<>(); |
| |
| for (TagInfo tag : tags) { |
| final int col = tag.getPosition().getColumn(); |
| |
| // Add the starting line of the comment to the line number to get the actual line number |
| // in the source. |
| // Lines are one-indexed, so need a off-by-one correction. |
| final int line = textBlock.getStartLineNo() + tag.getPosition().getLine() - 1; |
| |
| if (JavadocTagInfo.isValidName(tag.getName())) { |
| validTags.add( |
| new JavadocTag(line, col, tag.getName(), tag.getValue())); |
| } |
| else { |
| invalidTags.add(new InvalidJavadocTag(line, col, tag.getName())); |
| } |
| } |
| |
| return new JavadocTags(validTags, invalidTags); |
| } |
| |
| /** |
| * Checks that commentContent starts with '*' javadoc comment identifier. |
| * @param commentContent |
| * content of block comment |
| * @return true if commentContent starts with '*' javadoc comment |
| * identifier. |
| */ |
| public static boolean isJavadocComment(String commentContent) { |
| boolean result = false; |
| |
| if (!commentContent.isEmpty()) { |
| final char docCommentIdentificator = commentContent.charAt(0); |
| result = docCommentIdentificator == '*'; |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Checks block comment content starts with '*' javadoc comment identifier. |
| * @param blockCommentBegin |
| * block comment AST |
| * @return true if block comment content starts with '*' javadoc comment |
| * identifier. |
| */ |
| public static boolean isJavadocComment(DetailAST blockCommentBegin) { |
| final String commentContent = getBlockCommentContent(blockCommentBegin); |
| return isJavadocComment(commentContent) && isCorrectJavadocPosition(blockCommentBegin); |
| } |
| |
| /** |
| * Gets content of block comment. |
| * @param blockCommentBegin |
| * block comment AST. |
| * @return content of block comment. |
| */ |
| private static String getBlockCommentContent(DetailAST blockCommentBegin) { |
| final DetailAST commentContent = blockCommentBegin.getFirstChild(); |
| return commentContent.getText(); |
| } |
| |
| /** |
| * Get content of Javadoc comment. |
| * @param javadocCommentBegin |
| * Javadoc comment AST |
| * @return content of Javadoc comment. |
| */ |
| public static String getJavadocCommentContent(DetailAST javadocCommentBegin) { |
| final DetailAST commentContent = javadocCommentBegin.getFirstChild(); |
| return commentContent.getText().substring(1); |
| } |
| |
| /** |
| * Returns the first child token that has a specified type. |
| * @param detailNode |
| * Javadoc AST node |
| * @param type |
| * the token type to match |
| * @return the matching token, or null if no match |
| */ |
| public static DetailNode findFirstToken(DetailNode detailNode, int type) { |
| DetailNode returnValue = null; |
| DetailNode node = getFirstChild(detailNode); |
| while (node != null) { |
| if (node.getType() == type) { |
| returnValue = node; |
| break; |
| } |
| node = getNextSibling(node); |
| } |
| return returnValue; |
| } |
| |
| /** |
| * Gets first child node of specified node. |
| * |
| * @param node DetailNode |
| * @return first child |
| */ |
| public static DetailNode getFirstChild(DetailNode node) { |
| DetailNode resultNode = null; |
| |
| if (node.getChildren().length > 0) { |
| resultNode = node.getChildren()[0]; |
| } |
| return resultNode; |
| } |
| |
| /** |
| * Checks whether node contains any node of specified type among children on any deep level. |
| * |
| * @param node DetailNode |
| * @param type token type |
| * @return true if node contains any node of type type among children on any deep level. |
| */ |
| public static boolean containsInBranch(DetailNode node, int type) { |
| boolean result = true; |
| DetailNode curNode = node; |
| while (type != curNode.getType()) { |
| DetailNode toVisit = getFirstChild(curNode); |
| while (curNode != null && toVisit == null) { |
| toVisit = getNextSibling(curNode); |
| if (toVisit == null) { |
| curNode = curNode.getParent(); |
| } |
| } |
| |
| if (curNode == toVisit) { |
| result = false; |
| break; |
| } |
| |
| curNode = toVisit; |
| } |
| return result; |
| } |
| |
| /** |
| * Gets next sibling of specified node. |
| * |
| * @param node DetailNode |
| * @return next sibling. |
| */ |
| public static DetailNode getNextSibling(DetailNode node) { |
| DetailNode nextSibling = null; |
| final DetailNode parent = node.getParent(); |
| if (parent != null) { |
| final int nextSiblingIndex = node.getIndex() + 1; |
| final DetailNode[] children = parent.getChildren(); |
| if (nextSiblingIndex <= children.length - 1) { |
| nextSibling = children[nextSiblingIndex]; |
| } |
| } |
| return nextSibling; |
| } |
| |
| /** |
| * Gets next sibling of specified node with the specified type. |
| * |
| * @param node DetailNode |
| * @param tokenType javadoc token type |
| * @return next sibling. |
| */ |
| public static DetailNode getNextSibling(DetailNode node, int tokenType) { |
| DetailNode nextSibling = getNextSibling(node); |
| while (nextSibling != null && nextSibling.getType() != tokenType) { |
| nextSibling = getNextSibling(nextSibling); |
| } |
| return nextSibling; |
| } |
| |
| /** |
| * Gets previous sibling of specified node. |
| * @param node DetailNode |
| * @return previous sibling |
| */ |
| public static DetailNode getPreviousSibling(DetailNode node) { |
| DetailNode previousSibling = null; |
| final int previousSiblingIndex = node.getIndex() - 1; |
| if (previousSiblingIndex >= 0) { |
| final DetailNode parent = node.getParent(); |
| final DetailNode[] children = parent.getChildren(); |
| previousSibling = children[previousSiblingIndex]; |
| } |
| return previousSibling; |
| } |
| |
| /** |
| * Returns the name of a token for a given ID. |
| * @param id |
| * the ID of the token name to get |
| * @return a token name |
| */ |
| public static String getTokenName(int id) { |
| final String name; |
| if (id == JavadocTokenTypes.EOF) { |
| name = "EOF"; |
| } |
| else if (id > TOKEN_VALUE_TO_NAME.length - 1) { |
| throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id); |
| } |
| else { |
| name = TOKEN_VALUE_TO_NAME[id]; |
| if (name == null) { |
| throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id); |
| } |
| } |
| return name; |
| } |
| |
| /** |
| * Returns the ID of a token for a given name. |
| * @param name |
| * the name of the token ID to get |
| * @return a token ID |
| */ |
| public static int getTokenId(String name) { |
| final Integer id = TOKEN_NAME_TO_VALUE.get(name); |
| if (id == null) { |
| throw new IllegalArgumentException("Unknown javadoc token name. Given name " + name); |
| } |
| return id; |
| } |
| |
| /** |
| * Gets tag name from javadocTagSection. |
| * |
| * @param javadocTagSection to get tag name from. |
| * @return name, of the javadocTagSection's tag. |
| */ |
| public static String getTagName(DetailNode javadocTagSection) { |
| final String javadocTagName; |
| if (javadocTagSection.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) { |
| javadocTagName = getNextSibling( |
| getFirstChild(javadocTagSection)).getText(); |
| } |
| else { |
| javadocTagName = getFirstChild(javadocTagSection).getText(); |
| } |
| return javadocTagName; |
| } |
| |
| /** |
| * Replace all control chars with escaped symbols. |
| * @param text the String to process. |
| * @return the processed String with all control chars escaped. |
| */ |
| public static String escapeAllControlChars(String text) { |
| final String textWithoutNewlines = NEWLINE.matcher(text).replaceAll("\\\\n"); |
| final String textWithoutReturns = RETURN.matcher(textWithoutNewlines).replaceAll("\\\\r"); |
| return TAB.matcher(textWithoutReturns).replaceAll("\\\\t"); |
| } |
| |
| /** |
| * Checks Javadoc comment it's in right place. |
| * <p>From Javadoc util documentation: |
| * "Placement of comments - Documentation comments are recognized only when placed |
| * immediately before class, interface, constructor, method, field or annotation field |
| * declarations -- see the class example, method example, and field example. |
| * Documentation comments placed in the body of a method are ignored."</p> |
| * <p>If there are many documentation comments per declaration statement, |
| * only the last one will be recognized.</p> |
| * |
| * @param blockComment Block comment AST |
| * @return true if Javadoc is in right place |
| * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/tools/unix/javadoc.html"> |
| * Javadoc util documentation</a> |
| */ |
| private static boolean isCorrectJavadocPosition(DetailAST blockComment) { |
| // We must be sure that after this one there are no other documentation comments. |
| DetailAST sibling = blockComment.getNextSibling(); |
| while (sibling != null) { |
| if (sibling.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) { |
| if (isJavadocComment(getBlockCommentContent(sibling))) { |
| // Found another javadoc comment, so this one should be ignored. |
| break; |
| } |
| sibling = sibling.getNextSibling(); |
| } |
| else if (sibling.getType() == TokenTypes.SINGLE_LINE_COMMENT) { |
| sibling = sibling.getNextSibling(); |
| } |
| else { |
| // Annotation, declaration or modifier is here. Do not check further. |
| sibling = null; |
| } |
| } |
| return sibling == null |
| && (BlockCommentPosition.isOnType(blockComment) |
| || BlockCommentPosition.isOnMember(blockComment)); |
| } |
| } |