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