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 * Ctor that specifies the default for the format property 101 * as specified in the class javadocs. 102 */ 103 public SuppressWarningsCheck() 104 { 105 super("^$|^\\s+$"); 106 } 107 108 /** {@inheritDoc} */ 109 @Override 110 public final int[] getDefaultTokens() 111 { 112 return this.getAcceptableTokens(); 113 } 114 115 /** {@inheritDoc} */ 116 @Override 117 public final int[] getAcceptableTokens() 118 { 119 return new int[] { 120 TokenTypes.CLASS_DEF, 121 TokenTypes.INTERFACE_DEF, 122 TokenTypes.ENUM_DEF, 123 TokenTypes.ANNOTATION_DEF, 124 TokenTypes.ANNOTATION_FIELD_DEF, 125 TokenTypes.ENUM_CONSTANT_DEF, 126 TokenTypes.PARAMETER_DEF, 127 TokenTypes.VARIABLE_DEF, 128 TokenTypes.METHOD_DEF, 129 TokenTypes.CTOR_DEF, 130 }; 131 } 132 133 /** {@inheritDoc} */ 134 @Override 135 public void visitToken(final DetailAST aAST) 136 { 137 final DetailAST annotation = this.getSuppressWarnings(aAST); 138 139 if (annotation == null) { 140 return; 141 } 142 143 final DetailAST warningHolder = 144 this.findWarningsHolder(annotation); 145 146 DetailAST warning = warningHolder.findFirstToken(TokenTypes.EXPR); 147 148 //rare case with empty array ex: @SuppressWarnings({}) 149 if (warning == null) { 150 //check to see if empty warnings are forbidden -- are by default 151 this.logMatch(warningHolder.getLineNo(), 152 warningHolder.getColumnNo(), ""); 153 return; 154 } 155 156 while (warning != null) { 157 if (warning.getType() == TokenTypes.EXPR) { 158 final DetailAST fChild = warning.getFirstChild(); 159 160 //typical case 161 if (fChild.getType() == TokenTypes.STRING_LITERAL) { 162 final String warningText = 163 this.removeQuotes(warning.getFirstChild().getText()); 164 this.logMatch(warning.getLineNo(), 165 warning.getColumnNo(), warningText); 166 167 //conditional case 168 //ex: @SuppressWarnings((false) ? (true) ? "unchecked" : "foo" : "unused") 169 } 170 else if (fChild.getType() == TokenTypes.QUESTION) { 171 this.walkConditional(fChild); 172 } 173 else { 174 assert false : "Should never get here, type: " 175 + fChild.getType() + " text: " + fChild.getText(); 176 } 177 } 178 warning = warning.getNextSibling(); 179 } 180 } 181 182 /** 183 * Gets the {@link SuppressWarnings SuppressWarnings} annotation 184 * that is annotating the AST. If the annotation does not exist 185 * this method will return {@code null}. 186 * 187 * @param aAST the AST 188 * @return the {@link SuppressWarnings SuppressWarnings} annotation 189 */ 190 private DetailAST getSuppressWarnings(DetailAST aAST) 191 { 192 final DetailAST annotation = AnnotationUtility.getAnnotation( 193 aAST, SuppressWarningsCheck.SUPPRESS_WARNINGS); 194 195 return (annotation != null) ? annotation 196 : AnnotationUtility.getAnnotation( 197 aAST, SuppressWarningsCheck.FQ_SUPPRESS_WARNINGS); 198 } 199 200 /** 201 * This method looks for a warning that matches a configured expression. 202 * If found it logs a violation at the given line and column number. 203 * 204 * @param aLineNo the line number 205 * @param aColNum the column number 206 * @param aWarningText the warning. 207 */ 208 private void logMatch(final int aLineNo, 209 final int aColNum, final String aWarningText) 210 { 211 final Matcher matcher = this.getRegexp().matcher(aWarningText); 212 if (matcher.matches()) { 213 this.log(aLineNo, aColNum, 214 "suppressed.warning.not.allowed", aWarningText); 215 } 216 } 217 218 /** 219 * Find the parent (holder) of the of the warnings (Expr). 220 * 221 * @param aAnnotation the annotation 222 * @return a Token representing the expr. 223 */ 224 private DetailAST findWarningsHolder(final DetailAST aAnnotation) 225 { 226 final DetailAST annValuePair = 227 aAnnotation.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); 228 final DetailAST annArrayInit; 229 230 if (annValuePair != null) { 231 annArrayInit = 232 annValuePair.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT); 233 } 234 else { 235 annArrayInit = 236 aAnnotation.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT); 237 } 238 239 if (annArrayInit != null) { 240 return annArrayInit; 241 } 242 243 return aAnnotation; 244 } 245 246 /** 247 * Strips a single double quote from the front and back of a string. 248 * 249 * For example: 250 * <br/> 251 * Input String = "unchecked" 252 * <br/> 253 * Output String = unchecked 254 * 255 * @param aWarning the warning string 256 * @return the string without two quotes 257 */ 258 private String removeQuotes(final String aWarning) 259 { 260 assert aWarning != null : "the aWarning was null"; 261 assert aWarning.charAt(0) == '"'; 262 assert aWarning.charAt(aWarning.length() - 1) == '"'; 263 264 return aWarning.substring(1, aWarning.length() - 1); 265 } 266 267 /** 268 * Recursively walks a conditional expression checking the left 269 * and right sides, checking for matches and 270 * logging violations. 271 * 272 * @param aCond a Conditional type 273 * {@link TokenTypes#QUESTION QUESTION} 274 */ 275 private void walkConditional(final DetailAST aCond) 276 { 277 if (aCond.getType() != TokenTypes.QUESTION) { 278 final String warningText = 279 this.removeQuotes(aCond.getText()); 280 this.logMatch(aCond.getLineNo(), aCond.getColumnNo(), warningText); 281 return; 282 } 283 284 this.walkConditional(this.getCondLeft(aCond)); 285 this.walkConditional(this.getCondRight(aCond)); 286 } 287 288 /** 289 * Retrieves the left side of a conditional. 290 * 291 * @param aCond aCond a conditional type 292 * {@link TokenTypes#QUESTION QUESTION} 293 * @return either the value 294 * or another conditional 295 */ 296 private DetailAST getCondLeft(final DetailAST aCond) 297 { 298 final DetailAST colon = aCond.findFirstToken(TokenTypes.COLON); 299 return colon.getPreviousSibling(); 300 } 301 302 /** 303 * Retrieves the right side of a conditional. 304 * 305 * @param aCond a conditional type 306 * {@link TokenTypes#QUESTION QUESTION} 307 * @return either the value 308 * or another conditional 309 */ 310 private DetailAST getCondRight(final DetailAST aCond) 311 { 312 final DetailAST colon = aCond.findFirstToken(TokenTypes.COLON); 313 return colon.getNextSibling(); 314 } 315}