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