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