View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.qulice.checkstyle;
6   
7   import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
8   import com.puppycrawl.tools.checkstyle.api.DetailAST;
9   import com.puppycrawl.tools.checkstyle.api.TokenTypes;
10  import java.util.regex.Pattern;
11  
12  /**
13   * Checks that test classes do not declare instance fields, since
14   * such fields couple tests together through shared state.
15   *
16   * <p>Only files whose names match the configured pattern (by default
17   * {@code *Test.java}, {@code *IT.java}, {@code *ITCase.java}) are
18   * inspected. Within those files, only instance fields declared on the
19   * top-level type are flagged, unless the field carries at least one
20   * annotation (e.g. {@code @Rule}, {@code @ClassRule},
21   * {@code @Parameter}, {@code @TempDir}, {@code @Mock}). Static fields
22   * are ignored, because they represent compile-time constants or
23   * shared fixtures rather than per-test state. Fields declared on
24   * nested helper types (stubs, fakes, recorders) are not flagged,
25   * because they belong to the helper, not to the test class.
26   *
27   * <p>See also
28   * <a href="http://www.yegor256.com/2015/05/25/unit-test-scaffolding.html">
29   * Unit Test Scaffolding</a>.
30   *
31   * @since 0.24
32   */
33  public final class ProhibitFieldsInTestClassesCheck extends AbstractCheck {
34  
35      /**
36       * File names that this check applies to.
37       */
38      private Pattern include = Pattern.compile(".*(Test|IT|ITCase)\\.java$");
39  
40      /**
41       * Restrict the check to files matching the given pattern.
42       * @param regex Regex of file names to include
43       */
44      public void setIncludeFileNamePattern(final String regex) {
45          this.include = Pattern.compile(regex);
46      }
47  
48      @Override
49      public int[] getDefaultTokens() {
50          return new int[] {
51              TokenTypes.VARIABLE_DEF,
52          };
53      }
54  
55      @Override
56      public int[] getAcceptableTokens() {
57          return this.getDefaultTokens();
58      }
59  
60      @Override
61      public int[] getRequiredTokens() {
62          return this.getDefaultTokens();
63      }
64  
65      @Override
66      public void visitToken(final DetailAST ast) {
67          if (this.include.matcher(this.getFilePath()).find()
68              && ProhibitFieldsInTestClassesCheck.isUnannotatedInstanceField(ast)) {
69              final DetailAST name = ast.findFirstToken(TokenTypes.IDENT);
70              this.log(
71                  name.getLineNo(),
72                  String.format(
73                      "Field \"%s\" is not allowed in a test class, move it into a test method or annotate it",
74                      name.getText()
75                  )
76              );
77          }
78      }
79  
80      /**
81       * Is this VARIABLE_DEF an instance field of the top-level type
82       * that is neither static nor annotated? Fields of nested or
83       * anonymous types are not considered, because they belong to a
84       * helper rather than to the test class itself.
85       * @param node Variable definition node
86       * @return True if the field should be flagged
87       */
88      private static boolean isUnannotatedInstanceField(final DetailAST node) {
89          boolean flag = false;
90          final DetailAST parent = node.getParent();
91          if (parent != null && parent.getType() == TokenTypes.OBJBLOCK
92              && ProhibitFieldsInTestClassesCheck.isTopLevelType(parent.getParent())) {
93              final DetailAST modifiers = node.findFirstToken(TokenTypes.MODIFIERS);
94              flag = modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) == null
95                  && modifiers.findFirstToken(TokenTypes.ANNOTATION) == null;
96          }
97          return flag;
98      }
99  
100     /**
101      * Is this AST node a type declaration that sits at the top level
102      * of the compilation unit (i.e. its parent is not an
103      * {@code OBJBLOCK} of an enclosing type, and it is not the body
104      * of an anonymous inner class spawned by {@code LITERAL_NEW})?
105      * @param type Candidate type declaration node
106      * @return True if it is the file's top-level type
107      */
108     private static boolean isTopLevelType(final DetailAST type) {
109         return type != null
110             && ProhibitFieldsInTestClassesCheck.isTypeDef(type)
111             && ProhibitFieldsInTestClassesCheck.hasTopLevelParent(type);
112     }
113 
114     /**
115      * Is this AST node a class, enum, record, or interface declaration?
116      * @param type Candidate node
117      * @return True if it is one of the four type-defining tokens
118      */
119     private static boolean isTypeDef(final DetailAST type) {
120         final int kind = type.getType();
121         return kind == TokenTypes.CLASS_DEF
122             || kind == TokenTypes.ENUM_DEF
123             || kind == TokenTypes.RECORD_DEF
124             || kind == TokenTypes.INTERFACE_DEF;
125     }
126 
127     /**
128      * Is the parent of this type node such that the type is at the top
129      * level of the compilation unit, i.e. not nested inside an
130      * enclosing type's {@code OBJBLOCK} and not the body of an
131      * anonymous class created via {@code LITERAL_NEW}?
132      * @param type Type declaration node
133      * @return True if the parent indicates a top-level position
134      */
135     private static boolean hasTopLevelParent(final DetailAST type) {
136         final DetailAST parent = type.getParent();
137         return parent != null
138             && parent.getType() != TokenTypes.OBJBLOCK
139             && parent.getType() != TokenTypes.LITERAL_NEW;
140     }
141 }