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.regex.Matcher;
023import java.util.regex.Pattern;
024
025import javax.annotation.Nullable;
026
027import com.puppycrawl.tools.checkstyle.StatelessCheck;
028import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
029import com.puppycrawl.tools.checkstyle.api.DetailAST;
030import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
031import com.puppycrawl.tools.checkstyle.api.TokenTypes;
032import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
033import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
034
035/**
036 * <div>
037 * Requires user defined Javadoc tag to be present in Javadoc comment with defined format.
038 * To define the format for a tag, set property tagFormat to a regular expression.
039 * Property tagSeverity is used for severity of events when the tag exists.
040 * No violation reported in case there is no javadoc.
041 * </div>
042 *
043 * @since 4.2
044 */
045@StatelessCheck
046public class WriteTagCheck
047    extends AbstractCheck {
048
049    /**
050     * A key is pointing to the warning message text in "messages.properties"
051     * file.
052     */
053    public static final String MSG_MISSING_TAG = "type.missingTag";
054
055    /**
056     * A key is pointing to the warning message text in "messages.properties"
057     * file.
058     */
059    public static final String MSG_WRITE_TAG = "javadoc.writeTag";
060
061    /**
062     * A key is pointing to the warning message text in "messages.properties"
063     * file.
064     */
065    public static final String MSG_TAG_FORMAT = "type.tagFormat";
066
067    /** Line split pattern. */
068    private static final Pattern LINE_SPLIT_PATTERN = Pattern.compile("\\R");
069
070    /** Compiled regexp to match tag. */
071    private Pattern tagRegExp;
072    /** Specify the regexp to match tag content. */
073    private Pattern tagFormat;
074
075    /** Specify the name of tag. */
076    private String tag;
077    /** Specify the severity level when tag is found and printed. */
078    private SeverityLevel tagSeverity = SeverityLevel.INFO;
079
080    /**
081     * Setter to specify the name of tag.
082     *
083     * @param tag tag to check
084     * @since 4.2
085     */
086    public void setTag(String tag) {
087        this.tag = tag;
088        tagRegExp = CommonUtil.createPattern(tag + "\\s*(.*$)");
089    }
090
091    /**
092     * Setter to specify the regexp to match tag content.
093     *
094     * @param pattern a {@code String} value
095     * @since 4.2
096     */
097    public void setTagFormat(Pattern pattern) {
098        tagFormat = pattern;
099    }
100
101    /**
102     * Setter to specify the severity level when tag is found and printed.
103     *
104     * @param severity  The new severity level
105     * @see SeverityLevel
106     * @since 4.2
107     */
108    public final void setTagSeverity(SeverityLevel severity) {
109        tagSeverity = severity;
110    }
111
112    @Override
113    public int[] getDefaultTokens() {
114        return new int[] {
115            TokenTypes.INTERFACE_DEF,
116            TokenTypes.CLASS_DEF,
117            TokenTypes.ENUM_DEF,
118            TokenTypes.ANNOTATION_DEF,
119            TokenTypes.RECORD_DEF,
120        };
121    }
122
123    @Override
124    public int[] getAcceptableTokens() {
125        return new int[] {
126            TokenTypes.INTERFACE_DEF,
127            TokenTypes.CLASS_DEF,
128            TokenTypes.ENUM_DEF,
129            TokenTypes.ANNOTATION_DEF,
130            TokenTypes.METHOD_DEF,
131            TokenTypes.CTOR_DEF,
132            TokenTypes.ENUM_CONSTANT_DEF,
133            TokenTypes.ANNOTATION_FIELD_DEF,
134            TokenTypes.RECORD_DEF,
135            TokenTypes.COMPACT_CTOR_DEF,
136        };
137    }
138
139    @Override
140    public boolean isCommentNodesRequired() {
141        return true;
142    }
143
144    @Override
145    public int[] getRequiredTokens() {
146        return CommonUtil.EMPTY_INT_ARRAY;
147    }
148
149    @Override
150    public void visitToken(DetailAST ast) {
151        final DetailAST javadoc = getJavadoc(ast);
152
153        if (javadoc != null) {
154            final String[] cmtLines = LINE_SPLIT_PATTERN
155                    .split(JavadocUtil.getJavadocCommentContent(javadoc));
156
157            checkTag(javadoc.getLineNo(),
158                    javadoc.getLineNo() + countCommentLines(javadoc),
159                    cmtLines);
160        }
161    }
162
163    /**
164     * Retrieves the Javadoc comment associated with a given AST node.
165     *
166     * @param ast the AST node (e.g., class, method, constructor) to search above.
167     * @return the {@code DetailAST} representing the Javadoc comment if found and
168     *          valid; {@code null} otherwise.
169     */
170    @Nullable
171    private static DetailAST getJavadoc(DetailAST ast) {
172        // Prefer Javadoc directly above the node
173        DetailAST cmt = ast.findFirstToken(TokenTypes.BLOCK_COMMENT_BEGIN);
174        if (cmt == null) {
175            // Check MODIFIERS and TYPE block for comments
176            final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
177            final DetailAST type = ast.findFirstToken(TokenTypes.TYPE);
178
179            cmt = modifiers.findFirstToken(TokenTypes.BLOCK_COMMENT_BEGIN);
180            if (cmt == null && type != null) {
181                cmt = type.findFirstToken(TokenTypes.BLOCK_COMMENT_BEGIN);
182            }
183        }
184
185        final DetailAST javadoc;
186        if (cmt != null && JavadocUtil.isJavadocComment(cmt)) {
187            javadoc = cmt;
188        }
189        else {
190            javadoc = null;
191        }
192
193        return javadoc;
194    }
195
196    /**
197     * Counts the number of lines in a block comment.
198     *
199     * @param blockComment the AST node representing the block comment.
200     * @return the number of lines in the comment.
201     */
202    private static int countCommentLines(DetailAST blockComment) {
203        final String content = JavadocUtil.getBlockCommentContent(blockComment);
204        return LINE_SPLIT_PATTERN.split(content).length;
205    }
206
207    /**
208     * Validates the Javadoc comment against the configured requirements.
209     *
210     * @param astLineNo the line number of the type definition.
211     * @param javadocLineNo the starting line number of the Javadoc comment block.
212     * @param comment the lines of the Javadoc comment block.
213     */
214    private void checkTag(int astLineNo, int javadocLineNo, String... comment) {
215        if (tagRegExp != null) {
216            boolean hasTag = false;
217            for (int i = 0; i < comment.length; i++) {
218                final String commentValue = comment[i];
219                final Matcher matcher = tagRegExp.matcher(commentValue);
220                if (matcher.find()) {
221                    hasTag = true;
222                    final int contentStart = matcher.start(1);
223                    final String content = commentValue.substring(contentStart);
224                    if (tagFormat == null || tagFormat.matcher(content).find()) {
225                        logTag(astLineNo + i, tag, content);
226                    }
227                    else {
228                        log(astLineNo + i, MSG_TAG_FORMAT, tag, tagFormat.pattern());
229                    }
230                }
231            }
232            if (!hasTag) {
233                log(javadocLineNo, MSG_MISSING_TAG, tag);
234            }
235        }
236    }
237
238    /**
239     * Log a message.
240     *
241     * @param line the line number where the violation was found
242     * @param tagName the javadoc tag to be logged
243     * @param tagValue the contents of the tag
244     *
245     * @see java.text.MessageFormat
246     */
247    private void logTag(int line, String tagName, String tagValue) {
248        final String originalSeverity = getSeverity();
249        setSeverity(tagSeverity.getName());
250
251        log(line, MSG_WRITE_TAG, tagName, tagValue);
252
253        setSeverity(originalSeverity);
254    }
255}