001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2016 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.utils;
021
022import java.lang.reflect.Field;
023import java.lang.reflect.Modifier;
024import java.util.List;
025import java.util.regex.Matcher;
026import java.util.regex.Pattern;
027
028import org.apache.commons.lang3.ArrayUtils;
029
030import com.google.common.collect.ImmutableMap;
031import com.google.common.collect.Lists;
032import com.puppycrawl.tools.checkstyle.api.DetailAST;
033import com.puppycrawl.tools.checkstyle.api.DetailNode;
034import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
035import com.puppycrawl.tools.checkstyle.api.TextBlock;
036import com.puppycrawl.tools.checkstyle.checks.javadoc.InvalidJavadocTag;
037import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTag;
038import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTagInfo;
039import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTags;
040
041/**
042 * Contains utility methods for working with Javadoc.
043 * @author Lyle Hanson
044 */
045public final class JavadocUtils {
046
047    /**
048     * The type of Javadoc tag we want returned.
049     */
050    public enum JavadocTagType {
051        /** Block type. */
052        BLOCK,
053        /** Inline type. */
054        INLINE,
055        /** All validTags. */
056        ALL
057    }
058
059    /** Maps from a token name to value. */
060    private static final ImmutableMap<String, Integer> TOKEN_NAME_TO_VALUE;
061    /** Maps from a token value to name. */
062    private static final String[] TOKEN_VALUE_TO_NAME;
063
064    /** Exception message for unknown JavaDoc token id. */
065    private static final String UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE = "Unknown javadoc"
066            + " token id. Given id: ";
067
068    /** Comment pattern. */
069    private static final Pattern COMMENT_PATTERN = Pattern.compile(
070        "^\\s*(?:/\\*{2,}|\\*+)\\s*(.*)");
071
072    /** Block tag pattern for a first line. */
073    private static final Pattern BLOCK_TAG_PATTERN_FIRST_LINE = Pattern.compile(
074        "/\\*{2,}\\s*@(\\p{Alpha}+)\\s");
075
076    /** Block tag pattern. */
077    private static final Pattern BLOCK_TAG_PATTERN = Pattern.compile(
078        "^\\s*\\**\\s*@(\\p{Alpha}+)\\s");
079
080    /** Inline tag pattern. */
081    private static final Pattern INLINE_TAG_PATTERN = Pattern.compile(
082        ".*?\\{@(\\p{Alpha}+)\\s+(.*?)\\}");
083
084    // Using reflection gets all token names and values from JavadocTokenTypes class
085    // and saves to TOKEN_NAME_TO_VALUE and TOKEN_VALUE_TO_NAME collections.
086    static {
087        final ImmutableMap.Builder<String, Integer> builder = ImmutableMap.builder();
088
089        final Field[] fields = JavadocTokenTypes.class.getDeclaredFields();
090
091        String[] tempTokenValueToName = ArrayUtils.EMPTY_STRING_ARRAY;
092
093        for (final Field field : fields) {
094
095            // Only process public int fields.
096            if (!Modifier.isPublic(field.getModifiers())
097                    || field.getType() != Integer.TYPE) {
098                continue;
099            }
100
101            final String name = field.getName();
102
103            final int tokenValue = TokenUtils.getIntFromField(field, name);
104            builder.put(name, tokenValue);
105            if (tokenValue > tempTokenValueToName.length - 1) {
106                final String[] temp = new String[tokenValue + 1];
107                System.arraycopy(tempTokenValueToName, 0, temp, 0, tempTokenValueToName.length);
108                tempTokenValueToName = temp;
109            }
110            if (tokenValue == -1) {
111                tempTokenValueToName[0] = name;
112            }
113            else {
114                tempTokenValueToName[tokenValue] = name;
115            }
116        }
117
118        TOKEN_NAME_TO_VALUE = builder.build();
119        TOKEN_VALUE_TO_NAME = tempTokenValueToName;
120    }
121
122    /** Prevent instantiation. */
123    private JavadocUtils() {
124    }
125
126    /**
127     * Gets validTags from a given piece of Javadoc.
128     * @param textBlock
129     *        the Javadoc comment to process.
130     * @param tagType
131     *        the type of validTags we're interested in
132     * @return all standalone validTags from the given javadoc.
133     */
134    public static JavadocTags getJavadocTags(TextBlock textBlock,
135            JavadocTagType tagType) {
136        final String[] text = textBlock.getText();
137        final List<JavadocTag> tags = Lists.newArrayList();
138        final List<InvalidJavadocTag> invalidTags = Lists.newArrayList();
139        for (int i = 0; i < text.length; i++) {
140            final String textValue = text[i];
141            final Matcher blockTagMatcher = getBlockTagPattern(i).matcher(textValue);
142            if ((tagType == JavadocTagType.ALL || tagType == JavadocTagType.BLOCK)
143                    && blockTagMatcher.find()) {
144                final String tagName = blockTagMatcher.group(1);
145                String content = textValue.substring(blockTagMatcher.end(1));
146                if (content.endsWith("*/")) {
147                    content = content.substring(0, content.length() - 2);
148                }
149                final int line = textBlock.getStartLineNo() + i;
150                int col = blockTagMatcher.start(1) - 1;
151                if (i == 0) {
152                    col += textBlock.getStartColNo();
153                }
154                if (JavadocTagInfo.isValidName(tagName)) {
155                    tags.add(
156                            new JavadocTag(line, col, tagName, content.trim()));
157                }
158                else {
159                    invalidTags.add(new InvalidJavadocTag(line, col, tagName));
160                }
161            }
162            // No block tag, so look for inline validTags
163            else if (tagType == JavadocTagType.ALL || tagType == JavadocTagType.INLINE) {
164                lookForInlineTags(textBlock, i, tags, invalidTags);
165            }
166        }
167        return new JavadocTags(tags, invalidTags);
168    }
169
170    /**
171     * Get a block tag pattern depending on a line number of a javadoc.
172     * @param lineNumber the line number.
173     * @return a block tag pattern.
174     */
175    private static Pattern getBlockTagPattern(int lineNumber) {
176        final Pattern blockTagPattern;
177        if (lineNumber == 0) {
178            blockTagPattern = BLOCK_TAG_PATTERN_FIRST_LINE;
179        }
180        else {
181            blockTagPattern = BLOCK_TAG_PATTERN;
182        }
183        return blockTagPattern;
184    }
185
186    /**
187     * Looks for inline tags in comment and adds them to the proper tags collection.
188     * @param comment comment text block
189     * @param lineNumber line number in the comment
190     * @param validTags collection of valid tags
191     * @param invalidTags collection of invalid tags
192     */
193    private static void lookForInlineTags(TextBlock comment, int lineNumber,
194            final List<JavadocTag> validTags, final List<InvalidJavadocTag> invalidTags) {
195        final String text = comment.getText()[lineNumber];
196        // Match Javadoc text after comment characters
197        final Matcher commentMatcher = COMMENT_PATTERN.matcher(text);
198        final String commentContents;
199
200        // offset including comment characters
201        final int commentOffset;
202
203        if (commentMatcher.find()) {
204            commentContents = commentMatcher.group(1);
205            commentOffset = commentMatcher.start(1) - 1;
206        }
207        else {
208            // No leading asterisks, still valid
209            commentContents = text;
210            commentOffset = 0;
211        }
212        final Matcher tagMatcher = INLINE_TAG_PATTERN.matcher(commentContents);
213        while (tagMatcher.find()) {
214            final String tagName = tagMatcher.group(1);
215            final String tagValue = tagMatcher.group(2).trim();
216            final int line = comment.getStartLineNo() + lineNumber;
217            int col = commentOffset + tagMatcher.start(1) - 1;
218            if (lineNumber == 0) {
219                col += comment.getStartColNo();
220            }
221            if (JavadocTagInfo.isValidName(tagName)) {
222                validTags.add(new JavadocTag(line, col, tagName,
223                        tagValue));
224            }
225            else {
226                invalidTags.add(new InvalidJavadocTag(line, col,
227                        tagName));
228            }
229        }
230    }
231
232    /**
233     * Checks that commentContent starts with '*' javadoc comment identifier.
234     * @param commentContent
235     *        content of block comment
236     * @return true if commentContent starts with '*' javadoc comment
237     *         identifier.
238     */
239    public static boolean isJavadocComment(String commentContent) {
240        boolean result = false;
241
242        if (!commentContent.isEmpty()) {
243            final char docCommentIdentificator = commentContent.charAt(0);
244            result = docCommentIdentificator == '*';
245        }
246
247        return result;
248    }
249
250    /**
251     * Checks block comment content starts with '*' javadoc comment identifier.
252     * @param blockCommentBegin
253     *        block comment AST
254     * @return true if block comment content starts with '*' javadoc comment
255     *         identifier.
256     */
257    public static boolean isJavadocComment(DetailAST blockCommentBegin) {
258        final String commentContent = getBlockCommentContent(blockCommentBegin);
259        return isJavadocComment(commentContent);
260    }
261
262    /**
263     * Gets content of block comment.
264     * @param blockCommentBegin
265     *        block comment AST.
266     * @return content of block comment.
267     */
268    private static String getBlockCommentContent(DetailAST blockCommentBegin) {
269        final DetailAST commentContent = blockCommentBegin.getFirstChild();
270        return commentContent.getText();
271    }
272
273    /**
274     * Get content of Javadoc comment.
275     * @param javadocCommentBegin
276     *        Javadoc comment AST
277     * @return content of Javadoc comment.
278     */
279    public static String getJavadocCommentContent(DetailAST javadocCommentBegin) {
280        final DetailAST commentContent = javadocCommentBegin.getFirstChild();
281        return commentContent.getText().substring(1);
282    }
283
284    /**
285     * Returns the first child token that has a specified type.
286     * @param detailNode
287     *        Javadoc AST node
288     * @param type
289     *        the token type to match
290     * @return the matching token, or null if no match
291     */
292    public static DetailNode findFirstToken(DetailNode detailNode, int type) {
293        DetailNode returnValue = null;
294        DetailNode node = getFirstChild(detailNode);
295        while (node != null) {
296            if (node.getType() == type) {
297                returnValue = node;
298                break;
299            }
300            node = getNextSibling(node);
301        }
302        return returnValue;
303    }
304
305    /**
306     * Gets first child node of specified node.
307     *
308     * @param node DetailNode
309     * @return first child
310     */
311    public static DetailNode getFirstChild(DetailNode node) {
312        DetailNode resultNode = null;
313
314        if (node.getChildren().length > 0) {
315            resultNode = node.getChildren()[0];
316        }
317        return resultNode;
318    }
319
320    /**
321     * Checks whether node contains any node of specified type among children on any deep level.
322     *
323     * @param node DetailNode
324     * @param type token type
325     * @return true if node contains any node of type type among children on any deep level.
326     */
327    public static boolean containsInBranch(DetailNode node, int type) {
328        DetailNode curNode = node;
329        while (true) {
330
331            if (type == curNode.getType()) {
332                return true;
333            }
334
335            DetailNode toVisit = getFirstChild(curNode);
336            while (curNode != null && toVisit == null) {
337                toVisit = getNextSibling(curNode);
338                if (toVisit == null) {
339                    curNode = curNode.getParent();
340                }
341            }
342
343            if (curNode == toVisit) {
344                break;
345            }
346
347            curNode = toVisit;
348        }
349
350        return false;
351    }
352
353    /**
354     * Gets next sibling of specified node.
355     *
356     * @param node DetailNode
357     * @return next sibling.
358     */
359    public static DetailNode getNextSibling(DetailNode node) {
360        final DetailNode parent = node.getParent();
361        if (parent != null) {
362            final int nextSiblingIndex = node.getIndex() + 1;
363            final DetailNode[] children = parent.getChildren();
364            if (nextSiblingIndex <= children.length - 1) {
365                return children[nextSiblingIndex];
366            }
367        }
368        return null;
369    }
370
371    /**
372     * Gets next sibling of specified node with the specified type.
373     *
374     * @param node DetailNode
375     * @param tokenType javadoc token type
376     * @return next sibling.
377     */
378    public static DetailNode getNextSibling(DetailNode node, int tokenType) {
379        DetailNode nextSibling = getNextSibling(node);
380        while (nextSibling != null && nextSibling.getType() != tokenType) {
381            nextSibling = getNextSibling(nextSibling);
382        }
383        return nextSibling;
384    }
385
386    /**
387     * Gets previous sibling of specified node.
388     * @param node DetailNode
389     * @return previous sibling
390     */
391    public static DetailNode getPreviousSibling(DetailNode node) {
392        final DetailNode parent = node.getParent();
393        final int previousSiblingIndex = node.getIndex() - 1;
394        final DetailNode[] children = parent.getChildren();
395        if (previousSiblingIndex >= 0) {
396            return children[previousSiblingIndex];
397        }
398        return null;
399    }
400
401    /**
402     * Returns the name of a token for a given ID.
403     * @param id
404     *        the ID of the token name to get
405     * @return a token name
406     */
407    public static String getTokenName(int id) {
408        if (id == JavadocTokenTypes.EOF) {
409            return "EOF";
410        }
411        if (id > TOKEN_VALUE_TO_NAME.length - 1) {
412            throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id);
413        }
414        final String name = TOKEN_VALUE_TO_NAME[id];
415        if (name == null) {
416            throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id);
417        }
418        return name;
419    }
420
421    /**
422     * Returns the ID of a token for a given name.
423     * @param name
424     *        the name of the token ID to get
425     * @return a token ID
426     */
427    public static int getTokenId(String name) {
428        final Integer id = TOKEN_NAME_TO_VALUE.get(name);
429        if (id == null) {
430            throw new IllegalArgumentException("Unknown javadoc token name. Given name " + name);
431        }
432        return id;
433    }
434
435    /**
436     * Gets tag name from javadocTagSection.
437     *
438     * @param javadocTagSection to get tag name from.
439     * @return name, of the javadocTagSection's tag.
440     */
441    public static String getTagName(DetailNode javadocTagSection) {
442        final String javadocTagName;
443        if (javadocTagSection.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) {
444            javadocTagName = getNextSibling(
445                    getFirstChild(javadocTagSection)).getText();
446        }
447        else {
448            javadocTagName = getFirstChild(javadocTagSection).getText();
449        }
450        return javadocTagName;
451    }
452
453}