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.utils;
21  
22  import java.util.ArrayList;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Optional;
26  import java.util.regex.Pattern;
27  
28  import javax.annotation.Nullable;
29  
30  import com.puppycrawl.tools.checkstyle.api.DetailAST;
31  import com.puppycrawl.tools.checkstyle.api.DetailNode;
32  import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
33  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
34  
35  /**
36   * Contains utility methods for working with Javadoc.
37   */
38  public final class JavadocUtil {
39  
40      /** Maps from a token name to value. */
41      private static final Map<String, Integer> TOKEN_NAME_TO_VALUE;
42      /** Maps from a token value to name. */
43      private static final Map<Integer, String> TOKEN_VALUE_TO_NAME;
44  
45      /** Exception message for unknown JavaDoc token id. */
46      private static final String UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE = "Unknown javadoc"
47              + " token id. Given id: ";
48  
49      /** Newline pattern. */
50      private static final Pattern NEWLINE = Pattern.compile("\n");
51  
52      /** Return pattern. */
53      private static final Pattern RETURN = Pattern.compile("\r");
54  
55      /** Tab pattern. */
56      private static final Pattern TAB = Pattern.compile("\t");
57  
58      // initialise the constants
59      static {
60          TOKEN_NAME_TO_VALUE =
61                  TokenUtil.nameToValueMapFromPublicIntFields(JavadocCommentsTokenTypes.class);
62          TOKEN_VALUE_TO_NAME = TokenUtil.invertMap(TOKEN_NAME_TO_VALUE);
63      }
64  
65      /** Prevent instantiation. */
66      private JavadocUtil() {
67      }
68  
69      /**
70       * Checks that commentContent starts with '*' javadoc comment identifier.
71       *
72       * @param commentContent
73       *        content of block comment
74       * @return true if commentContent starts with '*' javadoc comment
75       *         identifier.
76       */
77      public static boolean isJavadocComment(String commentContent) {
78          boolean result = false;
79  
80          if (!commentContent.isEmpty()) {
81              final char docCommentIdentifier = commentContent.charAt(0);
82              result = docCommentIdentifier == '*';
83          }
84  
85          return result;
86      }
87  
88      /**
89       * Checks block comment content starts with '*' javadoc comment identifier.
90       *
91       * @param blockCommentBegin
92       *        block comment AST
93       * @return true if block comment content starts with '*' javadoc comment
94       *         identifier.
95       */
96      public static boolean isJavadocComment(DetailAST blockCommentBegin) {
97          final String commentContent = getBlockCommentContent(blockCommentBegin);
98          return isJavadocComment(commentContent) && isCorrectJavadocPosition(blockCommentBegin);
99      }
100 
101     /**
102      * Gets content of block comment.
103      *
104      * @param blockCommentBegin
105      *        block comment AST.
106      * @return content of block comment.
107      */
108     public static String getBlockCommentContent(DetailAST blockCommentBegin) {
109         final DetailAST commentContent = blockCommentBegin.getFirstChild();
110         return commentContent.getText();
111     }
112 
113     /**
114      * Get content of Javadoc comment.
115      *
116      * @param javadocCommentBegin
117      *        Javadoc comment AST
118      * @return content of Javadoc comment.
119      */
120     public static String getJavadocCommentContent(DetailAST javadocCommentBegin) {
121         final DetailAST commentContent = javadocCommentBegin.getFirstChild();
122         return commentContent.getText().substring(1);
123     }
124 
125     /**
126      * Returns the Javadoc block comment attached to the given declaration AST node.
127      *
128      * @param ast the declaration AST node
129      * @return the attached Javadoc block comment, or {@code null} if none is found
130      */
131     @Nullable
132     public static DetailAST getAttachedJavadocComment(final DetailAST ast) {
133         DetailAST result = null;
134         DetailAST child = ast.getFirstChild();
135         while (result == null && child.getType() != TokenTypes.IDENT) {
136             result = findJavadocComment(child);
137             child = child.getNextSibling();
138         }
139         return result;
140     }
141 
142     /**
143      * Returns the Javadoc block comment attached to the given package AST node.
144      * Because of <a href="https://github.com/checkstyle/checkstyle/issues/4392">parser bug</a>
145      * parser can place javadoc comment either as previous sibling of package definition
146      * or (if there is annotation between package def and javadoc) inside package definition tree.
147      * So we should look for javadoc in both places.
148      *
149      * @param ast the package declaration AST node
150      * @return the attached Javadoc block comment, or {@code null} if none is found
151      */
152     @Nullable
153     public static DetailAST getAttachedJavadocCommentForPackage(final DetailAST ast) {
154         DetailAST result = null;
155         final DetailAST prevSibling = ast.getPreviousSibling();
156         if (prevSibling != null
157                 && prevSibling.getType() == TokenTypes.BLOCK_COMMENT_BEGIN
158                 && isJavadocComment(prevSibling)) {
159             result = prevSibling;
160         }
161         else {
162             final Optional<DetailAST> firstAnnotationChild =
163                 Optional.ofNullable(ast.getFirstChild())
164                     .map(DetailAST::getFirstChild)
165                     .map(DetailAST::getFirstChild);
166             if (firstAnnotationChild.isPresent()) {
167                 for (DetailAST child = firstAnnotationChild.orElseThrow(); child != null;
168                      child = child.getNextSibling()) {
169                     if (child.getType() == TokenTypes.BLOCK_COMMENT_BEGIN
170                             && isJavadocComment(child)) {
171                         result = child;
172                         break;
173                     }
174                 }
175             }
176         }
177         return result;
178     }
179 
180     /**
181      * Finds the first Javadoc block comment under the given AST node.
182      *
183      * @param ast the AST node to search
184      * @return the Javadoc block comment, or {@code null} if none is found
185      */
186     @Nullable
187     private static DetailAST findJavadocComment(DetailAST ast) {
188         DetailAST result = null;
189         if (ast.getType() == TokenTypes.BLOCK_COMMENT_BEGIN && isJavadocComment(ast)) {
190             result = ast;
191         }
192         else {
193             DetailAST child = ast.getFirstChild();
194             while (result == null && child != null) {
195                 result = findJavadocComment(child);
196                 child = child.getNextSibling();
197             }
198         }
199         return result;
200     }
201 
202     /**
203      * Returns the first child token that has a specified type.
204      *
205      * @param detailNode
206      *        Javadoc AST node
207      * @param type
208      *        the token type to match
209      * @return the matching token, or null if no match
210      */
211     public static DetailNode findFirstToken(DetailNode detailNode, int type) {
212         DetailNode returnValue = null;
213         DetailNode node = detailNode.getFirstChild();
214         while (node != null) {
215             if (node.getType() == type) {
216                 returnValue = node;
217                 break;
218             }
219             node = node.getNextSibling();
220         }
221         return returnValue;
222     }
223 
224     /**
225      * Returns all child tokens that have a specified type.
226      *
227      * @param detailNode Javadoc AST node
228      * @param type the token type to match
229      * @return the matching tokens, or an empty list if no match
230      */
231     public static List<DetailNode> getAllNodesOfType(DetailNode detailNode, int type) {
232         final List<DetailNode> nodes = new ArrayList<>();
233         DetailNode node = detailNode.getFirstChild();
234         while (node != null) {
235             if (node.getType() == type) {
236                 nodes.add(node);
237             }
238             node = node.getNextSibling();
239         }
240         return nodes;
241     }
242 
243     /**
244      * Checks whether the given AST node is an HTML element with the specified tag name.
245      * This method ignore void elements.
246      *
247      * @param ast the AST node to check
248      *            (must be of type {@link JavadocCommentsTokenTypes#HTML_ELEMENT})
249      * @param expectedTagName the tag name to match (case-insensitive)
250      * @return {@code true} if the node has the given tag name, {@code false} otherwise
251      */
252     public static boolean isTag(DetailNode ast, String expectedTagName) {
253         final DetailNode htmlTagStart = findFirstToken(ast,
254                 JavadocCommentsTokenTypes.HTML_TAG_START);
255         boolean isTag = false;
256         if (htmlTagStart != null) {
257             final String tagName = findFirstToken(htmlTagStart,
258                 JavadocCommentsTokenTypes.TAG_NAME).getText();
259             isTag = expectedTagName.equalsIgnoreCase(tagName);
260         }
261         return isTag;
262     }
263 
264     /**
265      * Gets next sibling of specified node with the specified type.
266      *
267      * @param node DetailNode
268      * @param tokenType javadoc token type
269      * @return next sibling.
270      */
271     public static DetailNode getNextSibling(DetailNode node, int tokenType) {
272         DetailNode nextSibling = node.getNextSibling();
273         while (nextSibling != null && nextSibling.getType() != tokenType) {
274             nextSibling = nextSibling.getNextSibling();
275         }
276         return nextSibling;
277     }
278 
279     /**
280      * Returns the name of a token for a given ID.
281      *
282      * @param id
283      *        the ID of the token name to get
284      * @return a token name
285      * @throws IllegalArgumentException if an unknown token ID was specified.
286      */
287     public static String getTokenName(int id) {
288         final String name = TOKEN_VALUE_TO_NAME.get(id);
289         if (name == null) {
290             throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id);
291         }
292         return name;
293     }
294 
295     /**
296      * Returns the ID of a token for a given name.
297      *
298      * @param name
299      *        the name of the token ID to get
300      * @return a token ID
301      * @throws IllegalArgumentException if an unknown token name was specified.
302      */
303     public static int getTokenId(String name) {
304         final Integer id = TOKEN_NAME_TO_VALUE.get(name);
305         if (id == null) {
306             throw new IllegalArgumentException("Unknown javadoc token name. Given name " + name);
307         }
308         return id;
309     }
310 
311     /**
312      * Extracts the tag name from the given Javadoc tag section.
313      *
314      * @param javadocTagSection the node representing a Javadoc tag section.
315      *       This node must be of type {@link JavadocCommentsTokenTypes#JAVADOC_BLOCK_TAG}
316      *       or {@link JavadocCommentsTokenTypes#JAVADOC_INLINE_TAG}.
317      *  @return the tag name (e.g., "param", "return", "link")
318      */
319     public static String getTagName(DetailNode javadocTagSection) {
320         return findFirstToken(javadocTagSection.getFirstChild(),
321                     JavadocCommentsTokenTypes.TAG_NAME).getText();
322     }
323 
324     /**
325      * Replace all control chars with escaped symbols.
326      *
327      * @param text the String to process.
328      * @return the processed String with all control chars escaped.
329      */
330     public static String escapeAllControlChars(String text) {
331         final String textWithoutNewlines = NEWLINE.matcher(text).replaceAll("\\\\n");
332         final String textWithoutReturns = RETURN.matcher(textWithoutNewlines).replaceAll("\\\\r");
333         return TAB.matcher(textWithoutReturns).replaceAll("\\\\t");
334     }
335 
336     /**
337      * Checks Javadoc comment it's in right place.
338      *
339      * <p>From Javadoc util documentation:
340      * "Placement of comments - Documentation comments are recognized only when placed
341      * immediately before class, interface, constructor, method, field or annotation field
342      * declarations -- see the class example, method example, and field example.
343      * Documentation comments placed in the body of a method are ignored."</p>
344      *
345      * <p>If there are many documentation comments per declaration statement,
346      * only the last one will be recognized.</p>
347      *
348      * @param blockComment Block comment AST
349      * @return true if Javadoc is in right place
350      * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/tools/unix/javadoc.html">
351      *     Javadoc util documentation</a>
352      */
353     public static boolean isCorrectJavadocPosition(DetailAST blockComment) {
354         // We must be sure that after this one there are no other documentation comments.
355         DetailAST sibling = blockComment.getNextSibling();
356         while (sibling != null) {
357             if (sibling.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) {
358                 if (isJavadocComment(getBlockCommentContent(sibling))) {
359                     // Found another javadoc comment, so this one should be ignored.
360                     break;
361                 }
362                 sibling = sibling.getNextSibling();
363             }
364             else if (sibling.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
365                 sibling = sibling.getNextSibling();
366             }
367             else {
368                 // Annotation, declaration or modifier is here. Do not check further.
369                 sibling = null;
370             }
371         }
372         return sibling == null
373             && (BlockCommentPosition.isOnType(blockComment)
374                 || BlockCommentPosition.isOnMember(blockComment)
375                 || BlockCommentPosition.isOnPackage(blockComment)
376                 || BlockCommentPosition.isOnModule(blockComment));
377     }
378 
379 }