001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2016 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.regex.Matcher; 023import java.util.regex.Pattern; 024 025import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 026import com.puppycrawl.tools.checkstyle.api.DetailAST; 027import com.puppycrawl.tools.checkstyle.api.TokenTypes; 028import com.puppycrawl.tools.checkstyle.utils.AnnotationUtility; 029import com.puppycrawl.tools.checkstyle.utils.CommonUtils; 030 031/** 032 * <p> 033 * This check allows you to specify what warnings that 034 * {@link SuppressWarnings SuppressWarnings} is not 035 * allowed to suppress. You can also specify a list 036 * of TokenTypes that the configured warning(s) cannot 037 * be suppressed on. 038 * </p> 039 * 040 * <p> 041 * The {@link #setFormat warnings} property is a 042 * regex pattern. Any warning being suppressed matching 043 * this pattern will be flagged. 044 * </p> 045 * 046 * <p> 047 * By default, any warning specified will be disallowed on 048 * all legal TokenTypes unless otherwise specified via 049 * the 050 * {@link AbstractCheck#setTokens(String[]) tokens} 051 * property. 052 * 053 * Also, by default warnings that are empty strings or all 054 * whitespace (regex: ^$|^\s+$) are flagged. By specifying, 055 * the format property these defaults no longer apply. 056 * </p> 057 * 058 * <p>Limitations: This check does not consider conditionals 059 * inside the SuppressWarnings annotation. <br> 060 * For example: 061 * {@code @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused")}. 062 * According to the above example, the "unused" warning is being suppressed 063 * not the "unchecked" or "foo" warnings. All of these warnings will be 064 * considered and matched against regardless of what the conditional 065 * evaluates to. 066 * <br> 067 * The check also does not support code like {@code @SuppressWarnings("un" + "used")}, 068 * {@code @SuppressWarnings((String) "unused")} or 069 * {@code @SuppressWarnings({('u' + (char)'n') + (""+("used" + (String)"")),})}. 070 * </p> 071 * 072 * <p>This check can be configured so that the "unchecked" 073 * and "unused" warnings cannot be suppressed on 074 * anything but variable and parameter declarations. 075 * See below of an example. 076 * </p> 077 * 078 * <pre> 079 * <module name="SuppressWarnings"> 080 * <property name="format" 081 * value="^unchecked$|^unused$"/> 082 * <property name="tokens" 083 * value=" 084 * CLASS_DEF,INTERFACE_DEF,ENUM_DEF, 085 * ANNOTATION_DEF,ANNOTATION_FIELD_DEF, 086 * ENUM_CONSTANT_DEF,METHOD_DEF,CTOR_DEF 087 * "/> 088 * </module> 089 * </pre> 090 * @author Travis Schneeberger 091 */ 092public class SuppressWarningsCheck extends AbstractCheck { 093 /** 094 * A key is pointing to the warning message text in "messages.properties" 095 * file. 096 */ 097 public static final String MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED = 098 "suppressed.warning.not.allowed"; 099 100 /** {@link SuppressWarnings SuppressWarnings} annotation name. */ 101 private static final String SUPPRESS_WARNINGS = "SuppressWarnings"; 102 103 /** 104 * Fully-qualified {@link SuppressWarnings SuppressWarnings} 105 * annotation name. 106 */ 107 private static final String FQ_SUPPRESS_WARNINGS = 108 "java.lang." + SUPPRESS_WARNINGS; 109 110 /** The format string of the regexp. */ 111 private String format = "^$|^\\s+$"; 112 113 /** The regexp to match against. */ 114 private Pattern regexp = Pattern.compile(format); 115 116 /** 117 * Set the format to the specified regular expression. 118 * @param format a {@code String} value 119 * @throws org.apache.commons.beanutils.ConversionException unable to parse format 120 */ 121 public final void setFormat(String format) { 122 this.format = format; 123 regexp = CommonUtils.createPattern(format); 124 } 125 126 @Override 127 public final int[] getDefaultTokens() { 128 return getAcceptableTokens(); 129 } 130 131 @Override 132 public final int[] getAcceptableTokens() { 133 return new int[] { 134 TokenTypes.CLASS_DEF, 135 TokenTypes.INTERFACE_DEF, 136 TokenTypes.ENUM_DEF, 137 TokenTypes.ANNOTATION_DEF, 138 TokenTypes.ANNOTATION_FIELD_DEF, 139 TokenTypes.ENUM_CONSTANT_DEF, 140 TokenTypes.PARAMETER_DEF, 141 TokenTypes.VARIABLE_DEF, 142 TokenTypes.METHOD_DEF, 143 TokenTypes.CTOR_DEF, 144 }; 145 } 146 147 @Override 148 public int[] getRequiredTokens() { 149 return CommonUtils.EMPTY_INT_ARRAY; 150 } 151 152 @Override 153 public void visitToken(final DetailAST ast) { 154 final DetailAST annotation = getSuppressWarnings(ast); 155 156 if (annotation != null) { 157 final DetailAST warningHolder = 158 findWarningsHolder(annotation); 159 160 final DetailAST token = 161 warningHolder.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); 162 DetailAST warning; 163 164 if (token == null) { 165 warning = warningHolder.findFirstToken(TokenTypes.EXPR); 166 } 167 else { 168 // case like '@SuppressWarnings(value = UNUSED)' 169 warning = token.findFirstToken(TokenTypes.EXPR); 170 } 171 172 //rare case with empty array ex: @SuppressWarnings({}) 173 if (warning == null) { 174 //check to see if empty warnings are forbidden -- are by default 175 logMatch(warningHolder.getLineNo(), 176 warningHolder.getColumnNo(), ""); 177 } 178 else { 179 while (warning != null) { 180 if (warning.getType() == TokenTypes.EXPR) { 181 final DetailAST fChild = warning.getFirstChild(); 182 switch (fChild.getType()) { 183 //typical case 184 case TokenTypes.STRING_LITERAL: 185 final String warningText = 186 removeQuotes(warning.getFirstChild().getText()); 187 logMatch(warning.getLineNo(), 188 warning.getColumnNo(), warningText); 189 break; 190 // conditional case 191 // ex: 192 // @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused") 193 case TokenTypes.QUESTION: 194 walkConditional(fChild); 195 break; 196 // param in constant case 197 // ex: public static final String UNCHECKED = "unchecked"; 198 // @SuppressWarnings(UNCHECKED) 199 // or 200 // @SuppressWarnings(SomeClass.UNCHECKED) 201 case TokenTypes.IDENT: 202 case TokenTypes.DOT: 203 break; 204 default: 205 // Known limitation: cases like @SuppressWarnings("un" + "used") or 206 // @SuppressWarnings((String) "unused") are not properly supported, 207 // but they should not cause exceptions. 208 } 209 } 210 warning = warning.getNextSibling(); 211 } 212 } 213 } 214 } 215 216 /** 217 * Gets the {@link SuppressWarnings SuppressWarnings} annotation 218 * that is annotating the AST. If the annotation does not exist 219 * this method will return {@code null}. 220 * 221 * @param ast the AST 222 * @return the {@link SuppressWarnings SuppressWarnings} annotation 223 */ 224 private static DetailAST getSuppressWarnings(DetailAST ast) { 225 final DetailAST annotation = AnnotationUtility.getAnnotation( 226 ast, SUPPRESS_WARNINGS); 227 228 if (annotation == null) { 229 return AnnotationUtility.getAnnotation(ast, FQ_SUPPRESS_WARNINGS); 230 } 231 else { 232 return annotation; 233 } 234 } 235 236 /** 237 * This method looks for a warning that matches a configured expression. 238 * If found it logs a violation at the given line and column number. 239 * 240 * @param lineNo the line number 241 * @param colNum the column number 242 * @param warningText the warning. 243 */ 244 private void logMatch(final int lineNo, 245 final int colNum, final String warningText) { 246 final Matcher matcher = regexp.matcher(warningText); 247 if (matcher.matches()) { 248 log(lineNo, colNum, 249 MSG_KEY_SUPPRESSED_WARNING_NOT_ALLOWED, warningText); 250 } 251 } 252 253 /** 254 * Find the parent (holder) of the of the warnings (Expr). 255 * 256 * @param annotation the annotation 257 * @return a Token representing the expr. 258 */ 259 private static DetailAST findWarningsHolder(final DetailAST annotation) { 260 final DetailAST annValuePair = 261 annotation.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); 262 final DetailAST annArrayInit; 263 264 if (annValuePair == null) { 265 annArrayInit = 266 annotation.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT); 267 } 268 else { 269 annArrayInit = 270 annValuePair.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT); 271 } 272 273 if (annArrayInit != null) { 274 return annArrayInit; 275 } 276 277 return annotation; 278 } 279 280 /** 281 * Strips a single double quote from the front and back of a string. 282 * 283 * <p>For example: 284 * <br/> 285 * Input String = "unchecked" 286 * <br/> 287 * Output String = unchecked 288 * 289 * @param warning the warning string 290 * @return the string without two quotes 291 */ 292 private static String removeQuotes(final String warning) { 293 return warning.substring(1, warning.length() - 1); 294 } 295 296 /** 297 * Recursively walks a conditional expression checking the left 298 * and right sides, checking for matches and 299 * logging violations. 300 * 301 * @param cond a Conditional type 302 * {@link TokenTypes#QUESTION QUESTION} 303 */ 304 private void walkConditional(final DetailAST cond) { 305 if (cond.getType() == TokenTypes.QUESTION) { 306 walkConditional(getCondLeft(cond)); 307 walkConditional(getCondRight(cond)); 308 } 309 else { 310 final String warningText = 311 removeQuotes(cond.getText()); 312 logMatch(cond.getLineNo(), cond.getColumnNo(), warningText); 313 } 314 } 315 316 /** 317 * Retrieves the left side of a conditional. 318 * 319 * @param cond cond a conditional type 320 * {@link TokenTypes#QUESTION QUESTION} 321 * @return either the value 322 * or another conditional 323 */ 324 private static DetailAST getCondLeft(final DetailAST cond) { 325 final DetailAST colon = cond.findFirstToken(TokenTypes.COLON); 326 return colon.getPreviousSibling(); 327 } 328 329 /** 330 * Retrieves the right side of a conditional. 331 * 332 * @param cond a conditional type 333 * {@link TokenTypes#QUESTION QUESTION} 334 * @return either the value 335 * or another conditional 336 */ 337 private static DetailAST getCondRight(final DetailAST cond) { 338 final DetailAST colon = cond.findFirstToken(TokenTypes.COLON); 339 return colon.getNextSibling(); 340 } 341}