1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 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.JavadocTokenTypes;
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 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 = TokenUtil.nameToValueMapFromPublicIntFields(JavadocTokenTypes.class);
80          TOKEN_VALUE_TO_NAME = TokenUtil.valueToNameArrayFromNameToValueMap(TOKEN_NAME_TO_VALUE);
81      }
82  
83      /** Prevent instantiation. */
84      private JavadocUtil() {
85      }
86  
87      /**
88       * Gets validTags from a given piece of Javadoc.
89       * @param textBlock
90       *        the Javadoc comment to process.
91       * @param tagType
92       *        the type of validTags we're interested in
93       * @return all standalone validTags from the given javadoc.
94       */
95      public static JavadocTags getJavadocTags(TextBlock textBlock,
96              JavadocTagType tagType) {
97          final boolean getBlockTags = tagType == JavadocTagType.ALL
98                                           || tagType == JavadocTagType.BLOCK;
99          final boolean getInlineTags = tagType == JavadocTagType.ALL
100                                           || tagType == JavadocTagType.INLINE;
101 
102         final List<TagInfo> tags = new ArrayList<>();
103 
104         if (getBlockTags) {
105             tags.addAll(BlockTagUtil.extractBlockTags(textBlock.getText()));
106         }
107 
108         if (getInlineTags) {
109             tags.addAll(InlineTagUtil.extractInlineTags(textBlock.getText()));
110         }
111 
112         final List<JavadocTag> validTags = new ArrayList<>();
113         final List<InvalidJavadocTag> invalidTags = new ArrayList<>();
114 
115         for (TagInfo tag : tags) {
116             final int col = tag.getPosition().getColumn();
117 
118             // Add the starting line of the comment to the line number to get the actual line number
119             // in the source.
120             // Lines are one-indexed, so need a off-by-one correction.
121             final int line = textBlock.getStartLineNo() + tag.getPosition().getLine() - 1;
122 
123             if (JavadocTagInfo.isValidName(tag.getName())) {
124                 validTags.add(
125                     new JavadocTag(line, col, tag.getName(), tag.getValue()));
126             }
127             else {
128                 invalidTags.add(new InvalidJavadocTag(line, col, tag.getName()));
129             }
130         }
131 
132         return new JavadocTags(validTags, invalidTags);
133     }
134 
135     /**
136      * Checks that commentContent starts with '*' javadoc comment identifier.
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      * @param blockCommentBegin
156      *        block comment AST
157      * @return true if block comment content starts with '*' javadoc comment
158      *         identifier.
159      */
160     public static boolean isJavadocComment(DetailAST blockCommentBegin) {
161         final String commentContent = getBlockCommentContent(blockCommentBegin);
162         return isJavadocComment(commentContent) && isCorrectJavadocPosition(blockCommentBegin);
163     }
164 
165     /**
166      * Gets content of block comment.
167      * @param blockCommentBegin
168      *        block comment AST.
169      * @return content of block comment.
170      */
171     private static String getBlockCommentContent(DetailAST blockCommentBegin) {
172         final DetailAST commentContent = blockCommentBegin.getFirstChild();
173         return commentContent.getText();
174     }
175 
176     /**
177      * Get content of Javadoc comment.
178      * @param javadocCommentBegin
179      *        Javadoc comment AST
180      * @return content of Javadoc comment.
181      */
182     public static String getJavadocCommentContent(DetailAST javadocCommentBegin) {
183         final DetailAST commentContent = javadocCommentBegin.getFirstChild();
184         return commentContent.getText().substring(1);
185     }
186 
187     /**
188      * Returns the first child token that has a specified type.
189      * @param detailNode
190      *        Javadoc AST node
191      * @param type
192      *        the token type to match
193      * @return the matching token, or null if no match
194      */
195     public static DetailNode findFirstToken(DetailNode detailNode, int type) {
196         DetailNode returnValue = null;
197         DetailNode node = getFirstChild(detailNode);
198         while (node != null) {
199             if (node.getType() == type) {
200                 returnValue = node;
201                 break;
202             }
203             node = getNextSibling(node);
204         }
205         return returnValue;
206     }
207 
208     /**
209      * Gets first child node of specified node.
210      *
211      * @param node DetailNode
212      * @return first child
213      */
214     public static DetailNode getFirstChild(DetailNode node) {
215         DetailNode resultNode = null;
216 
217         if (node.getChildren().length > 0) {
218             resultNode = node.getChildren()[0];
219         }
220         return resultNode;
221     }
222 
223     /**
224      * Checks whether node contains any node of specified type among children on any deep level.
225      *
226      * @param node DetailNode
227      * @param type token type
228      * @return true if node contains any node of type type among children on any deep level.
229      */
230     public static boolean containsInBranch(DetailNode node, int type) {
231         boolean result = true;
232         DetailNode curNode = node;
233         while (type != curNode.getType()) {
234             DetailNode toVisit = getFirstChild(curNode);
235             while (curNode != null && toVisit == null) {
236                 toVisit = getNextSibling(curNode);
237                 if (toVisit == null) {
238                     curNode = curNode.getParent();
239                 }
240             }
241 
242             if (curNode == toVisit) {
243                 result = false;
244                 break;
245             }
246 
247             curNode = toVisit;
248         }
249         return result;
250     }
251 
252     /**
253      * Gets next sibling of specified node.
254      *
255      * @param node DetailNode
256      * @return next sibling.
257      */
258     public static DetailNode getNextSibling(DetailNode node) {
259         DetailNode nextSibling = null;
260         final DetailNode parent = node.getParent();
261         if (parent != null) {
262             final int nextSiblingIndex = node.getIndex() + 1;
263             final DetailNode[] children = parent.getChildren();
264             if (nextSiblingIndex <= children.length - 1) {
265                 nextSibling = children[nextSiblingIndex];
266             }
267         }
268         return nextSibling;
269     }
270 
271     /**
272      * Gets next sibling of specified node with the specified type.
273      *
274      * @param node DetailNode
275      * @param tokenType javadoc token type
276      * @return next sibling.
277      */
278     public static DetailNode getNextSibling(DetailNode node, int tokenType) {
279         DetailNode nextSibling = getNextSibling(node);
280         while (nextSibling != null && nextSibling.getType() != tokenType) {
281             nextSibling = getNextSibling(nextSibling);
282         }
283         return nextSibling;
284     }
285 
286     /**
287      * Gets previous sibling of specified node.
288      * @param node DetailNode
289      * @return previous sibling
290      */
291     public static DetailNode getPreviousSibling(DetailNode node) {
292         DetailNode previousSibling = null;
293         final int previousSiblingIndex = node.getIndex() - 1;
294         if (previousSiblingIndex >= 0) {
295             final DetailNode parent = node.getParent();
296             final DetailNode[] children = parent.getChildren();
297             previousSibling = children[previousSiblingIndex];
298         }
299         return previousSibling;
300     }
301 
302     /**
303      * Returns the name of a token for a given ID.
304      * @param id
305      *        the ID of the token name to get
306      * @return a token name
307      */
308     public static String getTokenName(int id) {
309         final String name;
310         if (id == JavadocTokenTypes.EOF) {
311             name = "EOF";
312         }
313         else if (id > TOKEN_VALUE_TO_NAME.length - 1) {
314             throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id);
315         }
316         else {
317             name = TOKEN_VALUE_TO_NAME[id];
318             if (name == null) {
319                 throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id);
320             }
321         }
322         return name;
323     }
324 
325     /**
326      * Returns the ID of a token for a given name.
327      * @param name
328      *        the name of the token ID to get
329      * @return a token ID
330      */
331     public static int getTokenId(String name) {
332         final Integer id = TOKEN_NAME_TO_VALUE.get(name);
333         if (id == null) {
334             throw new IllegalArgumentException("Unknown javadoc token name. Given name " + name);
335         }
336         return id;
337     }
338 
339     /**
340      * Gets tag name from javadocTagSection.
341      *
342      * @param javadocTagSection to get tag name from.
343      * @return name, of the javadocTagSection's tag.
344      */
345     public static String getTagName(DetailNode javadocTagSection) {
346         final String javadocTagName;
347         if (javadocTagSection.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) {
348             javadocTagName = getNextSibling(
349                     getFirstChild(javadocTagSection)).getText();
350         }
351         else {
352             javadocTagName = getFirstChild(javadocTagSection).getText();
353         }
354         return javadocTagName;
355     }
356 
357     /**
358      * Replace all control chars with escaped symbols.
359      * @param text the String to process.
360      * @return the processed String with all control chars escaped.
361      */
362     public static String escapeAllControlChars(String text) {
363         final String textWithoutNewlines = NEWLINE.matcher(text).replaceAll("\\\\n");
364         final String textWithoutReturns = RETURN.matcher(textWithoutNewlines).replaceAll("\\\\r");
365         return TAB.matcher(textWithoutReturns).replaceAll("\\\\t");
366     }
367 
368     /**
369      * Checks Javadoc comment it's in right place.
370      * <p>From Javadoc util documentation:
371      * "Placement of comments - Documentation comments are recognized only when placed
372      * immediately before class, interface, constructor, method, field or annotation field
373      * declarations -- see the class example, method example, and field example.
374      * Documentation comments placed in the body of a method are ignored."</p>
375      * <p>If there are many documentation comments per declaration statement,
376      * only the last one will be recognized.</p>
377      *
378      * @param blockComment Block comment AST
379      * @return true if Javadoc is in right place
380      * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/tools/unix/javadoc.html">
381      *     Javadoc util documentation</a>
382      */
383     private static boolean isCorrectJavadocPosition(DetailAST blockComment) {
384         // We must be sure that after this one there are no other documentation comments.
385         DetailAST sibling = blockComment.getNextSibling();
386         while (sibling != null) {
387             if (sibling.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) {
388                 if (isJavadocComment(getBlockCommentContent(sibling))) {
389                     // Found another javadoc comment, so this one should be ignored.
390                     break;
391                 }
392                 sibling = sibling.getNextSibling();
393             }
394             else if (sibling.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
395                 sibling = sibling.getNextSibling();
396             }
397             else {
398                 // Annotation, declaration or modifier is here. Do not check further.
399                 sibling = null;
400             }
401         }
402         return sibling == null
403             && (BlockCommentPosition.isOnType(blockComment)
404                 || BlockCommentPosition.isOnMember(blockComment)
405                 || BlockCommentPosition.isOnPackage(blockComment));
406     }
407 
408 }