001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2019 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.util.ArrayList;
023import java.util.List;
024import java.util.Map;
025import java.util.regex.Pattern;
026
027import com.puppycrawl.tools.checkstyle.api.DetailAST;
028import com.puppycrawl.tools.checkstyle.api.DetailNode;
029import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
030import com.puppycrawl.tools.checkstyle.api.TextBlock;
031import com.puppycrawl.tools.checkstyle.api.TokenTypes;
032import com.puppycrawl.tools.checkstyle.checks.javadoc.InvalidJavadocTag;
033import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTag;
034import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTagInfo;
035import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTags;
036import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.BlockTagUtil;
037import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.InlineTagUtil;
038import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.TagInfo;
039
040/**
041 * Contains utility methods for working with Javadoc.
042 */
043public final class JavadocUtil {
044
045    /**
046     * The type of Javadoc tag we want returned.
047     */
048    public enum JavadocTagType {
049
050        /** Block type. */
051        BLOCK,
052        /** Inline type. */
053        INLINE,
054        /** All validTags. */
055        ALL,
056
057    }
058
059    /** Maps from a token name to value. */
060    private static final Map<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    /** Newline pattern. */
069    private static final Pattern NEWLINE = Pattern.compile("\n");
070
071    /** Return pattern. */
072    private static final Pattern RETURN = Pattern.compile("\r");
073
074    /** Tab pattern. */
075    private static final Pattern TAB = Pattern.compile("\t");
076
077    // initialise the constants
078    static {
079        TOKEN_NAME_TO_VALUE = TokenUtil.nameToValueMapFromPublicIntFields(JavadocTokenTypes.class);
080        TOKEN_VALUE_TO_NAME = TokenUtil.valueToNameArrayFromNameToValueMap(TOKEN_NAME_TO_VALUE);
081    }
082
083    /** Prevent instantiation. */
084    private JavadocUtil() {
085    }
086
087    /**
088     * Gets validTags from a given piece of Javadoc.
089     * @param textBlock
090     *        the Javadoc comment to process.
091     * @param tagType
092     *        the type of validTags we're interested in
093     * @return all standalone validTags from the given javadoc.
094     */
095    public static JavadocTags getJavadocTags(TextBlock textBlock,
096            JavadocTagType tagType) {
097        final boolean getBlockTags = tagType == JavadocTagType.ALL
098                                         || tagType == JavadocTagType.BLOCK;
099        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    public 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    public 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}