1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2025 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.checks.javadoc;
21
22 import java.util.regex.Matcher;
23 import java.util.regex.Pattern;
24
25 import javax.annotation.Nullable;
26
27 import com.puppycrawl.tools.checkstyle.StatelessCheck;
28 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
29 import com.puppycrawl.tools.checkstyle.api.DetailAST;
30 import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
31 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
32 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
33 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
34
35 /**
36 * <div>
37 * Requires user defined Javadoc tag to be present in Javadoc comment with defined format.
38 * To define the format for a tag, set property tagFormat to a regular expression.
39 * Property tagSeverity is used for severity of events when the tag exists.
40 * No violation reported in case there is no javadoc.
41 * </div>
42 *
43 * @since 4.2
44 */
45 @StatelessCheck
46 public class WriteTagCheck
47 extends AbstractCheck {
48
49 /**
50 * A key is pointing to the warning message text in "messages.properties"
51 * file.
52 */
53 public static final String MSG_MISSING_TAG = "type.missingTag";
54
55 /**
56 * A key is pointing to the warning message text in "messages.properties"
57 * file.
58 */
59 public static final String MSG_WRITE_TAG = "javadoc.writeTag";
60
61 /**
62 * A key is pointing to the warning message text in "messages.properties"
63 * file.
64 */
65 public static final String MSG_TAG_FORMAT = "type.tagFormat";
66
67 /** Line split pattern. */
68 private static final Pattern LINE_SPLIT_PATTERN = Pattern.compile("\\R");
69
70 /** Compiled regexp to match tag. */
71 private Pattern tagRegExp;
72 /** Specify the regexp to match tag content. */
73 private Pattern tagFormat;
74
75 /** Specify the name of tag. */
76 private String tag;
77 /** Specify the severity level when tag is found and printed. */
78 private SeverityLevel tagSeverity = SeverityLevel.INFO;
79
80 /**
81 * Setter to specify the name of tag.
82 *
83 * @param tag tag to check
84 * @since 4.2
85 */
86 public void setTag(String tag) {
87 this.tag = tag;
88 tagRegExp = CommonUtil.createPattern(tag + "\\s*(.*$)");
89 }
90
91 /**
92 * Setter to specify the regexp to match tag content.
93 *
94 * @param pattern a {@code String} value
95 * @since 4.2
96 */
97 public void setTagFormat(Pattern pattern) {
98 tagFormat = pattern;
99 }
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 }