1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 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.annotation;
21
22 import java.util.Locale;
23
24 import com.puppycrawl.tools.checkstyle.StatelessCheck;
25 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
26 import com.puppycrawl.tools.checkstyle.api.DetailAST;
27 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
28
29 /**
30 * <div>
31 * Checks the style of elements in annotations.
32 * </div>
33 *
34 * <p>
35 * Annotations have three element styles starting with the least verbose.
36 * </p>
37 * <ul>
38 * <li>
39 * {@code ElementStyleOption.COMPACT_NO_ARRAY}
40 * </li>
41 * <li>
42 * {@code ElementStyleOption.COMPACT}
43 * </li>
44 * <li>
45 * {@code ElementStyleOption.EXPANDED}
46 * </li>
47 * </ul>
48 *
49 * <p>
50 * To not enforce an element style a {@code ElementStyleOption.IGNORE} type is provided.
51 * The desired style can be set through the {@code elementStyle} property.
52 * </p>
53 *
54 * <p>
55 * Using the {@code ElementStyleOption.EXPANDED} style is more verbose.
56 * The expanded version is sometimes referred to as "named parameters" in other languages.
57 * </p>
58 *
59 * <p>
60 * Using the {@code ElementStyleOption.COMPACT} style is less verbose.
61 * This style can only be used when there is an element called 'value' which is either
62 * the sole element or all other elements have default values.
63 * </p>
64 *
65 * <p>
66 * Using the {@code ElementStyleOption.COMPACT_NO_ARRAY} style is less verbose.
67 * It is similar to the {@code ElementStyleOption.COMPACT} style but single value arrays are
68 * flagged.
69 * With annotations a single value array does not need to be placed in an array initializer.
70 * </p>
71 *
72 * <p>
73 * The ending parenthesis are optional when using annotations with no elements.
74 * To always require ending parenthesis use the {@code ClosingParensOption.ALWAYS} type.
75 * To never have ending parenthesis use the {@code ClosingParensOption.NEVER} type.
76 * To not enforce a closing parenthesis preference a {@code ClosingParensOption.IGNORE} type is
77 * provided.
78 * Set this through the {@code closingParens} property.
79 * </p>
80 *
81 * <p>
82 * Annotations also allow you to specify arrays of elements in a standard format.
83 * As with normal arrays, a trailing comma is optional.
84 * To always require a trailing comma use the {@code TrailingArrayCommaOption.ALWAYS} type.
85 * To never have a trailing comma use the {@code TrailingArrayCommaOption.NEVER} type.
86 * To not enforce a trailing array comma preference a {@code TrailingArrayCommaOption.IGNORE} type
87 * is provided. Set this through the {@code trailingArrayComma} property.
88 * </p>
89 *
90 * <p>
91 * By default, the {@code ElementStyleOption} is set to {@code COMPACT_NO_ARRAY},
92 * the {@code TrailingArrayCommaOption} is set to {@code NEVER},
93 * and the {@code ClosingParensOption} is set to {@code NEVER}.
94 * </p>
95 *
96 * <p>
97 * According to the JLS, it is legal to include a trailing comma
98 * in arrays used in annotations but Sun's Java 5 {@literal &} 6 compilers will not
99 * compile with this syntax. This may in be a bug in Sun's compilers
100 * since eclipse 3.4's built-in compiler does allow this syntax as
101 * defined in the JLS. Note: this was tested with compilers included with
102 * JDK versions 1.5.0.17 and 1.6.0.11 and the compiler included with eclipse 3.4.1.
103 * </p>
104 *
105 * <p>
106 * See <a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-9.html#jls-9.7">
107 * Java Language specification, §9.7</a>.
108 * </p>
109 *
110 * @since 5.0
111 */
112 @StatelessCheck
113 public final class AnnotationUseStyleCheck extends AbstractCheck {
114
115 /**
116 * Defines the styles for defining elements in an annotation.
117 */
118 public enum ElementStyleOption {
119
120 /**
121 * Expanded example: {@code @SuppressWarnings(value={"unchecked","unused",})}.
122 */
123 EXPANDED,
124
125 /**
126 * Compact example
127 * <br>
128 * {@code @SuppressWarnings({"unchecked","unused",})}
129 * <br>or<br>
130 * {@code @SuppressWarnings("unchecked")}.
131 */
132 COMPACT,
133
134 /**
135 * Compact example: {@code @SuppressWarnings("unchecked")}.
136 */
137 COMPACT_NO_ARRAY,
138
139 /**
140 * Mixed styles.
141 */
142 IGNORE,
143
144 }
145
146 /**
147 * Defines the two styles for defining
148 * elements in an annotation.
149 *
150 */
151 public enum TrailingArrayCommaOption {
152
153 /**
154 * With comma example: {@code @SuppressWarnings(value={"unchecked","unused",})}.
155 */
156 ALWAYS,
157
158 /**
159 * Without comma example: {@code @SuppressWarnings(value={"unchecked","unused"})}.
160 */
161 NEVER,
162
163 /**
164 * Mixed styles.
165 */
166 IGNORE,
167
168 }
169
170 /**
171 * Defines the two styles for defining
172 * elements in an annotation.
173 *
174 */
175 public enum ClosingParensOption {
176
177 /**
178 * With parens example :
179 * {@code @Deprecated()}.
180 */
181 ALWAYS,
182
183 /**
184 * Without parens example: {@code @Deprecated}.
185 */
186 NEVER,
187
188 /**
189 * Mixed styles.
190 */
191 IGNORE,
192
193 }
194
195 /**
196 * A key is pointing to the warning message text in "messages.properties"
197 * file.
198 */
199 public static final String MSG_KEY_ANNOTATION_INCORRECT_STYLE =
200 "annotation.incorrect.style";
201
202 /**
203 * A key is pointing to the warning message text in "messages.properties"
204 * file.
205 */
206 public static final String MSG_KEY_ANNOTATION_PARENS_MISSING =
207 "annotation.parens.missing";
208
209 /**
210 * A key is pointing to the warning message text in "messages.properties"
211 * file.
212 */
213 public static final String MSG_KEY_ANNOTATION_PARENS_PRESENT =
214 "annotation.parens.present";
215
216 /**
217 * A key is pointing to the warning message text in "messages.properties"
218 * file.
219 */
220 public static final String MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING =
221 "annotation.trailing.comma.missing";
222
223 /**
224 * A key is pointing to the warning message text in "messages.properties"
225 * file.
226 */
227 public static final String MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT =
228 "annotation.trailing.comma.present";
229
230 /**
231 * The element name used to receive special linguistic support
232 * for annotation use.
233 */
234 private static final String ANNOTATION_ELEMENT_SINGLE_NAME =
235 "value";
236
237 /**
238 * Define the annotation element styles.
239 */
240 private ElementStyleOption elementStyle = ElementStyleOption.COMPACT_NO_ARRAY;
241
242 // defaulting to NEVER because of the strange compiler behavior
243 /**
244 * Define the policy for trailing comma in arrays.
245 */
246 private TrailingArrayCommaOption trailingArrayComma = TrailingArrayCommaOption.NEVER;
247
248 /**
249 * Define the policy for ending parenthesis.
250 */
251 private ClosingParensOption closingParens = ClosingParensOption.NEVER;
252
253 /**
254 * Creates a new {@code AnnotationUseStyleCheck} instance.
255 */
256 public AnnotationUseStyleCheck() {
257 // no code by default
258 }
259
260 /**
261 * Setter to define the annotation element styles.
262 *
263 * @param style string representation
264 * @since 5.0
265 */
266 public void setElementStyle(final String style) {
267 elementStyle = getOption(ElementStyleOption.class, style);
268 }
269
270 /**
271 * Setter to define the policy for trailing comma in arrays.
272 *
273 * @param comma string representation
274 * @since 5.0
275 */
276 public void setTrailingArrayComma(final String comma) {
277 trailingArrayComma = getOption(TrailingArrayCommaOption.class, comma);
278 }
279
280 /**
281 * Setter to define the policy for ending parenthesis.
282 *
283 * @param parens string representation
284 * @since 5.0
285 */
286 public void setClosingParens(final String parens) {
287 closingParens = getOption(ClosingParensOption.class, parens);
288 }
289
290 /**
291 * Retrieves an {@code Enum} type from a {@code String}.
292 *
293 * @param <T> the enum type
294 * @param enumClass the enum class
295 * @param value the string representing the enum
296 * @return the enum type
297 * @throws IllegalArgumentException when unable to parse value
298 */
299 private static <T extends Enum<T>> T getOption(final Class<T> enumClass,
300 final String value) {
301 try {
302 return Enum.valueOf(enumClass, value.trim().toUpperCase(Locale.ENGLISH));
303 }
304 catch (final IllegalArgumentException iae) {
305 throw new IllegalArgumentException("unable to parse " + value, iae);
306 }
307 }
308
309 @Override
310 public int[] getDefaultTokens() {
311 return getRequiredTokens();
312 }
313
314 @Override
315 public int[] getRequiredTokens() {
316 return new int[] {
317 TokenTypes.ANNOTATION,
318 };
319 }
320
321 @Override
322 public int[] getAcceptableTokens() {
323 return getRequiredTokens();
324 }
325
326 @Override
327 public void visitToken(final DetailAST ast) {
328 checkStyleType(ast);
329 checkCheckClosingParensOption(ast);
330 checkTrailingComma(ast);
331 }
332
333 /**
334 * Checks to see if the
335 * {@link ElementStyleOption AnnotationElementStyleOption}
336 * is correct.
337 *
338 * @param annotation the annotation token
339 */
340 private void checkStyleType(final DetailAST annotation) {
341 if (elementStyle == ElementStyleOption.COMPACT_NO_ARRAY) {
342 checkCompactNoArrayStyle(annotation);
343 }
344 else if (elementStyle == ElementStyleOption.COMPACT) {
345 checkCompactStyle(annotation);
346 }
347 else if (elementStyle == ElementStyleOption.EXPANDED) {
348 checkExpandedStyle(annotation);
349 }
350 }
351
352 /**
353 * Checks for expanded style type violations.
354 *
355 * @param annotation the annotation token
356 */
357 private void checkExpandedStyle(final DetailAST annotation) {
358 final int valuePairCount =
359 annotation.getChildCount(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
360
361 if (valuePairCount == 0 && hasArguments(annotation)) {
362 log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE, ElementStyleOption.EXPANDED);
363 }
364 }
365
366 /**
367 * Checks that annotation has arguments.
368 *
369 * @param annotation to check
370 * @return true if annotation has arguments, false otherwise
371 */
372 private static boolean hasArguments(DetailAST annotation) {
373 final DetailAST firstToken = annotation.findFirstToken(TokenTypes.LPAREN);
374 return firstToken != null && firstToken.getNextSibling().getType() != TokenTypes.RPAREN;
375 }
376
377 /**
378 * Checks for compact style type violations.
379 *
380 * @param annotation the annotation token
381 */
382 private void checkCompactStyle(final DetailAST annotation) {
383 final int valuePairCount =
384 annotation.getChildCount(
385 TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
386
387 final DetailAST valuePair =
388 annotation.findFirstToken(
389 TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
390
391 if (valuePairCount == 1
392 && ANNOTATION_ELEMENT_SINGLE_NAME.equals(
393 valuePair.getFirstChild().getText())) {
394 log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE,
395 ElementStyleOption.COMPACT);
396 }
397 }
398
399 /**
400 * Checks for compact no array style type violations.
401 *
402 * @param annotation the annotation token
403 */
404 private void checkCompactNoArrayStyle(final DetailAST annotation) {
405 final DetailAST arrayInit =
406 annotation.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
407
408 // in compact style with one value
409 if (arrayInit != null
410 && arrayInit.getChildCount(TokenTypes.EXPR) == 1) {
411 log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE,
412 ElementStyleOption.COMPACT_NO_ARRAY);
413 }
414 // in expanded style with pairs
415 else {
416 DetailAST ast = annotation.getFirstChild();
417 while (ast != null) {
418 final DetailAST nestedArrayInit =
419 ast.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
420 if (nestedArrayInit != null
421 && nestedArrayInit.getChildCount(TokenTypes.EXPR) == 1) {
422 log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE,
423 ElementStyleOption.COMPACT_NO_ARRAY);
424 }
425 ast = ast.getNextSibling();
426 }
427 }
428 }
429
430 /**
431 * Checks to see if the trailing comma is present if required or
432 * prohibited.
433 *
434 * @param annotation the annotation token
435 */
436 private void checkTrailingComma(final DetailAST annotation) {
437 if (trailingArrayComma != TrailingArrayCommaOption.IGNORE) {
438 DetailAST child = annotation.getFirstChild();
439
440 while (child != null) {
441 DetailAST arrayInit = null;
442
443 if (child.getType() == TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR) {
444 arrayInit = child.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
445 }
446 else if (child.getType() == TokenTypes.ANNOTATION_ARRAY_INIT) {
447 arrayInit = child;
448 }
449
450 if (arrayInit != null) {
451 logCommaViolation(arrayInit);
452 }
453 child = child.getNextSibling();
454 }
455 }
456 }
457
458 /**
459 * Logs a trailing array comma violation if one exists.
460 *
461 * @param ast the array init
462 * {@link TokenTypes#ANNOTATION_ARRAY_INIT ANNOTATION_ARRAY_INIT}.
463 */
464 private void logCommaViolation(final DetailAST ast) {
465 final DetailAST rCurly = ast.findFirstToken(TokenTypes.RCURLY);
466
467 // comma can be null if array is empty
468 final DetailAST comma = rCurly.getPreviousSibling();
469
470 if (trailingArrayComma == TrailingArrayCommaOption.NEVER) {
471 if (comma != null && comma.getType() == TokenTypes.COMMA) {
472 log(comma, MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT);
473 }
474 }
475 else if (comma == null || comma.getType() != TokenTypes.COMMA) {
476 log(rCurly, MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING);
477 }
478 }
479
480 /**
481 * Checks to see if the closing parenthesis are present if required or
482 * prohibited.
483 *
484 * @param ast the annotation token
485 */
486 private void checkCheckClosingParensOption(final DetailAST ast) {
487 if (closingParens != ClosingParensOption.IGNORE) {
488 final DetailAST paren = ast.getLastChild();
489
490 if (closingParens == ClosingParensOption.NEVER) {
491 if (paren.getPreviousSibling().getType() == TokenTypes.LPAREN) {
492 log(ast, MSG_KEY_ANNOTATION_PARENS_PRESENT);
493 }
494 }
495 else if (paren.getType() != TokenTypes.RPAREN) {
496 log(ast, MSG_KEY_ANNOTATION_PARENS_MISSING);
497 }
498 }
499 }
500
501 }