aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/puppycrawl/tools/checkstyle/checks/coding/HiddenFieldCheck.java
blob: 60928a130b22cf612f007b1f5bab9c14be41d9e2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2016 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
////////////////////////////////////////////////////////////////////////////////

package com.puppycrawl.tools.checkstyle.checks.coding;

import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;

import com.google.common.collect.Sets;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.Scope;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
import com.puppycrawl.tools.checkstyle.utils.ScopeUtils;

/**
 * Checks that a local variable or a parameter does not shadow
 * a field that is defined in the same class.
 *
 * <p>An example of how to configure the check is:
 * <pre>
 * &lt;module name="HiddenField"/&gt;
 * </pre>
 *
 * <p>An example of how to configure the check so that it checks variables but not
 * parameters is:
 * <pre>
 * &lt;module name="HiddenField"&gt;
 *    &lt;property name="tokens" value="VARIABLE_DEF"/&gt;
 * &lt;/module&gt;
 * </pre>
 *
 * <p>An example of how to configure the check so that it ignores the parameter of
 * a setter method is:
 * <pre>
 * &lt;module name="HiddenField"&gt;
 *    &lt;property name="ignoreSetter" value="true"/&gt;
 * &lt;/module&gt;
 * </pre>
 *
 * <p>A method is recognized as a setter if it is in the following form
 * <pre>
 * ${returnType} set${Name}(${anyType} ${name}) { ... }
 * </pre>
 * where ${anyType} is any primitive type, class or interface name;
 * ${name} is name of the variable that is being set and ${Name} its
 * capitalized form that appears in the method name. By default it is expected
 * that setter returns void, i.e. ${returnType} is 'void'. For example
 * <pre>
 * void setTime(long time) { ... }
 * </pre>
 * Any other return types will not let method match a setter pattern. However,
 * by setting <em>setterCanReturnItsClass</em> property to <em>true</em>
 * definition of a setter is expanded, so that setter return type can also be
 * a class in which setter is declared. For example
 * <pre>
 * class PageBuilder {
 *   PageBuilder setName(String name) { ... }
 * }
 * </pre>
 * Such methods are known as chain-setters and a common when Builder-pattern
 * is used. Property <em>setterCanReturnItsClass</em> has effect only if
 * <em>ignoreSetter</em> is set to true.
 *
 * <p>An example of how to configure the check so that it ignores the parameter
 * of either a setter that returns void or a chain-setter.
 * <pre>
 * &lt;module name="HiddenField"&gt;
 *    &lt;property name="ignoreSetter" value="true"/&gt;
 *    &lt;property name="setterCanReturnItsClass" value="true"/&gt;
 * &lt;/module&gt;
 * </pre>
 *
 * <p>An example of how to configure the check so that it ignores constructor
 * parameters is:
 * <pre>
 * &lt;module name="HiddenField"&gt;
 *    &lt;property name="ignoreConstructorParameter" value="true"/&gt;
 * &lt;/module&gt;
 * </pre>
 *
 * <p>An example of how to configure the check so that it ignores variables and parameters
 * named 'test':
 * <pre>
 * &lt;module name="HiddenField"&gt;
 *    &lt;property name="ignoreFormat" value="^test$"/&gt;
 * &lt;/module&gt;
 * </pre>
 *
 * <pre>
 * {@code
 * class SomeClass
 * {
 *     private List&lt;String&gt; test;
 *
 *     private void addTest(List&lt;String&gt; test) // no violation
 *     {
 *         this.test.addAll(test);
 *     }
 *
 *     private void foo()
 *     {
 *         final List&lt;String&gt; test = new ArrayList&lt;&gt;(); // no violation
 *         ...
 *     }
 * }
 * }
 * </pre>
 *
 * @author Dmitri Priimak
 */
public class HiddenFieldCheck
    extends AbstractCheck {
    /**
     * A key is pointing to the warning message text in "messages.properties"
     * file.
     */
    public static final String MSG_KEY = "hidden.field";

    /** Stack of sets of field names,
     * one for each class of a set of nested classes.
     */
    private FieldFrame frame;

    /** Pattern for names of variables and parameters to ignore. */
    private Pattern regexp;

    /** Controls whether to check the parameter of a property setter method. */
    private boolean ignoreSetter;

    /**
     * If ignoreSetter is set to true then this variable controls what
     * the setter method can return By default setter must return void.
     * However, is this variable is set to true then setter can also
     * return class in which is declared.
     */
    private boolean setterCanReturnItsClass;

    /** Controls whether to check the parameter of a constructor. */
    private boolean ignoreConstructorParameter;

    /** Controls whether to check the parameter of abstract methods. */
    private boolean ignoreAbstractMethods;

    @Override
    public int[] getDefaultTokens() {
        return getAcceptableTokens();
    }

    @Override
    public int[] getAcceptableTokens() {
        return new int[] {
            TokenTypes.VARIABLE_DEF,
            TokenTypes.PARAMETER_DEF,
            TokenTypes.CLASS_DEF,
            TokenTypes.ENUM_DEF,
            TokenTypes.ENUM_CONSTANT_DEF,
            TokenTypes.LAMBDA,
        };
    }

    @Override
    public int[] getRequiredTokens() {
        return new int[] {
            TokenTypes.CLASS_DEF,
            TokenTypes.ENUM_DEF,
            TokenTypes.ENUM_CONSTANT_DEF,
        };
    }

    @Override
    public void beginTree(DetailAST rootAST) {
        frame = new FieldFrame(null, true, null);
    }

    @Override
    public void visitToken(DetailAST ast) {
        final int type = ast.getType();
        switch (type) {
            case TokenTypes.VARIABLE_DEF:
            case TokenTypes.PARAMETER_DEF:
                processVariable(ast);
                break;
            case TokenTypes.LAMBDA:
                processLambda(ast);
                break;
            default:
                visitOtherTokens(ast, type);
        }
    }

    /**
     * Process a lambda token.
     * Checks whether a lambda parameter shadows a field.
     * Note, that when parameter of lambda expression is untyped,
     * ANTLR parses the parameter as an identifier.
     * @param ast the lambda token.
     */
    private void processLambda(DetailAST ast) {
        final DetailAST firstChild = ast.getFirstChild();
        if (firstChild.getType() == TokenTypes.IDENT) {
            final String untypedLambdaParameterName = firstChild.getText();
            if (isStaticOrInstanceField(firstChild, untypedLambdaParameterName)) {
                log(firstChild, MSG_KEY, untypedLambdaParameterName);
            }
        }
        else {
            // Type of lambda parameter is not omitted.
            processVariable(ast);
        }
    }

    /**
     * Called to process tokens other than {@link TokenTypes#VARIABLE_DEF}
     * and {@link TokenTypes#PARAMETER_DEF}.
     *
     * @param ast token to process
     * @param type type of the token
     */
    private void visitOtherTokens(DetailAST ast, int type) {
        //A more thorough check of enum constant class bodies is
        //possible (checking for hidden fields against the enum
        //class body in addition to enum constant class bodies)
        //but not attempted as it seems out of the scope of this
        //check.
        final DetailAST typeMods = ast.findFirstToken(TokenTypes.MODIFIERS);
        final boolean isStaticInnerType =
                typeMods != null
                        && typeMods.branchContains(TokenTypes.LITERAL_STATIC);
        final String frameName;

        if (type == TokenTypes.CLASS_DEF || type == TokenTypes.ENUM_DEF) {
            frameName = ast.findFirstToken(TokenTypes.IDENT).getText();
        }
        else {
            frameName = null;
        }
        final FieldFrame newFrame = new FieldFrame(frame, isStaticInnerType, frameName);

        //add fields to container
        final DetailAST objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK);
        // enum constants may not have bodies
        if (objBlock != null) {
            DetailAST child = objBlock.getFirstChild();
            while (child != null) {
                if (child.getType() == TokenTypes.VARIABLE_DEF) {
                    final String name =
                        child.findFirstToken(TokenTypes.IDENT).getText();
                    final DetailAST mods =
                        child.findFirstToken(TokenTypes.MODIFIERS);
                    if (mods.branchContains(TokenTypes.LITERAL_STATIC)) {
                        newFrame.addStaticField(name);
                    }
                    else {
                        newFrame.addInstanceField(name);
                    }
                }
                child = child.getNextSibling();
            }
        }
        // push container
        frame = newFrame;
    }

    @Override
    public void leaveToken(DetailAST ast) {
        if (ast.getType() == TokenTypes.CLASS_DEF
            || ast.getType() == TokenTypes.ENUM_DEF
            || ast.getType() == TokenTypes.ENUM_CONSTANT_DEF) {
            //pop
            frame = frame.getParent();
        }
    }

    /**
     * Process a variable token.
     * Check whether a local variable or parameter shadows a field.
     * Store a field for later comparison with local variables and parameters.
     * @param ast the variable token.
     */
    private void processVariable(DetailAST ast) {
        if (!ScopeUtils.isInInterfaceOrAnnotationBlock(ast)
            && (ScopeUtils.isLocalVariableDef(ast)
                || ast.getType() == TokenTypes.PARAMETER_DEF)) {
            // local variable or parameter. Does it shadow a field?
            final DetailAST nameAST = ast.findFirstToken(TokenTypes.IDENT);
            final String name = nameAST.getText();

            if ((isStaticFieldHiddenFromAnonymousClass(ast, name)
                        || isStaticOrInstanceField(ast, name))
                    && !isMatchingRegexp(name)
                    && !isIgnoredParam(ast, name)) {
                log(nameAST, MSG_KEY, name);
            }
        }
    }

    /**
     * Checks whether a static field is hidden from closure.
     * @param nameAST local variable or parameter.
     * @param name field name.
     * @return true if static field is hidden from closure.
     */
    private boolean isStaticFieldHiddenFromAnonymousClass(DetailAST nameAST, String name) {
        return isInStatic(nameAST)
            && frame.containsStaticField(name);
    }

    /**
     * Checks whether method or constructor parameter is ignored.
     * @param ast the parameter token.
     * @param name the parameter name.
     * @return true if parameter is ignored.
     */
    private boolean isIgnoredParam(DetailAST ast, String name) {
        return isIgnoredSetterParam(ast, name)
            || isIgnoredConstructorParam(ast)
            || isIgnoredParamOfAbstractMethod(ast);
    }

    /**
     * Check for static or instance field.
     * @param ast token
     * @param name identifier of token
     * @return true if static or instance field
     */
    private boolean isStaticOrInstanceField(DetailAST ast, String name) {
        return frame.containsStaticField(name)
                || !isInStatic(ast) && frame.containsInstanceField(name);
    }

    /**
     * Check name by regExp.
     * @param name string value to check
     * @return true is regexp is matching
     */
    private boolean isMatchingRegexp(String name) {
        return regexp != null && regexp.matcher(name).find();
    }

    /**
     * Determines whether an AST node is in a static method or static
     * initializer.
     * @param ast the node to check.
     * @return true if ast is in a static method or a static block;
     */
    private static boolean isInStatic(DetailAST ast) {
        DetailAST parent = ast.getParent();
        boolean inStatic = false;

        while (parent != null && !inStatic) {
            if (parent.getType() == TokenTypes.STATIC_INIT) {
                inStatic = true;
            }
            else if (parent.getType() == TokenTypes.METHOD_DEF
                        && !ScopeUtils.isInScope(parent, Scope.ANONINNER)
                        || parent.getType() == TokenTypes.VARIABLE_DEF) {
                final DetailAST mods =
                    parent.findFirstToken(TokenTypes.MODIFIERS);
                inStatic = mods.branchContains(TokenTypes.LITERAL_STATIC);
                break;
            }
            else {
                parent = parent.getParent();
            }
        }
        return inStatic;
    }

    /**
     * Decides whether to ignore an AST node that is the parameter of a
     * setter method, where the property setter method for field 'xyz' has
     * name 'setXyz', one parameter named 'xyz', and return type void
     * (default behavior) or return type is name of the class in which
     * such method is declared (allowed only if
     * {@link #setSetterCanReturnItsClass(boolean)} is called with
     * value <em>true</em>)
     *
     * @param ast the AST to check.
     * @param name the name of ast.
     * @return true if ast should be ignored because check property
     *     ignoreSetter is true and ast is the parameter of a setter method.
     */
    private boolean isIgnoredSetterParam(DetailAST ast, String name) {
        if (ignoreSetter && ast.getType() == TokenTypes.PARAMETER_DEF) {
            final DetailAST parametersAST = ast.getParent();
            final DetailAST methodAST = parametersAST.getParent();
            if (parametersAST.getChildCount() == 1
                && methodAST.getType() == TokenTypes.METHOD_DEF
                && isSetterMethod(methodAST, name)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Determine if a specific method identified by methodAST and a single
     * variable name aName is a setter. This recognition partially depends
     * on mSetterCanReturnItsClass property.
     *
     * @param aMethodAST AST corresponding to a method call
     * @param aName name of single parameter of this method.
     * @return true of false indicating of method is a setter or not.
     */
    private boolean isSetterMethod(DetailAST aMethodAST, String aName) {
        final String methodName =
            aMethodAST.findFirstToken(TokenTypes.IDENT).getText();
        boolean isSetterMethod = false;

        if (("set" + capitalize(aName)).equals(methodName)) {
            // method name did match set${Name}(${anyType} ${aName})
            // where ${Name} is capitalized version of ${aName}
            // therefore this method is potentially a setter
            final DetailAST typeAST = aMethodAST.findFirstToken(TokenTypes.TYPE);
            final String returnType = typeAST.getFirstChild().getText();
            if (typeAST.branchContains(TokenTypes.LITERAL_VOID)
                    || setterCanReturnItsClass && frame.isEmbeddedIn(returnType)) {
                // this method has signature
                //
                //     void set${Name}(${anyType} ${name})
                //
                // and therefore considered to be a setter
                //
                // or
                //
                // return type is not void, but it is the same as the class
                // where method is declared and and mSetterCanReturnItsClass
                // is set to true
                isSetterMethod = true;
            }
        }

        return isSetterMethod;
    }

    /**
     * Capitalizes a given property name the way we expect to see it in
     * a setter name.
     * @param name a property name
     * @return capitalized property name
     */
    private static String capitalize(final String name) {
        String setterName = name;
        // we should not capitalize the first character if the second
        // one is a capital one, since according to JavaBeans spec
        // setXYzz() is a setter for XYzz property, not for xYzz one.
        if (name.length() == 1 || !Character.isUpperCase(name.charAt(1))) {
            setterName = name.substring(0, 1).toUpperCase(Locale.ENGLISH) + name.substring(1);
        }
        return setterName;
    }

    /**
     * Decides whether to ignore an AST node that is the parameter of a
     * constructor.
     * @param ast the AST to check.
     * @return true if ast should be ignored because check property
     *     ignoreConstructorParameter is true and ast is a constructor parameter.
     */
    private boolean isIgnoredConstructorParam(DetailAST ast) {
        boolean result = false;
        if (ignoreConstructorParameter
                && ast.getType() == TokenTypes.PARAMETER_DEF) {
            final DetailAST parametersAST = ast.getParent();
            final DetailAST constructorAST = parametersAST.getParent();
            result = constructorAST.getType() == TokenTypes.CTOR_DEF;
        }
        return result;
    }

    /**
     * Decides whether to ignore an AST node that is the parameter of an
     * abstract method.
     * @param ast the AST to check.
     * @return true if ast should be ignored because check property
     *     ignoreAbstractMethods is true and ast is a parameter of abstract methods.
     */
    private boolean isIgnoredParamOfAbstractMethod(DetailAST ast) {
        boolean result = false;
        if (ignoreAbstractMethods
                && ast.getType() == TokenTypes.PARAMETER_DEF) {
            final DetailAST method = ast.getParent().getParent();
            if (method.getType() == TokenTypes.METHOD_DEF) {
                final DetailAST mods = method.findFirstToken(TokenTypes.MODIFIERS);
                result = mods.branchContains(TokenTypes.ABSTRACT);
            }
        }
        return result;
    }

    /**
     * Set the ignore format to the specified regular expression.
     * @param format a {@code String} value
     */
    public void setIgnoreFormat(String format) {
        regexp = CommonUtils.createPattern(format);
    }

    /**
     * Set whether to ignore the parameter of a property setter method.
     * @param ignoreSetter decide whether to ignore the parameter of
     *     a property setter method.
     */
    public void setIgnoreSetter(boolean ignoreSetter) {
        this.ignoreSetter = ignoreSetter;
    }

    /**
     * Controls if setter can return only void (default behavior) or it
     * can also return class in which it is declared.
     *
     * @param aSetterCanReturnItsClass if true then setter can return
     *        either void or class in which it is declared. If false then
     *        in order to be recognized as setter method (otherwise
     *        already recognized as a setter) must return void.  Later is
     *        the default behavior.
     */
    public void setSetterCanReturnItsClass(
        boolean aSetterCanReturnItsClass) {
        setterCanReturnItsClass = aSetterCanReturnItsClass;
    }

    /**
     * Set whether to ignore constructor parameters.
     * @param ignoreConstructorParameter decide whether to ignore
     *     constructor parameters.
     */
    public void setIgnoreConstructorParameter(
        boolean ignoreConstructorParameter) {
        this.ignoreConstructorParameter = ignoreConstructorParameter;
    }

    /**
     * Set whether to ignore parameters of abstract methods.
     * @param ignoreAbstractMethods decide whether to ignore
     *     parameters of abstract methods.
     */
    public void setIgnoreAbstractMethods(
        boolean ignoreAbstractMethods) {
        this.ignoreAbstractMethods = ignoreAbstractMethods;
    }

    /**
     * Holds the names of static and instance fields of a type.
     * @author Rick Giles
     */
    private static class FieldFrame {
        /** Name of the frame, such name of the class or enum declaration. */
        private final String frameName;

        /** Is this a static inner type. */
        private final boolean staticType;

        /** Parent frame. */
        private final FieldFrame parent;

        /** Set of instance field names. */
        private final Set<String> instanceFields = Sets.newHashSet();

        /** Set of static field names. */
        private final Set<String> staticFields = Sets.newHashSet();

        /**
         * Creates new frame.
         * @param parent parent frame.
         * @param staticType is this a static inner type (class or enum).
         * @param frameName name associated with the frame, which can be a
         */
        FieldFrame(FieldFrame parent, boolean staticType, String frameName) {
            this.parent = parent;
            this.staticType = staticType;
            this.frameName = frameName;
        }

        /**
         * Adds an instance field to this FieldFrame.
         * @param field  the name of the instance field.
         */
        public void addInstanceField(String field) {
            instanceFields.add(field);
        }

        /**
         * Adds a static field to this FieldFrame.
         * @param field  the name of the instance field.
         */
        public void addStaticField(String field) {
            staticFields.add(field);
        }

        /**
         * Determines whether this FieldFrame contains an instance field.
         * @param field the field to check.
         * @return true if this FieldFrame contains instance field field.
         */
        public boolean containsInstanceField(String field) {
            return instanceFields.contains(field)
                    || parent != null
                    && !staticType
                    && parent.containsInstanceField(field);

        }

        /**
         * Determines whether this FieldFrame contains a static field.
         * @param field the field to check.
         * @return true if this FieldFrame contains static field field.
         */
        public boolean containsStaticField(String field) {
            return staticFields.contains(field)
                    || parent != null
                    && parent.containsStaticField(field);
        }

        /**
         * Getter for parent frame.
         * @return parent frame.
         */
        public FieldFrame getParent() {
            return parent;
        }

        /**
         * Check if current frame is embedded in class or enum with
         * specific name.
         *
         * @param classOrEnumName name of class or enum that we are looking
         *     for in the chain of field frames.
         *
         * @return true if current frame is embedded in class or enum
         *     with name classOrNameName
         */
        private boolean isEmbeddedIn(String classOrEnumName) {
            FieldFrame currentFrame = this;
            while (currentFrame != null) {
                if (Objects.equals(currentFrame.frameName, classOrEnumName)) {
                    return true;
                }
                currentFrame = currentFrame.parent;
            }
            return false;
        }
    }
}