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