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