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