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.LineColumn;
39  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
40  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
41  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
42  
43  /**
44   * Base class for Checks that process Javadoc comments.
45   *
46   * @noinspection NoopMethodInAbstractClass
47   * @noinspectionreason NoopMethodInAbstractClass - we allow each
48   *      check to define these methods, as needed. They
49   *      should be overridden only by demand in subclasses
50   */
51  public abstract class AbstractJavadocCheck extends AbstractCheck {
52  
53      /**
54       * Message key of error message. Missed close HTML tag breaks structure
55       * of parse tree, so parser stops parsing and generates such error
56       * message. This case is special because parser prints error like
57       * {@code "no viable alternative at input 'b \n *\n'"} and it is not
58       * clear that error is about missed close HTML tag.
59       */
60      public static final String MSG_JAVADOC_MISSED_HTML_CLOSE =
61              JavadocDetailNodeParser.MSG_JAVADOC_MISSED_HTML_CLOSE;
62  
63      /**
64       * Message key of error message.
65       */
66      public static final String MSG_JAVADOC_WRONG_SINGLETON_TAG =
67              JavadocDetailNodeParser.MSG_JAVADOC_WRONG_SINGLETON_TAG;
68  
69      /**
70       * Parse error while rule recognition.
71       */
72      public static final String MSG_JAVADOC_PARSE_RULE_ERROR =
73              JavadocDetailNodeParser.MSG_JAVADOC_PARSE_RULE_ERROR;
74  
75      /**
76       * Message key of error message.
77       */
78      public static final String MSG_KEY_UNCLOSED_HTML_TAG =
79              JavadocDetailNodeParser.MSG_UNCLOSED_HTML_TAG;
80  
81      /**
82       * Key is "line:column". Value is {@link DetailNode} tree. 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<LineColumn, 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 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 }