1313use PhpParser \Node \Stmt \ClassMethod ;
1414use PHPStan \Reflection \ReflectionProvider ;
1515use Rector \Doctrine \NodeAnalyzer \AttributeFinder ;
16+ use Rector \PhpParser \Node \BetterNodeFinder ;
17+ use Rector \PhpParser \NodeFinder \PropertyFetchFinder ;
1618use Rector \PHPUnit \Enum \PHPUnitAttribute ;
1719use Rector \PHPUnit \Enum \PHPUnitClassName ;
1820use Rector \PHPUnit \NodeAnalyzer \TestsNodeAnalyzer ;
@@ -31,7 +33,8 @@ final class AllowMockObjectsWithoutExpectationsAttributeRector extends AbstractR
3133 public function __construct (
3234 private readonly TestsNodeAnalyzer $ testsNodeAnalyzer ,
3335 private readonly AttributeFinder $ attributeFinder ,
34- private readonly ReflectionProvider $ reflectionProvider
36+ private readonly ReflectionProvider $ reflectionProvider ,
37+ private readonly BetterNodeFinder $ betterNodeFinder ,
3538 ) {
3639 }
3740
@@ -57,18 +60,29 @@ public function refactor(Node $node): ?Class_
5760 }
5861
5962 // @todo add the attribute if has more than 1 public test* method
63+ $ missedTestMethodsByMockPropertyName = [];
6064 $ testMethodCount = 0 ;
6165
62- foreach ($ node ->getMethods () as $ classMethod ) {
63- if ($ this ->testsNodeAnalyzer ->isTestClassMethod ($ classMethod )) {
64- // is a mock property used in the method?
66+ foreach ($ mockObjectPropertyNames as $ mockObjectPropertyName ) {
67+ $ missedTestMethodsByMockPropertyName [$ mockObjectPropertyName ] = [];
68+
69+ foreach ($ node ->getMethods () as $ classMethod ) {
70+ if (! $ this ->testsNodeAnalyzer ->isTestClassMethod ($ classMethod )) {
71+ continue ;
72+ }
73+
74+ // is a mock property used in the class method, as part of some method call? guessing mock expectation is set
6575 // skip if so
76+ if ($ this ->isClassMethodUsingMethodCallOnPropertyNamed ($ classMethod , $ mockObjectPropertyName )) {
77+ continue ;
78+ }
6679
80+ $ missedTestMethodsByMockPropertyName [][] = $ this ->getName ($ classMethod );
6781 ++$ testMethodCount ;
6882 }
6983 }
7084
71- if ($ testMethodCount < 2 ) {
85+ if (! $ this -> shouldAddAttribute ( $ missedTestMethodsByMockPropertyName ) ) {
7286 return null ;
7387 }
7488
@@ -187,4 +201,38 @@ private function shouldSkipClass(Class_ $class): bool
187201 $ setupClassMethod = $ class ->getMethod (MethodName::SET_UP );
188202 return ! $ setupClassMethod instanceof ClassMethod;
189203 }
204+
205+ private function isClassMethodUsingMethodCallOnPropertyNamed (ClassMethod $ classMethod , string $ mockObjectPropertyName ): bool
206+ {
207+ /** @var Node\Expr\MethodCall[] $methodCalls */
208+ $ methodCalls = $ this ->betterNodeFinder ->findInstancesOfScoped ([$ classMethod ], [Node \Expr \MethodCall::class]);
209+ foreach ($ methodCalls as $ methodCall ) {
210+ if (!$ methodCall ->var instanceof Node \Expr \PropertyFetch) {
211+ continue ;
212+ }
213+
214+ $ propertyFetch = $ methodCall ->var ;
215+
216+ // we found a method call on a property fetch named
217+ if ($ this ->isName ($ propertyFetch , $ mockObjectPropertyName )) {
218+ return true ;
219+ }
220+ }
221+
222+ return false ;
223+ }
224+
225+ private function shouldAddAttribute (array $ missedTestMethodsByMockPropertyName ): bool
226+ {
227+ foreach ($ missedTestMethodsByMockPropertyName as $ propertyName => $ missedTestMethods ) {
228+ // all test methods are using method calls on the mock property, so skip
229+ if (count ($ missedTestMethods ) === 0 ) {
230+ continue ;
231+ }
232+
233+ return true ;
234+ }
235+
236+ return false ;
237+ }
190238}
0 commit comments