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