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