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