001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 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.checks.javadoc;
021
022import java.util.Arrays;
023import java.util.Set;
024import java.util.regex.Matcher;
025import java.util.regex.Pattern;
026import java.util.stream.Collectors;
027
028import com.puppycrawl.tools.checkstyle.StatelessCheck;
029import com.puppycrawl.tools.checkstyle.api.DetailNode;
030import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
031
032/**
033 * <div>
034 * Checks that a
035 * <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/javadoc/doc-comment-spec.html#block-tags">
036 * javadoc block tag</a> appears only at the beginning of a line, ignoring
037 * leading asterisks and white space. A block tag is a token that starts with
038 * {@code @} symbol and is preceded by a whitespace. This check ignores block
039 * tags in comments and inside inline tags {&#64;code } and {&#64;literal }.
040 * </div>
041 *
042 * <p>
043 * Rationale: according to
044 * <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/javadoc/doc-comment-spec.html#block-tags">
045 * the specification</a> all javadoc block tags should be placed at the beginning
046 * of a line. Tags that are not placed at the beginning are treated as plain text.
047 * To recognize intentional tag placement to text area it is better to escape the
048 * {@code @} symbol, and all non-escaped tags should be located at the beginning
049 * of the line. See NOTE section for details on how to escape.
050 * </p>
051 *
052 * <p>
053 * Notes:
054 * To place a tag explicitly as text, escape the {@code @} symbol with HTML entity
055 * &amp;#64; or place it inside {@code {@code }}, for example:
056 * </p>
057 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
058 * &#47;**
059 *  * &amp;#64;serial literal in {&#64;code &#64;serial} Javadoc tag.
060 *  *&#47;
061 * </code></pre></div>
062 *
063 * @since 8.24
064 */
065@StatelessCheck
066public class JavadocBlockTagLocationCheck extends AbstractJavadocCheck {
067
068    /**
069     * A key is pointing to the warning message text in "messages.properties" file.
070     */
071    public static final String MSG_BLOCK_TAG_LOCATION = "javadoc.blockTagLocation";
072
073    /**
074     * This regexp is used to extract the javadoc tags.
075     */
076    private static final Pattern JAVADOC_BLOCK_TAG_PATTERN = Pattern.compile("\\s@(\\w+)");
077
078    /**
079     * Block tags from Java 11
080     * <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/javadoc/doc-comment-spec.html">
081     * Documentation Comment Specification</a>.
082     */
083    private static final String[] DEFAULT_TAGS = {
084        "author",
085        "deprecated",
086        "exception",
087        "hidden",
088        "param",
089        "provides",
090        "return",
091        "see",
092        "serial",
093        "serialData",
094        "serialField",
095        "since",
096        "throws",
097        "uses",
098        "version",
099    };
100
101    /**
102     * Specify the javadoc tags to process.
103     */
104    private Set<String> tags;
105
106    /**
107     * Creates a new {@code JavadocBlockTagLocationCheck} instance with default settings.
108     */
109    public JavadocBlockTagLocationCheck() {
110        setTags(DEFAULT_TAGS);
111    }
112
113    /**
114     * Setter to specify the javadoc tags to process.
115     *
116     * @param values user's values.
117     * @since 8.24
118     */
119    public final void setTags(String... values) {
120        tags = Arrays.stream(values).collect(Collectors.toUnmodifiableSet());
121    }
122
123    /**
124     * The javadoc tokens that this check must be registered for. According to
125     * <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/javadoc/doc-comment-spec.html#block-tags">
126     * the specs</a> each block tag must appear at the beginning of a line, otherwise
127     * it will be interpreted as a plain text. This check looks for a block tag
128     * in the javadoc text, thus it needs the {@code TEXT} tokens.
129     *
130     * @return the javadoc token set this must be registered for.
131     * @see JavadocTokenTypes
132     */
133    @Override
134    public int[] getRequiredJavadocTokens() {
135        return new int[] {
136            JavadocTokenTypes.TEXT,
137        };
138    }
139
140    @Override
141    public int[] getAcceptableJavadocTokens() {
142        return getRequiredJavadocTokens();
143    }
144
145    @Override
146    public int[] getDefaultJavadocTokens() {
147        return getRequiredJavadocTokens();
148    }
149
150    @Override
151    public void visitJavadocToken(DetailNode ast) {
152        if (!isCommentOrInlineTag(ast.getParent())) {
153            final Matcher tagMatcher = JAVADOC_BLOCK_TAG_PATTERN.matcher(ast.getText());
154            while (tagMatcher.find()) {
155                final String tagName = tagMatcher.group(1);
156                if (tags.contains(tagName)) {
157                    log(ast.getLineNumber(), MSG_BLOCK_TAG_LOCATION, tagName);
158                }
159            }
160        }
161    }
162
163    /**
164     * Checks if the node can contain an unescaped block tag without violation.
165     *
166     * @param node to check
167     * @return {@code true} if node is {@code @code}, {@code @literal} or HTML comment.
168     */
169    private static boolean isCommentOrInlineTag(DetailNode node) {
170        return node.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG
171                || node.getType() == JavadocTokenTypes.HTML_COMMENT;
172    }
173
174}