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 }