001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2017 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.annotation; 021 022import java.util.Locale; 023 024import org.apache.commons.beanutils.ConversionException; 025 026import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 027import com.puppycrawl.tools.checkstyle.api.DetailAST; 028import com.puppycrawl.tools.checkstyle.api.TokenTypes; 029 030/** 031 * This check controls the style with the usage of annotations. 032 * 033 * <p>Annotations have three element styles starting with the least verbose. 034 * <ul> 035 * <li>{@link ElementStyle#COMPACT_NO_ARRAY COMPACT_NO_ARRAY}</li> 036 * <li>{@link ElementStyle#COMPACT COMPACT}</li> 037 * <li>{@link ElementStyle#EXPANDED EXPANDED}</li> 038 * </ul> 039 * To not enforce an element style 040 * a {@link ElementStyle#IGNORE IGNORE} type is provided. The desired style 041 * can be set through the {@code elementStyle} property. 042 * 043 * <p>Using the EXPANDED style is more verbose. The expanded version 044 * is sometimes referred to as "named parameters" in other languages. 045 * 046 * <p>Using the COMPACT style is less verbose. This style can only 047 * be used when there is an element called 'value' which is either 048 * the sole element or all other elements have default values. 049 * 050 * <p>Using the COMPACT_NO_ARRAY style is less verbose. It is similar 051 * to the COMPACT style but single value arrays are flagged. With 052 * annotations a single value array does not need to be placed in an 053 * array initializer. This style can only be used when there is an 054 * element called 'value' which is either the sole element or all other 055 * elements have default values. 056 * 057 * <p>The ending parenthesis are optional when using annotations with no elements. 058 * To always require ending parenthesis use the 059 * {@link ClosingParens#ALWAYS ALWAYS} type. To never have ending parenthesis 060 * use the {@link ClosingParens#NEVER NEVER} type. To not enforce a 061 * closing parenthesis preference a {@link ClosingParens#IGNORE IGNORE} type is 062 * provided. Set this through the {@code closingParens} property. 063 * 064 * <p>Annotations also allow you to specify arrays of elements in a standard 065 * format. As with normal arrays, a trailing comma is optional. To always 066 * require a trailing comma use the {@link TrailingArrayComma#ALWAYS ALWAYS} 067 * type. To never have a trailing comma use the 068 * {@link TrailingArrayComma#NEVER NEVER} type. To not enforce a trailing 069 * array comma preference a {@link TrailingArrayComma#IGNORE IGNORE} type 070 * is provided. Set this through the {@code trailingArrayComma} property. 071 * 072 * <p>By default the ElementStyle is set to EXPANDED, the TrailingArrayComma 073 * is set to NEVER, and the ClosingParens is set to ALWAYS. 074 * 075 * <p>According to the JLS, it is legal to include a trailing comma 076 * in arrays used in annotations but Sun's Java 5 & 6 compilers will not 077 * compile with this syntax. This may in be a bug in Sun's compilers 078 * since eclipse 3.4's built-in compiler does allow this syntax as 079 * defined in the JLS. Note: this was tested with compilers included with 080 * JDK versions 1.5.0.17 and 1.6.0.11 and the compiler included with eclipse 081 * 3.4.1. 082 * 083 * <p>See <a 084 * href="http://docs.oracle.com/javase/specs/jls/se8/html/jls-9.html#jls-9.7"> 085 * Java Language specification, §9.7</a>. 086 * 087 * <p>An example shown below is set to enforce an EXPANDED style, with a 088 * trailing array comma set to NEVER and always including the closing 089 * parenthesis. 090 * 091 * <pre> 092 * <module name="AnnotationUseStyle"> 093 * <property name="ElementStyle" 094 * value="EXPANDED"/> 095 * <property name="TrailingArrayComma" 096 * value="NEVER"/> 097 * <property name="ClosingParens" 098 * value="ALWAYS"/> 099 * </module> 100 * </pre> 101 * 102 * @author Travis Schneeberger 103 */ 104public final class AnnotationUseStyleCheck extends AbstractCheck { 105 106 /** 107 * Defines the styles for defining elements in an annotation. 108 * @author Travis Schneeberger 109 */ 110 public enum ElementStyle { 111 112 /** 113 * Expanded example 114 * 115 * <pre>@SuppressWarnings(value={"unchecked","unused",})</pre>. 116 */ 117 EXPANDED, 118 119 /** 120 * Compact example 121 * 122 * <pre>@SuppressWarnings({"unchecked","unused",})</pre> 123 * <br>or<br> 124 * <pre>@SuppressWarnings("unchecked")</pre>. 125 */ 126 COMPACT, 127 128 /** 129 * Compact example.] 130 * 131 * <pre>@SuppressWarnings("unchecked")</pre>. 132 */ 133 COMPACT_NO_ARRAY, 134 135 /** 136 * Mixed styles. 137 */ 138 IGNORE, 139 } 140 141 /** 142 * Defines the two styles for defining 143 * elements in an annotation. 144 * 145 * @author Travis Schneeberger 146 */ 147 public enum TrailingArrayComma { 148 149 /** 150 * With comma example 151 * 152 * <pre>@SuppressWarnings(value={"unchecked","unused",})</pre>. 153 */ 154 ALWAYS, 155 156 /** 157 * Without comma example 158 * 159 * <pre>@SuppressWarnings(value={"unchecked","unused"})</pre>. 160 */ 161 NEVER, 162 163 /** 164 * Mixed styles. 165 */ 166 IGNORE, 167 } 168 169 /** 170 * Defines the two styles for defining 171 * elements in an annotation. 172 * 173 * @author Travis Schneeberger 174 */ 175 public enum ClosingParens { 176 177 /** 178 * With parens example 179 * 180 * <pre>@Deprecated()</pre>. 181 */ 182 ALWAYS, 183 184 /** 185 * Without parens example 186 * 187 * <pre>@Deprecated</pre>. 188 */ 189 NEVER, 190 191 /** 192 * Mixed styles. 193 */ 194 IGNORE, 195 } 196 197 /** 198 * A key is pointing to the warning message text in "messages.properties" 199 * file. 200 */ 201 public static final String MSG_KEY_ANNOTATION_INCORRECT_STYLE = 202 "annotation.incorrect.style"; 203 204 /** 205 * A key is pointing to the warning message text in "messages.properties" 206 * file. 207 */ 208 public static final String MSG_KEY_ANNOTATION_PARENS_MISSING = 209 "annotation.parens.missing"; 210 211 /** 212 * A key is pointing to the warning message text in "messages.properties" 213 * file. 214 */ 215 public static final String MSG_KEY_ANNOTATION_PARENS_PRESENT = 216 "annotation.parens.present"; 217 218 /** 219 * A key is pointing to the warning message text in "messages.properties" 220 * file. 221 */ 222 public static final String MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING = 223 "annotation.trailing.comma.missing"; 224 225 /** 226 * A key is pointing to the warning message text in "messages.properties" 227 * file. 228 */ 229 public static final String MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT = 230 "annotation.trailing.comma.present"; 231 232 /** 233 * The element name used to receive special linguistic support 234 * for annotation use. 235 */ 236 private static final String ANNOTATION_ELEMENT_SINGLE_NAME = 237 "value"; 238 239 //not extending AbstractOptionCheck because check 240 //has more than one option type. 241 242 /** 243 * ElementStyle option. 244 * @see #setElementStyle(String) 245 */ 246 private ElementStyle elementStyle = ElementStyle.COMPACT_NO_ARRAY; 247 248 //defaulting to NEVER because of the strange compiler behavior 249 /** 250 * Trailing array comma option. 251 * @see #setTrailingArrayComma(String) 252 */ 253 private TrailingArrayComma trailingArrayComma = TrailingArrayComma.NEVER; 254 255 /** 256 * Closing parens option. 257 * @see #setClosingParens(String) 258 */ 259 private ClosingParens closingParens = ClosingParens.NEVER; 260 261 /** 262 * Sets the ElementStyle from a string. 263 * 264 * @param style string representation 265 * @throws ConversionException if cannot convert string. 266 */ 267 public void setElementStyle(final String style) { 268 elementStyle = getOption(ElementStyle.class, style); 269 } 270 271 /** 272 * Sets the TrailingArrayComma from a string. 273 * 274 * @param comma string representation 275 * @throws ConversionException if cannot convert string. 276 */ 277 public void setTrailingArrayComma(final String comma) { 278 trailingArrayComma = getOption(TrailingArrayComma.class, comma); 279 } 280 281 /** 282 * Sets the ClosingParens from a string. 283 * 284 * @param parens string representation 285 * @throws ConversionException if cannot convert string. 286 */ 287 public void setClosingParens(final String parens) { 288 closingParens = getOption(ClosingParens.class, parens); 289 } 290 291 /** 292 * Retrieves an {@link Enum Enum} type from a @{link String String}. 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 */ 298 private static <T extends Enum<T>> T getOption(final Class<T> enumClass, 299 final String value) { 300 try { 301 return Enum.valueOf(enumClass, value.trim().toUpperCase(Locale.ENGLISH)); 302 } 303 catch (final IllegalArgumentException iae) { 304 throw new IllegalArgumentException("unable to parse " + value, iae); 305 } 306 } 307 308 @Override 309 public int[] getDefaultTokens() { 310 return getRequiredTokens(); 311 } 312 313 @Override 314 public int[] getRequiredTokens() { 315 return new int[] { 316 TokenTypes.ANNOTATION, 317 }; 318 } 319 320 @Override 321 public int[] getAcceptableTokens() { 322 return getRequiredTokens(); 323 } 324 325 @Override 326 public void visitToken(final DetailAST ast) { 327 checkStyleType(ast); 328 checkCheckClosingParens(ast); 329 checkTrailingComma(ast); 330 } 331 332 /** 333 * Checks to see if the 334 * {@link ElementStyle AnnotationElementStyle} 335 * is correct. 336 * 337 * @param annotation the annotation token 338 */ 339 private void checkStyleType(final DetailAST annotation) { 340 341 switch (elementStyle) { 342 case COMPACT_NO_ARRAY: 343 checkCompactNoArrayStyle(annotation); 344 break; 345 case COMPACT: 346 checkCompactStyle(annotation); 347 break; 348 case EXPANDED: 349 checkExpandedStyle(annotation); 350 break; 351 case IGNORE: 352 default: 353 break; 354 } 355 } 356 357 /** 358 * Checks for expanded style type violations. 359 * 360 * @param annotation the annotation token 361 */ 362 private void checkExpandedStyle(final DetailAST annotation) { 363 final int valuePairCount = 364 annotation.getChildCount(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); 365 366 if (valuePairCount == 0 367 && annotation.branchContains(TokenTypes.EXPR)) { 368 log(annotation.getLineNo(), MSG_KEY_ANNOTATION_INCORRECT_STYLE, 369 ElementStyle.EXPANDED); 370 } 371 } 372 373 /** 374 * Checks for compact style type violations. 375 * 376 * @param annotation the annotation token 377 */ 378 private void checkCompactStyle(final DetailAST annotation) { 379 final int valuePairCount = 380 annotation.getChildCount( 381 TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); 382 383 final DetailAST valuePair = 384 annotation.findFirstToken( 385 TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); 386 387 if (valuePairCount == 1 388 && ANNOTATION_ELEMENT_SINGLE_NAME.equals( 389 valuePair.getFirstChild().getText())) { 390 log(annotation.getLineNo(), MSG_KEY_ANNOTATION_INCORRECT_STYLE, 391 ElementStyle.COMPACT); 392 } 393 } 394 395 /** 396 * Checks for compact no array style type violations. 397 * 398 * @param annotation the annotation token 399 */ 400 private void checkCompactNoArrayStyle(final DetailAST annotation) { 401 final DetailAST arrayInit = 402 annotation.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT); 403 404 final int valuePairCount = 405 annotation.getChildCount(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); 406 407 //in compact style with one value 408 if (arrayInit != null 409 && arrayInit.getChildCount(TokenTypes.EXPR) == 1) { 410 log(annotation.getLineNo(), MSG_KEY_ANNOTATION_INCORRECT_STYLE, 411 ElementStyle.COMPACT_NO_ARRAY); 412 } 413 //in expanded style with one value and the correct element name 414 else if (valuePairCount == 1) { 415 final DetailAST valuePair = 416 annotation.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); 417 final DetailAST nestedArrayInit = 418 valuePair.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT); 419 420 if (nestedArrayInit != null 421 && ANNOTATION_ELEMENT_SINGLE_NAME.equals( 422 valuePair.getFirstChild().getText()) 423 && nestedArrayInit.getChildCount(TokenTypes.EXPR) == 1) { 424 log(annotation.getLineNo(), MSG_KEY_ANNOTATION_INCORRECT_STYLE, 425 ElementStyle.COMPACT_NO_ARRAY); 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 != TrailingArrayComma.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 == TrailingArrayComma.ALWAYS 471 && (comma == null || comma.getType() != TokenTypes.COMMA)) { 472 log(rCurly.getLineNo(), 473 rCurly.getColumnNo(), MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING); 474 } 475 else if (trailingArrayComma == TrailingArrayComma.NEVER 476 && comma != null && comma.getType() == TokenTypes.COMMA) { 477 log(comma.getLineNo(), 478 comma.getColumnNo(), MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT); 479 } 480 } 481 482 /** 483 * Checks to see if the closing parenthesis are present if required or 484 * prohibited. 485 * 486 * @param ast the annotation token 487 */ 488 private void checkCheckClosingParens(final DetailAST ast) { 489 if (closingParens != ClosingParens.IGNORE) { 490 final DetailAST paren = ast.getLastChild(); 491 final boolean parenExists = paren.getType() == TokenTypes.RPAREN; 492 493 if (closingParens == ClosingParens.ALWAYS 494 && !parenExists) { 495 log(ast.getLineNo(), MSG_KEY_ANNOTATION_PARENS_MISSING); 496 } 497 else if (closingParens == ClosingParens.NEVER 498 && parenExists 499 && !ast.branchContains(TokenTypes.EXPR) 500 && !ast.branchContains(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR) 501 && !ast.branchContains(TokenTypes.ANNOTATION_ARRAY_INIT)) { 502 log(ast.getLineNo(), MSG_KEY_ANNOTATION_PARENS_PRESENT); 503 } 504 } 505 } 506}