001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2022 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 List<TagInfo> tags = new ArrayList<>();
099        final boolean isBlockTags = tagType == JavadocTagType.ALL
100                                        || tagType == JavadocTagType.BLOCK;
101        if (isBlockTags) {
102            tags.addAll(BlockTagUtil.extractBlockTags(textBlock.getText()));
103        }
104        final boolean isInlineTags = tagType == JavadocTagType.ALL
105                                        || tagType == JavadocTagType.INLINE;
106        if (isInlineTags) {
107            tags.addAll(InlineTagUtil.extractInlineTags(textBlock.getText()));
108        }
109
110        final List<JavadocTag> validTags = new ArrayList<>();
111        final List<InvalidJavadocTag> invalidTags = new ArrayList<>();
112
113        for (TagInfo tag : tags) {
114            final int col = tag.getPosition().getColumn();
115
116            // Add the starting line of the comment to the line number to get the actual line number
117            // in the source.
118            // Lines are one-indexed, so need an off-by-one correction.
119            final int line = textBlock.getStartLineNo() + tag.getPosition().getLine() - 1;
120
121            if (JavadocTagInfo.isValidName(tag.getName())) {
122                validTags.add(
123                    new JavadocTag(line, col, tag.getName(), tag.getValue()));
124            }
125            else {
126                invalidTags.add(new InvalidJavadocTag(line, col, tag.getName()));
127            }
128        }
129
130        return new JavadocTags(validTags, invalidTags);
131    }
132
133    /**
134     * Checks that commentContent starts with '*' javadoc comment identifier.
135     *
136     * @param commentContent
137     *        content of block comment
138     * @return true if commentContent starts with '*' javadoc comment
139     *         identifier.
140     */
141    public static boolean isJavadocComment(String commentContent) {
142        boolean result = false;
143
144        if (!commentContent.isEmpty()) {
145            final char docCommentIdentifier = commentContent.charAt(0);
146            result = docCommentIdentifier == '*';
147        }
148
149        return result;
150    }
151
152    /**
153     * Checks block comment content starts with '*' javadoc comment identifier.
154     *
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     *
168     * @param blockCommentBegin
169     *        block comment AST.
170     * @return content of block comment.
171     */
172    public static String getBlockCommentContent(DetailAST blockCommentBegin) {
173        final DetailAST commentContent = blockCommentBegin.getFirstChild();
174        return commentContent.getText();
175    }
176
177    /**
178     * Get content of Javadoc comment.
179     *
180     * @param javadocCommentBegin
181     *        Javadoc comment AST
182     * @return content of Javadoc comment.
183     */
184    public static String getJavadocCommentContent(DetailAST javadocCommentBegin) {
185        final DetailAST commentContent = javadocCommentBegin.getFirstChild();
186        return commentContent.getText().substring(1);
187    }
188
189    /**
190     * Returns the first child token that has a specified type.
191     *
192     * @param detailNode
193     *        Javadoc AST node
194     * @param type
195     *        the token type to match
196     * @return the matching token, or null if no match
197     */
198    public static DetailNode findFirstToken(DetailNode detailNode, int type) {
199        DetailNode returnValue = null;
200        DetailNode node = getFirstChild(detailNode);
201        while (node != null) {
202            if (node.getType() == type) {
203                returnValue = node;
204                break;
205            }
206            node = getNextSibling(node);
207        }
208        return returnValue;
209    }
210
211    /**
212     * Gets first child node of specified node.
213     *
214     * @param node DetailNode
215     * @return first child
216     */
217    public static DetailNode getFirstChild(DetailNode node) {
218        DetailNode resultNode = null;
219
220        if (node.getChildren().length > 0) {
221            resultNode = node.getChildren()[0];
222        }
223        return resultNode;
224    }
225
226    /**
227     * Checks whether node contains any node of specified type among children on any deep level.
228     *
229     * @param node DetailNode
230     * @param type token type
231     * @return true if node contains any node of type among children on any deep level.
232     */
233    public static boolean containsInBranch(DetailNode node, int type) {
234        boolean result = true;
235        DetailNode curNode = node;
236        while (type != curNode.getType()) {
237            DetailNode toVisit = getFirstChild(curNode);
238            while (curNode != null && toVisit == null) {
239                toVisit = getNextSibling(curNode);
240                if (toVisit == null) {
241                    curNode = curNode.getParent();
242                }
243            }
244
245            if (curNode == toVisit) {
246                result = false;
247                break;
248            }
249
250            curNode = toVisit;
251        }
252        return result;
253    }
254
255    /**
256     * Gets next sibling of specified node.
257     *
258     * @param node DetailNode
259     * @return next sibling.
260     */
261    public static DetailNode getNextSibling(DetailNode node) {
262        DetailNode nextSibling = null;
263        final DetailNode parent = node.getParent();
264        if (parent != null) {
265            final int nextSiblingIndex = node.getIndex() + 1;
266            final DetailNode[] children = parent.getChildren();
267            if (nextSiblingIndex <= children.length - 1) {
268                nextSibling = children[nextSiblingIndex];
269            }
270        }
271        return nextSibling;
272    }
273
274    /**
275     * Gets next sibling of specified node with the specified type.
276     *
277     * @param node DetailNode
278     * @param tokenType javadoc token type
279     * @return next sibling.
280     */
281    public static DetailNode getNextSibling(DetailNode node, int tokenType) {
282        DetailNode nextSibling = getNextSibling(node);
283        while (nextSibling != null && nextSibling.getType() != tokenType) {
284            nextSibling = getNextSibling(nextSibling);
285        }
286        return nextSibling;
287    }
288
289    /**
290     * Gets previous sibling of specified node.
291     *
292     * @param node DetailNode
293     * @return previous sibling
294     */
295    public static DetailNode getPreviousSibling(DetailNode node) {
296        DetailNode previousSibling = null;
297        final int previousSiblingIndex = node.getIndex() - 1;
298        if (previousSiblingIndex >= 0) {
299            final DetailNode parent = node.getParent();
300            final DetailNode[] children = parent.getChildren();
301            previousSibling = children[previousSiblingIndex];
302        }
303        return previousSibling;
304    }
305
306    /**
307     * Returns the name of a token for a given ID.
308     *
309     * @param id
310     *        the ID of the token name to get
311     * @return a token name
312     * @throws IllegalArgumentException if an unknown token ID was specified.
313     */
314    public static String getTokenName(int id) {
315        final String name = TOKEN_VALUE_TO_NAME.get(id);
316        if (name == null) {
317            throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id);
318        }
319        return name;
320    }
321
322    /**
323     * Returns the ID of a token for a given name.
324     *
325     * @param name
326     *        the name of the token ID to get
327     * @return a token ID
328     * @throws IllegalArgumentException if an unknown token name was specified.
329     */
330    public static int getTokenId(String name) {
331        final Integer id = TOKEN_NAME_TO_VALUE.get(name);
332        if (id == null) {
333            throw new IllegalArgumentException("Unknown javadoc token name. Given name " + name);
334        }
335        return id;
336    }
337
338    /**
339     * Gets tag name from javadocTagSection.
340     *
341     * @param javadocTagSection to get tag name from.
342     * @return name, of the javadocTagSection's tag.
343     */
344    public static String getTagName(DetailNode javadocTagSection) {
345        final String javadocTagName;
346        if (javadocTagSection.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) {
347            javadocTagName = getNextSibling(
348                    getFirstChild(javadocTagSection)).getText();
349        }
350        else {
351            javadocTagName = getFirstChild(javadocTagSection).getText();
352        }
353        return javadocTagName;
354    }
355
356    /**
357     * Replace all control chars with escaped symbols.
358     *
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}