001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 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.ArrayList;
025import java.util.List;
026import java.util.regex.Matcher;
027import java.util.regex.Pattern;
028
029import com.google.common.collect.ImmutableMap;
030import com.puppycrawl.tools.checkstyle.api.DetailAST;
031import com.puppycrawl.tools.checkstyle.api.DetailNode;
032import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
033import com.puppycrawl.tools.checkstyle.api.TextBlock;
034import com.puppycrawl.tools.checkstyle.checks.javadoc.InvalidJavadocTag;
035import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTag;
036import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTagInfo;
037import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTags;
038
039/**
040 * Contains utility methods for working with Javadoc.
041 * @author Lyle Hanson
042 */
043public final class JavadocUtils {
044
045    /**
046     * The type of Javadoc tag we want returned.
047     */
048    public enum JavadocTagType {
049        /** Block type. */
050        BLOCK,
051        /** Inline type. */
052        INLINE,
053        /** All validTags. */
054        ALL
055    }
056
057    /** Maps from a token name to value. */
058    private static final ImmutableMap<String, Integer> TOKEN_NAME_TO_VALUE;
059    /** Maps from a token value to name. */
060    private static final String[] TOKEN_VALUE_TO_NAME;
061
062    /** Exception message for unknown JavaDoc token id. */
063    private static final String UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE = "Unknown javadoc"
064            + " token id. Given id: ";
065
066    /** Comment pattern. */
067    private static final Pattern COMMENT_PATTERN = Pattern.compile(
068        "^\\s*(?:/\\*{2,}|\\*+)\\s*(.*)");
069
070    /** Block tag pattern for a first line. */
071    private static final Pattern BLOCK_TAG_PATTERN_FIRST_LINE = Pattern.compile(
072        "/\\*{2,}\\s*@(\\p{Alpha}+)\\s");
073
074    /** Block tag pattern. */
075    private static final Pattern BLOCK_TAG_PATTERN = Pattern.compile(
076        "^\\s*\\**\\s*@(\\p{Alpha}+)\\s");
077
078    /** Inline tag pattern. */
079    private static final Pattern INLINE_TAG_PATTERN = Pattern.compile(
080        ".*?\\{@(\\p{Alpha}+)\\s+(.*?)}");
081
082    /** Newline pattern. */
083    private static final Pattern NEWLINE = Pattern.compile("\n");
084
085    /** Return pattern. */
086    private static final Pattern RETURN = Pattern.compile("\r");
087
088    /** Tab pattern. */
089    private static final Pattern TAB = Pattern.compile("\t");
090
091    // Using reflection gets all token names and values from JavadocTokenTypes class
092    // and saves to TOKEN_NAME_TO_VALUE and TOKEN_VALUE_TO_NAME collections.
093    static {
094        final ImmutableMap.Builder<String, Integer> builder = ImmutableMap.builder();
095
096        final Field[] fields = JavadocTokenTypes.class.getDeclaredFields();
097
098        String[] tempTokenValueToName = CommonUtils.EMPTY_STRING_ARRAY;
099
100        for (final Field field : fields) {
101
102            // Only process public int fields.
103            if (!Modifier.isPublic(field.getModifiers())
104                    || field.getType() != Integer.TYPE) {
105                continue;
106            }
107
108            final String name = field.getName();
109
110            final int tokenValue = TokenUtils.getIntFromField(field, name);
111            builder.put(name, tokenValue);
112            if (tokenValue > tempTokenValueToName.length - 1) {
113                final String[] temp = new String[tokenValue + 1];
114                System.arraycopy(tempTokenValueToName, 0, temp, 0, tempTokenValueToName.length);
115                tempTokenValueToName = temp;
116            }
117            if (tokenValue == -1) {
118                tempTokenValueToName[0] = name;
119            }
120            else {
121                tempTokenValueToName[tokenValue] = name;
122            }
123        }
124
125        TOKEN_NAME_TO_VALUE = builder.build();
126        TOKEN_VALUE_TO_NAME = tempTokenValueToName;
127    }
128
129    /** Prevent instantiation. */
130    private JavadocUtils() {
131    }
132
133    /**
134     * Gets validTags from a given piece of Javadoc.
135     * @param textBlock
136     *        the Javadoc comment to process.
137     * @param tagType
138     *        the type of validTags we're interested in
139     * @return all standalone validTags from the given javadoc.
140     */
141    public static JavadocTags getJavadocTags(TextBlock textBlock,
142            JavadocTagType tagType) {
143        final String[] text = textBlock.getText();
144        final List<JavadocTag> tags = new ArrayList<>();
145        final List<InvalidJavadocTag> invalidTags = new ArrayList<>();
146        for (int i = 0; i < text.length; i++) {
147            final String textValue = text[i];
148            final Matcher blockTagMatcher = getBlockTagPattern(i).matcher(textValue);
149            if ((tagType == JavadocTagType.ALL || tagType == JavadocTagType.BLOCK)
150                    && blockTagMatcher.find()) {
151                final String tagName = blockTagMatcher.group(1);
152                String content = textValue.substring(blockTagMatcher.end(1));
153                if (content.endsWith("*/")) {
154                    content = content.substring(0, content.length() - 2);
155                }
156                final int line = textBlock.getStartLineNo() + i;
157                int col = blockTagMatcher.start(1) - 1;
158                if (i == 0) {
159                    col += textBlock.getStartColNo();
160                }
161                if (JavadocTagInfo.isValidName(tagName)) {
162                    tags.add(
163                            new JavadocTag(line, col, tagName, content.trim()));
164                }
165                else {
166                    invalidTags.add(new InvalidJavadocTag(line, col, tagName));
167                }
168            }
169            // No block tag, so look for inline validTags
170            else if (tagType == JavadocTagType.ALL || tagType == JavadocTagType.INLINE) {
171                lookForInlineTags(textBlock, i, tags, invalidTags);
172            }
173        }
174        return new JavadocTags(tags, invalidTags);
175    }
176
177    /**
178     * Get a block tag pattern depending on a line number of a javadoc.
179     * @param lineNumber the line number.
180     * @return a block tag pattern.
181     */
182    private static Pattern getBlockTagPattern(int lineNumber) {
183        final Pattern blockTagPattern;
184        if (lineNumber == 0) {
185            blockTagPattern = BLOCK_TAG_PATTERN_FIRST_LINE;
186        }
187        else {
188            blockTagPattern = BLOCK_TAG_PATTERN;
189        }
190        return blockTagPattern;
191    }
192
193    /**
194     * Looks for inline tags in comment and adds them to the proper tags collection.
195     * @param comment comment text block
196     * @param lineNumber line number in the comment
197     * @param validTags collection of valid tags
198     * @param invalidTags collection of invalid tags
199     */
200    private static void lookForInlineTags(TextBlock comment, int lineNumber,
201            final List<JavadocTag> validTags, final List<InvalidJavadocTag> invalidTags) {
202        final String text = comment.getText()[lineNumber];
203        // Match Javadoc text after comment characters
204        final Matcher commentMatcher = COMMENT_PATTERN.matcher(text);
205        final String commentContents;
206
207        // offset including comment characters
208        final int commentOffset;
209
210        if (commentMatcher.find()) {
211            commentContents = commentMatcher.group(1);
212            commentOffset = commentMatcher.start(1) - 1;
213        }
214        else {
215            // No leading asterisks, still valid
216            commentContents = text;
217            commentOffset = 0;
218        }
219        final Matcher tagMatcher = INLINE_TAG_PATTERN.matcher(commentContents);
220        while (tagMatcher.find()) {
221            final String tagName = tagMatcher.group(1);
222            final int line = comment.getStartLineNo() + lineNumber;
223            int col = commentOffset + tagMatcher.start(1) - 1;
224            if (lineNumber == 0) {
225                col += comment.getStartColNo();
226            }
227            if (JavadocTagInfo.isValidName(tagName)) {
228                final String tagValue = tagMatcher.group(2).trim();
229                validTags.add(new JavadocTag(line, col, tagName,
230                        tagValue));
231            }
232            else {
233                invalidTags.add(new InvalidJavadocTag(line, col,
234                        tagName));
235            }
236        }
237    }
238
239    /**
240     * Checks that commentContent starts with '*' javadoc comment identifier.
241     * @param commentContent
242     *        content of block comment
243     * @return true if commentContent starts with '*' javadoc comment
244     *         identifier.
245     */
246    public static boolean isJavadocComment(String commentContent) {
247        boolean result = false;
248
249        if (!commentContent.isEmpty()) {
250            final char docCommentIdentificator = commentContent.charAt(0);
251            result = docCommentIdentificator == '*';
252        }
253
254        return result;
255    }
256
257    /**
258     * Checks block comment content starts with '*' javadoc comment identifier.
259     * @param blockCommentBegin
260     *        block comment AST
261     * @return true if block comment content starts with '*' javadoc comment
262     *         identifier.
263     */
264    public static boolean isJavadocComment(DetailAST blockCommentBegin) {
265        final String commentContent = getBlockCommentContent(blockCommentBegin);
266        return isJavadocComment(commentContent) && isCorrectJavadocPosition(blockCommentBegin);
267    }
268
269    /**
270     * Gets content of block comment.
271     * @param blockCommentBegin
272     *        block comment AST.
273     * @return content of block comment.
274     */
275    private static String getBlockCommentContent(DetailAST blockCommentBegin) {
276        final DetailAST commentContent = blockCommentBegin.getFirstChild();
277        return commentContent.getText();
278    }
279
280    /**
281     * Get content of Javadoc comment.
282     * @param javadocCommentBegin
283     *        Javadoc comment AST
284     * @return content of Javadoc comment.
285     */
286    public static String getJavadocCommentContent(DetailAST javadocCommentBegin) {
287        final DetailAST commentContent = javadocCommentBegin.getFirstChild();
288        return commentContent.getText().substring(1);
289    }
290
291    /**
292     * Returns the first child token that has a specified type.
293     * @param detailNode
294     *        Javadoc AST node
295     * @param type
296     *        the token type to match
297     * @return the matching token, or null if no match
298     */
299    public static DetailNode findFirstToken(DetailNode detailNode, int type) {
300        DetailNode returnValue = null;
301        DetailNode node = getFirstChild(detailNode);
302        while (node != null) {
303            if (node.getType() == type) {
304                returnValue = node;
305                break;
306            }
307            node = getNextSibling(node);
308        }
309        return returnValue;
310    }
311
312    /**
313     * Gets first child node of specified node.
314     *
315     * @param node DetailNode
316     * @return first child
317     */
318    public static DetailNode getFirstChild(DetailNode node) {
319        DetailNode resultNode = null;
320
321        if (node.getChildren().length > 0) {
322            resultNode = node.getChildren()[0];
323        }
324        return resultNode;
325    }
326
327    /**
328     * Checks whether node contains any node of specified type among children on any deep level.
329     *
330     * @param node DetailNode
331     * @param type token type
332     * @return true if node contains any node of type type among children on any deep level.
333     */
334    public static boolean containsInBranch(DetailNode node, int type) {
335        boolean result = true;
336        DetailNode curNode = node;
337        while (type != curNode.getType()) {
338            DetailNode toVisit = getFirstChild(curNode);
339            while (curNode != null && toVisit == null) {
340                toVisit = getNextSibling(curNode);
341                if (toVisit == null) {
342                    curNode = curNode.getParent();
343                }
344            }
345
346            if (curNode == toVisit) {
347                result = false;
348                break;
349            }
350
351            curNode = toVisit;
352        }
353        return result;
354    }
355
356    /**
357     * Gets next sibling of specified node.
358     *
359     * @param node DetailNode
360     * @return next sibling.
361     */
362    public static DetailNode getNextSibling(DetailNode node) {
363        DetailNode nextSibling = null;
364        final DetailNode parent = node.getParent();
365        if (parent != null) {
366            final int nextSiblingIndex = node.getIndex() + 1;
367            final DetailNode[] children = parent.getChildren();
368            if (nextSiblingIndex <= children.length - 1) {
369                nextSibling = children[nextSiblingIndex];
370            }
371        }
372        return nextSibling;
373    }
374
375    /**
376     * Gets next sibling of specified node with the specified type.
377     *
378     * @param node DetailNode
379     * @param tokenType javadoc token type
380     * @return next sibling.
381     */
382    public static DetailNode getNextSibling(DetailNode node, int tokenType) {
383        DetailNode nextSibling = getNextSibling(node);
384        while (nextSibling != null && nextSibling.getType() != tokenType) {
385            nextSibling = getNextSibling(nextSibling);
386        }
387        return nextSibling;
388    }
389
390    /**
391     * Gets previous sibling of specified node.
392     * @param node DetailNode
393     * @return previous sibling
394     */
395    public static DetailNode getPreviousSibling(DetailNode node) {
396        DetailNode previousSibling = null;
397        final int previousSiblingIndex = node.getIndex() - 1;
398        if (previousSiblingIndex >= 0) {
399            final DetailNode parent = node.getParent();
400            final DetailNode[] children = parent.getChildren();
401            previousSibling = children[previousSiblingIndex];
402        }
403        return previousSibling;
404    }
405
406    /**
407     * Returns the name of a token for a given ID.
408     * @param id
409     *        the ID of the token name to get
410     * @return a token name
411     */
412    public static String getTokenName(int id) {
413        final String name;
414        if (id == JavadocTokenTypes.EOF) {
415            name = "EOF";
416        }
417        else if (id > TOKEN_VALUE_TO_NAME.length - 1) {
418            throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id);
419        }
420        else {
421            name = TOKEN_VALUE_TO_NAME[id];
422            if (name == null) {
423                throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id);
424            }
425        }
426        return name;
427    }
428
429    /**
430     * Returns the ID of a token for a given name.
431     * @param name
432     *        the name of the token ID to get
433     * @return a token ID
434     */
435    public static int getTokenId(String name) {
436        final Integer id = TOKEN_NAME_TO_VALUE.get(name);
437        if (id == null) {
438            throw new IllegalArgumentException("Unknown javadoc token name. Given name " + name);
439        }
440        return id;
441    }
442
443    /**
444     * Gets tag name from javadocTagSection.
445     *
446     * @param javadocTagSection to get tag name from.
447     * @return name, of the javadocTagSection's tag.
448     */
449    public static String getTagName(DetailNode javadocTagSection) {
450        final String javadocTagName;
451        if (javadocTagSection.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) {
452            javadocTagName = getNextSibling(
453                    getFirstChild(javadocTagSection)).getText();
454        }
455        else {
456            javadocTagName = getFirstChild(javadocTagSection).getText();
457        }
458        return javadocTagName;
459    }
460
461    /**
462     * Replace all control chars with escaped symbols.
463     * @param text the String to process.
464     * @return the processed String with all control chars escaped.
465     */
466    public static String escapeAllControlChars(String text) {
467        final String textWithoutNewlines = NEWLINE.matcher(text).replaceAll("\\\\n");
468        final String textWithoutReturns = RETURN.matcher(textWithoutNewlines).replaceAll("\\\\r");
469        return TAB.matcher(textWithoutReturns).replaceAll("\\\\t");
470    }
471
472    /**
473     * Checks Javadoc comment it's in right place.
474     * From Javadoc util documentation:
475     * "Placement of comments - Documentation comments are recognized only when placed
476     * immediately before class, interface, constructor, method, or field
477     * declarations -- see the class example, method example, and field example.
478     * Documentation comments placed in the body of a method are ignored. Only one
479     * documentation comment per declaration statement is recognized by the Javadoc tool."
480     *
481     * @param blockComment Block comment AST
482     * @return true if Javadoc is in right place
483     */
484    public static boolean isCorrectJavadocPosition(DetailAST blockComment) {
485        return BlockCommentPosition.isOnClass(blockComment)
486                || BlockCommentPosition.isOnInterface(blockComment)
487                || BlockCommentPosition.isOnEnum(blockComment)
488                || BlockCommentPosition.isOnMethod(blockComment)
489                || BlockCommentPosition.isOnField(blockComment)
490                || BlockCommentPosition.isOnConstructor(blockComment)
491                || BlockCommentPosition.isOnEnumConstant(blockComment)
492                || BlockCommentPosition.isOnAnnotationDef(blockComment);
493    }
494}