001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 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.List;
026import java.util.Locale;
027import java.util.Map;
028import java.util.Set;
029import java.util.stream.Collectors;
030
031import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser;
032import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser.ParseErrorMessage;
033import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser.ParseStatus;
034import com.puppycrawl.tools.checkstyle.PropertyType;
035import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
036import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
037import com.puppycrawl.tools.checkstyle.api.DetailAST;
038import com.puppycrawl.tools.checkstyle.api.DetailNode;
039import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
040import com.puppycrawl.tools.checkstyle.api.TokenTypes;
041import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
042import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
043
044/**
045 * Base class for Checks that process Javadoc comments.
046 *
047 * @noinspection NoopMethodInAbstractClass
048 * @noinspectionreason NoopMethodInAbstractClass - we allow each
049 *      check to define these methods, as needed. They
050 *      should be overridden only by demand in subclasses
051 */
052public abstract class AbstractJavadocCheck extends AbstractCheck {
053
054    /**
055     * Parse error while rule recognition.
056     */
057    public static final String MSG_JAVADOC_PARSE_RULE_ERROR =
058            JavadocDetailNodeParser.MSG_JAVADOC_PARSE_RULE_ERROR;
059
060    /**
061     * Message key of error message.
062     */
063    public static final String MSG_KEY_UNCLOSED_HTML_TAG =
064            JavadocDetailNodeParser.MSG_UNCLOSED_HTML_TAG;
065
066    /**
067     * Key is the block comment node "lineNo". Value is {@link DetailNode} tree.
068     * Map is stored in {@link ThreadLocal}
069     * to guarantee basic thread safety and avoid shared, mutable state when not necessary.
070     */
071    private static final ThreadLocal<Map<Integer, ParseStatus>> TREE_CACHE =
072            ThreadLocal.withInitial(HashMap::new);
073
074    /**
075     * The file context.
076     *
077     * @noinspection ThreadLocalNotStaticFinal
078     * @noinspectionreason ThreadLocalNotStaticFinal - static context is
079     *       problematic for multithreading
080     */
081    private final ThreadLocal<FileContext> context = ThreadLocal.withInitial(FileContext::new);
082
083    /** The javadoc tokens the check is interested in. */
084    @XdocsPropertyType(PropertyType.TOKEN_ARRAY)
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 JavadocCommentsTokenTypes
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 JavadocCommentsTokenTypes
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 JavadocCommentsTokenTypes
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     * @since 8.3
164     */
165    public void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) {
166        violateExecutionOnNonTightHtml = shouldReportViolation;
167    }
168
169    /**
170     * Adds a set of tokens the check is interested in.
171     *
172     * @param strRep the string representation of the tokens interested in
173     */
174    public void setJavadocTokens(String... strRep) {
175        for (String str : strRep) {
176            javadocTokens.add(JavadocUtil.getTokenId(str));
177        }
178    }
179
180    @Override
181    public void init() {
182        validateDefaultJavadocTokens();
183        if (javadocTokens.isEmpty()) {
184            javadocTokens.addAll(
185                    Arrays.stream(getDefaultJavadocTokens()).boxed()
186                        .toList());
187        }
188        else {
189            final int[] acceptableJavadocTokens = getAcceptableJavadocTokens();
190            Arrays.sort(acceptableJavadocTokens);
191            for (Integer javadocTokenId : javadocTokens) {
192                if (Arrays.binarySearch(acceptableJavadocTokens, javadocTokenId) < 0) {
193                    final String message = String.format(Locale.ROOT, "Javadoc Token \"%s\" was "
194                            + "not found in Acceptable javadoc tokens list in check %s",
195                            JavadocUtil.getTokenName(javadocTokenId), getClass().getName());
196                    throw new IllegalStateException(message);
197                }
198            }
199        }
200    }
201
202    /**
203     * Validates that check's required javadoc tokens are subset of default javadoc tokens.
204     *
205     * @throws IllegalStateException when validation of default javadoc tokens fails
206     */
207    private void validateDefaultJavadocTokens() {
208        final Set<Integer> defaultTokens = Arrays.stream(getDefaultJavadocTokens())
209                .boxed()
210                .collect(Collectors.toUnmodifiableSet());
211
212        final List<Integer> missingRequiredTokenNames = Arrays.stream(getRequiredJavadocTokens())
213                .boxed()
214                .filter(token -> !defaultTokens.contains(token))
215                .toList();
216
217        if (!missingRequiredTokenNames.isEmpty()) {
218            final String message = String.format(Locale.ROOT,
219                        "Javadoc Token \"%s\" from required javadoc "
220                            + "tokens was not found in default "
221                            + "javadoc tokens list in check %s",
222                        missingRequiredTokenNames.stream()
223                        .map(String::valueOf)
224                        .collect(Collectors.joining(", ")),
225                        getClass().getName());
226            throw new IllegalStateException(message);
227        }
228    }
229
230    /**
231     * Called before the starting to process a tree.
232     *
233     * @param rootAst
234     *        the root of the tree
235     * @noinspection WeakerAccess
236     * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
237     */
238    public void beginJavadocTree(DetailNode rootAst) {
239        // No code by default, should be overridden only by demand at subclasses
240    }
241
242    /**
243     * Called after finished processing a tree.
244     *
245     * @param rootAst
246     *        the root of the tree
247     * @noinspection WeakerAccess
248     * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
249     */
250    public void finishJavadocTree(DetailNode rootAst) {
251        // No code by default, should be overridden only by demand at subclasses
252    }
253
254    /**
255     * Called after all the child nodes have been process.
256     *
257     * @param ast
258     *        the token leaving
259     */
260    public void leaveJavadocToken(DetailNode ast) {
261        // No code by default, should be overridden only by demand at subclasses
262    }
263
264    /**
265     * Defined final to not allow JavadocChecks to change default tokens.
266     *
267     * @return default tokens
268     */
269    @Override
270    public final int[] getDefaultTokens() {
271        return getRequiredTokens();
272    }
273
274    @Override
275    public final int[] getAcceptableTokens() {
276        return getRequiredTokens();
277    }
278
279    @Override
280    public final int[] getRequiredTokens() {
281        return new int[] {TokenTypes.BLOCK_COMMENT_BEGIN };
282    }
283
284    /**
285     * Defined final because all JavadocChecks require comment nodes.
286     *
287     * @return true
288     */
289    @Override
290    public final boolean isCommentNodesRequired() {
291        return true;
292    }
293
294    @Override
295    public final void beginTree(DetailAST rootAST) {
296        TREE_CACHE.get().clear();
297    }
298
299    @Override
300    public final void finishTree(DetailAST rootAST) {
301        // No code, prevent override in subclasses
302    }
303
304    @Override
305    public final void visitToken(DetailAST blockCommentNode) {
306        if (JavadocUtil.isJavadocComment(blockCommentNode)) {
307            // store as field, to share with child Checks
308            context.get().blockCommentAst = blockCommentNode;
309
310            final int treeCacheKey = blockCommentNode.getLineNo();
311
312            final ParseStatus result = TREE_CACHE.get()
313                    .computeIfAbsent(treeCacheKey, lineNumber -> {
314                        return context.get().parser.parseJavadocComment(blockCommentNode);
315                    });
316
317            if (result.getParseErrorMessage() == null) {
318                if (acceptJavadocWithNonTightHtml() || !result.isNonTight()) {
319                    processTree(result.getTree());
320                }
321
322                if (violateExecutionOnNonTightHtml && result.isNonTight()) {
323                    log(result.getFirstNonTightHtmlTag().getLineNumber(),
324                            MSG_KEY_UNCLOSED_HTML_TAG,
325                            result.getFirstNonTightHtmlTag().getText());
326                }
327            }
328            else {
329                final ParseErrorMessage parseErrorMessage = result.getParseErrorMessage();
330                log(parseErrorMessage.getLineNumber(),
331                        parseErrorMessage.getMessageKey(),
332                        parseErrorMessage.getMessageArguments());
333            }
334        }
335    }
336
337    /**
338     * Getter for block comment in Java language syntax tree.
339     *
340     * @return A block comment in the syntax tree.
341     */
342    protected DetailAST getBlockCommentAst() {
343        return context.get().blockCommentAst;
344    }
345
346    /**
347     * Processes JavadocAST tree notifying Check.
348     *
349     * @param root
350     *        root of JavadocAST tree.
351     */
352    private void processTree(DetailNode root) {
353        beginJavadocTree(root);
354        walk(root);
355        finishJavadocTree(root);
356    }
357
358    /**
359     * Processes a node calling Check at interested nodes.
360     *
361     * @param root
362     *        the root of tree for process
363     */
364    private void walk(DetailNode root) {
365        DetailNode curNode = root;
366        while (curNode != null) {
367            boolean waitsForProcessing = shouldBeProcessed(curNode);
368
369            if (waitsForProcessing) {
370                visitJavadocToken(curNode);
371            }
372            DetailNode toVisit = curNode.getFirstChild();
373            while (curNode != null && toVisit == null) {
374                if (waitsForProcessing) {
375                    leaveJavadocToken(curNode);
376                }
377
378                toVisit = curNode.getNextSibling();
379                curNode = curNode.getParent();
380                if (curNode != null) {
381                    waitsForProcessing = shouldBeProcessed(curNode);
382                }
383            }
384            curNode = toVisit;
385        }
386    }
387
388    /**
389     * Checks whether the current node should be processed by the check.
390     *
391     * @param curNode current node.
392     * @return true if the current node should be processed by the check.
393     */
394    private boolean shouldBeProcessed(DetailNode curNode) {
395        return javadocTokens.contains(curNode.getType());
396    }
397
398    @Override
399    public void destroy() {
400        super.destroy();
401        context.remove();
402        TREE_CACHE.remove();
403    }
404
405    /**
406     * The file context holder.
407     */
408    private static final class FileContext {
409
410        /**
411         * Parses content of Javadoc comment as DetailNode tree.
412         */
413        private final JavadocDetailNodeParser parser = new JavadocDetailNodeParser();
414
415        /**
416         * DetailAST node of considered Javadoc comment that is just a block comment
417         * in Java language syntax tree.
418         */
419        private DetailAST blockCommentAst;
420
421    }
422
423}