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.CommonUtils;
038import com.puppycrawl.tools.checkstyle.utils.JavadocUtils;
039
040/**
041 * Base class for Checks that process Javadoc comments.
042 * @author Baratali Izmailov
043 * @noinspection NoopMethodInAbstractClass
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    /**
075     * Key is "line:column". Value is {@link DetailNode} tree. Map is stored in {@link ThreadLocal}
076     * to guarantee basic thread safety and avoid shared, mutable state when not necessary.
077     */
078    private static final ThreadLocal<Map<String, ParseStatus>> TREE_CACHE =
079            ThreadLocal.withInitial(HashMap::new);
080
081    /**
082     * The file context.
083     * @noinspection ThreadLocalNotStaticFinal
084     */
085    private final ThreadLocal<FileContext> context = ThreadLocal.withInitial(FileContext::new);
086
087    /** The javadoc tokens the check is interested in. */
088    private final Set<Integer> javadocTokens = new HashSet<>();
089
090    /**
091     * This property determines if a check should log a violation upon encountering javadoc with
092     * non-tight html. The default return value for this method is set to false since checks
093     * generally tend to be fine with non tight html. It can be set through config file if a check
094     * is to log violation upon encountering non-tight HTML in javadoc.
095     *
096     * @see ParseStatus#firstNonTightHtmlTag
097     * @see ParseStatus#isNonTight()
098     * @see <a href="http://checkstyle.sourceforge.net/writingjavadocchecks.html#Tight-HTML_rules">
099     *     Tight HTML rules</a>
100     */
101    private boolean violateExecutionOnNonTightHtml;
102
103    /**
104     * Returns the default javadoc token types a check is interested in.
105     * @return the default javadoc token types
106     * @see JavadocTokenTypes
107     */
108    public abstract int[] getDefaultJavadocTokens();
109
110    /**
111     * Called to process a Javadoc token.
112     * @param ast
113     *        the token to process
114     */
115    public abstract void visitJavadocToken(DetailNode ast);
116
117    /**
118     * The configurable javadoc token set.
119     * Used to protect Checks against malicious users who specify an
120     * unacceptable javadoc token set in the configuration file.
121     * The default implementation returns the check's default javadoc tokens.
122     * @return the javadoc token set this check is designed for.
123     * @see JavadocTokenTypes
124     */
125    public int[] getAcceptableJavadocTokens() {
126        final int[] defaultJavadocTokens = getDefaultJavadocTokens();
127        final int[] copy = new int[defaultJavadocTokens.length];
128        System.arraycopy(defaultJavadocTokens, 0, copy, 0, defaultJavadocTokens.length);
129        return copy;
130    }
131
132    /**
133     * The javadoc tokens that this check must be registered for.
134     * @return the javadoc token set this must be registered for.
135     * @see JavadocTokenTypes
136     */
137    public int[] getRequiredJavadocTokens() {
138        return CommonUtils.EMPTY_INT_ARRAY;
139    }
140
141    /**
142     * This method determines if a check should process javadoc containing non-tight html tags.
143     * This method must be overridden in checks extending {@code AbstractJavadocCheck} which
144     * are not supposed to process javadoc containing non-tight html tags.
145     *
146     * @return true if the check should or can process javadoc containing non-tight html tags;
147     *     false otherwise
148     * @see ParseStatus#isNonTight()
149     * @see <a href="http://checkstyle.sourceforge.net/writingjavadocchecks.html#Tight-HTML_rules">
150     *     Tight HTML rules</a>
151     */
152    public boolean acceptJavadocWithNonTightHtml() {
153        return true;
154    }
155
156    /**
157     * Setter for {@link #violateExecutionOnNonTightHtml}.
158     * @param shouldReportViolation value to which the field shall be set to
159     * @see <a href="http://checkstyle.sourceforge.net/writingjavadocchecks.html#Tight-HTML_rules">
160     *     Tight HTML rules</a>
161     */
162    public final void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) {
163        violateExecutionOnNonTightHtml = shouldReportViolation;
164    }
165
166    /**
167     * Adds a set of tokens the check is interested in.
168     * @param strRep the string representation of the tokens interested in
169     */
170    public final void setJavadocTokens(String... strRep) {
171        javadocTokens.clear();
172        for (String str : strRep) {
173            javadocTokens.add(JavadocUtils.getTokenId(str));
174        }
175    }
176
177    @Override
178    public void init() {
179        validateDefaultJavadocTokens();
180        if (javadocTokens.isEmpty()) {
181            for (int id : getDefaultJavadocTokens()) {
182                javadocTokens.add(id);
183            }
184        }
185        else {
186            final int[] acceptableJavadocTokens = getAcceptableJavadocTokens();
187            Arrays.sort(acceptableJavadocTokens);
188            for (Integer javadocTokenId : javadocTokens) {
189                if (Arrays.binarySearch(acceptableJavadocTokens, javadocTokenId) < 0) {
190                    final String message = String.format(Locale.ROOT, "Javadoc Token \"%s\" was "
191                            + "not found in Acceptable javadoc tokens list in check %s",
192                            JavadocUtils.getTokenName(javadocTokenId), getClass().getName());
193                    throw new IllegalStateException(message);
194                }
195            }
196        }
197    }
198
199    /**
200     * Validates that check's required javadoc tokens are subset of default javadoc tokens.
201     * @throws IllegalStateException when validation of default javadoc tokens fails
202     */
203    private void validateDefaultJavadocTokens() {
204        if (getRequiredJavadocTokens().length != 0) {
205            final int[] defaultJavadocTokens = getDefaultJavadocTokens();
206            Arrays.sort(defaultJavadocTokens);
207            for (final int javadocToken : getRequiredJavadocTokens()) {
208                if (Arrays.binarySearch(defaultJavadocTokens, javadocToken) < 0) {
209                    final String message = String.format(Locale.ROOT,
210                            "Javadoc Token \"%s\" from required javadoc "
211                                + "tokens was not found in default "
212                                + "javadoc tokens list in check %s",
213                            javadocToken, getClass().getName());
214                    throw new IllegalStateException(message);
215                }
216            }
217        }
218    }
219
220    /**
221     * Called before the starting to process a tree.
222     * @param rootAst
223     *        the root of the tree
224     * @noinspection WeakerAccess
225     */
226    public void beginJavadocTree(DetailNode rootAst) {
227        // No code by default, should be overridden only by demand at subclasses
228    }
229
230    /**
231     * Called after finished processing a tree.
232     * @param rootAst
233     *        the root of the tree
234     * @noinspection WeakerAccess
235     */
236    public void finishJavadocTree(DetailNode rootAst) {
237        // No code by default, should be overridden only by demand at subclasses
238    }
239
240    /**
241     * Called after all the child nodes have been process.
242     * @param ast
243     *        the token leaving
244     */
245    public void leaveJavadocToken(DetailNode ast) {
246        // No code by default, should be overridden only by demand at subclasses
247    }
248
249    /**
250     * Defined final to not allow JavadocChecks to change default tokens.
251     * @return default tokens
252     */
253    @Override
254    public final int[] getDefaultTokens() {
255        return new int[] {TokenTypes.BLOCK_COMMENT_BEGIN };
256    }
257
258    @Override
259    public final int[] getAcceptableTokens() {
260        return getDefaultTokens();
261    }
262
263    @Override
264    public final int[] getRequiredTokens() {
265        return getDefaultTokens();
266    }
267
268    /**
269     * Defined final because all JavadocChecks require comment nodes.
270     * @return true
271     */
272    @Override
273    public final boolean isCommentNodesRequired() {
274        return true;
275    }
276
277    @Override
278    public final void beginTree(DetailAST rootAST) {
279        TREE_CACHE.get().clear();
280    }
281
282    @Override
283    public final void finishTree(DetailAST rootAST) {
284        TREE_CACHE.get().clear();
285    }
286
287    @Override
288    public final void visitToken(DetailAST blockCommentNode) {
289        if (JavadocUtils.isJavadocComment(blockCommentNode)) {
290            // store as field, to share with child Checks
291            context.get().blockCommentAst = blockCommentNode;
292
293            final String treeCacheKey = blockCommentNode.getLineNo() + ":"
294                    + blockCommentNode.getColumnNo();
295
296            final ParseStatus result;
297
298            if (TREE_CACHE.get().containsKey(treeCacheKey)) {
299                result = TREE_CACHE.get().get(treeCacheKey);
300            }
301            else {
302                result = context.get().parser
303                        .parseJavadocAsDetailNode(blockCommentNode);
304                TREE_CACHE.get().put(treeCacheKey, result);
305            }
306
307            if (result.getParseErrorMessage() == null) {
308                if (acceptJavadocWithNonTightHtml() || !result.isNonTight()) {
309                    processTree(result.getTree());
310                }
311
312                if (violateExecutionOnNonTightHtml && result.isNonTight()) {
313                    log(result.getFirstNonTightHtmlTag().getLine(),
314                            JavadocDetailNodeParser.MSG_UNCLOSED_HTML_TAG,
315                            result.getFirstNonTightHtmlTag().getText());
316                }
317            }
318            else {
319                final ParseErrorMessage parseErrorMessage = result.getParseErrorMessage();
320                log(parseErrorMessage.getLineNumber(),
321                        parseErrorMessage.getMessageKey(),
322                        parseErrorMessage.getMessageArguments());
323            }
324        }
325
326    }
327
328    /**
329     * Getter for block comment in Java language syntax tree.
330     * @return A block comment in the syntax tree.
331     */
332    protected DetailAST getBlockCommentAst() {
333        return context.get().blockCommentAst;
334    }
335
336    /**
337     * Processes JavadocAST tree notifying Check.
338     * @param root
339     *        root of JavadocAST tree.
340     */
341    private void processTree(DetailNode root) {
342        beginJavadocTree(root);
343        walk(root);
344        finishJavadocTree(root);
345    }
346
347    /**
348     * Processes a node calling Check at interested nodes.
349     * @param root
350     *        the root of tree for process
351     */
352    private void walk(DetailNode root) {
353        DetailNode curNode = root;
354        while (curNode != null) {
355            boolean waitsForProcessing = shouldBeProcessed(curNode);
356
357            if (waitsForProcessing) {
358                visitJavadocToken(curNode);
359            }
360            DetailNode toVisit = JavadocUtils.getFirstChild(curNode);
361            while (curNode != null && toVisit == null) {
362
363                if (waitsForProcessing) {
364                    leaveJavadocToken(curNode);
365                }
366
367                toVisit = JavadocUtils.getNextSibling(curNode);
368                if (toVisit == null) {
369                    curNode = curNode.getParent();
370                    if (curNode != null) {
371                        waitsForProcessing = shouldBeProcessed(curNode);
372                    }
373                }
374            }
375            curNode = toVisit;
376        }
377    }
378
379    /**
380     * Checks whether the current node should be processed by the check.
381     * @param curNode current node.
382     * @return true if the current node should be processed by the check.
383     */
384    private boolean shouldBeProcessed(DetailNode curNode) {
385        return javadocTokens.contains(curNode.getType());
386    }
387
388    /**
389     * The file context holder.
390     */
391    private static class FileContext {
392        /**
393         * Parses content of Javadoc comment as DetailNode tree.
394         */
395        private final JavadocDetailNodeParser parser = new JavadocDetailNodeParser();
396
397        /**
398         * DetailAST node of considered Javadoc comment that is just a block comment
399         * in Java language syntax tree.
400         */
401        private DetailAST blockCommentAst;
402    }
403}