diff --git a/src/Type/Pest/PestTestCaseType.php b/src/Type/Pest/PestTestCaseType.php index 2971d87..d034495 100644 --- a/src/Type/Pest/PestTestCaseType.php +++ b/src/Type/Pest/PestTestCaseType.php @@ -25,17 +25,7 @@ public function resolve(string $filePath): Type public function resolveIfBound(string $filePath): ?Type { // @note: returns null when no binding covers the file, so callers can tell an unbound file from one bound to PHPUnit's TestCase. - [$classNames, $traitNames] = $this->partition( - $this->pestConfigReader->resolveFileBindings($filePath), - ); - - if ($classNames === []) { - [$classNames, $directoryTraitNames] = $this->partition( - $this->pestConfigReader->resolveBindings($filePath), - ); - - $traitNames = array_values(array_unique([...$directoryTraitNames, ...$traitNames])); - } + [$classNames, $traitNames] = $this->resolveFileClassAndTraitNames($filePath); if ($classNames === [] && $traitNames === []) { return null; @@ -45,17 +35,19 @@ public function resolveIfBound(string $filePath): ?Type $classNames[] = TestCase::class; } - $classType = $this->toObjectType($classNames); + return $this->toTestCaseType($classNames, $traitNames); + } - if ($traitNames === []) { - return $classType; + public function resolveIfClassBound(string $filePath): ?Type + { + // @note: returns null unless a binding covers the file with a test case class, as traits bound without in() also cover files that are not tests. + [$classNames, $traitNames] = $this->resolveFileClassAndTraitNames($filePath); + + if ($classNames === []) { + return null; } - return new PestTestCaseWithTraitsType( - $classNames[0], - $traitNames, - $this->reflectionProvider, - ); + return $this->toTestCaseType($classNames, $traitNames); } /** @@ -70,6 +62,35 @@ public function resolveFromBindings(array $bindings): ?Type return null; } + return $this->toTestCaseType($classNames, $traitNames); + } + + /** + * @return array{list, list} + */ + private function resolveFileClassAndTraitNames(string $filePath): array + { + [$classNames, $traitNames] = $this->partition( + $this->pestConfigReader->resolveFileBindings($filePath), + ); + + if ($classNames === []) { + [$classNames, $directoryTraitNames] = $this->partition( + $this->pestConfigReader->resolveBindings($filePath), + ); + + $traitNames = array_values(array_unique([...$directoryTraitNames, ...$traitNames])); + } + + return [$classNames, $traitNames]; + } + + /** + * @param non-empty-list $classNames + * @param list $traitNames + */ + private function toTestCaseType(array $classNames, array $traitNames): Type + { if ($traitNames === []) { return $this->toObjectType($classNames); } diff --git a/src/Type/Pest/TestFunctionProxyMethodExtension.php b/src/Type/Pest/TestFunctionProxyMethodExtension.php index 40f9136..422ff2b 100644 --- a/src/Type/Pest/TestFunctionProxyMethodExtension.php +++ b/src/Type/Pest/TestFunctionProxyMethodExtension.php @@ -40,7 +40,7 @@ private function isTodoCall(Node $node, Scope $scope): bool && $node->name instanceof Identifier && $node->name->toLowerString() === 'todo' && $this->testFunction->isBareCall($node->var, $scope) - && $this->testCaseType->resolveIfBound($scope->getFile()) instanceof Type + && $this->testCaseType->resolveIfClassBound($scope->getFile()) instanceof Type && ! $scope->getType($node->var)->hasMethod('todo')->yes() && $this->testFunction->supportsTodoHelper($scope); } diff --git a/src/Type/Pest/TestFunctionReturnTypeExtension.php b/src/Type/Pest/TestFunctionReturnTypeExtension.php index 3cbd806..13f2252 100644 --- a/src/Type/Pest/TestFunctionReturnTypeExtension.php +++ b/src/Type/Pest/TestFunctionReturnTypeExtension.php @@ -33,7 +33,7 @@ public function getTypeFromFunctionCall( return null; } - $boundType = $this->pestTestCaseType->resolveIfBound($scope->getFile()); + $boundType = $this->pestTestCaseType->resolveIfClassBound($scope->getFile()); if (! $boundType instanceof Type) { return null; diff --git a/tests/Fixtures/GlobalTraitBinding/modules/Billing/tests/Feature/billing-test-function.php b/tests/Fixtures/GlobalTraitBinding/modules/Billing/tests/Feature/billing-test-function.php new file mode 100644 index 0000000..17d7058 --- /dev/null +++ b/tests/Fixtures/GlobalTraitBinding/modules/Billing/tests/Feature/billing-test-function.php @@ -0,0 +1,15 @@ +helperMethod()); +} diff --git a/tests/Fixtures/GlobalTraitBinding/modules/Billing/tests/Helpers.php b/tests/Fixtures/GlobalTraitBinding/modules/Billing/tests/Helpers.php new file mode 100644 index 0000000..b5dcb97 --- /dev/null +++ b/tests/Fixtures/GlobalTraitBinding/modules/Billing/tests/Helpers.php @@ -0,0 +1,12 @@ +use(HelperTrait::class); + +pest()->extend(CustomTestCase::class)->in('../modules/*/tests/Feature'); + +// @note: a hook-only chain binds neither a class nor a trait. +pest()->beforeEach(function (): void { + // +})->in('../modules/*/tests'); diff --git a/tests/GlobalTraitBindingTestCase.php b/tests/GlobalTraitBindingTestCase.php new file mode 100644 index 0000000..5abf67a --- /dev/null +++ b/tests/GlobalTraitBindingTestCase.php @@ -0,0 +1,22 @@ +assertFileAsserts($assertType, $file, ...$args); +})->with(function (): Iterator { + yield from GlobalTraitBindingTestCase::gatherAssertTypes(__DIR__.'/../Fixtures/GlobalTraitBinding/modules/Billing/tests/Helpers.php'); + yield from GlobalTraitBindingTestCase::gatherAssertTypes(__DIR__.'/../Fixtures/GlobalTraitBinding/modules/Billing/tests/Feature/billing-test-function.php'); +}); diff --git a/tests/Type/global-trait-binding-extension.neon b/tests/Type/global-trait-binding-extension.neon new file mode 100644 index 0000000..2dced31 --- /dev/null +++ b/tests/Type/global-trait-binding-extension.neon @@ -0,0 +1,6 @@ +includes: + - ../../extension.neon + +parameters: + paths: + - ../Fixtures/GlobalTraitBinding