View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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.api.AbstractCheck;
35  import com.puppycrawl.tools.checkstyle.api.DetailAST;
36  import com.puppycrawl.tools.checkstyle.api.DetailNode;
37  import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
38  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
39  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
40  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
41  
42  /**
43   * Base class for Checks that process Javadoc comments.
44   *
45   * @noinspection NoopMethodInAbstractClass
46   * @noinspectionreason NoopMethodInAbstractClass - we allow each
47   *      check to define these methods, as needed. They
48   *      should be overridden only by demand in subclasses
49   */
50  public abstract class AbstractJavadocCheck extends AbstractCheck {
51  
52      /**
53       * Message key of error message. Missed close HTML tag breaks structure
54       * of parse tree, so parser stops parsing and generates such error
55       * message. This case is special because parser prints error like
56       * {@code "no viable alternative at input 'b \n *\n'"} and it is not
57       * clear that error is about missed close HTML tag.
58       */
59      public static final String MSG_JAVADOC_MISSED_HTML_CLOSE =
60              JavadocDetailNodeParser.MSG_JAVADOC_MISSED_HTML_CLOSE;
61  
62      /**
63       * Message key of error message.
64       */
65      public static final String MSG_JAVADOC_WRONG_SINGLETON_TAG =
66              JavadocDetailNodeParser.MSG_JAVADOC_WRONG_SINGLETON_TAG;
67  
68      /**
69       * Parse error while rule recognition.
70       */
71      public static final String MSG_JAVADOC_PARSE_RULE_ERROR =
72              JavadocDetailNodeParser.MSG_JAVADOC_PARSE_RULE_ERROR;
73  
74      /**
75       * Message key of error message.
76       */
77      public static final String MSG_KEY_UNCLOSED_HTML_TAG =
78              JavadocDetailNodeParser.MSG_UNCLOSED_HTML_TAG;
79  
80      /**
81       * Key is the block comment node "lineNo". Value is {@link DetailNode} tree.
82       * Map is stored in {@link ThreadLocal}
83       * to guarantee basic thread safety and avoid shared, mutable state when not necessary.
84       */
85      private static final ThreadLocal<Map<Integer, ParseStatus>> TREE_CACHE =
86              ThreadLocal.withInitial(HashMap::new);
87  
88      /**
89       * The file context.
90       *
91       * @noinspection ThreadLocalNotStaticFinal
92       * @noinspectionreason ThreadLocalNotStaticFinal - static context is
93       *       problematic for multithreading
94       */
95      private final ThreadLocal<FileContext> context = ThreadLocal.withInitial(FileContext::new);
96  
97      /** The javadoc tokens the check is interested in. */
98      private final Set<Integer> javadocTokens = new HashSet<>();
99  
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 int treeCacheKey = blockCommentNode.getLineNo();
324 
325             final ParseStatus result = TREE_CACHE.get()
326                     .computeIfAbsent(treeCacheKey, lineNumber -> {
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                 curNode = curNode.getParent();
393                 if (curNode != null) {
394                     waitsForProcessing = shouldBeProcessed(curNode);
395                 }
396             }
397             curNode = toVisit;
398         }
399     }
400 
401     /**
402      * Checks whether the current node should be processed by the check.
403      *
404      * @param curNode current node.
405      * @return true if the current node should be processed by the check.
406      */
407     private boolean shouldBeProcessed(DetailNode curNode) {
408         return javadocTokens.contains(curNode.getType());
409     }
410 
411     @Override
412     public void destroy() {
413         super.destroy();
414         context.remove();
415         TREE_CACHE.remove();
416     }
417 
418     /**
419      * The file context holder.
420      */
421     private static final class FileContext {
422 
423         /**
424          * Parses content of Javadoc comment as DetailNode tree.
425          */
426         private final JavadocDetailNodeParser parser = new JavadocDetailNodeParser();
427 
428         /**
429          * DetailAST node of considered Javadoc comment that is just a block comment
430          * in Java language syntax tree.
431          */
432         private DetailAST blockCommentAst;
433 
434     }
435 
436 }