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     * Key is "line:column". Value is {@link DetailNode} tree. Map is stored in {@link ThreadLocal}
076     * to guarantee basic thread safety and avoid shared, mutable state when not necessary.
077     */
078    private static final ThreadLocal<Map<LineColumn, ParseStatus>> TREE_CACHE =
079            ThreadLocal.withInitial(HashMap::new);
080
081    /**
082     * The file context.
083     *
084     * @noinspection ThreadLocalNotStaticFinal
085     * @noinspectionreason ThreadLocalNotStaticFinal - static context is
086     *       problematic for multithreading
087     */
088    private final ThreadLocal<FileContext> context = ThreadLocal.withInitial(FileContext::new);
089
090    /** The javadoc tokens the check is interested in. */
091    private final Set<Integer> javadocTokens = new HashSet<>();
092
093    /**
094     * This property determines if a check should log a violation upon encountering javadoc with
095     * non-tight html. The default return value for this method is set to false since checks
096     * generally tend to be fine with non-tight html. It can be set through config file if a check
097     * is to log violation upon encountering non-tight HTML in javadoc.
098     *
099     * @see ParseStatus#isNonTight()
100     * @see <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
101     *     Tight HTML rules</a>
102     */
103    private boolean violateExecutionOnNonTightHtml;
104
105    /**
106     * Returns the default javadoc token types a check is interested in.
107     *
108     * @return the default javadoc token types
109     * @see JavadocTokenTypes
110     */
111    public abstract int[] getDefaultJavadocTokens();
112
113    /**
114     * Called to process a Javadoc token.
115     *
116     * @param ast
117     *        the token to process
118     */
119    public abstract void visitJavadocToken(DetailNode ast);
120
121    /**
122     * The configurable javadoc token set.
123     * Used to protect Checks against malicious users who specify an
124     * unacceptable javadoc token set in the configuration file.
125     * The default implementation returns the check's default javadoc tokens.
126     *
127     * @return the javadoc token set this check is designed for.
128     * @see JavadocTokenTypes
129     */
130    public int[] getAcceptableJavadocTokens() {
131        final int[] defaultJavadocTokens = getDefaultJavadocTokens();
132        final int[] copy = new int[defaultJavadocTokens.length];
133        System.arraycopy(defaultJavadocTokens, 0, copy, 0, defaultJavadocTokens.length);
134        return copy;
135    }
136
137    /**
138     * The javadoc tokens that this check must be registered for.
139     *
140     * @return the javadoc token set this must be registered for.
141     * @see JavadocTokenTypes
142     */
143    public int[] getRequiredJavadocTokens() {
144        return CommonUtil.EMPTY_INT_ARRAY;
145    }
146
147    /**
148     * This method determines if a check should process javadoc containing non-tight html tags.
149     * This method must be overridden in checks extending {@code AbstractJavadocCheck} which
150     * are not supposed to process javadoc containing non-tight html tags.
151     *
152     * @return true if the check should or can process javadoc containing non-tight html tags;
153     *     false otherwise
154     * @see ParseStatus#isNonTight()
155     * @see <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
156     *     Tight HTML rules</a>
157     */
158    public boolean acceptJavadocWithNonTightHtml() {
159        return true;
160    }
161
162    /**
163     * Setter to control when to print violations if the Javadoc being examined by this check
164     * violates the tight html rules defined at
165     * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
166     *     Tight-HTML Rules</a>.
167     *
168     * @param shouldReportViolation value to which the field shall be set to
169     */
170    public final void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) {
171        violateExecutionOnNonTightHtml = shouldReportViolation;
172    }
173
174    /**
175     * Adds a set of tokens the check is interested in.
176     *
177     * @param strRep the string representation of the tokens interested in
178     */
179    public final void setJavadocTokens(String... strRep) {
180        for (String str : strRep) {
181            javadocTokens.add(JavadocUtil.getTokenId(str));
182        }
183    }
184
185    @Override
186    public void init() {
187        validateDefaultJavadocTokens();
188        if (javadocTokens.isEmpty()) {
189            javadocTokens.addAll(
190                    Arrays.stream(getDefaultJavadocTokens()).boxed().collect(Collectors.toList()));
191        }
192        else {
193            final int[] acceptableJavadocTokens = getAcceptableJavadocTokens();
194            Arrays.sort(acceptableJavadocTokens);
195            for (Integer javadocTokenId : javadocTokens) {
196                if (Arrays.binarySearch(acceptableJavadocTokens, javadocTokenId) < 0) {
197                    final String message = String.format(Locale.ROOT, "Javadoc Token \"%s\" was "
198                            + "not found in Acceptable javadoc tokens list in check %s",
199                            JavadocUtil.getTokenName(javadocTokenId), getClass().getName());
200                    throw new IllegalStateException(message);
201                }
202            }
203        }
204    }
205
206    /**
207     * Validates that check's required javadoc tokens are subset of default javadoc tokens.
208     *
209     * @throws IllegalStateException when validation of default javadoc tokens fails
210     */
211    private void validateDefaultJavadocTokens() {
212        if (getRequiredJavadocTokens().length != 0) {
213            final int[] defaultJavadocTokens = getDefaultJavadocTokens();
214            Arrays.sort(defaultJavadocTokens);
215            for (final int javadocToken : getRequiredJavadocTokens()) {
216                if (Arrays.binarySearch(defaultJavadocTokens, javadocToken) < 0) {
217                    final String message = String.format(Locale.ROOT,
218                            "Javadoc Token \"%s\" from required javadoc "
219                                + "tokens was not found in default "
220                                + "javadoc tokens list in check %s",
221                            javadocToken, getClass().getName());
222                    throw new IllegalStateException(message);
223                }
224            }
225        }
226    }
227
228    /**
229     * Called before the starting to process a tree.
230     *
231     * @param rootAst
232     *        the root of the tree
233     * @noinspection WeakerAccess
234     * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
235     */
236    public void beginJavadocTree(DetailNode rootAst) {
237        // No code by default, should be overridden only by demand at subclasses
238    }
239
240    /**
241     * Called after finished processing a tree.
242     *
243     * @param rootAst
244     *        the root of the tree
245     * @noinspection WeakerAccess
246     * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
247     */
248    public void finishJavadocTree(DetailNode rootAst) {
249        // No code by default, should be overridden only by demand at subclasses
250    }
251
252    /**
253     * Called after all the child nodes have been process.
254     *
255     * @param ast
256     *        the token leaving
257     */
258    public void leaveJavadocToken(DetailNode ast) {
259        // No code by default, should be overridden only by demand at subclasses
260    }
261
262    /**
263     * Defined final to not allow JavadocChecks to change default tokens.
264     *
265     * @return default tokens
266     */
267    @Override
268    public final int[] getDefaultTokens() {
269        return getRequiredTokens();
270    }
271
272    @Override
273    public final int[] getAcceptableTokens() {
274        return getRequiredTokens();
275    }
276
277    @Override
278    public final int[] getRequiredTokens() {
279        return new int[] {TokenTypes.BLOCK_COMMENT_BEGIN };
280    }
281
282    /**
283     * Defined final because all JavadocChecks require comment nodes.
284     *
285     * @return true
286     */
287    @Override
288    public final boolean isCommentNodesRequired() {
289        return true;
290    }
291
292    @Override
293    public final void beginTree(DetailAST rootAST) {
294        TREE_CACHE.get().clear();
295    }
296
297    @Override
298    public final void finishTree(DetailAST rootAST) {
299        // No code, prevent override in subclasses
300    }
301
302    @Override
303    public final void visitToken(DetailAST blockCommentNode) {
304        if (JavadocUtil.isJavadocComment(blockCommentNode)) {
305            // store as field, to share with child Checks
306            context.get().blockCommentAst = blockCommentNode;
307
308            final LineColumn treeCacheKey = new LineColumn(blockCommentNode.getLineNo(),
309                    blockCommentNode.getColumnNo());
310
311            final ParseStatus result = TREE_CACHE.get().computeIfAbsent(treeCacheKey, key -> {
312                return context.get().parser.parseJavadocAsDetailNode(blockCommentNode);
313            });
314
315            if (result.getParseErrorMessage() == null) {
316                if (acceptJavadocWithNonTightHtml() || !result.isNonTight()) {
317                    processTree(result.getTree());
318                }
319
320                if (violateExecutionOnNonTightHtml && result.isNonTight()) {
321                    log(result.getFirstNonTightHtmlTag().getLine(),
322                            JavadocDetailNodeParser.MSG_UNCLOSED_HTML_TAG,
323                            result.getFirstNonTightHtmlTag().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 = JavadocUtil.getFirstChild(curNode);
371            while (curNode != null && toVisit == null) {
372                if (waitsForProcessing) {
373                    leaveJavadocToken(curNode);
374                }
375
376                toVisit = JavadocUtil.getNextSibling(curNode);
377                if (toVisit == null) {
378                    curNode = curNode.getParent();
379                    if (curNode != null) {
380                        waitsForProcessing = shouldBeProcessed(curNode);
381                    }
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 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}