1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2025 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 com.puppycrawl.tools.checkstyle.FileStatefulCheck;
29 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
30 import com.puppycrawl.tools.checkstyle.api.DetailAST;
31 import com.puppycrawl.tools.checkstyle.api.Scope;
32 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
33 import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
34 import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
35 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
36
37 /**
38 * <div>
39 * Checks that a local variable or a parameter does not shadow
40 * a field that is defined in the same class.
41 * </div>
42 *
43 * <p>
44 * Notes:
45 * It is possible to configure the check to ignore all property setter methods.
46 * </p>
47 *
48 * <p>
49 * A method is recognized as a setter if it is in the following form
50 * </p>
51 * <div class="wrapper"><pre class="prettyprint"><code class="language-text">
52 * ${returnType} set${Name}(${anyType} ${name}) { ... }
53 * </code></pre></div>
54 *
55 * <p>
56 * where ${anyType} is any primitive type, class or interface name;
57 * ${name} is name of the variable that is being set and ${Name} its
58 * capitalized form that appears in the method name. By default, it is expected
59 * that setter returns void, i.e. ${returnType} is 'void'. For example
60 * </p>
61 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
62 * void setTime(long time) { ... }
63 * </code></pre></div>
64 *
65 * <p>
66 * Any other return types will not let method match a setter pattern. However,
67 * by setting <em>setterCanReturnItsClass</em> property to <em>true</em>
68 * definition of a setter is expanded, so that setter return type can also be
69 * a class in which setter is declared. For example
70 * </p>
71 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
72 * class PageBuilder {
73 * PageBuilder setName(String name) { ... }
74 * }
75 * </code></pre></div>
76 *
77 * <p>
78 * Such methods are known as chain-setters and a common when Builder-pattern
79 * is used. Property <em>setterCanReturnItsClass</em> has effect only if
80 * <em>ignoreSetter</em> is set to true.
81 * </p>
82 *
83 * @since 3.0
84 */
85 @FileStatefulCheck
86 public class HiddenFieldCheck
87 extends AbstractCheck {
88
89 /**
90 * A key is pointing to the warning message text in "messages.properties"
91 * file.
92 */
93 public static final String MSG_KEY = "hidden.field";
94
95 /**
96 * Stack of sets of field names,
97 * one for each class of a set of nested classes.
98 */
99 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 }