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