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 @Override 124 public int[] getDefaultTokens() { 125 return getAcceptableTokens(); 126 } 127 128 @Override 129 public int[] getAcceptableTokens() { 130 return new int[] { 131 TokenTypes.VARIABLE_DEF, 132 TokenTypes.PARAMETER_DEF, 133 TokenTypes.CLASS_DEF, 134 TokenTypes.ENUM_DEF, 135 TokenTypes.ENUM_CONSTANT_DEF, 136 TokenTypes.PATTERN_VARIABLE_DEF, 137 TokenTypes.LAMBDA, 138 TokenTypes.RECORD_DEF, 139 TokenTypes.RECORD_COMPONENT_DEF, 140 TokenTypes.COMPACT_COMPILATION_UNIT, 141 }; 142 } 143 144 @Override 145 public int[] getRequiredTokens() { 146 return new int[] { 147 TokenTypes.CLASS_DEF, 148 TokenTypes.ENUM_DEF, 149 TokenTypes.ENUM_CONSTANT_DEF, 150 TokenTypes.RECORD_DEF, 151 TokenTypes.COMPACT_COMPILATION_UNIT, 152 }; 153 } 154 155 @Override 156 public void beginTree(DetailAST rootAST) { 157 frame = new FieldFrame(null, true, null); 158 } 159 160 @Override 161 public void visitToken(DetailAST ast) { 162 final int type = ast.getType(); 163 switch (type) { 164 case TokenTypes.VARIABLE_DEF, 165 TokenTypes.PARAMETER_DEF, 166 TokenTypes.PATTERN_VARIABLE_DEF, 167 TokenTypes.RECORD_COMPONENT_DEF -> processVariable(ast); 168 case TokenTypes.LAMBDA -> processLambda(ast); 169 default -> visitOtherTokens(ast, type); 170 } 171 } 172 173 /** 174 * Process a lambda token. 175 * Checks whether a lambda parameter shadows a field. 176 * Note, that when parameter of lambda expression is untyped, 177 * ANTLR parses the parameter as an identifier. 178 * 179 * @param ast the lambda token. 180 */ 181 private void processLambda(DetailAST ast) { 182 final DetailAST firstChild = ast.getFirstChild(); 183 if (TokenUtil.isOfType(firstChild, TokenTypes.IDENT)) { 184 final String untypedLambdaParameterName = firstChild.getText(); 185 if (frame.containsStaticField(untypedLambdaParameterName) 186 || isInstanceField(firstChild, untypedLambdaParameterName)) { 187 log(firstChild, MSG_KEY, untypedLambdaParameterName); 188 } 189 } 190 } 191 192 /** 193 * Called to process tokens other than {@link TokenTypes#VARIABLE_DEF} 194 * and {@link TokenTypes#PARAMETER_DEF}. 195 * 196 * @param ast token to process 197 * @param type type of the token 198 */ 199 private void visitOtherTokens(DetailAST ast, int type) { 200 // A more thorough check of enum constant class bodies is 201 // possible (checking for hidden fields against the enum 202 // class body in addition to enum constant class bodies) 203 // but not attempted as it seems out of the scope of this 204 // check. 205 final DetailAST typeMods = ast.findFirstToken(TokenTypes.MODIFIERS); 206 final boolean isStaticInnerType = 207 typeMods != null 208 && typeMods.findFirstToken(TokenTypes.LITERAL_STATIC) != null 209 // inner record is implicitly static 210 || ast.getType() == TokenTypes.RECORD_DEF; 211 final String frameName; 212 213 if (type == TokenTypes.CLASS_DEF 214 || type == TokenTypes.ENUM_DEF) { 215 frameName = ast.findFirstToken(TokenTypes.IDENT).getText(); 216 } 217 else { 218 frameName = null; 219 } 220 final FieldFrame newFrame = new FieldFrame(frame, isStaticInnerType, frameName); 221 222 // add fields to container 223 final DetailAST objBlock = getFieldContainer(ast); 224 // enum constants may not have bodies 225 if (objBlock != null) { 226 DetailAST child = objBlock.getFirstChild(); 227 while (child != null) { 228 if (child.getType() == TokenTypes.VARIABLE_DEF) { 229 final String name = 230 child.findFirstToken(TokenTypes.IDENT).getText(); 231 final DetailAST mods = 232 child.findFirstToken(TokenTypes.MODIFIERS); 233 if (mods.findFirstToken(TokenTypes.LITERAL_STATIC) == null) { 234 newFrame.addInstanceField(name); 235 } 236 else { 237 newFrame.addStaticField(name); 238 } 239 } 240 child = child.getNextSibling(); 241 } 242 } 243 if (ast.getType() == TokenTypes.RECORD_DEF) { 244 final DetailAST recordComponents = 245 ast.findFirstToken(TokenTypes.RECORD_COMPONENTS); 246 247 // For each record component definition, we will add it to this frame. 248 TokenUtil.forEachChild(recordComponents, 249 TokenTypes.RECORD_COMPONENT_DEF, node -> { 250 final String name = node.findFirstToken(TokenTypes.IDENT).getText(); 251 newFrame.addInstanceField(name); 252 }); 253 } 254 // push container 255 frame = newFrame; 256 } 257 258 /** 259 * Gets the member container for field declaration harvesting. 260 * 261 * @param ast the type definition node. 262 * @return the member container, either the compact compilation unit 263 * itself or the OBJBLOCK child of a standard type definition. 264 */ 265 @Nullable 266 private static DetailAST getFieldContainer(DetailAST ast) { 267 final DetailAST result; 268 if (ast.getType() == TokenTypes.COMPACT_COMPILATION_UNIT) { 269 result = ast; 270 } 271 else { 272 result = ast.findFirstToken(TokenTypes.OBJBLOCK); 273 } 274 return result; 275 } 276 277 @Override 278 public void leaveToken(DetailAST ast) { 279 if (ast.getType() == TokenTypes.CLASS_DEF 280 || ast.getType() == TokenTypes.ENUM_DEF 281 || ast.getType() == TokenTypes.ENUM_CONSTANT_DEF 282 || ast.getType() == TokenTypes.RECORD_DEF) { 283 // pop 284 frame = frame.getParent(); 285 } 286 } 287 288 /** 289 * Process a variable token. 290 * Check whether a local variable or parameter shadows a field. 291 * Store a field for later comparison with local variables and parameters. 292 * 293 * @param ast the variable token. 294 */ 295 private void processVariable(DetailAST ast) { 296 if (!ScopeUtil.isInInterfaceOrAnnotationBlock(ast) 297 && !CheckUtil.isReceiverParameter(ast) 298 && (ScopeUtil.isLocalVariableDef(ast) 299 || ast.getType() == TokenTypes.PARAMETER_DEF 300 || ast.getType() == TokenTypes.PATTERN_VARIABLE_DEF)) { 301 // local variable or parameter. Does it shadow a field? 302 final DetailAST nameAST = ast.findFirstToken(TokenTypes.IDENT); 303 final String name = nameAST.getText(); 304 305 if ((frame.containsStaticField(name) || isInstanceField(ast, name)) 306 && !isMatchingRegexp(name) 307 && !isIgnoredParam(ast, name)) { 308 log(nameAST, MSG_KEY, name); 309 } 310 } 311 } 312 313 /** 314 * Checks whether method or constructor parameter is ignored. 315 * 316 * @param ast the parameter token. 317 * @param name the parameter name. 318 * @return true if parameter is ignored. 319 */ 320 private boolean isIgnoredParam(DetailAST ast, String name) { 321 return isIgnoredSetterParam(ast, name) 322 || isIgnoredConstructorParam(ast) 323 || isIgnoredParamOfAbstractMethod(ast); 324 } 325 326 /** 327 * Check for instance field. 328 * 329 * @param ast token 330 * @param name identifier of token 331 * @return true if instance field 332 */ 333 private boolean isInstanceField(DetailAST ast, String name) { 334 return !isInStatic(ast) && frame.containsInstanceField(name); 335 } 336 337 /** 338 * Check name by regExp. 339 * 340 * @param name string value to check 341 * @return true is regexp is matching 342 */ 343 private boolean isMatchingRegexp(String name) { 344 return ignoreFormat != null && ignoreFormat.matcher(name).find(); 345 } 346 347 /** 348 * Determines whether an AST node is in a static method or static 349 * initializer. 350 * 351 * @param ast the node to check. 352 * @return true if ast is in a static method or a static block; 353 */ 354 private static boolean isInStatic(DetailAST ast) { 355 DetailAST parent = ast.getParent(); 356 boolean inStatic = false; 357 358 while (parent != null && !inStatic) { 359 if (parent.getType() == TokenTypes.STATIC_INIT) { 360 inStatic = true; 361 } 362 else if (parent.getType() == TokenTypes.METHOD_DEF 363 && !ScopeUtil.isInScope(parent, Scope.ANONINNER) 364 || parent.getType() == TokenTypes.VARIABLE_DEF) { 365 final DetailAST mods = 366 parent.findFirstToken(TokenTypes.MODIFIERS); 367 inStatic = mods.findFirstToken(TokenTypes.LITERAL_STATIC) != null; 368 break; 369 } 370 else { 371 parent = parent.getParent(); 372 } 373 } 374 return inStatic; 375 } 376 377 /** 378 * Decides whether to ignore an AST node that is the parameter of a 379 * setter method, where the property setter method for field 'xyz' has 380 * name 'setXyz', one parameter named 'xyz', and return type void 381 * (default behavior) or return type is name of the class in which 382 * such method is declared (allowed only if 383 * {@link #setSetterCanReturnItsClass(boolean)} is called with 384 * value <em>true</em>). 385 * 386 * @param ast the AST to check. 387 * @param name the name of ast. 388 * @return true if ast should be ignored because check property 389 * ignoreSetter is true and ast is the parameter of a setter method. 390 */ 391 private boolean isIgnoredSetterParam(DetailAST ast, String name) { 392 boolean isIgnoredSetterParam = false; 393 if (ignoreSetter) { 394 final DetailAST parametersAST = ast.getParent(); 395 final DetailAST methodAST = parametersAST.getParent(); 396 if (parametersAST.getChildCount() == 1 397 && methodAST.getType() == TokenTypes.METHOD_DEF 398 && isSetterMethod(methodAST, name)) { 399 isIgnoredSetterParam = true; 400 } 401 } 402 return isIgnoredSetterParam; 403 } 404 405 /** 406 * Determine if a specific method identified by methodAST and a single 407 * variable name parameterName is a setter. This recognition partially depends 408 * on setterCanReturnItsClass property. 409 * 410 * @param methodAST AST corresponding to a method call 411 * @param parameterName name of single parameter of this method. 412 * @return true of false indicating of method is a setter or not. 413 */ 414 private boolean isSetterMethod(DetailAST methodAST, String parameterName) { 415 final String methodName = 416 methodAST.findFirstToken(TokenTypes.IDENT).getText(); 417 boolean isSetterMethod = false; 418 419 if (("set" + capitalize(parameterName)).equals(methodName)) { 420 // method name did match set${Name}(${anyType} ${parameterName}) 421 // where ${Name} is capitalized version of ${parameterName} 422 // therefore this method is potentially a setter 423 final DetailAST typeAST = methodAST.findFirstToken(TokenTypes.TYPE); 424 final String returnType = typeAST.getFirstChild().getText(); 425 if (typeAST.findFirstToken(TokenTypes.LITERAL_VOID) != null 426 || setterCanReturnItsClass && frame.isEmbeddedIn(returnType)) { 427 // this method has signature 428 // 429 // void set${Name}(${anyType} ${name}) 430 // 431 // and therefore considered to be a setter 432 // 433 // or 434 // 435 // return type is not void, but it is the same as the class 436 // where method is declared and setterCanReturnItsClass 437 // is set to true 438 isSetterMethod = true; 439 } 440 } 441 442 return isSetterMethod; 443 } 444 445 /** 446 * Capitalizes a given property name the way we expect to see it in 447 * a setter name. 448 * 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 // setFooBar() is a setter for FooBar property, not for fooBar 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 * 467 * @param ast the AST to check. 468 * @return true if ast should be ignored because check property 469 * ignoreConstructorParameter is true and ast is a constructor parameter. 470 */ 471 private boolean isIgnoredConstructorParam(DetailAST ast) { 472 boolean result = false; 473 if (ignoreConstructorParameter 474 && ast.getType() == TokenTypes.PARAMETER_DEF) { 475 final DetailAST parametersAST = ast.getParent(); 476 final DetailAST constructorAST = parametersAST.getParent(); 477 result = constructorAST.getType() == TokenTypes.CTOR_DEF; 478 } 479 return result; 480 } 481 482 /** 483 * Decides whether to ignore an AST node that is the parameter of an 484 * abstract method. 485 * 486 * @param ast the AST to check. 487 * @return true if ast should be ignored because check property 488 * ignoreAbstractMethods is true and ast is a parameter of abstract methods. 489 */ 490 private boolean isIgnoredParamOfAbstractMethod(DetailAST ast) { 491 boolean result = false; 492 if (ignoreAbstractMethods) { 493 final DetailAST method = ast.getParent().getParent(); 494 if (method.getType() == TokenTypes.METHOD_DEF) { 495 final DetailAST mods = method.findFirstToken(TokenTypes.MODIFIERS); 496 result = mods.findFirstToken(TokenTypes.ABSTRACT) != null; 497 } 498 } 499 return result; 500 } 501 502 /** 503 * Setter to define the RegExp for names of variables and parameters to ignore. 504 * 505 * @param pattern a pattern. 506 * @since 3.2 507 */ 508 public void setIgnoreFormat(Pattern pattern) { 509 ignoreFormat = pattern; 510 } 511 512 /** 513 * Setter to allow to ignore the parameter of a property setter method. 514 * 515 * @param ignoreSetter decide whether to ignore the parameter of 516 * a property setter method. 517 * @since 3.2 518 */ 519 public void setIgnoreSetter(boolean ignoreSetter) { 520 this.ignoreSetter = ignoreSetter; 521 } 522 523 /** 524 * Setter to allow to expand the definition of a setter method to include methods 525 * that return the class' instance. 526 * 527 * @param setterCanReturnItsClass if true then setter can return 528 * either void or class in which it is declared. If false then 529 * in order to be recognized as setter method (otherwise 530 * already recognized as a setter) must return void. Later is 531 * the default behavior. 532 * @since 6.3 533 */ 534 public void setSetterCanReturnItsClass( 535 boolean setterCanReturnItsClass) { 536 this.setterCanReturnItsClass = setterCanReturnItsClass; 537 } 538 539 /** 540 * Setter to control whether to ignore constructor parameters. 541 * 542 * @param ignoreConstructorParameter decide whether to ignore 543 * constructor parameters. 544 * @since 3.2 545 */ 546 public void setIgnoreConstructorParameter( 547 boolean ignoreConstructorParameter) { 548 this.ignoreConstructorParameter = ignoreConstructorParameter; 549 } 550 551 /** 552 * Setter to control whether to ignore parameters of abstract methods. 553 * 554 * @param ignoreAbstractMethods decide whether to ignore 555 * parameters of abstract methods. 556 * @since 4.0 557 */ 558 public void setIgnoreAbstractMethods( 559 boolean ignoreAbstractMethods) { 560 this.ignoreAbstractMethods = ignoreAbstractMethods; 561 } 562 563 /** 564 * Holds the names of static and instance fields of a type. 565 */ 566 private static final class FieldFrame { 567 568 /** Name of the frame, such name of the class or enum declaration. */ 569 private final String frameName; 570 571 /** Is this a static inner type. */ 572 private final boolean staticType; 573 574 /** Parent frame. */ 575 private final FieldFrame parent; 576 577 /** Set of instance field names. */ 578 private final Set<String> instanceFields = new HashSet<>(); 579 580 /** Set of static field names. */ 581 private final Set<String> staticFields = new HashSet<>(); 582 583 /** 584 * Creates new frame. 585 * 586 * @param parent parent frame. 587 * @param staticType is this a static inner type (class or enum). 588 * @param frameName name associated with the frame, which can be a 589 */ 590 private FieldFrame(FieldFrame parent, boolean staticType, String frameName) { 591 this.parent = parent; 592 this.staticType = staticType; 593 this.frameName = frameName; 594 } 595 596 /** 597 * Adds an instance field to this FieldFrame. 598 * 599 * @param field the name of the instance field. 600 */ 601 /* package */ void addInstanceField(String field) { 602 instanceFields.add(field); 603 } 604 605 /** 606 * Adds a static field to this FieldFrame. 607 * 608 * @param field the name of the instance field. 609 */ 610 /* package */ void addStaticField(String field) { 611 staticFields.add(field); 612 } 613 614 /** 615 * Determines whether this FieldFrame contains an instance field. 616 * 617 * @param field the field to check 618 * @return true if this FieldFrame contains instance field 619 */ 620 /* package */ boolean containsInstanceField(String field) { 621 FieldFrame currentParent = parent; 622 boolean contains = instanceFields.contains(field); 623 boolean isStaticType = staticType; 624 while (!isStaticType && !contains) { 625 contains = currentParent.instanceFields.contains(field); 626 isStaticType = currentParent.staticType; 627 currentParent = currentParent.parent; 628 } 629 return contains; 630 } 631 632 /** 633 * Determines whether this FieldFrame contains a static field. 634 * 635 * @param field the field to check 636 * @return true if this FieldFrame contains static field 637 */ 638 /* package */ boolean containsStaticField(String field) { 639 FieldFrame currentParent = parent; 640 boolean contains = staticFields.contains(field); 641 while (currentParent != null && !contains) { 642 contains = currentParent.staticFields.contains(field); 643 currentParent = currentParent.parent; 644 } 645 return contains; 646 } 647 648 /** 649 * Getter for parent frame. 650 * 651 * @return parent frame. 652 */ 653 /* package */ FieldFrame getParent() { 654 return parent; 655 } 656 657 /** 658 * Check if current frame is embedded in class or enum with 659 * specific name. 660 * 661 * @param classOrEnumName name of class or enum that we are looking 662 * for in the chain of field frames. 663 * 664 * @return true if current frame is embedded in class or enum 665 * with name classOrNameName 666 */ 667 private boolean isEmbeddedIn(String classOrEnumName) { 668 FieldFrame currentFrame = this; 669 boolean isEmbeddedIn = false; 670 while (currentFrame != null) { 671 if (Objects.equals(currentFrame.frameName, classOrEnumName)) { 672 isEmbeddedIn = true; 673 break; 674 } 675 currentFrame = currentFrame.parent; 676 } 677 return isEmbeddedIn; 678 } 679 680 } 681 682}