From 167ec7b0ea0375e85282f62f575a61841b6b4550 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 27 Jan 2026 12:36:41 +0100 Subject: [PATCH 1/3] [phpunit 12] Add AllowMockObjectsWithoutExpectationsAttributeRector --- config/sets/phpunit120.php | 5 + ...WithoutExpectationsAttributeRectorTest.php | 28 +++ .../Fixture/skip_only_single_test.php.inc | 19 ++ .../Fixture/some_class.php.inc | 54 ++++++ .../config/configured_rule.php | 9 + ...ectsWithoutExpectationsAttributeRector.php | 170 ++++++++++++++++++ src/Enum/PHPUnitAttribute.php | 5 + src/Enum/PHPUnitClassName.php | 2 + 8 files changed, 292 insertions(+) create mode 100644 rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/AllowMockObjectsWithoutExpectationsAttributeRectorTest.php create mode 100644 rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/skip_only_single_test.php.inc create mode 100644 rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/some_class.php.inc create mode 100644 rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/config/configured_rule.php create mode 100644 rules/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector.php diff --git a/config/sets/phpunit120.php b/config/sets/phpunit120.php index 611a42b8..cd06fef1 100644 --- a/config/sets/phpunit120.php +++ b/config/sets/phpunit120.php @@ -4,6 +4,7 @@ use Rector\Config\RectorConfig; use Rector\PHPUnit\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector; +use Rector\PHPUnit\PHPUnit120\Rector\Class_\AllowMockObjectsWithoutExpectationsAttributeRector; use Rector\PHPUnit\PHPUnit120\Rector\Class_\AssertIsTypeMethodCallRector; use Rector\PHPUnit\PHPUnit120\Rector\Class_\RemoveOverrideFinalConstructTestCaseRector; @@ -14,5 +15,9 @@ // stubs over mocks CreateStubOverCreateMockArgRector::class, + + // experimental, from PHPUnit 12.5.2 + // @see https://github.com/sebastianbergmann/phpunit/commit/24c208d6a340c3071f28a9b5cce02b9377adfd43 + // AllowMockObjectsWithoutExpectationsAttributeRector::class, ]); }; diff --git a/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/AllowMockObjectsWithoutExpectationsAttributeRectorTest.php b/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/AllowMockObjectsWithoutExpectationsAttributeRectorTest.php new file mode 100644 index 00000000..1d6c75c6 --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/AllowMockObjectsWithoutExpectationsAttributeRectorTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/skip_only_single_test.php.inc b/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/skip_only_single_test.php.inc new file mode 100644 index 00000000..106dfbbe --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/skip_only_single_test.php.inc @@ -0,0 +1,19 @@ +someMock = $this->createMock(\stdClass::class); + } + + public function testOne() + { + } +} diff --git a/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/some_class.php.inc b/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/some_class.php.inc new file mode 100644 index 00000000..a6e639b1 --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/some_class.php.inc @@ -0,0 +1,54 @@ +someMock = $this->createMock(\stdClass::class); + } + + public function testOne() + { + } + + public function testTwo() + { + + } +} + +?> +----- +someMock = $this->createMock(\stdClass::class); + } + + public function testOne() + { + } + + public function testTwo() + { + + } +} + +?> diff --git a/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/config/configured_rule.php b/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/config/configured_rule.php new file mode 100644 index 00000000..baae2bb5 --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/config/configured_rule.php @@ -0,0 +1,9 @@ +withRules([AllowMockObjectsWithoutExpectationsAttributeRector::class]); diff --git a/rules/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector.php b/rules/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector.php new file mode 100644 index 00000000..7261d589 --- /dev/null +++ b/rules/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector.php @@ -0,0 +1,170 @@ +testsNodeAnalyzer->isInTestClass($node)) { + return null; + } + + // attribute must exist for the rule to work + if (! $this->reflectionProvider->hasClass(PHPUnitAttribute::ALLOW_MOCK_OBJECTS_WITHOUT_EXPECTATIONS)) { + return null; + } + + // already filled + if ($this->attributeFinder->hasAttributeByClasses( + $node, + [PHPUnitAttribute::ALLOW_MOCK_OBJECTS_WITHOUT_EXPECTATIONS] + )) { + return null; + } + + // has mock objects properties and setUp() method? + + if (! $node->getMethod('setUp') instanceof ClassMethod) { + return null; + } + + if (! $this->hasMockObjectProperty($node)) { + return null; + } + + // @todo add the attribute if has more than 1 public test* method + $testMethodCount = 0; + + foreach ($node->getMethods() as $classMethod) { + if ($this->testsNodeAnalyzer->isTestClassMethod($classMethod)) { + ++$testMethodCount; + } + } + + if ($testMethodCount < 2) { + return null; + } + + // add attribute + $node->attrGroups[] = new AttributeGroup([ + new Attribute(new FullyQualified(PHPUnitAttribute::ALLOW_MOCK_OBJECTS_WITHOUT_EXPECTATIONS)), + ]); + + return $node; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Add #[AllowMockObjectsWithoutExpectations] attribute to PHPUnit test classes with mock properties used in multiple methods', + [ + new CodeSample( + <<<'CODE_SAMPLE' +use PHPUnit\Framework\TestCase; +final class SomeTest extends TestCase +{ + private \PHPUnit\Framework\MockObject\MockObject $someServiceMock; + + protected function setUp(): void + { + $this->someServiceMock = $this->createMock(SomeService::class); + } + + public function testOne(): void + { + // use $this->someServiceMock + } + + public function testTwo(): void + { + // use $this->someServiceMock + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; + +#[AllowMockObjectsWithoutExpectations] +final class SomeTest extends TestCase +{ + private \PHPUnit\Framework\MockObject\MockObject $someServiceMock; + + protected function setUp(): void + { + $this->someServiceMock = $this->createMock(SomeService::class); + } + + public function testOne(): void + { + // use $this->someServiceMock + } + + public function testTwo(): void + { + // use $this->someServiceMock + } +} +CODE_SAMPLE + ), + + ] + ); + + } + + private function hasMockObjectProperty(Class_ $class): bool + { + foreach ($class->getProperties() as $property) { + if (! $property->type instanceof Name) { + continue; + } + + if ($this->isName($property->type, PHPUnitClassName::MOCK_OBJECT)) { + return true; + } + } + + return false; + } +} diff --git a/src/Enum/PHPUnitAttribute.php b/src/Enum/PHPUnitAttribute.php index bf09c0fa..a540f7f6 100644 --- a/src/Enum/PHPUnitAttribute.php +++ b/src/Enum/PHPUnitAttribute.php @@ -23,4 +23,9 @@ final class PHPUnitAttribute public const string REQUIRES_SETTING = 'PHPUnit\Framework\Attributes\RequiresSetting'; public const string TEST = 'PHPUnit\Framework\Attributes\Test'; + + /** + * @see https://github.com/sebastianbergmann/phpunit/commit/24c208d6a340c3071f28a9b5cce02b9377adfd43 + */ + public const string ALLOW_MOCK_OBJECTS_WITHOUT_EXPECTATIONS = 'PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations'; } diff --git a/src/Enum/PHPUnitClassName.php b/src/Enum/PHPUnitClassName.php index 51a4ee21..5171edfa 100644 --- a/src/Enum/PHPUnitClassName.php +++ b/src/Enum/PHPUnitClassName.php @@ -23,6 +23,8 @@ final class PHPUnitClassName public const string TEST_LISTENER = 'PHPUnit\Framework\TestListener'; + public const string MOCK_OBJECT = 'PHPUnit\Framework\MockObject\MockObject'; + /** * @var string[] */ From b7c30876625e2e1ec3e65605c8a7c510e7db895f Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 27 Jan 2026 12:58:22 +0100 Subject: [PATCH 2/3] add fixtures to handle --- ..._tests_methods_define_expectations.php.inc | 25 +++++++ ...if_mock_not_used_in_2_test_methods.php.inc | 24 +++++++ .../Fixture/some_class.php.inc | 4 +- ...ectsWithoutExpectationsAttributeRector.php | 68 ++++++++++++------- 4 files changed, 95 insertions(+), 26 deletions(-) create mode 100644 rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/skip_if_all_tests_methods_define_expectations.php.inc create mode 100644 rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/skip_if_mock_not_used_in_2_test_methods.php.inc diff --git a/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/skip_if_all_tests_methods_define_expectations.php.inc b/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/skip_if_all_tests_methods_define_expectations.php.inc new file mode 100644 index 00000000..da0e0722 --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/skip_if_all_tests_methods_define_expectations.php.inc @@ -0,0 +1,25 @@ +someMock = $this->createMock(\stdClass::class); + } + + public function testOne() + { + $this->someMock->method('doSomething')->willReturn('value'); + } + + public function testTwo() + { + $this->someMock->method('doSomethingElse')->willReturn('another value'); + } +} diff --git a/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/skip_if_mock_not_used_in_2_test_methods.php.inc b/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/skip_if_mock_not_used_in_2_test_methods.php.inc new file mode 100644 index 00000000..2d61aa44 --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/skip_if_mock_not_used_in_2_test_methods.php.inc @@ -0,0 +1,24 @@ +someMock = $this->createMock(\stdClass::class); + } + + public function testOne() + { + } + + public function testTwo() + { + + } +} diff --git a/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/some_class.php.inc b/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/some_class.php.inc index a6e639b1..18f0548d 100644 --- a/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/some_class.php.inc +++ b/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/some_class.php.inc @@ -15,11 +15,11 @@ final class SomeClass extends TestCase public function testOne() { + $this->someMock->method('doSomething')->willReturn('value'); } public function testTwo() { - } } @@ -43,11 +43,11 @@ final class SomeClass extends TestCase public function testOne() { + $this->someMock->method('doSomething')->willReturn('value'); } public function testTwo() { - } } diff --git a/rules/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector.php b/rules/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector.php index 7261d589..544afa53 100644 --- a/rules/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector.php +++ b/rules/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector.php @@ -17,6 +17,7 @@ use Rector\PHPUnit\Enum\PHPUnitClassName; use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer; use Rector\Rector\AbstractRector; +use Rector\ValueObject\MethodName; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -44,30 +45,14 @@ public function getNodeTypes(): array */ public function refactor(Node $node): ?Class_ { - if (! $this->testsNodeAnalyzer->isInTestClass($node)) { + if ($this->shouldSkipClass($node)) { return null; } - // attribute must exist for the rule to work - if (! $this->reflectionProvider->hasClass(PHPUnitAttribute::ALLOW_MOCK_OBJECTS_WITHOUT_EXPECTATIONS)) { - return null; - } - - // already filled - if ($this->attributeFinder->hasAttributeByClasses( - $node, - [PHPUnitAttribute::ALLOW_MOCK_OBJECTS_WITHOUT_EXPECTATIONS] - )) { - return null; - } - - // has mock objects properties and setUp() method? - - if (! $node->getMethod('setUp') instanceof ClassMethod) { - return null; - } + $mockObjectPropertyNames = $this->matchMockObjectPropertyNames($node); - if (! $this->hasMockObjectProperty($node)) { + // there are no mock object properties + if ($mockObjectPropertyNames === []) { return null; } @@ -76,6 +61,9 @@ public function refactor(Node $node): ?Class_ foreach ($node->getMethods() as $classMethod) { if ($this->testsNodeAnalyzer->isTestClassMethod($classMethod)) { + // is a mock property used in the method? + // skip if so + ++$testMethodCount; } } @@ -153,18 +141,50 @@ public function testTwo(): void } - private function hasMockObjectProperty(Class_ $class): bool + /** + * @return string[] + */ + private function matchMockObjectPropertyNames(Class_ $class): array { + $propertyNames = []; + foreach ($class->getProperties() as $property) { if (! $property->type instanceof Name) { continue; } - if ($this->isName($property->type, PHPUnitClassName::MOCK_OBJECT)) { - return true; + if (! $this->isName($property->type, PHPUnitClassName::MOCK_OBJECT)) { + continue; } + + $propertyNames[] = $this->getName($property->props[0]); + } + + return $propertyNames; + } + + private function shouldSkipClass(Class_ $class): bool + { + if (! $this->testsNodeAnalyzer->isInTestClass($class)) { + return true; } - return false; + // attribute must exist for the rule to work + if (! $this->reflectionProvider->hasClass(PHPUnitAttribute::ALLOW_MOCK_OBJECTS_WITHOUT_EXPECTATIONS)) { + return true; + } + + // already filled + if ($this->attributeFinder->hasAttributeByClasses( + $class, + [PHPUnitAttribute::ALLOW_MOCK_OBJECTS_WITHOUT_EXPECTATIONS] + )) { + return true; + } + + // has mock objects properties and setUp() method? + + $setupClassMethod = $class->getMethod(MethodName::SET_UP); + return ! $setupClassMethod instanceof ClassMethod; } } From 236d1865fb0f7c19c5fc7d15c6c42bfe044c53cf Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 27 Jan 2026 15:00:13 +0100 Subject: [PATCH 3/3] addd attribute if msising method --- config/sets/phpunit120.php | 1 - ...if_mock_not_used_in_2_test_methods.php.inc | 1 - ...ectsWithoutExpectationsAttributeRector.php | 104 ++++++++++++++++-- 3 files changed, 93 insertions(+), 13 deletions(-) diff --git a/config/sets/phpunit120.php b/config/sets/phpunit120.php index cd06fef1..5990bd7b 100644 --- a/config/sets/phpunit120.php +++ b/config/sets/phpunit120.php @@ -4,7 +4,6 @@ use Rector\Config\RectorConfig; use Rector\PHPUnit\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector; -use Rector\PHPUnit\PHPUnit120\Rector\Class_\AllowMockObjectsWithoutExpectationsAttributeRector; use Rector\PHPUnit\PHPUnit120\Rector\Class_\AssertIsTypeMethodCallRector; use Rector\PHPUnit\PHPUnit120\Rector\Class_\RemoveOverrideFinalConstructTestCaseRector; diff --git a/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/skip_if_mock_not_used_in_2_test_methods.php.inc b/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/skip_if_mock_not_used_in_2_test_methods.php.inc index 2d61aa44..a0e82d7a 100644 --- a/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/skip_if_mock_not_used_in_2_test_methods.php.inc +++ b/rules-tests/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector/Fixture/skip_if_mock_not_used_in_2_test_methods.php.inc @@ -19,6 +19,5 @@ final class SkipIfMockNotUsedIn2TestMethods extends TestCase public function testTwo() { - } } diff --git a/rules/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector.php b/rules/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector.php index 544afa53..14c23faf 100644 --- a/rules/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector.php +++ b/rules/PHPUnit120/Rector/Class_/AllowMockObjectsWithoutExpectationsAttributeRector.php @@ -7,12 +7,15 @@ use PhpParser\Node; use PhpParser\Node\Attribute; use PhpParser\Node\AttributeGroup; +use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Name; use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use PHPStan\Reflection\ReflectionProvider; use Rector\Doctrine\NodeAnalyzer\AttributeFinder; +use Rector\PhpParser\Node\BetterNodeFinder; use Rector\PHPUnit\Enum\PHPUnitAttribute; use Rector\PHPUnit\Enum\PHPUnitClassName; use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer; @@ -31,7 +34,8 @@ final class AllowMockObjectsWithoutExpectationsAttributeRector extends AbstractR public function __construct( private readonly TestsNodeAnalyzer $testsNodeAnalyzer, private readonly AttributeFinder $attributeFinder, - private readonly ReflectionProvider $reflectionProvider + private readonly ReflectionProvider $reflectionProvider, + private readonly BetterNodeFinder $betterNodeFinder, ) { } @@ -56,22 +60,45 @@ public function refactor(Node $node): ?Class_ return null; } - // @todo add the attribute if has more than 1 public test* method + $missedTestMethodsByMockPropertyName = []; + $usingTestMethodsByMockPropertyName = []; $testMethodCount = 0; - foreach ($node->getMethods() as $classMethod) { - if ($this->testsNodeAnalyzer->isTestClassMethod($classMethod)) { - // is a mock property used in the method? - // skip if so + foreach ($mockObjectPropertyNames as $mockObjectPropertyName) { + $missedTestMethodsByMockPropertyName[$mockObjectPropertyName] = []; + $usingTestMethodsByMockPropertyName[$mockObjectPropertyName] = []; + + foreach ($node->getMethods() as $classMethod) { + if (! $this->testsNodeAnalyzer->isTestClassMethod($classMethod)) { + continue; + } ++$testMethodCount; + + // is a mock property used in the class method, as part of some method call? guessing mock expectation is set + // skip if so + if ($this->isClassMethodUsingMethodCallOnPropertyNamed($classMethod, $mockObjectPropertyName)) { + $usingTestMethodsByMockPropertyName[$mockObjectPropertyName][] = $this->getName($classMethod); + continue; + } + + $missedTestMethodsByMockPropertyName[$mockObjectPropertyName][] = $this->getName($classMethod); } } + if (! $this->shouldAddAttribute($missedTestMethodsByMockPropertyName)) { + return null; + } + + // skip sole test method, as those are expected to use all mocks if ($testMethodCount < 2) { return null; } + if (! $this->isAtLeastOneMockPropertyMockedOnce($usingTestMethodsByMockPropertyName)) { + return null; + } + // add attribute $node->attrGroups[] = new AttributeGroup([ new Attribute(new FullyQualified(PHPUnitAttribute::ALLOW_MOCK_OBJECTS_WITHOUT_EXPECTATIONS)), @@ -83,7 +110,7 @@ public function refactor(Node $node): ?Class_ public function getRuleDefinition(): RuleDefinition { return new RuleDefinition( - 'Add #[AllowMockObjectsWithoutExpectations] attribute to PHPUnit test classes with mock properties used in multiple methods', + 'Add #[AllowMockObjectsWithoutExpectations] attribute to PHPUnit test classes with mock properties used in multiple methods but one, to avoid irrelevant notices in tests run', [ new CodeSample( <<<'CODE_SAMPLE' @@ -125,20 +152,22 @@ protected function setUp(): void public function testOne(): void { - // use $this->someServiceMock + $this->someServiceMock->expects($this->once()) + ->method('someMethod') + ->willReturn('someValue'); } public function testTwo(): void { - // use $this->someServiceMock + $this->someServiceMock->expects($this->once()) + ->method('someMethod') + ->willReturn('anotherValue'); } } CODE_SAMPLE ), - ] ); - } /** @@ -187,4 +216,57 @@ private function shouldSkipClass(Class_ $class): bool $setupClassMethod = $class->getMethod(MethodName::SET_UP); return ! $setupClassMethod instanceof ClassMethod; } + + private function isClassMethodUsingMethodCallOnPropertyNamed( + ClassMethod $classMethod, + string $mockObjectPropertyName + ): bool { + /** @var MethodCall[] $methodCalls */ + $methodCalls = $this->betterNodeFinder->findInstancesOfScoped([$classMethod], [MethodCall::class]); + foreach ($methodCalls as $methodCall) { + if (! $methodCall->var instanceof PropertyFetch) { + continue; + } + + $propertyFetch = $methodCall->var; + + // we found a method call on a property fetch named + if ($this->isName($propertyFetch, $mockObjectPropertyName)) { + return true; + } + } + + return false; + } + + /** + * @param array $missedTestMethodsByMockPropertyName + */ + private function shouldAddAttribute(array $missedTestMethodsByMockPropertyName): bool + { + foreach ($missedTestMethodsByMockPropertyName as $missedTestMethods) { + // all test methods are using method calls on the mock property, so skip + if (count($missedTestMethods) === 0) { + continue; + } + + return true; + } + + return false; + } + + /** + * @param array $usingTestMethodsByMockPropertyName + */ + private function isAtLeastOneMockPropertyMockedOnce(array $usingTestMethodsByMockPropertyName): bool + { + foreach ($usingTestMethodsByMockPropertyName as $usingTestMethods) { + if (count($usingTestMethods) !== 0) { + return true; + } + } + + return false; + } }