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