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 * <div class="wrapper"><pre class="prettyprint"><code class="language-text">
54 * ${returnType} set${Name}(${anyType} ${name}) { ... }
55 * </code></pre></div>
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 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
64 * void setTime(long time) { ... }
65 * </code></pre></div>
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 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
74 * class PageBuilder {
75 * PageBuilder setName(String name) { ... }
76 * }
77 * </code></pre></div>
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 @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 }