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 com.google.common.collect.ImmutableMap;
029import com.google.common.collect.Lists;
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 = Lists.newArrayList();
145        final List<InvalidJavadocTag> invalidTags = Lists.newArrayList();
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 String tagValue = tagMatcher.group(2).trim();
223            final int line = comment.getStartLineNo() + lineNumber;
224            int col = commentOffset + tagMatcher.start(1) - 1;
225            if (lineNumber == 0) {
226                col += comment.getStartColNo();
227            }
228            if (JavadocTagInfo.isValidName(tagName)) {
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);
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        DetailNode curNode = node;
336        while (true) {
337
338            if (type == curNode.getType()) {
339                return true;
340            }
341
342            DetailNode toVisit = getFirstChild(curNode);
343            while (curNode != null && toVisit == null) {
344                toVisit = getNextSibling(curNode);
345                if (toVisit == null) {
346                    curNode = curNode.getParent();
347                }
348            }
349
350            if (curNode == toVisit) {
351                break;
352            }
353
354            curNode = toVisit;
355        }
356
357        return false;
358    }
359
360    /**
361     * Gets next sibling of specified node.
362     *
363     * @param node DetailNode
364     * @return next sibling.
365     */
366    public static DetailNode getNextSibling(DetailNode node) {
367        final DetailNode parent = node.getParent();
368        if (parent != null) {
369            final int nextSiblingIndex = node.getIndex() + 1;
370            final DetailNode[] children = parent.getChildren();
371            if (nextSiblingIndex <= children.length - 1) {
372                return children[nextSiblingIndex];
373            }
374        }
375        return null;
376    }
377
378    /**
379     * Gets next sibling of specified node with the specified type.
380     *
381     * @param node DetailNode
382     * @param tokenType javadoc token type
383     * @return next sibling.
384     */
385    public static DetailNode getNextSibling(DetailNode node, int tokenType) {
386        DetailNode nextSibling = getNextSibling(node);
387        while (nextSibling != null && nextSibling.getType() != tokenType) {
388            nextSibling = getNextSibling(nextSibling);
389        }
390        return nextSibling;
391    }
392
393    /**
394     * Gets previous sibling of specified node.
395     * @param node DetailNode
396     * @return previous sibling
397     */
398    public static DetailNode getPreviousSibling(DetailNode node) {
399        final DetailNode parent = node.getParent();
400        final int previousSiblingIndex = node.getIndex() - 1;
401        final DetailNode[] children = parent.getChildren();
402        if (previousSiblingIndex >= 0) {
403            return children[previousSiblingIndex];
404        }
405        return null;
406    }
407
408    /**
409     * Returns the name of a token for a given ID.
410     * @param id
411     *        the ID of the token name to get
412     * @return a token name
413     */
414    public static String getTokenName(int id) {
415        if (id == JavadocTokenTypes.EOF) {
416            return "EOF";
417        }
418        if (id > TOKEN_VALUE_TO_NAME.length - 1) {
419            throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id);
420        }
421        final String name = TOKEN_VALUE_TO_NAME[id];
422        if (name == null) {
423            throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id);
424        }
425        return name;
426    }
427
428    /**
429     * Returns the ID of a token for a given name.
430     * @param name
431     *        the name of the token ID to get
432     * @return a token ID
433     */
434    public static int getTokenId(String name) {
435        final Integer id = TOKEN_NAME_TO_VALUE.get(name);
436        if (id == null) {
437            throw new IllegalArgumentException("Unknown javadoc token name. Given name " + name);
438        }
439        return id;
440    }
441
442    /**
443     * Gets tag name from javadocTagSection.
444     *
445     * @param javadocTagSection to get tag name from.
446     * @return name, of the javadocTagSection's tag.
447     */
448    public static String getTagName(DetailNode javadocTagSection) {
449        final String javadocTagName;
450        if (javadocTagSection.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) {
451            javadocTagName = getNextSibling(
452                    getFirstChild(javadocTagSection)).getText();
453        }
454        else {
455            javadocTagName = getFirstChild(javadocTagSection).getText();
456        }
457        return javadocTagName;
458    }
459
460    /**
461     * Replace all control chars with excaped symbols.
462     * @param text the String to process.
463     * @return the processed String with all control chars excaped.
464     */
465    public static String excapeAllControlChars(String text) {
466        final String textWithoutNewlines = NEWLINE.matcher(text).replaceAll("\\\\n");
467        final String textWithoutReturns = RETURN.matcher(textWithoutNewlines).replaceAll("\\\\r");
468        return TAB.matcher(textWithoutReturns).replaceAll("\\\\t");
469    }
470}