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