//////////////////////////////////////////////////////////////////////////////// // 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.checks.javadoc; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.Set; import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser; import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser.ParseErrorMessage; import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser.ParseStatus; import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 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.TokenTypes; import com.puppycrawl.tools.checkstyle.utils.BlockCommentPosition; import com.puppycrawl.tools.checkstyle.utils.CommonUtils; import com.puppycrawl.tools.checkstyle.utils.JavadocUtils; /** * Base class for Checks that process Javadoc comments. * @author Baratali Izmailov */ public abstract class AbstractJavadocCheck extends AbstractCheck { /** * Message key of error message. Missed close HTML tag breaks structure * of parse tree, so parser stops parsing and generates such error * message. This case is special because parser prints error like * {@code "no viable alternative at input 'b \n *\n'"} and it is not * clear that error is about missed close HTML tag. */ public static final String MSG_JAVADOC_MISSED_HTML_CLOSE = JavadocDetailNodeParser.MSG_JAVADOC_MISSED_HTML_CLOSE; /** * Message key of error message. */ public static final String MSG_JAVADOC_WRONG_SINGLETON_TAG = JavadocDetailNodeParser.MSG_JAVADOC_WRONG_SINGLETON_TAG; /** * Parse error while rule recognition. */ public static final String MSG_JAVADOC_PARSE_RULE_ERROR = JavadocDetailNodeParser.MSG_JAVADOC_PARSE_RULE_ERROR; /** * Error message key for common javadoc errors. */ public static final String MSG_KEY_PARSE_ERROR = JavadocDetailNodeParser.MSG_KEY_PARSE_ERROR; /** * Unrecognized error from antlr parser. */ public static final String MSG_KEY_UNRECOGNIZED_ANTLR_ERROR = JavadocDetailNodeParser.MSG_KEY_UNRECOGNIZED_ANTLR_ERROR; /** * Key is "line:column". Value is {@link DetailNode} tree. Map is stored in {@link ThreadLocal} * to guarantee basic thread safety and avoid shared, mutable state when not necessary. */ private static final ThreadLocal<Map<String, ParseStatus>> TREE_CACHE = new ThreadLocal<Map<String, ParseStatus>>() { @Override protected Map<String, ParseStatus> initialValue() { return new HashMap<>(); } }; /** * Parses content of Javadoc comment as DetailNode tree. */ private final JavadocDetailNodeParser parser = new JavadocDetailNodeParser(); /** The javadoc tokens the check is interested in. */ private final Set<Integer> javadocTokens = new HashSet<>(); /** * DetailAST node of considered Javadoc comment that is just a block comment * in Java language syntax tree. */ private DetailAST blockCommentAst; /** * Returns the default javadoc token types a check is interested in. * @return the default javadoc token types * @see JavadocTokenTypes */ public abstract int[] getDefaultJavadocTokens(); /** * Called to process a Javadoc token. * @param ast * the token to process */ public abstract void visitJavadocToken(DetailNode ast); /** * The configurable javadoc token set. * Used to protect Checks against malicious users who specify an * unacceptable javadoc token set in the configuration file. * The default implementation returns the check's default javadoc tokens. * @return the javadoc token set this check is designed for. * @see JavadocTokenTypes */ public int[] getAcceptableJavadocTokens() { final int[] defaultJavadocTokens = getDefaultJavadocTokens(); final int[] copy = new int[defaultJavadocTokens.length]; System.arraycopy(defaultJavadocTokens, 0, copy, 0, defaultJavadocTokens.length); return copy; } /** * The javadoc tokens that this check must be registered for. * @return the javadoc token set this must be registered for. * @see JavadocTokenTypes */ public int[] getRequiredJavadocTokens() { return CommonUtils.EMPTY_INT_ARRAY; } /** * Adds a set of tokens the check is interested in. * @param strRep the string representation of the tokens interested in */ public final void setJavadocTokens(String... strRep) { javadocTokens.clear(); for (String str : strRep) { javadocTokens.add(JavadocUtils.getTokenId(str)); } } @Override public void init() { validateDefaultJavadocTokens(); if (javadocTokens.isEmpty()) { for (int id : getDefaultJavadocTokens()) { javadocTokens.add(id); } } else { final int[] acceptableJavadocTokens = getAcceptableJavadocTokens(); Arrays.sort(acceptableJavadocTokens); for (Integer javadocTokenId : javadocTokens) { if (Arrays.binarySearch(acceptableJavadocTokens, javadocTokenId) < 0) { final String message = String.format(Locale.ROOT, "Javadoc Token \"%s\" was " + "not found in Acceptable javadoc tokens list in check %s", JavadocUtils.getTokenName(javadocTokenId), getClass().getName()); throw new IllegalStateException(message); } } } } /** * Validates that check's required javadoc tokens are subset of default javadoc tokens. * @throws IllegalStateException when validation of default javadoc tokens fails */ private void validateDefaultJavadocTokens() { if (getRequiredJavadocTokens().length != 0) { final int[] defaultJavadocTokens = getDefaultJavadocTokens(); Arrays.sort(defaultJavadocTokens); for (final int javadocToken : getRequiredJavadocTokens()) { if (Arrays.binarySearch(defaultJavadocTokens, javadocToken) < 0) { final String message = String.format(Locale.ROOT, "Javadoc Token \"%s\" from required javadoc " + "tokens was not found in default " + "javadoc tokens list in check %s", javadocToken, getClass().getName()); throw new IllegalStateException(message); } } } } /** * Called before the starting to process a tree. * @param rootAst * the root of the tree */ public void beginJavadocTree(DetailNode rootAst) { // No code by default, should be overridden only by demand at subclasses } /** * Called after finished processing a tree. * @param rootAst * the root of the tree */ public void finishJavadocTree(DetailNode rootAst) { // No code by default, should be overridden only by demand at subclasses } /** * Called after all the child nodes have been process. * @param ast * the token leaving */ public void leaveJavadocToken(DetailNode ast) { // No code by default, should be overridden only by demand at subclasses } /** * Defined final to not allow JavadocChecks to change default tokens. * @return default tokens */ @Override public final int[] getDefaultTokens() { return new int[] {TokenTypes.BLOCK_COMMENT_BEGIN }; } /** * Defined final because all JavadocChecks require comment nodes. * @return true */ @Override public final boolean isCommentNodesRequired() { return true; } @Override public final void beginTree(DetailAST rootAST) { TREE_CACHE.get().clear(); } @Override public final void finishTree(DetailAST rootAST) { TREE_CACHE.get().clear(); } @Override public final void visitToken(DetailAST blockCommentNode) { if (JavadocUtils.isJavadocComment(blockCommentNode) && isCorrectJavadocPosition(blockCommentNode)) { // store as field, to share with child Checks blockCommentAst = blockCommentNode; final String treeCacheKey = blockCommentNode.getLineNo() + ":" + blockCommentNode.getColumnNo(); final ParseStatus result; if (TREE_CACHE.get().containsKey(treeCacheKey)) { result = TREE_CACHE.get().get(treeCacheKey); } else { result = parser.parseJavadocAsDetailNode(blockCommentNode); TREE_CACHE.get().put(treeCacheKey, result); } if (result.getParseErrorMessage() == null) { processTree(result.getTree()); } else { final ParseErrorMessage parseErrorMessage = result.getParseErrorMessage(); log(parseErrorMessage.getLineNumber(), parseErrorMessage.getMessageKey(), parseErrorMessage.getMessageArguments()); } } } /** * Getter for block comment in Java language syntax tree. * @return A block comment in the syntax tree. */ protected DetailAST getBlockCommentAst() { return blockCommentAst; } /** * Checks Javadoc comment it's in right place. * From Javadoc util documentation: * "Placement of comments - Documentation comments are recognized only when placed * immediately before class, interface, constructor, method, or field * declarations -- see the class example, method example, and field example. * Documentation comments placed in the body of a method are ignored. Only one * documentation comment per declaration statement is recognized by the Javadoc tool." * * @param blockComment Block comment AST * @return true if Javadoc is in right place */ private static boolean isCorrectJavadocPosition(DetailAST blockComment) { return BlockCommentPosition.isOnClass(blockComment) || BlockCommentPosition.isOnInterface(blockComment) || BlockCommentPosition.isOnEnum(blockComment) || BlockCommentPosition.isOnMethod(blockComment) || BlockCommentPosition.isOnField(blockComment) || BlockCommentPosition.isOnConstructor(blockComment) || BlockCommentPosition.isOnEnumConstant(blockComment) || BlockCommentPosition.isOnAnnotationDef(blockComment); } /** * Processes JavadocAST tree notifying Check. * @param root * root of JavadocAST tree. */ private void processTree(DetailNode root) { beginJavadocTree(root); walk(root); finishJavadocTree(root); } /** * Processes a node calling Check at interested nodes. * @param root * the root of tree for process */ private void walk(DetailNode root) { DetailNode curNode = root; while (curNode != null) { boolean waitsForProcessing = shouldBeProcessed(curNode); if (waitsForProcessing) { visitJavadocToken(curNode); } DetailNode toVisit = JavadocUtils.getFirstChild(curNode); while (curNode != null && toVisit == null) { if (waitsForProcessing) { leaveJavadocToken(curNode); } toVisit = JavadocUtils.getNextSibling(curNode); if (toVisit == null) { curNode = curNode.getParent(); if (curNode != null) { waitsForProcessing = shouldBeProcessed(curNode); } } } curNode = toVisit; } } /** * Checks whether the current node should be processed by the check. * @param curNode current node. * @return true if the current node should be processed by the check. */ private boolean shouldBeProcessed(DetailNode curNode) { return javadocTokens.contains(curNode.getType()); } }