View Javadoc
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   * <ul>
44   * <li>
45   * Property {@code tag} - Specify the name of tag.
46   * Type is {@code java.lang.String}.
47   * Default value is {@code null}.
48   * </li>
49   * <li>
50   * Property {@code tagFormat} - Specify the regexp to match tag content.
51   * Type is {@code java.util.regex.Pattern}.
52   * Default value is {@code null}.
53   * </li>
54   * <li>
55   * Property {@code tagSeverity} - Specify the severity level when tag is found and printed.
56   * Type is {@code com.puppycrawl.tools.checkstyle.api.SeverityLevel}.
57   * Default value is {@code info}.
58   * </li>
59   * <li>
60   * Property {@code tokens} - tokens to check
61   * Type is {@code java.lang.String[]}.
62   * Validation type is {@code tokenSet}.
63   * Default value is:
64   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INTERFACE_DEF">
65   * INTERFACE_DEF</a>,
66   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CLASS_DEF">
67   * CLASS_DEF</a>,
68   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ENUM_DEF">
69   * ENUM_DEF</a>,
70   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_DEF">
71   * ANNOTATION_DEF</a>,
72   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#RECORD_DEF">
73   * RECORD_DEF</a>.
74   * </li>
75   * </ul>
76   *
77   * <p>
78   * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
79   * </p>
80   *
81   * <p>
82   * Violation Message Keys:
83   * </p>
84   * <ul>
85   * <li>
86   * {@code javadoc.writeTag}
87   * </li>
88   * <li>
89   * {@code type.missingTag}
90   * </li>
91   * <li>
92   * {@code type.tagFormat}
93   * </li>
94   * </ul>
95   *
96   * @since 4.2
97   */
98  @StatelessCheck
99  public class WriteTagCheck
100     extends AbstractCheck {
101 
102     /**
103      * A key is pointing to the warning message text in "messages.properties"
104      * file.
105      */
106     public static final String MSG_MISSING_TAG = "type.missingTag";
107 
108     /**
109      * A key is pointing to the warning message text in "messages.properties"
110      * file.
111      */
112     public static final String MSG_WRITE_TAG = "javadoc.writeTag";
113 
114     /**
115      * A key is pointing to the warning message text in "messages.properties"
116      * file.
117      */
118     public static final String MSG_TAG_FORMAT = "type.tagFormat";
119 
120     /** Line split pattern. */
121     private static final Pattern LINE_SPLIT_PATTERN = Pattern.compile("\\R");
122 
123     /** Compiled regexp to match tag. */
124     private Pattern tagRegExp;
125     /** Specify the regexp to match tag content. */
126     private Pattern tagFormat;
127 
128     /** Specify the name of tag. */
129     private String tag;
130     /** Specify the severity level when tag is found and printed. */
131     private SeverityLevel tagSeverity = SeverityLevel.INFO;
132 
133     /**
134      * Setter to specify the name of tag.
135      *
136      * @param tag tag to check
137      * @since 4.2
138      */
139     public void setTag(String tag) {
140         this.tag = tag;
141         tagRegExp = CommonUtil.createPattern(tag + "\\s*(.*$)");
142     }
143 
144     /**
145      * Setter to specify the regexp to match tag content.
146      *
147      * @param pattern a {@code String} value
148      * @since 4.2
149      */
150     public void setTagFormat(Pattern pattern) {
151         tagFormat = pattern;
152     }
153 
154     /**
155      * Setter to specify the severity level when tag is found and printed.
156      *
157      * @param severity  The new severity level
158      * @see SeverityLevel
159      * @since 4.2
160      */
161     public final void setTagSeverity(SeverityLevel severity) {
162         tagSeverity = severity;
163     }
164 
165     @Override
166     public int[] getDefaultTokens() {
167         return new int[] {
168             TokenTypes.INTERFACE_DEF,
169             TokenTypes.CLASS_DEF,
170             TokenTypes.ENUM_DEF,
171             TokenTypes.ANNOTATION_DEF,
172             TokenTypes.RECORD_DEF,
173         };
174     }
175 
176     @Override
177     public int[] getAcceptableTokens() {
178         return new int[] {
179             TokenTypes.INTERFACE_DEF,
180             TokenTypes.CLASS_DEF,
181             TokenTypes.ENUM_DEF,
182             TokenTypes.ANNOTATION_DEF,
183             TokenTypes.METHOD_DEF,
184             TokenTypes.CTOR_DEF,
185             TokenTypes.ENUM_CONSTANT_DEF,
186             TokenTypes.ANNOTATION_FIELD_DEF,
187             TokenTypes.RECORD_DEF,
188             TokenTypes.COMPACT_CTOR_DEF,
189         };
190     }
191 
192     @Override
193     public boolean isCommentNodesRequired() {
194         return true;
195     }
196 
197     @Override
198     public int[] getRequiredTokens() {
199         return CommonUtil.EMPTY_INT_ARRAY;
200     }
201 
202     @Override
203     public void visitToken(DetailAST ast) {
204         final DetailAST javadoc = getJavadoc(ast);
205 
206         if (javadoc != null) {
207             final String[] cmtLines = LINE_SPLIT_PATTERN
208                     .split(JavadocUtil.getJavadocCommentContent(javadoc));
209 
210             checkTag(javadoc.getLineNo(),
211                     javadoc.getLineNo() + countCommentLines(javadoc),
212                     cmtLines);
213         }
214     }
215 
216     /**
217      * Retrieves the Javadoc comment associated with a given AST node.
218      *
219      * @param ast the AST node (e.g., class, method, constructor) to search above.
220      * @return the {@code DetailAST} representing the Javadoc comment if found and
221      *          valid; {@code null} otherwise.
222      */
223     @Nullable
224     private static DetailAST getJavadoc(DetailAST ast) {
225         // Prefer Javadoc directly above the node
226         DetailAST cmt = ast.findFirstToken(TokenTypes.BLOCK_COMMENT_BEGIN);
227         if (cmt == null) {
228             // Check MODIFIERS and TYPE block for comments
229             final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
230             final DetailAST type = ast.findFirstToken(TokenTypes.TYPE);
231 
232             cmt = modifiers.findFirstToken(TokenTypes.BLOCK_COMMENT_BEGIN);
233             if (cmt == null && type != null) {
234                 cmt = type.findFirstToken(TokenTypes.BLOCK_COMMENT_BEGIN);
235             }
236         }
237 
238         final DetailAST javadoc;
239         if (cmt != null && JavadocUtil.isJavadocComment(cmt)) {
240             javadoc = cmt;
241         }
242         else {
243             javadoc = null;
244         }
245 
246         return javadoc;
247     }
248 
249     /**
250      * Counts the number of lines in a block comment.
251      *
252      * @param blockComment the AST node representing the block comment.
253      * @return the number of lines in the comment.
254      */
255     private static int countCommentLines(DetailAST blockComment) {
256         final String content = JavadocUtil.getBlockCommentContent(blockComment);
257         return LINE_SPLIT_PATTERN.split(content).length;
258     }
259 
260     /**
261      * Validates the Javadoc comment against the configured requirements.
262      *
263      * @param astLineNo the line number of the type definition.
264      * @param javadocLineNo the starting line number of the Javadoc comment block.
265      * @param comment the lines of the Javadoc comment block.
266      */
267     private void checkTag(int astLineNo, int javadocLineNo, String... comment) {
268         if (tagRegExp != null) {
269             boolean hasTag = false;
270             for (int i = 0; i < comment.length; i++) {
271                 final String commentValue = comment[i];
272                 final Matcher matcher = tagRegExp.matcher(commentValue);
273                 if (matcher.find()) {
274                     hasTag = true;
275                     final int contentStart = matcher.start(1);
276                     final String content = commentValue.substring(contentStart);
277                     if (tagFormat == null || tagFormat.matcher(content).find()) {
278                         logTag(astLineNo + i, tag, content);
279                     }
280                     else {
281                         log(astLineNo + i, MSG_TAG_FORMAT, tag, tagFormat.pattern());
282                     }
283                 }
284             }
285             if (!hasTag) {
286                 log(javadocLineNo, MSG_MISSING_TAG, tag);
287             }
288         }
289     }
290 
291     /**
292      * Log a message.
293      *
294      * @param line the line number where the violation was found
295      * @param tagName the javadoc tag to be logged
296      * @param tagValue the contents of the tag
297      *
298      * @see java.text.MessageFormat
299      */
300     private void logTag(int line, String tagName, String tagValue) {
301         final String originalSeverity = getSeverity();
302         setSeverity(tagSeverity.getName());
303 
304         log(line, MSG_WRITE_TAG, tagName, tagValue);
305 
306         setSeverity(originalSeverity);
307     }
308 }