chore: improves type coverage

This commit is contained in:
Nuno Maduro
2022-12-07 09:17:35 +00:00
parent c5f6923e5a
commit 70f447a8bc
22 changed files with 109 additions and 68 deletions

View File

@ -51,6 +51,7 @@
}, },
"require-dev": { "require-dev": {
"pestphp/pest-dev-tools": "^2.0.0", "pestphp/pest-dev-tools": "^2.0.0",
"rector/rector": "^0.15.0",
"symfony/process": "^6.2.0" "symfony/process": "^6.2.0"
}, },
"minimum-stability": "dev", "minimum-stability": "dev",
@ -66,7 +67,9 @@
"bin/pest" "bin/pest"
], ],
"scripts": { "scripts": {
"refacto": "rector",
"lint": "pint", "lint": "pint",
"test:refactor": "rector --dry-run",
"test:lint": "pint --test", "test:lint": "pint --test",
"test:types": "phpstan analyse --ansi --memory-limit=-1 --debug", "test:types": "phpstan analyse --ansi --memory-limit=-1 --debug",
"test:unit": "php bin/pest --colors=always --exclude-group=integration --compact", "test:unit": "php bin/pest --colors=always --exclude-group=integration --compact",
@ -75,6 +78,7 @@
"test:integration": "php bin/pest --colors=always --group=integration -v", "test:integration": "php bin/pest --colors=always --group=integration -v",
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always", "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always",
"test": [ "test": [
"@rest:refacto",
"@test:lint", "@test:lint",
"@test:types", "@test:types",
"@test:unit", "@test:unit",

27
rector.php Normal file
View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
use Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyRector;
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([
__DIR__.'/src',
]);
$rectorConfig->rules([
InlineConstructorDefaultToPropertyRector::class,
]);
$rectorConfig->sets([
LevelSetList::UP_TO_PHP_81,
SetList::CODE_QUALITY,
SetList::DEAD_CODE,
SetList::EARLY_RETURN,
SetList::TYPE_DECLARATION,
SetList::PRIVATIZATION,
]);
};

View File

@ -5,16 +5,17 @@ declare(strict_types=1);
namespace Pest\Bootstrappers; namespace Pest\Bootstrappers;
use NunoMaduro\Collision; use NunoMaduro\Collision;
use Pest\Contracts\Bootstrapper;
/** /**
* @internal * @internal
*/ */
final class BootExceptionHandler final class BootExceptionHandler implements Bootstrapper
{ {
/** /**
* Boots the Exception Handler. * Boots the Exception Handler.
*/ */
public function __invoke(): void public function boot(): void
{ {
$handler = new Collision\Provider(); $handler = new Collision\Provider();

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Pest\Bootstrappers; namespace Pest\Bootstrappers;
use Pest\Contracts\Bootstrapper;
use Pest\Support\DatasetInfo; use Pest\Support\DatasetInfo;
use Pest\Support\Str; use Pest\Support\Str;
use function Pest\testDirectory; use function Pest\testDirectory;
@ -15,7 +16,7 @@ use SebastianBergmann\FileIterator\Facade as PhpUnitFileIterator;
/** /**
* @internal * @internal
*/ */
final class BootFiles final class BootFiles implements Bootstrapper
{ {
/** /**
* The Pest convention. * The Pest convention.
@ -33,7 +34,7 @@ final class BootFiles
/** /**
* Boots the Subscribers. * Boots the Subscribers.
*/ */
public function __invoke(): void public function boot(): void
{ {
$rootPath = TestSuite::getInstance()->rootPath; $rootPath = TestSuite::getInstance()->rootPath;
$testsPath = $rootPath.DIRECTORY_SEPARATOR.testDirectory(); $testsPath = $rootPath.DIRECTORY_SEPARATOR.testDirectory();

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Pest\Bootstrappers; namespace Pest\Bootstrappers;
use Pest\Contracts\Bootstrapper;
use Pest\Subscribers; use Pest\Subscribers;
use PHPUnit\Event; use PHPUnit\Event;
use PHPUnit\Event\Subscriber; use PHPUnit\Event\Subscriber;
@ -11,7 +12,7 @@ use PHPUnit\Event\Subscriber;
/** /**
* @internal * @internal
*/ */
final class BootSubscribers final class BootSubscribers implements Bootstrapper
{ {
/** /**
* The Kernel subscribers. * The Kernel subscribers.
@ -28,7 +29,7 @@ final class BootSubscribers
/** /**
* Boots the Subscribers. * Boots the Subscribers.
*/ */
public function __invoke(): void public function boot(): void
{ {
foreach (self::SUBSCRIBERS as $subscriber) { foreach (self::SUBSCRIBERS as $subscriber) {
Event\Facade::registerSubscriber( Event\Facade::registerSubscriber(

View File

@ -4,13 +4,14 @@ declare(strict_types=1);
namespace Pest\Bootstrappers; namespace Pest\Bootstrappers;
use Pest\Contracts\Bootstrapper;
use Pest\Support\View; use Pest\Support\View;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
/** /**
* @internal * @internal
*/ */
final class BootView final class BootView implements Bootstrapper
{ {
public function __construct( public function __construct(
private readonly OutputInterface $output private readonly OutputInterface $output
@ -21,7 +22,7 @@ final class BootView
/** /**
* Boots the view renderer. * Boots the view renderer.
*/ */
public function __invoke(): void public function boot(): void
{ {
View::renderUsing($this->output); View::renderUsing($this->output);
} }

View File

@ -58,6 +58,6 @@ trait Pipeable
*/ */
private function pipes(string $name, object $context, string $scope): array private function pipes(string $name, object $context, string $scope): array
{ {
return array_map(fn (Closure $pipe) => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []); return array_map(fn (Closure $pipe): \Closure => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []);
} }
} }

View File

@ -259,7 +259,7 @@ trait Testable
*/ */
private function __callClosure(Closure $closure, array $arguments): mixed private function __callClosure(Closure $closure, array $arguments): mixed
{ {
return ExceptionTrace::ensure(fn () => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments)); return ExceptionTrace::ensure(fn (): mixed => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments));
} }
/** /**

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Pest\Contracts;
/**
* @internal
*/
interface Bootstrapper
{
/**
* Boots the bootstrapper.
*/
public function boot(): void;
}

View File

@ -12,11 +12,9 @@ use InvalidArgumentException;
final class InvalidExpectationValue extends InvalidArgumentException final class InvalidExpectationValue extends InvalidArgumentException
{ {
/** /**
* @return never
*
* @throws self * @throws self
*/ */
public static function expected(string $type): void public static function expected(string $type): never
{ {
throw new self(sprintf('Invalid expectation value type. Expected [%s].', $type)); throw new self(sprintf('Invalid expectation value type. Expected [%s].', $type));
} }

View File

@ -70,10 +70,12 @@ final class Expectation
InvalidExpectationValue::expected('string'); InvalidExpectationValue::expected('string');
} }
/** @var array<int|string, mixed>|bool $value */ $this->toBeJson();
$value = json_decode($this->value, true, 512);
return $this->toBeJson()->and($value); /** @var array<int|string, mixed>|bool $value */
$value = json_decode($this->value, true, 512, JSON_THROW_ON_ERROR);
return $this->and($value);
} }
/** /**

View File

@ -90,9 +90,8 @@ final class OppositeExpectation
* Creates a new expectation failed exception with a nice readable message. * Creates a new expectation failed exception with a nice readable message.
* *
* @param array<int, mixed> $arguments * @param array<int, mixed> $arguments
* @return never
*/ */
private function throwExpectationFailedException(string $name, array $arguments = []): void private function throwExpectationFailedException(string $name, array $arguments = []): never
{ {
$exporter = new Exporter(); $exporter = new Exporter();

View File

@ -85,7 +85,7 @@ final class TestCaseFactory
$methods = array_values(array_filter( $methods = array_values(array_filter(
$this->methods, $this->methods,
fn ($method) => $methodsUsingOnly === [] || in_array($method, $methodsUsingOnly, true) fn ($method): bool => $methodsUsingOnly === [] || in_array($method, $methodsUsingOnly, true)
)); ));
if ($methods !== []) { if ($methods !== []) {
@ -165,21 +165,21 @@ final class TestCaseFactory
$classFQN .= $className; $classFQN .= $className;
} }
$classAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute) => $attribute::ABOVE_CLASS); $classAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute): bool => $attribute::ABOVE_CLASS);
$methodAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute) => ! $attribute::ABOVE_CLASS); $methodAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute): bool => ! $attribute::ABOVE_CLASS);
$classAttributes = []; $classAttributes = [];
foreach ($classAvailableAttributes as $attribute) { foreach ($classAvailableAttributes as $attribute) {
$classAttributes = array_reduce( $classAttributes = array_reduce(
$methods, $methods,
fn (array $carry, TestCaseMethodFactory $methodFactory) => (new $attribute())->__invoke($methodFactory, $carry), fn (array $carry, TestCaseMethodFactory $methodFactory): array => (new $attribute())->__invoke($methodFactory, $carry),
$classAttributes $classAttributes
); );
} }
$methodsCode = implode('', array_map( $methodsCode = implode('', array_map(
fn (TestCaseMethodFactory $methodFactory) => $methodFactory->buildForEvaluation( fn (TestCaseMethodFactory $methodFactory): string => $methodFactory->buildForEvaluation(
$classFQN, $classFQN,
self::ANNOTATIONS, self::ANNOTATIONS,
$methodAvailableAttributes $methodAvailableAttributes
@ -188,7 +188,7 @@ final class TestCaseFactory
)); ));
$classAttributesCode = implode('', array_map( $classAttributesCode = implode('', array_map(
static fn (string $attribute) => sprintf("\n%s", $attribute), static fn (string $attribute): string => sprintf("\n%s", $attribute),
array_unique($classAttributes), array_unique($classAttributes),
)); ));

View File

@ -97,7 +97,7 @@ final class TestCaseMethodFactory
$testCase->chains->chain($this); $testCase->chains->chain($this);
$method->chains->chain($this); $method->chains->chain($this);
return \Pest\Support\Closure::bind($closure, $this, $this::class)(...func_get_args()); return \Pest\Support\Closure::bind($closure, $this, self::class)(...func_get_args());
}; };
} }
@ -147,11 +147,11 @@ final class TestCaseMethodFactory
} }
$annotations = implode('', array_map( $annotations = implode('', array_map(
static fn ($annotation) => sprintf("\n * %s", $annotation), $annotations, static fn ($annotation): string => sprintf("\n * %s", $annotation), $annotations,
)); ));
$attributes = implode('', array_map( $attributes = implode('', array_map(
static fn ($attribute) => sprintf("\n %s", $attribute), $attributes, static fn ($attribute): string => sprintf("\n %s", $attribute), $attributes,
)); ));
return <<<PHP return <<<PHP

View File

@ -89,7 +89,7 @@ if (! function_exists('test')) {
* *
* @return TestCall|TestCase|mixed * @return TestCall|TestCase|mixed
*/ */
function test(string $description = null, Closure $closure = null) function test(string $description = null, Closure $closure = null): HigherOrderTapProxy|TestCall
{ {
if ($description === null && TestSuite::getInstance()->test !== null) { if ($description === null && TestSuite::getInstance()->test !== null) {
return new HigherOrderTapProxy(TestSuite::getInstance()->test); return new HigherOrderTapProxy(TestSuite::getInstance()->test);

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Pest; namespace Pest;
use Pest\Contracts\Bootstrapper;
use Pest\Plugins\Actions\CallsAddsOutput; use Pest\Plugins\Actions\CallsAddsOutput;
use Pest\Plugins\Actions\CallsBoot; use Pest\Plugins\Actions\CallsBoot;
use Pest\Plugins\Actions\CallsShutdown; use Pest\Plugins\Actions\CallsShutdown;
@ -49,7 +50,10 @@ final class Kernel
public static function boot(): self public static function boot(): self
{ {
foreach (self::BOOTSTRAPPERS as $bootstrapper) { foreach (self::BOOTSTRAPPERS as $bootstrapper) {
Container::getInstance()->get($bootstrapper)->__invoke(); $bootstrapper = Container::getInstance()->get($bootstrapper);
assert($bootstrapper instanceof Bootstrapper);
$bootstrapper->boot();
} }
(new CallsBoot())->__invoke(); (new CallsBoot())->__invoke();

View File

@ -154,7 +154,7 @@ final class TestCall
$condition = is_callable($condition) $condition = is_callable($condition)
? $condition ? $condition
: fn () => $condition; : fn (): bool => $condition;
$message = is_string($conditionOrMessage) $message = is_string($conditionOrMessage)
? $conditionOrMessage ? $conditionOrMessage

View File

@ -90,8 +90,7 @@ final class DatasetsRepository
*/ */
public static function resolve(array $dataset, string $currentTestFile): array|null public static function resolve(array $dataset, string $currentTestFile): array|null
{ {
/* @phpstan-ignore-next-line */ if ($dataset === []) {
if (empty($dataset)) {
return null; return null;
} }
@ -177,25 +176,21 @@ final class DatasetsRepository
/** /**
* @return Closure|iterable<int|string, mixed> * @return Closure|iterable<int|string, mixed>
*/ */
private static function getScopedDataset(string $name, string $currentTestFile) private static function getScopedDataset(string $name, string $currentTestFile): Closure|iterable
{ {
$matchingDatasets = array_filter(self::$datasets, function (string $key) use ($name, $currentTestFile) { $matchingDatasets = array_filter(self::$datasets, function (string $key) use ($name, $currentTestFile): bool {
[$datasetScope, $datasetName] = explode(self::SEPARATOR, $key); [$datasetScope, $datasetName] = explode(self::SEPARATOR, $key);
if ($name !== $datasetName) { if ($name !== $datasetName) {
return false; return false;
} }
if (! str_starts_with($currentTestFile, $datasetScope)) { return str_starts_with($currentTestFile, $datasetScope);
return false;
}
return true;
}, ARRAY_FILTER_USE_KEY); }, ARRAY_FILTER_USE_KEY);
$closestScopeDatasetKey = array_reduce( $closestScopeDatasetKey = array_reduce(
array_keys($matchingDatasets), array_keys($matchingDatasets),
fn ($keyA, $keyB) => $keyA !== null && strlen($keyA) > strlen($keyB) ? $keyA : $keyB fn ($keyA, $keyB) => $keyA !== null && strlen((string) $keyA) > strlen($keyB) ? $keyA : $keyB
); );
if ($closestScopeDatasetKey === null) { if ($closestScopeDatasetKey === null) {

View File

@ -42,7 +42,7 @@ final class TestRepository
*/ */
public function getFilenames(): array public function getFilenames(): array
{ {
$testCases = array_filter($this->testCases, static fn (TestCaseFactory $testCase) => $testCase->methodsUsingOnly() !== []); $testCases = array_filter($this->testCases, static fn (TestCaseFactory $testCase): bool => $testCase->methodsUsingOnly() !== []);
if ($testCases === []) { if ($testCases === []) {
$testCases = $this->testCases; $testCases = $this->testCases;

View File

@ -22,8 +22,8 @@ final class ChainableClosure
throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.'); throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.');
} }
\Pest\Support\Closure::bind($closure, $this, $this::class)(...func_get_args()); \Pest\Support\Closure::bind($closure, $this, self::class)(...func_get_args());
\Pest\Support\Closure::bind($next, $this, $this::class)(...func_get_args()); \Pest\Support\Closure::bind($next, $this, self::class)(...func_get_args());
}; };
} }

View File

@ -16,7 +16,7 @@ final class Container
private static ?Container $instance = null; private static ?Container $instance = null;
/** /**
* @var array<string, mixed> * @var array<string, object|string>
*/ */
private array $instances = []; private array $instances = [];
@ -25,37 +25,30 @@ final class Container
*/ */
public static function getInstance(): self public static function getInstance(): self
{ {
if (static::$instance === null) { if (self::$instance === null) {
static::$instance = new self(); self::$instance = new self();
} }
return static::$instance; return self::$instance;
} }
/** /**
* Gets a dependency from the container. * Gets a dependency from the container.
*
* @template TObject of object
*
* @param class-string<TObject> $id
* @return TObject
*/ */
public function get(string $id): mixed public function get(string $id): object|string
{ {
if (! array_key_exists($id, $this->instances)) { if (! array_key_exists($id, $this->instances)) {
/** @var class-string $id */
$this->instances[$id] = $this->build($id); $this->instances[$id] = $this->build($id);
} }
/** @var TObject $concrete */ return $this->instances[$id];
$concrete = $this->instances[$id];
return $concrete;
} }
/** /**
* Adds the given instance to the container. * Adds the given instance to the container.
*/ */
public function add(string $id, mixed $instance): void public function add(string $id, object|string $instance): void
{ {
$this->instances[$id] = $instance; $this->instances[$id] = $instance;
} }
@ -68,7 +61,7 @@ final class Container
* @param class-string<TObject> $id * @param class-string<TObject> $id
* @return TObject * @return TObject
*/ */
private function build(string $id): mixed private function build(string $id): object
{ {
$reflectionClass = new ReflectionClass($id); $reflectionClass = new ReflectionClass($id);
@ -77,7 +70,7 @@ final class Container
if ($constructor !== null) { if ($constructor !== null) {
$params = array_map( $params = array_map(
function (ReflectionParameter $param) use ($id) { function (ReflectionParameter $param) use ($id): object|string {
$candidate = Reflection::getParameterClassName($param); $candidate = Reflection::getParameterClassName($param);
if ($candidate === null) { if ($candidate === null) {
@ -90,7 +83,6 @@ final class Container
} }
} }
// @phpstan-ignore-next-line
return $this->get($candidate); return $this->get($candidate);
}, },
$constructor->getParameters() $constructor->getParameters()

View File

@ -42,17 +42,17 @@ final class Coverage
return false; return false;
} }
if ($runtime->hasXdebug()) { if (! $runtime->hasXdebug()) {
if (version_compare((string) phpversion('xdebug'), '3.1', '>=')) { return true;
if (! in_array('coverage', xdebug_info('mode'), true)) {
return false;
}
}
} }
if (! version_compare((string) phpversion('xdebug'), '3.1', '>=')) {
return true; return true;
} }
return in_array('coverage', xdebug_info('mode'), true);
}
/** /**
* If the user is using Xdebug. * If the user is using Xdebug.
*/ */