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