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.JavadocTags;
038import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.BlockTagUtil;
039import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.InlineTagUtil;
040import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.TagInfo;
041
042/**
043 * Contains utility methods for working with Javadoc.
044 */
045public final class JavadocUtil {
046
047    /**
048     * The type of Javadoc tag we want returned.
049     */
050    public enum JavadocTagType {
051
052        /** Block type. */
053        BLOCK,
054        /** Inline type. */
055        INLINE,
056        /** All validTags. */
057        ALL,
058
059    }
060
061    /** Maps from a token name to value. */
062    private static final Map<String, Integer> TOKEN_NAME_TO_VALUE;
063    /** Maps from a token value to name. */
064    private static final Map<Integer, String> TOKEN_VALUE_TO_NAME;
065
066    /** Exception message for unknown JavaDoc token id. */
067    private static final String UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE = "Unknown javadoc"
068            + " token id. Given id: ";
069
070    /** Newline pattern. */
071    private static final Pattern NEWLINE = Pattern.compile("\n");
072
073    /** Return pattern. */
074    private static final Pattern RETURN = Pattern.compile("\r");
075
076    /** Tab pattern. */
077    private static final Pattern TAB = Pattern.compile("\t");
078
079    // initialise the constants
080    static {
081        TOKEN_NAME_TO_VALUE =
082                TokenUtil.nameToValueMapFromPublicIntFields(JavadocCommentsTokenTypes.class);
083        TOKEN_VALUE_TO_NAME = TokenUtil.invertMap(TOKEN_NAME_TO_VALUE);
084    }
085
086    /** Prevent instantiation. */
087    private JavadocUtil() {
088    }
089
090    /**
091     * Gets validTags from a given piece of Javadoc.
092     *
093     * @param textBlock
094     *        the Javadoc comment to process.
095     * @param tagType
096     *        the type of validTags we're interested in
097     * @return all standalone validTags from the given javadoc.
098     */
099    public static JavadocTags getJavadocTags(TextBlock textBlock,
100            JavadocTagType tagType) {
101        final String[] text = textBlock.getText();
102        final List<TagInfo> tags = new ArrayList<>();
103        final boolean isBlockTags = tagType == JavadocTagType.ALL
104                                        || tagType == JavadocTagType.BLOCK;
105        if (isBlockTags) {
106            tags.addAll(BlockTagUtil.extractBlockTags(text));
107        }
108        final boolean isInlineTags = tagType == JavadocTagType.ALL
109                                        || tagType == JavadocTagType.INLINE;
110        if (isInlineTags) {
111            tags.addAll(InlineTagUtil.extractInlineTags(text));
112        }
113
114        final List<JavadocTag> validTags = new ArrayList<>();
115        final List<InvalidJavadocTag> invalidTags = new ArrayList<>();
116
117        for (TagInfo tag : tags) {
118            final LineColumn position = tag.getPosition();
119            final int col = position.getColumn();
120            // Add the starting line of the comment to the line number to get the actual line number
121            // in the source.
122            // Lines are one-indexed, so need an off-by-one correction.
123            final int line = textBlock.getStartLineNo() + position.getLine() - 1;
124
125            final String tagName = tag.getName();
126            try {
127                validTags.add(new JavadocTag(line, col, tagName, tag.getValue()));
128            }
129            catch (IllegalArgumentException ignored) {
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 Javadoc block comment attached to the given declaration AST node.
195     *
196     * @param ast the declaration AST node
197     * @return the attached Javadoc block comment, or {@code null} if none is found
198     */
199    @Nullable
200    public static DetailAST getAttachedJavadocComment(final DetailAST ast) {
201        DetailAST result = null;
202        DetailAST child = ast.getFirstChild();
203        while (result == null && child.getType() != TokenTypes.IDENT) {
204            result = findJavadocComment(child);
205            child = child.getNextSibling();
206        }
207        return result;
208    }
209
210    /**
211     * Finds the first Javadoc block comment under the given AST node.
212     *
213     * @param ast the AST node to search
214     * @return the Javadoc block comment, or {@code null} if none is found
215     */
216    @Nullable
217    private static DetailAST findJavadocComment(DetailAST ast) {
218        DetailAST result = null;
219        if (ast.getType() == TokenTypes.BLOCK_COMMENT_BEGIN && isJavadocComment(ast)) {
220            result = ast;
221        }
222        else {
223            DetailAST child = ast.getFirstChild();
224            while (result == null && child != null) {
225                result = findJavadocComment(child);
226                child = child.getNextSibling();
227            }
228        }
229        return result;
230    }
231
232    /**
233     * Returns the first child token that has a specified type.
234     *
235     * @param detailNode
236     *        Javadoc AST node
237     * @param type
238     *        the token type to match
239     * @return the matching token, or null if no match
240     */
241    public static DetailNode findFirstToken(DetailNode detailNode, int type) {
242        DetailNode returnValue = null;
243        DetailNode node = detailNode.getFirstChild();
244        while (node != null) {
245            if (node.getType() == type) {
246                returnValue = node;
247                break;
248            }
249            node = node.getNextSibling();
250        }
251        return returnValue;
252    }
253
254    /**
255     * Returns all child tokens that have a specified type.
256     *
257     * @param detailNode Javadoc AST node
258     * @param type the token type to match
259     * @return the matching tokens, or an empty list if no match
260     */
261    public static List<DetailNode> getAllNodesOfType(DetailNode detailNode, int type) {
262        final List<DetailNode> nodes = new ArrayList<>();
263        DetailNode node = detailNode.getFirstChild();
264        while (node != null) {
265            if (node.getType() == type) {
266                nodes.add(node);
267            }
268            node = node.getNextSibling();
269        }
270        return nodes;
271    }
272
273    /**
274     * Checks whether the given AST node is an HTML element with the specified tag name.
275     * This method ignore void elements.
276     *
277     * @param ast the AST node to check
278     *            (must be of type {@link JavadocCommentsTokenTypes#HTML_ELEMENT})
279     * @param expectedTagName the tag name to match (case-insensitive)
280     * @return {@code true} if the node has the given tag name, {@code false} otherwise
281     */
282    public static boolean isTag(DetailNode ast, String expectedTagName) {
283        final DetailNode htmlTagStart = findFirstToken(ast,
284                JavadocCommentsTokenTypes.HTML_TAG_START);
285        boolean isTag = false;
286        if (htmlTagStart != null) {
287            final String tagName = findFirstToken(htmlTagStart,
288                JavadocCommentsTokenTypes.TAG_NAME).getText();
289            isTag = expectedTagName.equalsIgnoreCase(tagName);
290        }
291        return isTag;
292    }
293
294    /**
295     * Gets next sibling of specified node with the specified type.
296     *
297     * @param node DetailNode
298     * @param tokenType javadoc token type
299     * @return next sibling.
300     */
301    public static DetailNode getNextSibling(DetailNode node, int tokenType) {
302        DetailNode nextSibling = node.getNextSibling();
303        while (nextSibling != null && nextSibling.getType() != tokenType) {
304            nextSibling = nextSibling.getNextSibling();
305        }
306        return nextSibling;
307    }
308
309    /**
310     * Returns the name of a token for a given ID.
311     *
312     * @param id
313     *        the ID of the token name to get
314     * @return a token name
315     * @throws IllegalArgumentException if an unknown token ID was specified.
316     */
317    public static String getTokenName(int id) {
318        final String name = TOKEN_VALUE_TO_NAME.get(id);
319        if (name == null) {
320            throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id);
321        }
322        return name;
323    }
324
325    /**
326     * Returns the ID of a token for a given name.
327     *
328     * @param name
329     *        the name of the token ID to get
330     * @return a token ID
331     * @throws IllegalArgumentException if an unknown token name was specified.
332     */
333    public static int getTokenId(String name) {
334        final Integer id = TOKEN_NAME_TO_VALUE.get(name);
335        if (id == null) {
336            throw new IllegalArgumentException("Unknown javadoc token name. Given name " + name);
337        }
338        return id;
339    }
340
341    /**
342     * Extracts the tag name from the given Javadoc tag section.
343     *
344     * @param javadocTagSection the node representing a Javadoc tag section.
345     *       This node must be of type {@link JavadocCommentsTokenTypes#JAVADOC_BLOCK_TAG}
346     *       or {@link JavadocCommentsTokenTypes#JAVADOC_INLINE_TAG}.
347     *  @return the tag name (e.g., "param", "return", "link")
348     */
349    public static String getTagName(DetailNode javadocTagSection) {
350        return findFirstToken(javadocTagSection.getFirstChild(),
351                    JavadocCommentsTokenTypes.TAG_NAME).getText();
352    }
353
354    /**
355     * Replace all control chars with escaped symbols.
356     *
357     * @param text the String to process.
358     * @return the processed String with all control chars escaped.
359     */
360    public static String escapeAllControlChars(String text) {
361        final String textWithoutNewlines = NEWLINE.matcher(text).replaceAll("\\\\n");
362        final String textWithoutReturns = RETURN.matcher(textWithoutNewlines).replaceAll("\\\\r");
363        return TAB.matcher(textWithoutReturns).replaceAll("\\\\t");
364    }
365
366    /**
367     * Checks Javadoc comment it's in right place.
368     *
369     * <p>From Javadoc util documentation:
370     * "Placement of comments - Documentation comments are recognized only when placed
371     * immediately before class, interface, constructor, method, field or annotation field
372     * declarations -- see the class example, method example, and field example.
373     * Documentation comments placed in the body of a method are ignored."</p>
374     *
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}