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