23
23
import java .util .HashSet ;
24
24
import java .util .List ;
25
25
import java .util .Map ;
26
+ import java .util .Objects ;
26
27
import java .util .Optional ;
27
28
import java .util .Set ;
29
+ import java .util .stream .Collectors ;
28
30
import org .sonar .check .Rule ;
29
31
import org .sonar .plugins .python .api .PythonSubscriptionCheck ;
30
32
import org .sonar .plugins .python .api .SubscriptionContext ;
31
- import org .sonar .plugins .python .api .symbols .ClassSymbol ;
32
33
import org .sonar .plugins .python .api .symbols .FunctionSymbol ;
33
34
import org .sonar .plugins .python .api .symbols .Symbol ;
34
35
import org .sonar .plugins .python .api .symbols .Usage ;
35
36
import org .sonar .plugins .python .api .tree .ClassDef ;
37
+ import org .sonar .plugins .python .api .tree .Decorator ;
36
38
import org .sonar .plugins .python .api .tree .FunctionDef ;
37
39
import org .sonar .plugins .python .api .tree .Statement ;
38
40
import org .sonar .plugins .python .api .tree .Tree ;
43
45
public class NotDiscoverableTestMethodCheck extends PythonSubscriptionCheck {
44
46
45
47
private static final String MESSAGE = "Rename this method so that it starts with \" test\" or remove this unused helper." ;
48
+ private static final Set <String > globalFixture = new HashSet <>();
46
49
47
50
@ Override
48
51
public void initialize (Context context ) {
52
+ context .registerSyntaxNodeConsumer (Tree .Kind .FUNCDEF , ctx -> NotDiscoverableTestMethodCheck .lookForGlobalFixture ((FunctionDef ) ctx .syntaxNode ()));
53
+
49
54
context .registerSyntaxNodeConsumer (Tree .Kind .CLASSDEF , ctx -> {
50
55
ClassDef classDefinition = (ClassDef ) ctx .syntaxNode ();
51
56
52
57
if (inheritsOnlyFromUnitTest (classDefinition )) {
53
58
Map <FunctionSymbol , FunctionDef > suspiciousFunctionsAndDefinitions = new HashMap <>();
54
59
Set <Tree > allDefinitions = new HashSet <>();
60
+ Set <String > classFixtures = getFixturesFromClass (classDefinition );
55
61
// We only consider method definitions, and not nested functions
56
62
for (Statement statement : classDefinition .body ().statements ()) {
57
63
if (statement .is (Tree .Kind .FUNCDEF )) {
58
64
FunctionDef functionDef = ((FunctionDef ) statement );
59
- String functionName = functionDef .name ().name ();
60
- Symbol symbol = functionDef .name ().symbol ();
61
- // If it doesn't override existing methods and doesn't start with test, it is added to the map
62
- if (!overrideExistingMethod (functionName ) && !functionName .startsWith ("test" )) {
63
- Optional .ofNullable (symbol )
64
- .filter (s -> s .is (Symbol .Kind .FUNCTION ))
65
- .ifPresent (s -> suspiciousFunctionsAndDefinitions .put (((FunctionSymbol ) s ), functionDef ));
65
+ if (!isException (functionDef , classFixtures )) {
66
+ Optional .ofNullable (functionDef .name ().symbol ())
67
+ .filter (symbol -> symbol .is (Symbol .Kind .FUNCTION ))
68
+ .ifPresent (symbol -> suspiciousFunctionsAndDefinitions .put (((FunctionSymbol ) symbol ), functionDef ));
66
69
}
67
70
allDefinitions .add (functionDef );
68
71
}
@@ -73,9 +76,38 @@ public void initialize(Context context) {
73
76
});
74
77
}
75
78
76
- @ Override
77
- public CheckScope scope () {
78
- return CheckScope .ALL ;
79
+ private static void lookForGlobalFixture (FunctionDef functionDef ) {
80
+ if (functionDef .isMethodDefinition ()) {
81
+ return ;
82
+ }
83
+ if (functionDef .decorators ().stream ().anyMatch (NotDiscoverableTestMethodCheck ::isPytestFixture )) {
84
+ globalFixture .add (functionDef .name ().name ());
85
+ }
86
+ }
87
+
88
+ /** https://docs.pytest.org/en/6.2.x/fixture.html
89
+ * Retrieve all Fixtures defined in a class.
90
+ * A fixture is a method which has the specific decorator @pytest.fixture
91
+ * In those cases, pytest will invoke the method fixture and inject the result in any test method
92
+ * for which one of their parameter name match with the fixture method name.
93
+ */
94
+ private static Set <String > getFixturesFromClass (ClassDef classDefinition ) {
95
+ return classDefinition .body ().statements ().stream ()
96
+ .filter (statement -> statement .is (Tree .Kind .FUNCDEF ))
97
+ .map (FunctionDef .class ::cast )
98
+ .filter (functionDef -> functionDef .decorators ().stream ().anyMatch (NotDiscoverableTestMethodCheck ::isPytestFixture ))
99
+ .map (functionDef -> functionDef .name ().name ())
100
+ .collect (Collectors .toSet ());
101
+ }
102
+
103
+ private static boolean isException (FunctionDef functionDef , Set <String > classFixtures ) {
104
+ String functionName = functionDef .name ().name ();
105
+ return overrideExistingMethod (functionName ) || functionName .startsWith ("test" ) || isHelper (functionDef , classFixtures );
106
+ }
107
+
108
+ private static boolean isPytestFixture (Decorator decorator ) {
109
+ String decoratorName = TreeUtils .decoratorNameFromExpression (decorator .expression ());
110
+ return "pytest.fixture" .equals (decoratorName );
79
111
}
80
112
81
113
// Only raises issue when the (non-test) method is not used inside the class
@@ -88,19 +120,26 @@ private static void checkSuspiciousFunctionsUsages(SubscriptionContext ctx, Map<
88
120
});
89
121
}
90
122
91
- private static boolean inheritsOnlyFromUnitTest (ClassDef classDefinition ) {
92
- Optional <ClassSymbol > classSymbolFromDef = Optional .ofNullable (TreeUtils .getClassSymbolFromDef (classDefinition ));
93
- return classSymbolFromDef .filter (classSymbol -> !classSymbol .superClasses ().isEmpty ()).isPresent () &&
94
- classSymbolFromDef
95
- .map (ClassSymbol ::superClasses )
96
- .stream ()
97
- .flatMap (List ::stream )
98
- .map (Symbol ::fullyQualifiedName )
99
- .allMatch ("unittest.case.TestCase" ::equals );
123
+ private static boolean inheritsOnlyFromUnitTest (ClassDef classDef ) {
124
+ return TreeUtils .getParentClassesFQN (classDef ).stream ().anyMatch (name -> name .contains ("unittest" ) && name .contains ("TestCase" ))
125
+ && Optional .ofNullable (TreeUtils .getClassSymbolFromDef (classDef )).stream ()
126
+ .anyMatch (classSym -> classSym .superClasses ().size () == 1 );
100
127
}
101
128
102
129
private static boolean overrideExistingMethod (String functionName ) {
103
130
return UnittestUtils .allMethods ().contains (functionName ) || functionName .startsWith ("_" );
104
131
}
105
132
133
+ private static boolean isHelper (FunctionDef functionDef , Set <String > currentClassFixture ) {
134
+ return Optional .ofNullable (TreeUtils .getFunctionSymbolFromDef (functionDef )).stream ()
135
+ .anyMatch (functionSymbol -> functionSymbol .hasDecorators () || !functionSymbol .parameters ().stream ()
136
+ .map (FunctionSymbol .Parameter ::name )
137
+ .filter (Objects ::nonNull )
138
+ .allMatch (name -> name .equals ("self" ) || globalFixture .contains (name ) || currentClassFixture .contains (name )));
139
+ }
140
+
141
+ @ Override
142
+ public CheckScope scope () {
143
+ return CheckScope .ALL ;
144
+ }
106
145
}
0 commit comments