1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.checks.coding;
21
22 import java.util.HashSet;
23 import java.util.Locale;
24 import java.util.Objects;
25 import java.util.Set;
26 import java.util.regex.Pattern;
27
28 import javax.annotation.Nullable;
29
30 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
31 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
32 import com.puppycrawl.tools.checkstyle.api.DetailAST;
33 import com.puppycrawl.tools.checkstyle.api.Scope;
34 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
35 import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
36 import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
37 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
38
39 /**
40 * <div>
41 * Checks that a local variable or a parameter does not shadow
42 * a field that is defined in the same class.
43 * </div>
44 *
45 * <p>
46 * Notes:
47 * It is possible to configure the check to ignore all property setter methods.
48 * </p>
49 *
50 * <p>
51 * A method is recognized as a setter if it is in the following form
52 * </p>
53 * {@snippet lang="text" :
54 * ${returnType} set${Name}(${anyType} ${name}) { ... }
55 * }
56 *
57 * <p>
58 * where ${anyType} is any primitive type, class or interface name;
59 * ${name} is name of the variable that is being set and ${Name} its
60 * capitalized form that appears in the method name. By default, it is expected
61 * that setter returns void, i.e. ${returnType} is 'void'. For example
62 * </p>
63 * {@snippet lang="text" :
64 * void setTime(long time) { }
65 * }
66 *
67 * <p>
68 * Any other return types will not let method match a setter pattern. However,
69 * by setting <em>setterCanReturnItsClass</em> property to <em>true</em>
70 * definition of a setter is expanded, so that setter return type can also be
71 * a class in which setter is declared. For example
72 * </p>
73 * {@snippet lang="text" :
74 * class PageBuilder {
75 * PageBuilder setName(String name) { }
76 * }
77 * }
78 *
79 * <p>
80 * Such methods are known as chain-setters and a common when Builder-pattern
81 * is used. Property <em>setterCanReturnItsClass</em> has effect only if
82 * <em>ignoreSetter</em> is set to true.
83 * </p>
84 *
85 * @since 3.0
86 */
87 @FileStatefulCheck
88 public class HiddenFieldCheck
89 extends AbstractCheck {
90
91 /**
92 * A key is pointing to the warning message text in "messages.properties"
93 * file.
94 */
95 public static final String MSG_KEY = "hidden.field";
96
97 /**
98 * Stack of sets of field names,
99 * 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 }