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