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