001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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     * Creates a new {@code AbstractJavadocCheck} instance.
101     */
102    protected AbstractJavadocCheck() {
103        // no code by default
104    }
105
106    /**
107     * Returns the default javadoc token types a check is interested in.
108     *
109     * @return the default javadoc token types
110     * @see JavadocCommentsTokenTypes
111     */
112    public abstract int[] getDefaultJavadocTokens();
113
114    /**
115     * Called to process a Javadoc token.
116     *
117     * @param ast
118     *        the token to process
119     */
120    public abstract void visitJavadocToken(DetailNode ast);
121
122    /**
123     * The configurable javadoc token set.
124     * Used to protect Checks against malicious users who specify an
125     * unacceptable javadoc token set in the configuration file.
126     * The default implementation returns the check's default javadoc tokens.
127     *
128     * @return the javadoc token set this check is designed for.
129     * @see JavadocCommentsTokenTypes
130     */
131    public int[] getAcceptableJavadocTokens() {
132        final int[] defaultJavadocTokens = getDefaultJavadocTokens();
133        final int[] copy = new int[defaultJavadocTokens.length];
134        System.arraycopy(defaultJavadocTokens, 0, copy, 0, defaultJavadocTokens.length);
135        return copy;
136    }
137
138    /**
139     * The javadoc tokens that this check must be registered for.
140     *
141     * @return the javadoc token set this must be registered for.
142     * @see JavadocCommentsTokenTypes
143     */
144    public int[] getRequiredJavadocTokens() {
145        return CommonUtil.EMPTY_INT_ARRAY;
146    }
147
148    /**
149     * This method determines if a check should process javadoc containing non-tight html tags.
150     * This method must be overridden in checks extending {@code AbstractJavadocCheck} which
151     * are not supposed to process javadoc containing non-tight html tags.
152     *
153     * @return true if the check should or can process javadoc containing non-tight html tags;
154     *     false otherwise
155     * @see ParseStatus#isNonTight()
156     * @see <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
157     *     Tight HTML rules</a>
158     */
159    public boolean acceptJavadocWithNonTightHtml() {
160        return true;
161    }
162
163    /**
164     * Setter to control when to print violations if the Javadoc being examined by this check
165     * violates the tight html rules defined at
166     * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
167     *     Tight-HTML Rules</a>.
168     *
169     * @param shouldReportViolation value to which the field shall be set to
170     * @since 8.3
171     */
172    public void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) {
173        violateExecutionOnNonTightHtml = shouldReportViolation;
174    }
175
176    /**
177     * Adds a set of tokens the check is interested in.
178     *
179     * @param strRep the string representation of the tokens interested in
180     */
181    public void setJavadocTokens(String... strRep) {
182        for (String str : strRep) {
183            javadocTokens.add(JavadocUtil.getTokenId(str));
184        }
185    }
186
187    @Override
188    public void init() {
189        validateDefaultJavadocTokens();
190        if (javadocTokens.isEmpty()) {
191            javadocTokens.addAll(
192                    Arrays.stream(getDefaultJavadocTokens()).boxed()
193                        .toList());
194        }
195        else {
196            final int[] acceptableJavadocTokens = getAcceptableJavadocTokens();
197            Arrays.sort(acceptableJavadocTokens);
198            for (Integer javadocTokenId : javadocTokens) {
199                if (Arrays.binarySearch(acceptableJavadocTokens, javadocTokenId) < 0) {
200                    final String message = String.format(Locale.ROOT, "Javadoc Token \"%s\" was "
201                            + "not found in Acceptable javadoc tokens list in check %s",
202                            JavadocUtil.getTokenName(javadocTokenId), getClass().getName());
203                    throw new IllegalStateException(message);
204                }
205            }
206        }
207    }
208
209    /**
210     * Validates that check's required javadoc tokens are subset of default javadoc tokens.
211     *
212     * @throws IllegalStateException when validation of default javadoc tokens fails
213     */
214    private void validateDefaultJavadocTokens() {
215        final Set<Integer> defaultTokens = Arrays.stream(getDefaultJavadocTokens())
216                .boxed()
217                .collect(Collectors.toUnmodifiableSet());
218
219        final List<Integer> missingRequiredTokenNames = Arrays.stream(getRequiredJavadocTokens())
220                .boxed()
221                .filter(token -> !defaultTokens.contains(token))
222                .toList();
223
224        if (!missingRequiredTokenNames.isEmpty()) {
225            final String message = String.format(Locale.ROOT,
226                        "Javadoc Token \"%s\" from required javadoc "
227                            + "tokens was not found in default "
228                            + "javadoc tokens list in check %s",
229                        missingRequiredTokenNames.stream()
230                        .map(String::valueOf)
231                        .collect(Collectors.joining(", ")),
232                        getClass().getName());
233            throw new IllegalStateException(message);
234        }
235    }
236
237    /**
238     * Called before the starting to process a tree.
239     *
240     * @param rootAst
241     *        the root of the tree
242     * @noinspection WeakerAccess
243     * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
244     */
245    public void beginJavadocTree(DetailNode rootAst) {
246        // No code by default, should be overridden only by demand at subclasses
247    }
248
249    /**
250     * Called after finished processing a tree.
251     *
252     * @param rootAst
253     *        the root of the tree
254     * @noinspection WeakerAccess
255     * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
256     */
257    public void finishJavadocTree(DetailNode rootAst) {
258        // No code by default, should be overridden only by demand at subclasses
259    }
260
261    /**
262     * Called after all the child nodes have been process.
263     *
264     * @param ast
265     *        the token leaving
266     */
267    public void leaveJavadocToken(DetailNode ast) {
268        // No code by default, should be overridden only by demand at subclasses
269    }
270
271    @Override
272    public int[] getDefaultTokens() {
273        return getRequiredTokens();
274    }
275
276    @Override
277    public int[] getAcceptableTokens() {
278        return getRequiredTokens();
279    }
280
281    @Override
282    public int[] getRequiredTokens() {
283        return new int[] {TokenTypes.BLOCK_COMMENT_BEGIN };
284    }
285
286    /**
287     * Defined final because all JavadocChecks require comment nodes.
288     *
289     * @return true
290     */
291    @Override
292    public final boolean isCommentNodesRequired() {
293        return true;
294    }
295
296    @Override
297    public void beginTree(DetailAST rootAST) {
298        TREE_CACHE.get().clear();
299    }
300
301    @Override
302    public void visitToken(DetailAST blockCommentNode) {
303        if (JavadocUtil.isJavadocComment(blockCommentNode)) {
304            // store as field, to share with child Checks
305            context.get().blockCommentAst = blockCommentNode;
306
307            final int treeCacheKey = blockCommentNode.getLineNo();
308
309            final ParseStatus result = TREE_CACHE.get()
310                    .computeIfAbsent(treeCacheKey, lineNumber -> {
311                        return context.get().parser.parseJavadocComment(blockCommentNode);
312                    });
313
314            if (result.getParseErrorMessage() == null) {
315                if (acceptJavadocWithNonTightHtml() || !result.isNonTight()) {
316                    processTree(result.getTree());
317                }
318
319                if (violateExecutionOnNonTightHtml && result.isNonTight()) {
320                    final DetailNode firstNonTightHtmlTag = result.getFirstNonTightHtmlTag();
321                    log(firstNonTightHtmlTag.getLineNumber(),
322                            MSG_KEY_UNCLOSED_HTML_TAG,
323                            firstNonTightHtmlTag.getText());
324                }
325            }
326            else {
327                final ParseErrorMessage parseErrorMessage = result.getParseErrorMessage();
328                log(parseErrorMessage.getLineNumber(),
329                        parseErrorMessage.getMessageKey(),
330                        parseErrorMessage.getMessageArguments());
331            }
332        }
333    }
334
335    /**
336     * Getter for block comment in Java language syntax tree.
337     *
338     * @return A block comment in the syntax tree.
339     */
340    protected DetailAST getBlockCommentAst() {
341        return context.get().blockCommentAst;
342    }
343
344    /**
345     * Processes JavadocAST tree notifying Check.
346     *
347     * @param root
348     *        root of JavadocAST tree.
349     */
350    private void processTree(DetailNode root) {
351        beginJavadocTree(root);
352        walk(root);
353        finishJavadocTree(root);
354    }
355
356    /**
357     * Processes a node calling Check at interested nodes.
358     *
359     * @param root
360     *        the root of tree for process
361     */
362    private void walk(DetailNode root) {
363        DetailNode curNode = root;
364        while (curNode != null) {
365            boolean waitsForProcessing = shouldBeProcessed(curNode);
366
367            if (waitsForProcessing) {
368                visitJavadocToken(curNode);
369            }
370            DetailNode toVisit = curNode.getFirstChild();
371            while (curNode != null && toVisit == null) {
372                if (waitsForProcessing) {
373                    leaveJavadocToken(curNode);
374                }
375
376                toVisit = curNode.getNextSibling();
377                curNode = curNode.getParent();
378                if (curNode != null) {
379                    waitsForProcessing = shouldBeProcessed(curNode);
380                }
381            }
382            curNode = toVisit;
383        }
384    }
385
386    /**
387     * Checks whether the current node should be processed by the check.
388     *
389     * @param curNode current node.
390     * @return true if the current node should be processed by the check.
391     */
392    private boolean shouldBeProcessed(DetailNode curNode) {
393        return javadocTokens.contains(curNode.getType());
394    }
395
396    @Override
397    public void destroy() {
398        super.destroy();
399        context.remove();
400        TREE_CACHE.remove();
401    }
402
403    /**
404     * Logs a message against a DetailNode.
405     * This is a wrapper method to log violations using a DetailNode
406     * instead of manually specifying line and column numbers.
407     *
408     * @param node the DetailNode that has the violation
409     * @param key the message key from the check messages
410     * @param args the arguments to the message
411     */
412    protected final void log(DetailNode node, String key, Object... args) {
413        log(node.getLineNumber(), node.getColumnNumber(), key, args);
414    }
415
416    /**
417     * The file context holder.
418     */
419    private static final class FileContext {
420
421        /**
422         * Parses content of Javadoc comment as DetailNode tree.
423         */
424        private final JavadocDetailNodeParser parser = new JavadocDetailNodeParser();
425
426        /**
427         * DetailAST node of considered Javadoc comment that is just a block comment
428         * in Java language syntax tree.
429         */
430        private DetailAST blockCommentAst;
431
432        /**
433         * Creates a new {@code FileContext} instance.
434         */
435        private FileContext() {
436            // no code by default
437        }
438    }
439
440}