chore: type improvements

This commit is contained in:
nuno maduro
2026-04-10 16:50:10 +01:00
parent 89006d83a9
commit 5948bcd71e
8 changed files with 71 additions and 103 deletions

View File

@ -55,6 +55,7 @@
] ]
}, },
"require-dev": { "require-dev": {
"mrpunyapal/peststan": "^0.2.5",
"pestphp/pest-dev-tools": "^4.1.0", "pestphp/pest-dev-tools": "^4.1.0",
"pestphp/pest-plugin-browser": "^4.3.1", "pestphp/pest-plugin-browser": "^4.3.1",
"pestphp/pest-plugin-type-coverage": "^4.0.4", "pestphp/pest-plugin-type-coverage": "^4.0.4",

View File

@ -1,11 +1,5 @@
parameters: parameters:
ignoreErrors: ignoreErrors:
-
message: '#^Parameter \#1 of callable callable\(Pest\\Expectation\<string\|null\>\)\: Pest\\Arch\\Contracts\\ArchExpectation expects Pest\\Expectation\<string\|null\>, Pest\\Expectation\<string\|null\> given\.$#'
identifier: argument.type
count: 1
path: src/ArchPresets/AbstractPreset.php
- -
message: '#^Trait Pest\\Concerns\\Expectable is used zero times and is not analysed\.$#' message: '#^Trait Pest\\Concerns\\Expectable is used zero times and is not analysed\.$#'
identifier: trait.unused identifier: trait.unused
@ -24,12 +18,6 @@ parameters:
count: 1 count: 1
path: src/Concerns/Testable.php path: src/Concerns/Testable.php
-
message: '#^Loose comparison using \!\= between \(Closure\|null\) and false will always evaluate to false\.$#'
identifier: notEqual.alwaysFalse
count: 1
path: src/Expectation.php
- -
message: '#^Method Pest\\Expectation\:\:and\(\) should return Pest\\Expectation\<TAndValue\> but returns \(Pest\\Expectation&TAndValue\)\|Pest\\Expectation\<TAndValue of mixed\>\.$#' message: '#^Method Pest\\Expectation\:\:and\(\) should return Pest\\Expectation\<TAndValue\> but returns \(Pest\\Expectation&TAndValue\)\|Pest\\Expectation\<TAndValue of mixed\>\.$#'
identifier: return.type identifier: return.type
@ -102,78 +90,12 @@ parameters:
count: 1 count: 1
path: src/PendingCalls/TestCall.php path: src/PendingCalls/TestCall.php
-
message: '#^Parameter \#1 \$argv of class Symfony\\Component\\Console\\Input\\ArgvInput constructor expects list\<string\>\|null, array\<int, string\> given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel.php
-
message: '#^Parameter \#13 \$testRunnerTriggeredDeprecationEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\TestRunner\\DeprecationTriggered\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#14 \$testRunnerTriggeredWarningEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\TestRunner\\WarningTriggered\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#15 \$errors of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\TestRunner\\TestResult\\Issues\\Issue\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#16 \$deprecations of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\TestRunner\\TestResult\\Issues\\Issue\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#17 \$notices of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\TestRunner\\TestResult\\Issues\\Issue\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#18 \$warnings of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\TestRunner\\TestResult\\Issues\\Issue\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#19 \$phpDeprecations of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\TestRunner\\TestResult\\Issues\\Issue\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#20 \$phpNotices of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\TestRunner\\TestResult\\Issues\\Issue\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#21 \$phpWarnings of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\TestRunner\\TestResult\\Issues\\Issue\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
- -
message: '#^Parameter \#4 \$testErroredEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\Test\\AfterLastTestMethodErrored\|PHPUnit\\Event\\Test\\BeforeFirstTestMethodErrored\|PHPUnit\\Event\\Test\\Errored\>, array given\.$#' message: '#^Parameter \#4 \$testErroredEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\Test\\AfterLastTestMethodErrored\|PHPUnit\\Event\\Test\\BeforeFirstTestMethodErrored\|PHPUnit\\Event\\Test\\Errored\>, array given\.$#'
identifier: argument.type identifier: argument.type
count: 1 count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php path: src/Plugins/Parallel/Paratest/WrapperRunner.php
-
message: '#^Parameter \#5 \$testFailedEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\Test\\Failed\>, array given\.$#'
identifier: argument.type
count: 1
path: src/Plugins/Parallel/Paratest/WrapperRunner.php
- -
message: '#^Parameter \#7 \$testSuiteSkippedEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\TestSuite\\Skipped\>, array given\.$#' message: '#^Parameter \#7 \$testSuiteSkippedEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\<PHPUnit\\Event\\TestSuite\\Skipped\>, array given\.$#'
identifier: argument.type identifier: argument.type

View File

@ -0,0 +1,5 @@
services:
-
class: Pest\PHPStan\HigherOrderExpectationTypeExtension
tags:
- phpstan.broker.expressionTypeResolverExtension

View File

@ -1,5 +1,7 @@
includes: includes:
- phpstan-baseline.neon - phpstan-baseline.neon
- phpstan-pest-extension.neon
- vendor/mrpunyapal/peststan/extension.neon
parameters: parameters:
level: 7 level: 7
@ -7,6 +9,3 @@ parameters:
- src - src
reportUnmatchedIgnoredErrors: false reportUnmatchedIgnoredErrors: false
ignoreErrors:
- "#type mixed is not subtype of native#"

View File

@ -53,7 +53,7 @@ abstract class AbstractPreset // @pest-arch-ignore-line
/** /**
* Runs the given callback for each namespace. * Runs the given callback for each namespace.
* *
* @param callable(Expectation<string|null>): ArchExpectation ...$callbacks * @param callable(Expectation<string>): ArchExpectation ...$callbacks
*/ */
final public function eachUserNamespace(callable ...$callbacks): void final public function eachUserNamespace(callable ...$callbacks): void
{ {

View File

@ -4,7 +4,6 @@ declare(strict_types=1);
use Pest\Browser\Api\ArrayablePendingAwaitablePage; use Pest\Browser\Api\ArrayablePendingAwaitablePage;
use Pest\Browser\Api\PendingAwaitablePage; use Pest\Browser\Api\PendingAwaitablePage;
use Pest\Concerns\Expectable;
use Pest\Configuration; use Pest\Configuration;
use Pest\Exceptions\AfterAllWithinDescribe; use Pest\Exceptions\AfterAllWithinDescribe;
use Pest\Exceptions\BeforeAllWithinDescribe; use Pest\Exceptions\BeforeAllWithinDescribe;
@ -62,8 +61,6 @@ if (! function_exists('beforeEach')) {
* Runs the given closure before each test in the current file. * Runs the given closure before each test in the current file.
* *
* @param-closure-this TestCase $closure * @param-closure-this TestCase $closure
*
* @return HigherOrderTapProxy<Expectable|TestCall|TestCase>|Expectable|TestCall|TestCase|mixed
*/ */
function beforeEach(?Closure $closure = null): BeforeEachCall function beforeEach(?Closure $closure = null): BeforeEachCall
{ {
@ -92,8 +89,6 @@ if (! function_exists('describe')) {
* Adds the given closure as a group of tests. The first argument * Adds the given closure as a group of tests. The first argument
* is the group description; the second argument is a closure * is the group description; the second argument is a closure
* that contains the group tests. * that contains the group tests.
*
* @return HigherOrderTapProxy<Expectable|TestCall|TestCase>|Expectable|TestCall|TestCase|mixed
*/ */
function describe(string $description, Closure $tests): DescribeCall function describe(string $description, Closure $tests): DescribeCall
{ {
@ -136,7 +131,7 @@ if (! function_exists('test')) {
* *
* @param-closure-this TestCase $closure * @param-closure-this TestCase $closure
* *
* @return Expectable|TestCall|TestCase|mixed * @return ($description is string ? TestCall : HigherOrderTapProxy|TestCall)
*/ */
function test(?string $description = null, ?Closure $closure = null): HigherOrderTapProxy|TestCall function test(?string $description = null, ?Closure $closure = null): HigherOrderTapProxy|TestCall
{ {
@ -157,33 +152,22 @@ if (! function_exists('it')) {
* a closure that contains the test expectations. * a closure that contains the test expectations.
* *
* @param-closure-this TestCase $closure * @param-closure-this TestCase $closure
*
* @return Expectable|TestCall|TestCase|mixed
*/ */
function it(string $description, ?Closure $closure = null): TestCall function it(string $description, ?Closure $closure = null): TestCall
{ {
$description = sprintf('it %s', $description); $description = sprintf('it %s', $description);
/** @var TestCall $test */ return test($description, $closure);
$test = test($description, $closure);
return $test;
} }
} }
if (! function_exists('todo')) { if (! function_exists('todo')) {
/** /**
* Creates a new test that is marked as "todo". * Creates a new test that is marked as "todo".
*
* @return Expectable|TestCall|TestCase|mixed
*/ */
function todo(string $description): TestCall function todo(string $description): TestCall
{ {
$test = test($description); return test($description)->todo();
assert($test instanceof TestCall);
return $test->todo();
} }
} }
@ -192,8 +176,6 @@ if (! function_exists('afterEach')) {
* Runs the given closure after each test in the current file. * Runs the given closure after each test in the current file.
* *
* @param-closure-this TestCase $closure * @param-closure-this TestCase $closure
*
* @return Expectable|HigherOrderTapProxy<Expectable|TestCall|TestCase>|TestCall|mixed
*/ */
function afterEach(?Closure $closure = null): AfterEachCall function afterEach(?Closure $closure = null): AfterEachCall
{ {

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Pest\PHPStan;
use Pest\Expectations\HigherOrderExpectation;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Identifier;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\ExpressionTypeResolverExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
/**
* Prevents native declared properties of HigherOrderExpectation (like $original,
* $expectation, $opposite, $shouldReset) from being incorrectly resolved as
* higher-order value property accesses by downstream ExpressionTypeResolverExtensions.
*
* This extension must be registered BEFORE the peststan HigherOrderExpectationTypeExtension.
*
* @internal
*/
final readonly class HigherOrderExpectationTypeExtension implements ExpressionTypeResolverExtension
{
public function __construct(
private ReflectionProvider $reflectionProvider,
) {}
public function getType(Expr $expr, Scope $scope): ?Type
{
if (! $expr instanceof PropertyFetch || ! $expr->name instanceof Identifier) {
return null;
}
$varType = $scope->getType($expr->var);
if (! (new ObjectType(HigherOrderExpectation::class))->isSuperTypeOf($varType)->yes()) {
return null;
}
if (! $this->reflectionProvider->hasClass(HigherOrderExpectation::class)) {
return null;
}
$propertyName = $expr->name->name;
$classReflection = $this->reflectionProvider->getClass(HigherOrderExpectation::class);
if (! $classReflection->hasNativeProperty($propertyName)) {
return null;
}
return $varType->getProperty($propertyName, $scope)->getReadableType();
}
}

View File

@ -130,6 +130,8 @@
CODE COVERAGE OPTIONS: CODE COVERAGE OPTIONS:
--coverage ..... Generate code coverage report and output to standard output --coverage ..... Generate code coverage report and output to standard output
--coverage --min Set the minimum required coverage percentage, and fail if not met --coverage --min Set the minimum required coverage percentage, and fail if not met
--coverage --exactly Set the exact required coverage percentage, and fail if not met
--coverage --only-covered Hide files with 0% coverage from the code coverage report
--coverage-clover [file] Write code coverage report in Clover XML format to file --coverage-clover [file] Write code coverage report in Clover XML format to file
--coverage-openclover [file] Write code coverage report in OpenClover XML format to file --coverage-openclover [file] Write code coverage report in OpenClover XML format to file
--coverage-cobertura [file] Write code coverage report in Cobertura XML format to file --coverage-cobertura [file] Write code coverage report in Cobertura XML format to file