diff --git a/rules-tests/AnnotationsToAttributes/Rector/ClassMethod/DataProviderAnnotationToAttributeRector/Fixture/in_test_trait.php.inc b/rules-tests/AnnotationsToAttributes/Rector/ClassMethod/DataProviderAnnotationToAttributeRector/Fixture/in_test_trait.php.inc new file mode 100644 index 00000000..80b3a76c --- /dev/null +++ b/rules-tests/AnnotationsToAttributes/Rector/ClassMethod/DataProviderAnnotationToAttributeRector/Fixture/in_test_trait.php.inc @@ -0,0 +1,29 @@ + +----- + diff --git a/rules-tests/AnnotationsToAttributes/Rector/ClassMethod/DataProviderAnnotationToAttributeRector/Fixture/skip_non_public_trait_method.php.inc b/rules-tests/AnnotationsToAttributes/Rector/ClassMethod/DataProviderAnnotationToAttributeRector/Fixture/skip_non_public_trait_method.php.inc new file mode 100644 index 00000000..907a7df0 --- /dev/null +++ b/rules-tests/AnnotationsToAttributes/Rector/ClassMethod/DataProviderAnnotationToAttributeRector/Fixture/skip_non_public_trait_method.php.inc @@ -0,0 +1,13 @@ +reflectionResolver->resolveClassReflection($node); - if (! $classReflection instanceof ClassReflection) { - return null; - } - - if (! $classReflection->isClass()) { - return null; - } - foreach ($desiredTagValueNodes as $desiredTagValueNode) { if (! $desiredTagValueNode->value instanceof GenericTagValueNode && ! $desiredTagValueNode->value instanceof DoctrineAnnotationTagValueNode) { continue; diff --git a/src/NodeAnalyzer/TestsNodeAnalyzer.php b/src/NodeAnalyzer/TestsNodeAnalyzer.php index b6cc8d8f..cf3d2790 100644 --- a/src/NodeAnalyzer/TestsNodeAnalyzer.php +++ b/src/NodeAnalyzer/TestsNodeAnalyzer.php @@ -35,6 +35,11 @@ public function isInTestClass(Node $node): bool return false; } + // traits have no parent, so the test case check below can never match them + if ($classReflection->isTrait()) { + return $this->isInTestTrait($classReflection, $node); + } + return array_any( PHPUnitClassName::TEST_CLASSES, fn (string $testCaseObjectClass): bool => $classReflection->is($testCaseObjectClass) @@ -83,6 +88,45 @@ public function isAssertMethodCallName(Node $node, string $name): bool return $this->nodeNameResolver->isName($node->name, $name); } + /** + * Test traits live next to the test cases that use them, so the namespace is the main hint. + * Only public non-static methods can be test methods, and a "test" prefixed one is a test + * method even outside a tests namespace. + */ + private function isInTestTrait(ClassReflection $classReflection, Node $node): bool + { + if (! $node instanceof ClassMethod) { + return $this->isInTestsNamespace($classReflection); + } + + if (! $node->isPublic()) { + return false; + } + + if ($node->isStatic()) { + return false; + } + + if ($this->isInTestsNamespace($classReflection)) { + return true; + } + + return str_starts_with($node->name->toString(), 'test'); + } + + private function isInTestsNamespace(ClassReflection $classReflection): bool + { + $nameParts = explode('\\', $classReflection->getName()); + + // drop the short trait name, only the namespace matters here + array_pop($nameParts); + + return array_any( + $nameParts, + static fn (string $namePart): bool => in_array($namePart, ['Test', 'Tests'], true) + ); + } + /** * @param string[] $names */