001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.javadoc;
021
022import java.util.Arrays;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.Locale;
026import java.util.Map;
027import java.util.Set;
028
029import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser;
030import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser.ParseErrorMessage;
031import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser.ParseStatus;
032import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
033import com.puppycrawl.tools.checkstyle.api.DetailAST;
034import com.puppycrawl.tools.checkstyle.api.DetailNode;
035import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
036import com.puppycrawl.tools.checkstyle.api.TokenTypes;
037import com.puppycrawl.tools.checkstyle.utils.BlockCommentPosition;
038import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
039import com.puppycrawl.tools.checkstyle.utils.JavadocUtils;
040
041/**
042 * Base class for Checks that process Javadoc comments.
043 * @author Baratali Izmailov
044 */
045public abstract class AbstractJavadocCheck extends AbstractCheck {
046    /**
047     * Message key of error message. Missed close HTML tag breaks structure
048     * of parse tree, so parser stops parsing and generates such error
049     * message. This case is special because parser prints error like
050     * {@code "no viable alternative at input 'b \n *\n'"} and it is not
051     * clear that error is about missed close HTML tag.
052     */
053    public static final String MSG_JAVADOC_MISSED_HTML_CLOSE =
054            JavadocDetailNodeParser.MSG_JAVADOC_MISSED_HTML_CLOSE;
055
056    /**
057     * Message key of error message.
058     */
059    public static final String MSG_JAVADOC_WRONG_SINGLETON_TAG =
060            JavadocDetailNodeParser.MSG_JAVADOC_WRONG_SINGLETON_TAG;
061
062    /**
063     * Parse error while rule recognition.
064     */
065    public static final String MSG_JAVADOC_PARSE_RULE_ERROR =
066            JavadocDetailNodeParser.MSG_JAVADOC_PARSE_RULE_ERROR;
067
068    /**
069     * Error message key for common javadoc errors.
070     */
071    public static final String MSG_KEY_PARSE_ERROR =
072            JavadocDetailNodeParser.MSG_KEY_PARSE_ERROR;
073    /**
074     * Unrecognized error from antlr parser.
075     */
076    public static final String MSG_KEY_UNRECOGNIZED_ANTLR_ERROR =
077            JavadocDetailNodeParser.MSG_KEY_UNRECOGNIZED_ANTLR_ERROR;
078
079    /**
080     * Key is "line:column". Value is {@link DetailNode} tree. Map is stored in {@link ThreadLocal}
081     * to guarantee basic thread safety and avoid shared, mutable state when not necessary.
082     */
083    private static final ThreadLocal<Map<String, ParseStatus>> TREE_CACHE =
084        new ThreadLocal<Map<String, ParseStatus>>() {
085            @Override
086            protected Map<String, ParseStatus> initialValue() {
087                return new HashMap<>();
088            }
089        };
090
091    /**
092     * Parses content of Javadoc comment as DetailNode tree.
093     */
094    private final JavadocDetailNodeParser parser = new JavadocDetailNodeParser();
095
096    /** The javadoc tokens the check is interested in. */
097    private final Set<Integer> javadocTokens = new HashSet<>();
098
099    /**
100     * DetailAST node of considered Javadoc comment that is just a block comment
101     * in Java language syntax tree.
102     */
103    private DetailAST blockCommentAst;
104
105    /**
106     * Returns the default javadoc token types a check is interested in.
107     * @return the default javadoc token types
108     * @see JavadocTokenTypes
109     */
110    public abstract int[] getDefaultJavadocTokens();
111
112    /**
113     * Called to process a Javadoc token.
114     * @param ast
115     *        the token to process
116     */
117    public abstract void visitJavadocToken(DetailNode ast);
118
119    /**
120     * The configurable javadoc token set.
121     * Used to protect Checks against malicious users who specify an
122     * unacceptable javadoc token set in the configuration file.
123     * The default implementation returns the check's default javadoc tokens.
124     * @return the javadoc token set this check is designed for.
125     * @see JavadocTokenTypes
126     */
127    public int[] getAcceptableJavadocTokens() {
128        final int[] defaultJavadocTokens = getDefaultJavadocTokens();
129        final int[] copy = new int[defaultJavadocTokens.length];
130        System.arraycopy(defaultJavadocTokens, 0, copy, 0, defaultJavadocTokens.length);
131        return copy;
132    }
133
134    /**
135     * The javadoc tokens that this check must be registered for.
136     * @return the javadoc token set this must be registered for.
137     * @see JavadocTokenTypes
138     */
139    public int[] getRequiredJavadocTokens() {
140        return CommonUtils.EMPTY_INT_ARRAY;
141    }
142
143    /**
144     * Adds a set of tokens the check is interested in.
145     * @param strRep the string representation of the tokens interested in
146     */
147    public final void setJavadocTokens(String... strRep) {
148        javadocTokens.clear();
149        for (String str : strRep) {
150            javadocTokens.add(JavadocUtils.getTokenId(str));
151        }
152    }
153
154    @Override
155    public void init() {
156        validateDefaultJavadocTokens();
157        if (javadocTokens.isEmpty()) {
158            for (int id : getDefaultJavadocTokens()) {
159                javadocTokens.add(id);
160            }
161        }
162        else {
163            final int[] acceptableJavadocTokens = getAcceptableJavadocTokens();
164            Arrays.sort(acceptableJavadocTokens);
165            for (Integer javadocTokenId : javadocTokens) {
166                if (Arrays.binarySearch(acceptableJavadocTokens, javadocTokenId) < 0) {
167                    final String message = String.format(Locale.ROOT, "Javadoc Token \"%s\" was "
168                            + "not found in Acceptable javadoc tokens list in check %s",
169                            JavadocUtils.getTokenName(javadocTokenId), getClass().getName());
170                    throw new IllegalStateException(message);
171                }
172            }
173        }
174    }
175
176    /**
177     * Validates that check's required javadoc tokens are subset of default javadoc tokens.
178     * @throws IllegalStateException when validation of default javadoc tokens fails
179     */
180    private void validateDefaultJavadocTokens() {
181        if (getRequiredJavadocTokens().length != 0) {
182            final int[] defaultJavadocTokens = getDefaultJavadocTokens();
183            Arrays.sort(defaultJavadocTokens);
184            for (final int javadocToken : getRequiredJavadocTokens()) {
185                if (Arrays.binarySearch(defaultJavadocTokens, javadocToken) < 0) {
186                    final String message = String.format(Locale.ROOT,
187                            "Javadoc Token \"%s\" from required javadoc "
188                                + "tokens was not found in default "
189                                + "javadoc tokens list in check %s",
190                            javadocToken, getClass().getName());
191                    throw new IllegalStateException(message);
192                }
193            }
194        }
195    }
196
197    /**
198     * Called before the starting to process a tree.
199     * @param rootAst
200     *        the root of the tree
201     */
202    public void beginJavadocTree(DetailNode rootAst) {
203        // No code by default, should be overridden only by demand at subclasses
204    }
205
206    /**
207     * Called after finished processing a tree.
208     * @param rootAst
209     *        the root of the tree
210     */
211    public void finishJavadocTree(DetailNode rootAst) {
212        // No code by default, should be overridden only by demand at subclasses
213    }
214
215    /**
216     * Called after all the child nodes have been process.
217     * @param ast
218     *        the token leaving
219     */
220    public void leaveJavadocToken(DetailNode ast) {
221        // No code by default, should be overridden only by demand at subclasses
222    }
223
224    /**
225     * Defined final to not allow JavadocChecks to change default tokens.
226     * @return default tokens
227     */
228    @Override
229    public final int[] getDefaultTokens() {
230        return new int[] {TokenTypes.BLOCK_COMMENT_BEGIN };
231    }
232
233    /**
234     * Defined final because all JavadocChecks require comment nodes.
235     * @return true
236     */
237    @Override
238    public final boolean isCommentNodesRequired() {
239        return true;
240    }
241
242    @Override
243    public final void beginTree(DetailAST rootAST) {
244        TREE_CACHE.get().clear();
245    }
246
247    @Override
248    public final void finishTree(DetailAST rootAST) {
249        TREE_CACHE.get().clear();
250    }
251
252    @Override
253    public final void visitToken(DetailAST blockCommentNode) {
254        if (JavadocUtils.isJavadocComment(blockCommentNode)
255              && isCorrectJavadocPosition(blockCommentNode)) {
256            // store as field, to share with child Checks
257            blockCommentAst = blockCommentNode;
258
259            final String treeCacheKey = blockCommentNode.getLineNo() + ":"
260                    + blockCommentNode.getColumnNo();
261
262            final ParseStatus result;
263
264            if (TREE_CACHE.get().containsKey(treeCacheKey)) {
265                result = TREE_CACHE.get().get(treeCacheKey);
266            }
267            else {
268                result = parser.parseJavadocAsDetailNode(blockCommentNode);
269                TREE_CACHE.get().put(treeCacheKey, result);
270            }
271
272            if (result.getParseErrorMessage() == null) {
273                processTree(result.getTree());
274            }
275            else {
276                final ParseErrorMessage parseErrorMessage = result.getParseErrorMessage();
277                log(parseErrorMessage.getLineNumber(),
278                        parseErrorMessage.getMessageKey(),
279                        parseErrorMessage.getMessageArguments());
280            }
281        }
282
283    }
284
285    /**
286     * Getter for block comment in Java language syntax tree.
287     * @return A block comment in the syntax tree.
288     */
289    protected DetailAST getBlockCommentAst() {
290        return blockCommentAst;
291    }
292
293    /**
294     * Checks Javadoc comment it's in right place.
295     * From Javadoc util documentation:
296     * "Placement of comments - Documentation comments are recognized only when placed
297     * immediately before class, interface, constructor, method, or field
298     * declarations -- see the class example, method example, and field example.
299     * Documentation comments placed in the body of a method are ignored. Only one
300     * documentation comment per declaration statement is recognized by the Javadoc tool."
301     *
302     * @param blockComment Block comment AST
303     * @return true if Javadoc is in right place
304     */
305    private static boolean isCorrectJavadocPosition(DetailAST blockComment) {
306        return BlockCommentPosition.isOnClass(blockComment)
307                || BlockCommentPosition.isOnInterface(blockComment)
308                || BlockCommentPosition.isOnEnum(blockComment)
309                || BlockCommentPosition.isOnMethod(blockComment)
310                || BlockCommentPosition.isOnField(blockComment)
311                || BlockCommentPosition.isOnConstructor(blockComment)
312                || BlockCommentPosition.isOnEnumConstant(blockComment)
313                || BlockCommentPosition.isOnAnnotationDef(blockComment);
314    }
315
316    /**
317     * Processes JavadocAST tree notifying Check.
318     * @param root
319     *        root of JavadocAST tree.
320     */
321    private void processTree(DetailNode root) {
322        beginJavadocTree(root);
323        walk(root);
324        finishJavadocTree(root);
325    }
326
327    /**
328     * Processes a node calling Check at interested nodes.
329     * @param root
330     *        the root of tree for process
331     */
332    private void walk(DetailNode root) {
333        DetailNode curNode = root;
334        while (curNode != null) {
335            boolean waitsForProcessing = shouldBeProcessed(curNode);
336
337            if (waitsForProcessing) {
338                visitJavadocToken(curNode);
339            }
340            DetailNode toVisit = JavadocUtils.getFirstChild(curNode);
341            while (curNode != null && toVisit == null) {
342
343                if (waitsForProcessing) {
344                    leaveJavadocToken(curNode);
345                }
346
347                toVisit = JavadocUtils.getNextSibling(curNode);
348                if (toVisit == null) {
349                    curNode = curNode.getParent();
350                    if (curNode != null) {
351                        waitsForProcessing = shouldBeProcessed(curNode);
352                    }
353                }
354            }
355            curNode = toVisit;
356        }
357    }
358
359    /**
360     * Checks whether the current node should be processed by the check.
361     * @param curNode current node.
362     * @return true if the current node should be processed by the check.
363     */
364    private boolean shouldBeProcessed(DetailNode curNode) {
365        return javadocTokens.contains(curNode.getType());
366    }
367
368}