2323import java .util .HashSet ;
2424import java .util .List ;
2525import java .util .Map ;
26+ import java .util .Objects ;
2627import java .util .Optional ;
2728import java .util .Set ;
29+ import java .util .stream .Collectors ;
2830import org .sonar .check .Rule ;
2931import org .sonar .plugins .python .api .PythonSubscriptionCheck ;
3032import org .sonar .plugins .python .api .SubscriptionContext ;
31- import org .sonar .plugins .python .api .symbols .ClassSymbol ;
3233import org .sonar .plugins .python .api .symbols .FunctionSymbol ;
3334import org .sonar .plugins .python .api .symbols .Symbol ;
3435import org .sonar .plugins .python .api .symbols .Usage ;
3536import org .sonar .plugins .python .api .tree .ClassDef ;
37+ import org .sonar .plugins .python .api .tree .Decorator ;
3638import org .sonar .plugins .python .api .tree .FunctionDef ;
3739import org .sonar .plugins .python .api .tree .Statement ;
3840import org .sonar .plugins .python .api .tree .Tree ;
4345public class NotDiscoverableTestMethodCheck extends PythonSubscriptionCheck {
4446
4547 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 <>();
4649
4750 @ Override
4851 public void initialize (Context context ) {
52+ context .registerSyntaxNodeConsumer (Tree .Kind .FUNCDEF , ctx -> NotDiscoverableTestMethodCheck .lookForGlobalFixture ((FunctionDef ) ctx .syntaxNode ()));
53+
4954 context .registerSyntaxNodeConsumer (Tree .Kind .CLASSDEF , ctx -> {
5055 ClassDef classDefinition = (ClassDef ) ctx .syntaxNode ();
5156
5257 if (inheritsOnlyFromUnitTest (classDefinition )) {
5358 Map <FunctionSymbol , FunctionDef > suspiciousFunctionsAndDefinitions = new HashMap <>();
5459 Set <Tree > allDefinitions = new HashSet <>();
60+ Set <String > classFixtures = getFixturesFromClass (classDefinition );
5561 // We only consider method definitions, and not nested functions
5662 for (Statement statement : classDefinition .body ().statements ()) {
5763 if (statement .is (Tree .Kind .FUNCDEF )) {
5864 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 ));
6669 }
6770 allDefinitions .add (functionDef );
6871 }
@@ -73,9 +76,38 @@ public void initialize(Context context) {
7376 });
7477 }
7578
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 );
79111 }
80112
81113 // 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<
88120 });
89121 }
90122
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 );
100127 }
101128
102129 private static boolean overrideExistingMethod (String functionName ) {
103130 return UnittestUtils .allMethods ().contains (functionName ) || functionName .startsWith ("_" );
104131 }
105132
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+ }
106145}
0 commit comments