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