001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 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.JavadocCommentsTokenTypes;
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 =
080                TokenUtil.nameToValueMapFromPublicIntFields(JavadocCommentsTokenTypes.class);
081        TOKEN_VALUE_TO_NAME = TokenUtil.invertMap(TOKEN_NAME_TO_VALUE);
082    }
083
084    /** Prevent instantiation. */
085    private JavadocUtil() {
086    }
087
088    /**
089     * Gets validTags from a given piece of Javadoc.
090     *
091     * @param textBlock
092     *        the Javadoc comment to process.
093     * @param tagType
094     *        the type of validTags we're interested in
095     * @return all standalone validTags from the given javadoc.
096     */
097    public static JavadocTags getJavadocTags(TextBlock textBlock,
098            JavadocTagType tagType) {
099        final List<TagInfo> tags = new ArrayList<>();
100        final boolean isBlockTags = tagType == JavadocTagType.ALL
101                                        || tagType == JavadocTagType.BLOCK;
102        if (isBlockTags) {
103            tags.addAll(BlockTagUtil.extractBlockTags(textBlock.getText()));
104        }
105        final boolean isInlineTags = tagType == JavadocTagType.ALL
106                                        || tagType == JavadocTagType.INLINE;
107        if (isInlineTags) {
108            tags.addAll(InlineTagUtil.extractInlineTags(textBlock.getText()));
109        }
110
111        final List<JavadocTag> validTags = new ArrayList<>();
112        final List<InvalidJavadocTag> invalidTags = new ArrayList<>();
113
114        for (TagInfo tag : tags) {
115            final int col = tag.getPosition().getColumn();
116
117            // Add the starting line of the comment to the line number to get the actual line number
118            // in the source.
119            // Lines are one-indexed, so need an off-by-one correction.
120            final int line = textBlock.getStartLineNo() + tag.getPosition().getLine() - 1;
121
122            if (JavadocTagInfo.isValidName(tag.getName())) {
123                validTags.add(
124                    new JavadocTag(line, col, tag.getName(), tag.getValue()));
125            }
126            else {
127                invalidTags.add(new InvalidJavadocTag(line, col, tag.getName()));
128            }
129        }
130
131        return new JavadocTags(validTags, invalidTags);
132    }
133
134    /**
135     * Checks that commentContent starts with '*' javadoc comment identifier.
136     *
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     *
156     * @param blockCommentBegin
157     *        block comment AST
158     * @return true if block comment content starts with '*' javadoc comment
159     *         identifier.
160     */
161    public static boolean isJavadocComment(DetailAST blockCommentBegin) {
162        final String commentContent = getBlockCommentContent(blockCommentBegin);
163        return isJavadocComment(commentContent) && isCorrectJavadocPosition(blockCommentBegin);
164    }
165
166    /**
167     * Gets content of block comment.
168     *
169     * @param blockCommentBegin
170     *        block comment AST.
171     * @return content of block comment.
172     */
173    public static String getBlockCommentContent(DetailAST blockCommentBegin) {
174        final DetailAST commentContent = blockCommentBegin.getFirstChild();
175        return commentContent.getText();
176    }
177
178    /**
179     * Get content of Javadoc comment.
180     *
181     * @param javadocCommentBegin
182     *        Javadoc comment AST
183     * @return content of Javadoc comment.
184     */
185    public static String getJavadocCommentContent(DetailAST javadocCommentBegin) {
186        final DetailAST commentContent = javadocCommentBegin.getFirstChild();
187        return commentContent.getText().substring(1);
188    }
189
190    /**
191     * Returns the first child token that has a specified type.
192     *
193     * @param detailNode
194     *        Javadoc AST node
195     * @param type
196     *        the token type to match
197     * @return the matching token, or null if no match
198     */
199    public static DetailNode findFirstToken(DetailNode detailNode, int type) {
200        DetailNode returnValue = null;
201        DetailNode node = detailNode.getFirstChild();
202        while (node != null) {
203            if (node.getType() == type) {
204                returnValue = node;
205                break;
206            }
207            node = node.getNextSibling();
208        }
209        return returnValue;
210    }
211
212    /**
213     * Returns all child tokens that have a specified type.
214     *
215     * @param detailNode Javadoc AST node
216     * @param type the token type to match
217     * @return the matching tokens, or an empty list if no match
218     */
219    public static List<DetailNode> getAllNodesOfType(DetailNode detailNode, int type) {
220        final List<DetailNode> nodes = new ArrayList<>();
221        DetailNode node = detailNode.getFirstChild();
222        while (node != null) {
223            if (node.getType() == type) {
224                nodes.add(node);
225            }
226            node = node.getNextSibling();
227        }
228        return nodes;
229    }
230
231    /**
232     * Checks whether the given AST node is an HTML element with the specified tag name.
233     * This method ignore void elements.
234     *
235     * @param ast the AST node to check
236     *            (must be of type {@link JavadocCommentsTokenTypes#HTML_ELEMENT})
237     * @param expectedTagName the tag name to match (case-insensitive)
238     * @return {@code true} if the node has the given tag name, {@code false} otherwise
239     */
240    public static boolean isTag(DetailNode ast, String expectedTagName) {
241        final DetailNode htmlTagStart = findFirstToken(ast,
242                JavadocCommentsTokenTypes.HTML_TAG_START);
243        boolean isTag = false;
244        if (htmlTagStart != null) {
245            final String tagName = findFirstToken(htmlTagStart,
246                JavadocCommentsTokenTypes.TAG_NAME).getText();
247            isTag = expectedTagName.equalsIgnoreCase(tagName);
248        }
249        return isTag;
250    }
251
252    /**
253     * Gets next sibling of specified node with the specified type.
254     *
255     * @param node DetailNode
256     * @param tokenType javadoc token type
257     * @return next sibling.
258     */
259    public static DetailNode getNextSibling(DetailNode node, int tokenType) {
260        DetailNode nextSibling = node.getNextSibling();
261        while (nextSibling != null && nextSibling.getType() != tokenType) {
262            nextSibling = nextSibling.getNextSibling();
263        }
264        return nextSibling;
265    }
266
267    /**
268     * Returns the name of a token for a given ID.
269     *
270     * @param id
271     *        the ID of the token name to get
272     * @return a token name
273     * @throws IllegalArgumentException if an unknown token ID was specified.
274     */
275    public static String getTokenName(int id) {
276        final String name = TOKEN_VALUE_TO_NAME.get(id);
277        if (name == null) {
278            throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id);
279        }
280        return name;
281    }
282
283    /**
284     * Returns the ID of a token for a given name.
285     *
286     * @param name
287     *        the name of the token ID to get
288     * @return a token ID
289     * @throws IllegalArgumentException if an unknown token name was specified.
290     */
291    public static int getTokenId(String name) {
292        final Integer id = TOKEN_NAME_TO_VALUE.get(name);
293        if (id == null) {
294            throw new IllegalArgumentException("Unknown javadoc token name. Given name " + name);
295        }
296        return id;
297    }
298
299    /**
300     * Extracts the tag name from the given Javadoc tag section.
301     *
302     * @param javadocTagSection the node representing a Javadoc tag section.
303     *       This node must be of type {@link JavadocCommentsTokenTypes#JAVADOC_BLOCK_TAG}
304     *       or {@link JavadocCommentsTokenTypes#JAVADOC_INLINE_TAG}.
305     *  @return the tag name (e.g., "param", "return", "link")
306     */
307    public static String getTagName(DetailNode javadocTagSection) {
308        return findFirstToken(javadocTagSection.getFirstChild(),
309                    JavadocCommentsTokenTypes.TAG_NAME).getText();
310    }
311
312    /**
313     * Replace all control chars with escaped symbols.
314     *
315     * @param text the String to process.
316     * @return the processed String with all control chars escaped.
317     */
318    public static String escapeAllControlChars(String text) {
319        final String textWithoutNewlines = NEWLINE.matcher(text).replaceAll("\\\\n");
320        final String textWithoutReturns = RETURN.matcher(textWithoutNewlines).replaceAll("\\\\r");
321        return TAB.matcher(textWithoutReturns).replaceAll("\\\\t");
322    }
323
324    /**
325     * Checks Javadoc comment it's in right place.
326     *
327     * <p>From Javadoc util documentation:
328     * "Placement of comments - Documentation comments are recognized only when placed
329     * immediately before class, interface, constructor, method, field or annotation field
330     * declarations -- see the class example, method example, and field example.
331     * Documentation comments placed in the body of a method are ignored."</p>
332     *
333     * <p>If there are many documentation comments per declaration statement,
334     * only the last one will be recognized.</p>
335     *
336     * @param blockComment Block comment AST
337     * @return true if Javadoc is in right place
338     * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/tools/unix/javadoc.html">
339     *     Javadoc util documentation</a>
340     */
341    public static boolean isCorrectJavadocPosition(DetailAST blockComment) {
342        // We must be sure that after this one there are no other documentation comments.
343        DetailAST sibling = blockComment.getNextSibling();
344        while (sibling != null) {
345            if (sibling.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) {
346                if (isJavadocComment(getBlockCommentContent(sibling))) {
347                    // Found another javadoc comment, so this one should be ignored.
348                    break;
349                }
350                sibling = sibling.getNextSibling();
351            }
352            else if (sibling.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
353                sibling = sibling.getNextSibling();
354            }
355            else {
356                // Annotation, declaration or modifier is here. Do not check further.
357                sibling = null;
358            }
359        }
360        return sibling == null
361            && (BlockCommentPosition.isOnType(blockComment)
362                || BlockCommentPosition.isOnMember(blockComment)
363                || BlockCommentPosition.isOnPackage(blockComment));
364    }
365
366}