//////////////////////////////////////////////////////////////////////////////// // 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.Matcher; 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.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; /** * 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: "; /** Comment pattern. */ private static final Pattern COMMENT_PATTERN = Pattern.compile( "^\\s*(?:/\\*{2,}|\\*+)\\s*(.*)"); /** Block tag pattern for a first line. */ private static final Pattern BLOCK_TAG_PATTERN_FIRST_LINE = Pattern.compile( "/\\*{2,}\\s*@(\\p{Alpha}+)\\s"); /** Block tag pattern. */ private static final Pattern BLOCK_TAG_PATTERN = Pattern.compile( "^\\s*\\**\\s*@(\\p{Alpha}+)\\s"); /** Inline tag pattern. */ private static final Pattern INLINE_TAG_PATTERN = Pattern.compile( ".*?\\{@(\\p{Alpha}+)\\s+(.*?)}"); /** 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 String[] text = textBlock.getText(); final List<JavadocTag> tags = new ArrayList<>(); final List<InvalidJavadocTag> invalidTags = new ArrayList<>(); for (int i = 0; i < text.length; i++) { final String textValue = text[i]; final Matcher blockTagMatcher = getBlockTagPattern(i).matcher(textValue); if ((tagType == JavadocTagType.ALL || tagType == JavadocTagType.BLOCK) && blockTagMatcher.find()) { final String tagName = blockTagMatcher.group(1); String content = textValue.substring(blockTagMatcher.end(1)); if (content.endsWith("*/")) { content = content.substring(0, content.length() - 2); } final int line = textBlock.getStartLineNo() + i; int col = blockTagMatcher.start(1) - 1; if (i == 0) { col += textBlock.getStartColNo(); } if (JavadocTagInfo.isValidName(tagName)) { tags.add( new JavadocTag(line, col, tagName, content.trim())); } else { invalidTags.add(new InvalidJavadocTag(line, col, tagName)); } } // No block tag, so look for inline validTags else if (tagType == JavadocTagType.ALL || tagType == JavadocTagType.INLINE) { lookForInlineTags(textBlock, i, tags, invalidTags); } } return new JavadocTags(tags, invalidTags); } /** * Get a block tag pattern depending on a line number of a javadoc. * @param lineNumber the line number. * @return a block tag pattern. */ private static Pattern getBlockTagPattern(int lineNumber) { final Pattern blockTagPattern; if (lineNumber == 0) { blockTagPattern = BLOCK_TAG_PATTERN_FIRST_LINE; } else { blockTagPattern = BLOCK_TAG_PATTERN; } return blockTagPattern; } /** * Looks for inline tags in comment and adds them to the proper tags collection. * @param comment comment text block * @param lineNumber line number in the comment * @param validTags collection of valid tags * @param invalidTags collection of invalid tags */ private static void lookForInlineTags(TextBlock comment, int lineNumber, final List<JavadocTag> validTags, final List<InvalidJavadocTag> invalidTags) { final String text = comment.getText()[lineNumber]; // Match Javadoc text after comment characters final Matcher commentMatcher = COMMENT_PATTERN.matcher(text); final String commentContents; // offset including comment characters final int commentOffset; if (commentMatcher.find()) { commentContents = commentMatcher.group(1); commentOffset = commentMatcher.start(1) - 1; } else { // No leading asterisks, still valid commentContents = text; commentOffset = 0; } final Matcher tagMatcher = INLINE_TAG_PATTERN.matcher(commentContents); while (tagMatcher.find()) { final String tagName = tagMatcher.group(1); final int line = comment.getStartLineNo() + lineNumber; int col = commentOffset + tagMatcher.start(1) - 1; if (lineNumber == 0) { col += comment.getStartColNo(); } if (JavadocTagInfo.isValidName(tagName)) { final String tagValue = tagMatcher.group(2).trim(); validTags.add(new JavadocTag(line, col, tagName, tagValue)); } else { invalidTags.add(new InvalidJavadocTag(line, col, tagName)); } } } /** * 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); } /** * 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"); } }