2727import org .sonar .check .RuleProperty ;
2828import org .sonar .java .checks .helpers .AbstractAssertionVisitor ;
2929import org .sonar .java .model .ModifiersUtils ;
30+ import org .sonar .java .model .declaration .ClassTreeImpl ;
31+ import org .sonar .java .model .expression .MethodInvocationTreeImpl ;
3032import org .sonar .plugins .java .api .JavaFileScanner ;
3133import org .sonar .plugins .java .api .JavaFileScannerContext ;
3234import org .sonar .plugins .java .api .semantic .MethodMatchers ;
3335import org .sonar .plugins .java .api .semantic .Symbol ;
3436import org .sonar .plugins .java .api .semantic .SymbolMetadata ;
37+ import org .sonar .plugins .java .api .semantic .Type ;
3538import org .sonar .plugins .java .api .tree .BaseTreeVisitor ;
3639import org .sonar .plugins .java .api .tree .ClassTree ;
40+ import org .sonar .plugins .java .api .tree .MethodInvocationTree ;
3741import org .sonar .plugins .java .api .tree .MethodTree ;
3842import org .sonar .plugins .java .api .tree .Modifier ;
3943import org .sonar .plugins .java .api .tree .Tree ;
@@ -54,6 +58,13 @@ public class AssertionsInTestsCheck extends BaseTreeVisitor implements JavaFileS
5458 public String customAssertionMethods = "" ;
5559 private MethodMatchers customAssertionMethodsMatcher = null ;
5660
61+ private static final MethodMatchers SPRING_BOOT_APP_CTX_RUNNER_RUN_MATCHER = MethodMatchers .create ()
62+ .ofTypes ("org.springframework.boot.test.context.runner.ApplicationContextRunner" )
63+ .names ("run" )
64+ .addParametersMatcher ("org.springframework.boot.test.context.runner.ContextConsumer" )
65+ .build ();
66+ private static final MethodInvocationMatcherVisitor SPRING_BOOT_APP_CTX_RUNNER_VISITOR = new MethodInvocationMatcherVisitor (SPRING_BOOT_APP_CTX_RUNNER_RUN_MATCHER );
67+
5768 private final Map <Symbol , Boolean > assertionInMethod = new HashMap <>();
5869 private JavaFileScannerContext context ;
5970
@@ -75,13 +86,86 @@ public void visitMethod(MethodTree methodTree) {
7586 return ;
7687 }
7788
78- if (isUnitTest (methodTree ) && !isSpringBootSanityTest (methodTree ) && !expectAssertion (methodTree ) && !isLocalMethodWithAssertion (methodTree .symbol ())) {
79- context .reportIssue (this , methodTree .simpleName (), "Add at least one assertion to this test case." );
89+ if (isUnitTest (methodTree )) {
90+ if (isSpringBootAssertableContext (methodTree )) {
91+ return ;
92+ }
93+ if (!isSpringBootSanityTest (methodTree ) && !expectAssertion (methodTree ) && !isLocalMethodWithAssertion (methodTree .symbol ())) {
94+ context .reportIssue (this , methodTree .simpleName (), "Add at least one assertion to this test case." );
95+ }
96+ }
97+ }
98+
99+ private boolean isSpringBootAssertableContext (MethodTree methodTree ) {
100+ var runMethodInvocation = SPRING_BOOT_APP_CTX_RUNNER_VISITOR .findMethodInvocation (methodTree );
101+ if (runMethodInvocation != null ) {
102+ var contextConsumerImplSymbol = runMethodInvocation .arguments ().get (0 ).symbolType ().symbol ();
103+ if (contextConsumerImplSymbol .isUnknown ()) {
104+ // In this case we cannot know if the provided ContextConsumer has the type param <AssertableApplicationContext>, but we want to avoid FPs
105+ return true ;
106+ }
107+ Type contextConsumerType ;
108+ if (contextConsumerImplSymbol .isInterface ()) {
109+ contextConsumerType = contextConsumerImplSymbol .type ();
110+ } else {
111+ contextConsumerType = contextConsumerImplSymbol .interfaces ().get (0 );
112+ }
113+ return isAssertableApplicationContext (contextConsumerType ) && hasDeclaredAssertions (contextConsumerImplSymbol );
114+ }
115+ return false ;
116+ }
117+
118+ private static boolean isAssertableApplicationContext (Type contextConsumerType ) {
119+ return contextConsumerType .typeArguments ().get (0 ).is ("org.springframework.boot.test.context.assertj.AssertableApplicationContext" );
120+ }
121+
122+ /**
123+ * Takes a Symbol as input and checks if it has a declaring class available. If so, it will also check that the class has at least
124+ * one method with an assertion.
125+ * Used by {@link #isSpringBootAssertableContext(MethodTree)} to check if a ContextConsumer of AssertableApplicationContext
126+ * has at least an assertion in its methods.
127+ * @param contextConsumerImplSymbol The symbol for which we want to check the declaration of
128+ * @return true if the symbol has no declaration (to avoid FPs), or if its declaration is a class with at least one method with assertions
129+ */
130+ private boolean hasDeclaredAssertions (Symbol contextConsumerImplSymbol ) {
131+ Tree declaration = contextConsumerImplSymbol .declaration ();
132+ if (declaration instanceof ClassTreeImpl contextConsumerImpl ) {
133+ return contextConsumerImpl .members ().stream ()
134+ .anyMatch (m -> m instanceof MethodTree method && isLocalMethodWithAssertion (method .symbol ()));
135+ }
136+ return true ;
137+ }
138+
139+ /**
140+ * Finds the first nested method invocation in a tree that matches the MethodMatchers provided in the constructor
141+ */
142+ private static class MethodInvocationMatcherVisitor extends BaseTreeVisitor {
143+
144+ private MethodInvocationTreeImpl methodInvocationTree ;
145+ private final MethodMatchers matcher ;
146+
147+ private MethodInvocationMatcherVisitor (MethodMatchers matcher ) {
148+ this .matcher = matcher ;
149+ }
150+
151+ @ Override
152+ public void visitMethodInvocation (MethodInvocationTree tree ) {
153+ if (matcher .matches (tree )) {
154+ methodInvocationTree = (MethodInvocationTreeImpl ) tree ;
155+ return ;
156+ }
157+ super .visitMethodInvocation (tree );
158+ }
159+
160+ public MethodInvocationTreeImpl findMethodInvocation (Tree tree ) {
161+ methodInvocationTree = null ;
162+ tree .accept (this );
163+ return methodInvocationTree ;
80164 }
81165 }
82166
83- private static boolean isSpringBootSanityTest (MethodTree methodTree ){
84- if ("contextLoads" .equals (methodTree .simpleName ().name ())){
167+ private static boolean isSpringBootSanityTest (MethodTree methodTree ) {
168+ if ("contextLoads" .equals (methodTree .simpleName ().name ())) {
85169 ClassTree classTree = (ClassTree ) methodTree .parent ();
86170 return classTree .symbol ().metadata ().isAnnotatedWith ("org.springframework.boot.test.context.SpringBootTest" );
87171 }
0 commit comments