Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 40 additions & 19 deletions src/Type/Pest/PestTestCaseType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}

/**
Expand All @@ -70,6 +62,35 @@ public function resolveFromBindings(array $bindings): ?Type
return null;
}

return $this->toTestCaseType($classNames, $traitNames);
}

/**
* @return array{list<class-string>, list<class-string>}
*/
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<class-string> $classNames
* @param list<class-string> $traitNames
*/
private function toTestCaseType(array $classNames, array $traitNames): Type
{
if ($traitNames === []) {
return $this->toObjectType($classNames);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Type/Pest/TestFunctionProxyMethodExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Type/Pest/TestFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace GlobalTraitBindingFeature;

use Tests\Type\Fixtures\CustomTestCase;

use function PHPStan\Testing\assertType;

function helperInsideATestCaseBinding(): void
{
assertType(CustomTestCase::class, test());
assertType('string', test()->helperMethod());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace GlobalTraitBindingHelpers;

use function PHPStan\Testing\assertType;

function helperOutsideAnyTestCaseBinding(): void
{
assertType('Pest\PendingCalls\TestCall|Pest\Support\HigherOrderTapProxy', test());
}
16 changes: 16 additions & 0 deletions tests/Fixtures/GlobalTraitBinding/tests/Pest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

use Tests\Type\Fixtures\CustomTestCase;
use Tests\Type\Fixtures\HelperTrait;

// @note: a trait bound without in() applies to every test file, but binds no test case class.
pest()->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');
22 changes: 22 additions & 0 deletions tests/GlobalTraitBindingTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Tests;

use Override;
use PHPStan\Testing\TypeInferenceTestCase;

abstract class GlobalTraitBindingTestCase extends TypeInferenceTestCase
{
/**
* @return string[]
*/
#[Override]
final public static function getAdditionalConfigFiles(): array
{
return [
__DIR__.'/Type/global-trait-binding-extension.neon',
];
}
}
12 changes: 12 additions & 0 deletions tests/Type/GlobalTraitBindingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

use Tests\GlobalTraitBindingTestCase;

test('a bare test() keeps the declared type when only global traits cover the file', function (string $assertType, string $file, mixed ...$args): void {
$this->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');
});
6 changes: 6 additions & 0 deletions tests/Type/global-trait-binding-extension.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
includes:
- ../../extension.neon

parameters:
paths:
- ../Fixtures/GlobalTraitBinding
Loading