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