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