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