View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.javadoc;
21  
22  import java.util.Arrays;
23  import java.util.HashMap;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.Locale;
27  import java.util.Map;
28  import java.util.Set;
29  import java.util.stream.Collectors;
30  
31  import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser;
32  import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser.ParseErrorMessage;
33  import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser.ParseStatus;
34  import com.puppycrawl.tools.checkstyle.PropertyType;
35  import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
36  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
37  import com.puppycrawl.tools.checkstyle.api.DetailAST;
38  import com.puppycrawl.tools.checkstyle.api.DetailNode;
39  import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
40  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
41  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
42  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
43  
44  /**
45   * Base class for Checks that process Javadoc comments.
46   *
47   * @noinspection NoopMethodInAbstractClass
48   * @noinspectionreason NoopMethodInAbstractClass - we allow each
49   *      check to define these methods, as needed. They
50   *      should be overridden only by demand in subclasses
51   */
52  public abstract class AbstractJavadocCheck extends AbstractCheck {
53  
54      /**
55       * Parse error while rule recognition.
56       */
57      public static final String MSG_JAVADOC_PARSE_RULE_ERROR =
58              JavadocDetailNodeParser.MSG_JAVADOC_PARSE_RULE_ERROR;
59  
60      /**
61       * Message key of error message.
62       */
63      public static final String MSG_KEY_UNCLOSED_HTML_TAG =
64              JavadocDetailNodeParser.MSG_UNCLOSED_HTML_TAG;
65  
66      /**
67       * Key is the block comment node "lineNo". Value is {@link DetailNode} tree.
68       * Map is stored in {@link ThreadLocal}
69       * to guarantee basic thread safety and avoid shared, mutable state when not necessary.
70       */
71      private static final ThreadLocal<Map<Integer, ParseStatus>> TREE_CACHE =
72              ThreadLocal.withInitial(HashMap::new);
73  
74      /**
75       * The file context.
76       *
77       * @noinspection ThreadLocalNotStaticFinal
78       * @noinspectionreason ThreadLocalNotStaticFinal - static context is
79       *       problematic for multithreading
80       */
81      private final ThreadLocal<FileContext> context = ThreadLocal.withInitial(FileContext::new);
82  
83      /** The javadoc tokens the check is interested in. */
84      @XdocsPropertyType(PropertyType.TOKEN_ARRAY)
85      private final Set<Integer> javadocTokens = new HashSet<>();
86  
87      /**
88       * This property determines if a check should log a violation upon encountering javadoc with
89       * non-tight html. The default return value for this method is set to false since checks
90       * generally tend to be fine with non-tight html. It can be set through config file if a check
91       * is to log violation upon encountering non-tight HTML in javadoc.
92       *
93       * @see ParseStatus#isNonTight()
94       * @see <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
95       *     Tight HTML rules</a>
96       */
97      private boolean violateExecutionOnNonTightHtml;
98  
99      /**
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 }