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