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.coding; 021 022import java.util.HashSet; 023import java.util.Locale; 024import java.util.Objects; 025import java.util.Set; 026import java.util.regex.Pattern; 027 028import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 029import com.puppycrawl.tools.checkstyle.api.DetailAST; 030import com.puppycrawl.tools.checkstyle.api.Scope; 031import com.puppycrawl.tools.checkstyle.api.TokenTypes; 032import com.puppycrawl.tools.checkstyle.utils.CheckUtils; 033import com.puppycrawl.tools.checkstyle.utils.ScopeUtils; 034 035/** 036 * Checks that a local variable or a parameter does not shadow 037 * a field that is defined in the same class. 038 * 039 * <p>An example of how to configure the check is: 040 * <pre> 041 * <module name="HiddenField"/> 042 * </pre> 043 * 044 * <p>An example of how to configure the check so that it checks variables but not 045 * parameters is: 046 * <pre> 047 * <module name="HiddenField"> 048 * <property name="tokens" value="VARIABLE_DEF"/> 049 * </module> 050 * </pre> 051 * 052 * <p>An example of how to configure the check so that it ignores the parameter of 053 * a setter method is: 054 * <pre> 055 * <module name="HiddenField"> 056 * <property name="ignoreSetter" value="true"/> 057 * </module> 058 * </pre> 059 * 060 * <p>A method is recognized as a setter if it is in the following form 061 * <pre> 062 * ${returnType} set${Name}(${anyType} ${name}) { ... } 063 * </pre> 064 * where ${anyType} is any primitive type, class or interface name; 065 * ${name} is name of the variable that is being set and ${Name} its 066 * capitalized form that appears in the method name. By default it is expected 067 * that setter returns void, i.e. ${returnType} is 'void'. For example 068 * <pre> 069 * void setTime(long time) { ... } 070 * </pre> 071 * Any other return types will not let method match a setter pattern. However, 072 * by setting <em>setterCanReturnItsClass</em> property to <em>true</em> 073 * definition of a setter is expanded, so that setter return type can also be 074 * a class in which setter is declared. For example 075 * <pre> 076 * class PageBuilder { 077 * PageBuilder setName(String name) { ... } 078 * } 079 * </pre> 080 * Such methods are known as chain-setters and a common when Builder-pattern 081 * is used. Property <em>setterCanReturnItsClass</em> has effect only if 082 * <em>ignoreSetter</em> is set to true. 083 * 084 * <p>An example of how to configure the check so that it ignores the parameter 085 * of either a setter that returns void or a chain-setter. 086 * <pre> 087 * <module name="HiddenField"> 088 * <property name="ignoreSetter" value="true"/> 089 * <property name="setterCanReturnItsClass" value="true"/> 090 * </module> 091 * </pre> 092 * 093 * <p>An example of how to configure the check so that it ignores constructor 094 * parameters is: 095 * <pre> 096 * <module name="HiddenField"> 097 * <property name="ignoreConstructorParameter" value="true"/> 098 * </module> 099 * </pre> 100 * 101 * <p>An example of how to configure the check so that it ignores variables and parameters 102 * named 'test': 103 * <pre> 104 * <module name="HiddenField"> 105 * <property name="ignoreFormat" value="^test$"/> 106 * </module> 107 * </pre> 108 * 109 * <pre> 110 * {@code 111 * class SomeClass 112 * { 113 * private List<String> test; 114 * 115 * private void addTest(List<String> test) // no violation 116 * { 117 * this.test.addAll(test); 118 * } 119 * 120 * private void foo() 121 * { 122 * final List<String> test = new ArrayList<>(); // no violation 123 * ... 124 * } 125 * } 126 * } 127 * </pre> 128 * 129 * @author Dmitri Priimak 130 */ 131public class HiddenFieldCheck 132 extends AbstractCheck { 133 /** 134 * A key is pointing to the warning message text in "messages.properties" 135 * file. 136 */ 137 public static final String MSG_KEY = "hidden.field"; 138 139 /** Stack of sets of field names, 140 * one for each class of a set of nested classes. 141 */ 142 private FieldFrame frame; 143 144 /** Pattern for names of variables and parameters to ignore. */ 145 private Pattern ignoreFormat; 146 147 /** Controls whether to check the parameter of a property setter method. */ 148 private boolean ignoreSetter; 149 150 /** 151 * If ignoreSetter is set to true then this variable controls what 152 * the setter method can return By default setter must return void. 153 * However, is this variable is set to true then setter can also 154 * return class in which is declared. 155 */ 156 private boolean setterCanReturnItsClass; 157 158 /** Controls whether to check the parameter of a constructor. */ 159 private boolean ignoreConstructorParameter; 160 161 /** Controls whether to check the parameter of abstract methods. */ 162 private boolean ignoreAbstractMethods; 163 164 @Override 165 public int[] getDefaultTokens() { 166 return getAcceptableTokens(); 167 } 168 169 @Override 170 public int[] getAcceptableTokens() { 171 return new int[] { 172 TokenTypes.VARIABLE_DEF, 173 TokenTypes.PARAMETER_DEF, 174 TokenTypes.CLASS_DEF, 175 TokenTypes.ENUM_DEF, 176 TokenTypes.ENUM_CONSTANT_DEF, 177 TokenTypes.LAMBDA, 178 }; 179 } 180 181 @Override 182 public int[] getRequiredTokens() { 183 return new int[] { 184 TokenTypes.CLASS_DEF, 185 TokenTypes.ENUM_DEF, 186 TokenTypes.ENUM_CONSTANT_DEF, 187 }; 188 } 189 190 @Override 191 public void beginTree(DetailAST rootAST) { 192 frame = new FieldFrame(null, true, null); 193 } 194 195 @Override 196 public void visitToken(DetailAST ast) { 197 final int type = ast.getType(); 198 switch (type) { 199 case TokenTypes.VARIABLE_DEF: 200 case TokenTypes.PARAMETER_DEF: 201 processVariable(ast); 202 break; 203 case TokenTypes.LAMBDA: 204 processLambda(ast); 205 break; 206 default: 207 visitOtherTokens(ast, type); 208 } 209 } 210 211 /** 212 * Process a lambda token. 213 * Checks whether a lambda parameter shadows a field. 214 * Note, that when parameter of lambda expression is untyped, 215 * ANTLR parses the parameter as an identifier. 216 * @param ast the lambda token. 217 */ 218 private void processLambda(DetailAST ast) { 219 final DetailAST firstChild = ast.getFirstChild(); 220 if (firstChild.getType() == TokenTypes.IDENT) { 221 final String untypedLambdaParameterName = firstChild.getText(); 222 if (frame.containsStaticField(untypedLambdaParameterName) 223 || isInstanceField(firstChild, untypedLambdaParameterName)) { 224 log(firstChild, MSG_KEY, untypedLambdaParameterName); 225 } 226 } 227 else { 228 // Type of lambda parameter is not omitted. 229 processVariable(ast); 230 } 231 } 232 233 /** 234 * Called to process tokens other than {@link TokenTypes#VARIABLE_DEF} 235 * and {@link TokenTypes#PARAMETER_DEF}. 236 * 237 * @param ast token to process 238 * @param type type of the token 239 */ 240 private void visitOtherTokens(DetailAST ast, int type) { 241 //A more thorough check of enum constant class bodies is 242 //possible (checking for hidden fields against the enum 243 //class body in addition to enum constant class bodies) 244 //but not attempted as it seems out of the scope of this 245 //check. 246 final DetailAST typeMods = ast.findFirstToken(TokenTypes.MODIFIERS); 247 final boolean isStaticInnerType = 248 typeMods != null 249 && typeMods.branchContains(TokenTypes.LITERAL_STATIC); 250 final String frameName; 251 252 if (type == TokenTypes.CLASS_DEF || type == TokenTypes.ENUM_DEF) { 253 frameName = ast.findFirstToken(TokenTypes.IDENT).getText(); 254 } 255 else { 256 frameName = null; 257 } 258 final FieldFrame newFrame = new FieldFrame(frame, isStaticInnerType, frameName); 259 260 //add fields to container 261 final DetailAST objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK); 262 // enum constants may not have bodies 263 if (objBlock != null) { 264 DetailAST child = objBlock.getFirstChild(); 265 while (child != null) { 266 if (child.getType() == TokenTypes.VARIABLE_DEF) { 267 final String name = 268 child.findFirstToken(TokenTypes.IDENT).getText(); 269 final DetailAST mods = 270 child.findFirstToken(TokenTypes.MODIFIERS); 271 if (mods.branchContains(TokenTypes.LITERAL_STATIC)) { 272 newFrame.addStaticField(name); 273 } 274 else { 275 newFrame.addInstanceField(name); 276 } 277 } 278 child = child.getNextSibling(); 279 } 280 } 281 // push container 282 frame = newFrame; 283 } 284 285 @Override 286 public void leaveToken(DetailAST ast) { 287 if (ast.getType() == TokenTypes.CLASS_DEF 288 || ast.getType() == TokenTypes.ENUM_DEF 289 || ast.getType() == TokenTypes.ENUM_CONSTANT_DEF) { 290 //pop 291 frame = frame.getParent(); 292 } 293 } 294 295 /** 296 * Process a variable token. 297 * Check whether a local variable or parameter shadows a field. 298 * Store a field for later comparison with local variables and parameters. 299 * @param ast the variable token. 300 */ 301 private void processVariable(DetailAST ast) { 302 if (!ScopeUtils.isInInterfaceOrAnnotationBlock(ast) 303 && !CheckUtils.isReceiverParameter(ast) 304 && (ScopeUtils.isLocalVariableDef(ast) 305 || ast.getType() == TokenTypes.PARAMETER_DEF)) { 306 // local variable or parameter. Does it shadow a field? 307 final DetailAST nameAST = ast.findFirstToken(TokenTypes.IDENT); 308 final String name = nameAST.getText(); 309 310 if ((frame.containsStaticField(name) || isInstanceField(ast, name)) 311 && !isMatchingRegexp(name) 312 && !isIgnoredParam(ast, name)) { 313 log(nameAST, MSG_KEY, name); 314 } 315 } 316 } 317 318 /** 319 * Checks whether method or constructor parameter is ignored. 320 * @param ast the parameter token. 321 * @param name the parameter name. 322 * @return true if parameter is ignored. 323 */ 324 private boolean isIgnoredParam(DetailAST ast, String name) { 325 return isIgnoredSetterParam(ast, name) 326 || isIgnoredConstructorParam(ast) 327 || isIgnoredParamOfAbstractMethod(ast); 328 } 329 330 /** 331 * Check for instance field. 332 * @param ast token 333 * @param name identifier of token 334 * @return true if instance field 335 */ 336 private boolean isInstanceField(DetailAST ast, String name) { 337 return !isInStatic(ast) && frame.containsInstanceField(name); 338 } 339 340 /** 341 * Check name by regExp. 342 * @param name string value to check 343 * @return true is regexp is matching 344 */ 345 private boolean isMatchingRegexp(String name) { 346 return ignoreFormat != null && ignoreFormat.matcher(name).find(); 347 } 348 349 /** 350 * Determines whether an AST node is in a static method or static 351 * initializer. 352 * @param ast the node to check. 353 * @return true if ast is in a static method or a static block; 354 */ 355 private static boolean isInStatic(DetailAST ast) { 356 DetailAST parent = ast.getParent(); 357 boolean inStatic = false; 358 359 while (parent != null && !inStatic) { 360 if (parent.getType() == TokenTypes.STATIC_INIT) { 361 inStatic = true; 362 } 363 else if (parent.getType() == TokenTypes.METHOD_DEF 364 && !ScopeUtils.isInScope(parent, Scope.ANONINNER) 365 || parent.getType() == TokenTypes.VARIABLE_DEF) { 366 final DetailAST mods = 367 parent.findFirstToken(TokenTypes.MODIFIERS); 368 inStatic = mods.branchContains(TokenTypes.LITERAL_STATIC); 369 break; 370 } 371 else { 372 parent = parent.getParent(); 373 } 374 } 375 return inStatic; 376 } 377 378 /** 379 * Decides whether to ignore an AST node that is the parameter of a 380 * setter method, where the property setter method for field 'xyz' has 381 * name 'setXyz', one parameter named 'xyz', and return type void 382 * (default behavior) or return type is name of the class in which 383 * such method is declared (allowed only if 384 * {@link #setSetterCanReturnItsClass(boolean)} is called with 385 * value <em>true</em>). 386 * 387 * @param ast the AST to check. 388 * @param name the name of ast. 389 * @return true if ast should be ignored because check property 390 * ignoreSetter is true and ast is the parameter of a setter method. 391 */ 392 private boolean isIgnoredSetterParam(DetailAST ast, String name) { 393 boolean isIgnoredSetterParam = false; 394 if (ignoreSetter && ast.getType() == TokenTypes.PARAMETER_DEF) { 395 final DetailAST parametersAST = ast.getParent(); 396 final DetailAST methodAST = parametersAST.getParent(); 397 if (parametersAST.getChildCount() == 1 398 && methodAST.getType() == TokenTypes.METHOD_DEF 399 && isSetterMethod(methodAST, name)) { 400 isIgnoredSetterParam = true; 401 } 402 } 403 return isIgnoredSetterParam; 404 } 405 406 /** 407 * Determine if a specific method identified by methodAST and a single 408 * variable name aName is a setter. This recognition partially depends 409 * on mSetterCanReturnItsClass property. 410 * 411 * @param aMethodAST AST corresponding to a method call 412 * @param aName name of single parameter of this method. 413 * @return true of false indicating of method is a setter or not. 414 */ 415 private boolean isSetterMethod(DetailAST aMethodAST, String aName) { 416 final String methodName = 417 aMethodAST.findFirstToken(TokenTypes.IDENT).getText(); 418 boolean isSetterMethod = false; 419 420 if (("set" + capitalize(aName)).equals(methodName)) { 421 // method name did match set${Name}(${anyType} ${aName}) 422 // where ${Name} is capitalized version of ${aName} 423 // therefore this method is potentially a setter 424 final DetailAST typeAST = aMethodAST.findFirstToken(TokenTypes.TYPE); 425 final String returnType = typeAST.getFirstChild().getText(); 426 if (typeAST.branchContains(TokenTypes.LITERAL_VOID) 427 || setterCanReturnItsClass && frame.isEmbeddedIn(returnType)) { 428 // this method has signature 429 // 430 // void set${Name}(${anyType} ${name}) 431 // 432 // and therefore considered to be a setter 433 // 434 // or 435 // 436 // return type is not void, but it is the same as the class 437 // where method is declared and and mSetterCanReturnItsClass 438 // is set to true 439 isSetterMethod = true; 440 } 441 } 442 443 return isSetterMethod; 444 } 445 446 /** 447 * Capitalizes a given property name the way we expect to see it in 448 * a setter name. 449 * @param name a property name 450 * @return capitalized property name 451 */ 452 private static String capitalize(final String name) { 453 String setterName = name; 454 // we should not capitalize the first character if the second 455 // one is a capital one, since according to JavaBeans spec 456 // setXYzz() is a setter for XYzz property, not for xYzz one. 457 if (name.length() == 1 || !Character.isUpperCase(name.charAt(1))) { 458 setterName = name.substring(0, 1).toUpperCase(Locale.ENGLISH) + name.substring(1); 459 } 460 return setterName; 461 } 462 463 /** 464 * Decides whether to ignore an AST node that is the parameter of a 465 * constructor. 466 * @param ast the AST to check. 467 * @return true if ast should be ignored because check property 468 * ignoreConstructorParameter is true and ast is a constructor parameter. 469 */ 470 private boolean isIgnoredConstructorParam(DetailAST ast) { 471 boolean result = false; 472 if (ignoreConstructorParameter 473 && ast.getType() == TokenTypes.PARAMETER_DEF) { 474 final DetailAST parametersAST = ast.getParent(); 475 final DetailAST constructorAST = parametersAST.getParent(); 476 result = constructorAST.getType() == TokenTypes.CTOR_DEF; 477 } 478 return result; 479 } 480 481 /** 482 * Decides whether to ignore an AST node that is the parameter of an 483 * abstract method. 484 * @param ast the AST to check. 485 * @return true if ast should be ignored because check property 486 * ignoreAbstractMethods is true and ast is a parameter of abstract methods. 487 */ 488 private boolean isIgnoredParamOfAbstractMethod(DetailAST ast) { 489 boolean result = false; 490 if (ignoreAbstractMethods 491 && ast.getType() == TokenTypes.PARAMETER_DEF) { 492 final DetailAST method = ast.getParent().getParent(); 493 if (method.getType() == TokenTypes.METHOD_DEF) { 494 final DetailAST mods = method.findFirstToken(TokenTypes.MODIFIERS); 495 result = mods.branchContains(TokenTypes.ABSTRACT); 496 } 497 } 498 return result; 499 } 500 501 /** 502 * Set the ignore format for the specified regular expression. 503 * @param pattern a pattern. 504 */ 505 public void setIgnoreFormat(Pattern pattern) { 506 ignoreFormat = pattern; 507 } 508 509 /** 510 * Set whether to ignore the parameter of a property setter method. 511 * @param ignoreSetter decide whether to ignore the parameter of 512 * a property setter method. 513 */ 514 public void setIgnoreSetter(boolean ignoreSetter) { 515 this.ignoreSetter = ignoreSetter; 516 } 517 518 /** 519 * Controls if setter can return only void (default behavior) or it 520 * can also return class in which it is declared. 521 * 522 * @param aSetterCanReturnItsClass if true then setter can return 523 * either void or class in which it is declared. If false then 524 * in order to be recognized as setter method (otherwise 525 * already recognized as a setter) must return void. Later is 526 * the default behavior. 527 */ 528 public void setSetterCanReturnItsClass( 529 boolean aSetterCanReturnItsClass) { 530 setterCanReturnItsClass = aSetterCanReturnItsClass; 531 } 532 533 /** 534 * Set whether to ignore constructor parameters. 535 * @param ignoreConstructorParameter decide whether to ignore 536 * constructor parameters. 537 */ 538 public void setIgnoreConstructorParameter( 539 boolean ignoreConstructorParameter) { 540 this.ignoreConstructorParameter = ignoreConstructorParameter; 541 } 542 543 /** 544 * Set whether to ignore parameters of abstract methods. 545 * @param ignoreAbstractMethods decide whether to ignore 546 * parameters of abstract methods. 547 */ 548 public void setIgnoreAbstractMethods( 549 boolean ignoreAbstractMethods) { 550 this.ignoreAbstractMethods = ignoreAbstractMethods; 551 } 552 553 /** 554 * Holds the names of static and instance fields of a type. 555 * @author Rick Giles 556 */ 557 private static class FieldFrame { 558 /** Name of the frame, such name of the class or enum declaration. */ 559 private final String frameName; 560 561 /** Is this a static inner type. */ 562 private final boolean staticType; 563 564 /** Parent frame. */ 565 private final FieldFrame parent; 566 567 /** Set of instance field names. */ 568 private final Set<String> instanceFields = new HashSet<>(); 569 570 /** Set of static field names. */ 571 private final Set<String> staticFields = new HashSet<>(); 572 573 /** 574 * Creates new frame. 575 * @param parent parent frame. 576 * @param staticType is this a static inner type (class or enum). 577 * @param frameName name associated with the frame, which can be a 578 */ 579 FieldFrame(FieldFrame parent, boolean staticType, String frameName) { 580 this.parent = parent; 581 this.staticType = staticType; 582 this.frameName = frameName; 583 } 584 585 /** 586 * Adds an instance field to this FieldFrame. 587 * @param field the name of the instance field. 588 */ 589 public void addInstanceField(String field) { 590 instanceFields.add(field); 591 } 592 593 /** 594 * Adds a static field to this FieldFrame. 595 * @param field the name of the instance field. 596 */ 597 public void addStaticField(String field) { 598 staticFields.add(field); 599 } 600 601 /** 602 * Determines whether this FieldFrame contains an instance field. 603 * @param field the field to check. 604 * @return true if this FieldFrame contains instance field field. 605 */ 606 public boolean containsInstanceField(String field) { 607 return instanceFields.contains(field) 608 || parent != null 609 && !staticType 610 && parent.containsInstanceField(field); 611 612 } 613 614 /** 615 * Determines whether this FieldFrame contains a static field. 616 * @param field the field to check. 617 * @return true if this FieldFrame contains static field field. 618 */ 619 public boolean containsStaticField(String field) { 620 return staticFields.contains(field) 621 || parent != null 622 && parent.containsStaticField(field); 623 } 624 625 /** 626 * Getter for parent frame. 627 * @return parent frame. 628 */ 629 public FieldFrame getParent() { 630 return parent; 631 } 632 633 /** 634 * Check if current frame is embedded in class or enum with 635 * specific name. 636 * 637 * @param classOrEnumName name of class or enum that we are looking 638 * for in the chain of field frames. 639 * 640 * @return true if current frame is embedded in class or enum 641 * with name classOrNameName 642 */ 643 private boolean isEmbeddedIn(String classOrEnumName) { 644 FieldFrame currentFrame = this; 645 boolean isEmbeddedIn = false; 646 while (currentFrame != null) { 647 if (Objects.equals(currentFrame.frameName, classOrEnumName)) { 648 isEmbeddedIn = true; 649 break; 650 } 651 currentFrame = currentFrame.parent; 652 } 653 return isEmbeddedIn; 654 } 655 } 656}