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": {
"pestphp/pest-dev-tools": "^2.0.0",
"rector/rector": "^0.15.0",
"symfony/process": "^6.2.0"
},
"minimum-stability": "dev",
@ -66,7 +67,9 @@
"bin/pest"
],
"scripts": {
"refacto": "rector",
"lint": "pint",
"test:refactor": "rector --dry-run",
"test:lint": "pint --test",
"test:types": "phpstan analyse --ansi --memory-limit=-1 --debug",
"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",
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always",
"test": [
"@rest:refacto",
"@test:lint",
"@test:types",
"@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;
use NunoMaduro\Collision;
use Pest\Contracts\Bootstrapper;
/**
* @internal
*/
final class BootExceptionHandler
final class BootExceptionHandler implements Bootstrapper
{
/**
* Boots the Exception Handler.
*/
public function __invoke(): void
public function boot(): void
{
$handler = new Collision\Provider();

View File

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

View File

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

View File

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

View File

@ -58,6 +58,6 @@ trait Pipeable
*/
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
{
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
{
/**
* @return never
*
* @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));
}

View File

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

View File

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

View File

@ -97,7 +97,7 @@ final class TestCaseMethodFactory
$testCase->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(
static fn ($annotation) => sprintf("\n * %s", $annotation), $annotations,
static fn ($annotation): string => sprintf("\n * %s", $annotation), $annotations,
));
$attributes = implode('', array_map(
static fn ($attribute) => sprintf("\n %s", $attribute), $attributes,
static fn ($attribute): string => sprintf("\n %s", $attribute), $attributes,
));
return <<<PHP

View File

@ -89,7 +89,7 @@ if (! function_exists('test')) {
*
* @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) {
return new HigherOrderTapProxy(TestSuite::getInstance()->test);

View File

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

View File

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

View File

@ -90,8 +90,7 @@ final class DatasetsRepository
*/
public static function resolve(array $dataset, string $currentTestFile): array|null
{
/* @phpstan-ignore-next-line */
if (empty($dataset)) {
if ($dataset === []) {
return null;
}
@ -177,25 +176,21 @@ final class DatasetsRepository
/**
* @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);
if ($name !== $datasetName) {
return false;
}
if (! str_starts_with($currentTestFile, $datasetScope)) {
return false;
}
return true;
return str_starts_with($currentTestFile, $datasetScope);
}, ARRAY_FILTER_USE_KEY);
$closestScopeDatasetKey = array_reduce(
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) {

View File

@ -42,7 +42,7 @@ final class TestRepository
*/
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 === []) {
$testCases = $this->testCases;

View File

@ -22,8 +22,8 @@ final class ChainableClosure
throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.');
}
\Pest\Support\Closure::bind($closure, $this, $this::class)(...func_get_args());
\Pest\Support\Closure::bind($next, $this, $this::class)(...func_get_args());
\Pest\Support\Closure::bind($closure, $this, self::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;
/**
* @var array<string, mixed>
* @var array<string, object|string>
*/
private array $instances = [];
@ -25,37 +25,30 @@ final class Container
*/
public static function getInstance(): self
{
if (static::$instance === null) {
static::$instance = new self();
if (self::$instance === null) {
self::$instance = new self();
}
return static::$instance;
return self::$instance;
}
/**
* 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)) {
/** @var class-string $id */
$this->instances[$id] = $this->build($id);
}
/** @var TObject $concrete */
$concrete = $this->instances[$id];
return $concrete;
return $this->instances[$id];
}
/**
* 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;
}
@ -68,7 +61,7 @@ final class Container
* @param class-string<TObject> $id
* @return TObject
*/
private function build(string $id): mixed
private function build(string $id): object
{
$reflectionClass = new ReflectionClass($id);
@ -77,7 +70,7 @@ final class Container
if ($constructor !== null) {
$params = array_map(
function (ReflectionParameter $param) use ($id) {
function (ReflectionParameter $param) use ($id): object|string {
$candidate = Reflection::getParameterClassName($param);
if ($candidate === null) {
@ -90,7 +83,6 @@ final class Container
}
}
// @phpstan-ignore-next-line
return $this->get($candidate);
},
$constructor->getParameters()

View File

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