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