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.</p>
26   *
27   * <p>See also
28   * <a href="http://www.yegor256.com/2015/05/25/unit-test-scaffolding.html">
29   * Unit Test Scaffolding</a>.</p>
30   *
31   * @since 0.24
32   */
33  public final class ProhibitFieldsInTestClassesCheck extends AbstractCheck {
34  
35      /**
36       * File names of test classes.
37       */
38      private static final Pattern TESTS =
39          Pattern.compile(".*(Test|IT|ITCase)\\.java$");
40  
41      /**
42       * File names that this check applies to.
43       */
44      private Pattern include;
45  
46      /**
47       * Default constructor.
48       */
49      public ProhibitFieldsInTestClassesCheck() {
50          this.include = ProhibitFieldsInTestClassesCheck.TESTS;
51      }
52  
53      /**
54       * Restrict the check to files matching the given pattern.
55       *
56       * @param regex Regex of file names to include
57       */
58      public void setIncludeFileNamePattern(final String regex) {
59          this.include = Pattern.compile(regex);
60      }
61  
62      @Override
63      public int[] getDefaultTokens() {
64          return new int[] {
65              TokenTypes.VARIABLE_DEF,
66          };
67      }
68  
69      @Override
70      public int[] getAcceptableTokens() {
71          return this.getDefaultTokens();
72      }
73  
74      @Override
75      public int[] getRequiredTokens() {
76          return this.getDefaultTokens();
77      }
78  
79      @Override
80      public void visitToken(final DetailAST ast) {
81          if (this.include.matcher(this.getFilePath()).find()
82              && ProhibitFieldsInTestClassesCheck.isUnannotatedInstanceField(ast)) {
83              final DetailAST name = ast.findFirstToken(TokenTypes.IDENT);
84              this.log(
85                  name.getLineNo(),
86                  String.format(
87                      "Field \"%s\" is not allowed in a test class, move it into a test method or annotate it",
88                      name.getText()
89                  )
90              );
91          }
92      }
93  
94      private static boolean isUnannotatedInstanceField(final DetailAST node) {
95          boolean flag = false;
96          final DetailAST parent = node.getParent();
97          if (parent != null && parent.getType() == TokenTypes.OBJBLOCK
98              && ProhibitFieldsInTestClassesCheck.isTopLevelType(parent.getParent())) {
99              final DetailAST modifiers = node.findFirstToken(TokenTypes.MODIFIERS);
100             flag = modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) == null
101                 && modifiers.findFirstToken(TokenTypes.ANNOTATION) == null;
102         }
103         return flag;
104     }
105 
106     private static boolean isTopLevelType(final DetailAST type) {
107         return type != null
108             && ProhibitFieldsInTestClassesCheck.isTypeDef(type)
109             && ProhibitFieldsInTestClassesCheck.hasTopLevelParent(type);
110     }
111 
112     private static boolean isTypeDef(final DetailAST type) {
113         final int kind = type.getType();
114         return kind == TokenTypes.CLASS_DEF
115             || kind == TokenTypes.ENUM_DEF
116             || kind == TokenTypes.RECORD_DEF
117             || kind == TokenTypes.INTERFACE_DEF;
118     }
119 
120     private static boolean hasTopLevelParent(final DetailAST type) {
121         final DetailAST parent = type.getParent();
122         return parent != null
123             && parent.getType() != TokenTypes.OBJBLOCK
124             && parent.getType() != TokenTypes.LITERAL_NEW;
125     }
126 }