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